diff --git a/.github/.htaccess b/.github/.htaccess new file mode 100644 index 0000000000000..707c26b075e16 --- /dev/null +++ b/.github/.htaccess @@ -0,0 +1,8 @@ + + order allow,deny + deny from all + += 2.4> + Require all denied + + diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000..4e82725a7fb08 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engcom@magento.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000000..dae954a0970b7 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing to Magento 2 code + +Contributions to the Magento 2 codebase are done using the fork & pull model. +This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes. For more information on pull requests please refer to [GitHub Help](https://help.github.com/articles/about-pull-requests/). + +Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes or optimizations. + +The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor within two weeks, the pull request will be closed. + + +## Contribution requirements + +1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.2/coding-standards/bk-coding-standards.html). +2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. +3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/2.2-develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. +4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. +3. PRs which include new logic or new features must be submitted along with: +* Unit/integration test coverage +* Proposed [documentation](http://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). +4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. +5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). + +## Contribution process + +If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). This will allow you to collaborate with the Magento 2 development team, fork the Magento 2 project and send pull requests. + +1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. +2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. +3. Create and test your work. +4. Fork the Magento 2 repository according to the [Fork A Repository instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). +5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. +The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000000..2b1720ccaabae --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,36 @@ + + +### Preconditions (*) + +1. +2. + +### Steps to reproduce (*) + +1. +2. +3. + +### Expected result (*) + +1. [Screenshots, logs or description] + +### Actual result (*) + +1. [Screenshots, logs or description] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000..33a6ef02ace11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Technical issue with the Magento 2 core components + +--- + + + +### Preconditions (*) + +1. +2. + +### Steps to reproduce (*) + +1. +2. + +### Expected result (*) + +1. [Screenshots, logs or description] +2. + +### Actual result (*) + +1. [Screenshots, logs or description] +2. diff --git a/.github/ISSUE_TEMPLATE/developer-experience-issue.md b/.github/ISSUE_TEMPLATE/developer-experience-issue.md new file mode 100644 index 0000000000000..423d4818fb31c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/developer-experience-issue.md @@ -0,0 +1,19 @@ +--- +name: Developer experience issue +about: Issues related to customization, extensibility, modularity + +--- + + + +### Summary (*) + + +### Examples (*) + + +### Proposed solution + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000..f64185773cab4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Please consider reporting directly to https://github.com/magento/community-features + +--- + + + +### Description (*) + + +### Expected behavior (*) + + +### Benefits + + +### Additional information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..9d66ee40d6f59 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,38 @@ + + + + +### Description (*) + + +### Fixed Issues (if relevant) + +1. magento/magento2#: Issue title +2. ... + +### Manual testing scenarios (*) + +1. ... +2. ... + +### Contribution checklist (*) + - [ ] Pull request has a meaningful description of its purpose + - [ ] All commits are accompanied by meaningful commit messages + - [ ] All new or changed code is covered with unit/integration tests (if applicable) + - [ ] All automated tests passed successfully (all builds are green) diff --git a/.gitignore b/.gitignore index 94c3bf76a2bd1..a79b7990a7576 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ atlassian* /.php_cs /.php_cs.cache /grunt-config.json -/dev/tools/grunt/configs/local-themes.js /pub/media/*.* !/pub/media/.htaccess @@ -50,6 +49,8 @@ atlassian* /pub/media/import/* !/pub/media/import/.htaccess /pub/media/logo/* +/pub/media/custom_options/* +!/pub/media/custom_options/.htaccess /pub/media/theme/* /pub/media/theme_customization/* !/pub/media/theme_customization/.htaccess diff --git a/.htaccess b/.htaccess index fd4f5a63de051..cc59be5480798 100644 --- a/.htaccess +++ b/.htaccess @@ -29,6 +29,8 @@ ############################################ ## default index file +## Specifies option, to use methods arguments in backtrace or not + SetEnv MAGE_DEBUG_SHOW_ARGS 1 DirectoryIndex index.php @@ -274,15 +276,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny @@ -373,6 +366,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.htaccess.sample b/.htaccess.sample index a6c1bc4caf30b..b405fd3a22b75 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -251,15 +251,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny @@ -350,6 +341,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.php_cs.dist b/.php_cs.dist index 0f254c63283bd..87483d5b33a15 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ -/** - * Pre-commit hook installation: - * vendor/bin/static-review.php hook:install dev/tools/Magento/Tools/StaticReview/pre-commit .git/hooks/pre-commit - */ $finder = PhpCsFixer\Finder::create() ->name('*.phtml') ->exclude('dev/tests/functional/generated') diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3265cc575cdca..0000000000000 --- a/.travis.yml +++ /dev/null @@ -1,60 +0,0 @@ -sudo: required -dist: trusty -group: edge -addons: - apt: - packages: - - mysql-server-5.6 - - mysql-client-core-5.6 - - mysql-client-5.6 - - postfix - firefox: "46.0" - hosts: - - magento2.travis -language: php -php: - - 7.0 - - 7.1 -env: - global: - - COMPOSER_BIN_DIR=~/bin - - INTEGRATION_SETS=3 - - NODE_JS_VERSION=6 - - MAGENTO_HOST_NAME="magento2.travis" - matrix: - - TEST_SUITE=unit - - TEST_SUITE=static - - TEST_SUITE=js GRUNT_COMMAND=spec - - TEST_SUITE=js GRUNT_COMMAND=static - - TEST_SUITE=integration INTEGRATION_INDEX=1 - - TEST_SUITE=integration INTEGRATION_INDEX=2 - - TEST_SUITE=integration INTEGRATION_INDEX=3 - - TEST_SUITE=functional -matrix: - exclude: - - php: 7.0 - env: TEST_SUITE=static - - php: 7.0 - env: TEST_SUITE=js GRUNT_COMMAND=spec - - php: 7.0 - env: TEST_SUITE=js GRUNT_COMMAND=static - - php: 7.0 - env: TEST_SUITE=functional -cache: - apt: true - directories: - - $HOME/.composer/cache - - $HOME/.nvm - - $HOME/node_modules - - $HOME/yarn.lock -before_install: ./dev/travis/before_install.sh -install: composer install --no-interaction --prefer-dist -before_script: ./dev/travis/before_script.sh -script: - # Set arguments for variants of phpunit based tests; '|| true' prevents failing script when leading test fails - - test $TEST_SUITE = "functional" && TEST_FILTER='dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests.php' || true - - # The scripts for grunt/phpunit type tests - - if [ $TEST_SUITE == "functional" ]; then dev/tests/functional/vendor/phpunit/phpunit/phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi - - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi - - if [ $TEST_SUITE == "js" ]; then grunt $GRUNT_COMMAND; fi diff --git a/.travis.yml.sample b/.travis.yml.sample new file mode 100644 index 0000000000000..6e6f3359767b2 --- /dev/null +++ b/.travis.yml.sample @@ -0,0 +1,62 @@ +sudo: required +dist: trusty +group: edge +addons: + apt: + packages: + - mysql-server-5.6 + - mysql-client-core-5.6 + - mysql-client-5.6 + - postfix + firefox: "46.0" + hosts: + - magento2.travis +language: php +php: + - 7.0 + - 7.1 +git: + depth: 5 +env: + global: + - COMPOSER_BIN_DIR=~/bin + - INTEGRATION_SETS=3 + - NODE_JS_VERSION=8 + - MAGENTO_HOST_NAME="magento2.travis" + matrix: + - TEST_SUITE=unit + - TEST_SUITE=static + - TEST_SUITE=js GRUNT_COMMAND=spec + - TEST_SUITE=js GRUNT_COMMAND=static + - TEST_SUITE=integration INTEGRATION_INDEX=1 + - TEST_SUITE=integration INTEGRATION_INDEX=2 + - TEST_SUITE=integration INTEGRATION_INDEX=3 + - TEST_SUITE=functional +matrix: + exclude: + - php: 7.0 + env: TEST_SUITE=static + - php: 7.0 + env: TEST_SUITE=js GRUNT_COMMAND=spec + - php: 7.0 + env: TEST_SUITE=js GRUNT_COMMAND=static + - php: 7.0 + env: TEST_SUITE=functional +cache: + apt: true + directories: + - $HOME/.composer/cache + - $HOME/.nvm + - $HOME/node_modules + - $HOME/yarn.lock +before_install: ./dev/travis/before_install.sh +install: composer install --no-interaction --prefer-dist +before_script: ./dev/travis/before_script.sh +script: + # Set arguments for variants of phpunit based tests; '|| true' prevents failing script when leading test fails + - test $TEST_SUITE = "functional" && TEST_FILTER='dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests.php' || true + + # The scripts for grunt/phpunit type tests + - if [ $TEST_SUITE == "functional" ]; then dev/tests/functional/vendor/phpunit/phpunit/phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi + - if [ $TEST_SUITE != "functional" ] && [ $TEST_SUITE != "js" ]; then phpunit -c dev/tests/$TEST_SUITE $TEST_FILTER; fi + - if [ $TEST_SUITE == "js" ]; then grunt $GRUNT_COMMAND; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e94e46f89d1..474580c546921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,2233 @@ +2.2.10 +============= +* GitHub issues: + * [#12802](https://github.com/magento/magento2/issues/12802) -- QuoteRepository get methods won't return CartInterface but Quote model (fixed in [magento/magento2#22549](https://github.com/magento/magento2/pull/22549)) + * [#21473](https://github.com/magento/magento2/issues/21473) -- Form element validation is not triggered when validation rules change (fixed in [magento/magento2#22801](https://github.com/magento/magento2/pull/22801)) + * [#22395](https://github.com/magento/magento2/issues/22395) -- config:set -le and -lc short form options don't work (fixed in [magento/magento2#22836](https://github.com/magento/magento2/pull/22836)) + * [#21842](https://github.com/magento/magento2/issues/21842) -- Checkout error for registered customer with cache_id_prefix on multi server setup (fixed in [magento/magento2#22805](https://github.com/magento/magento2/pull/22805)) + * [#22788](https://github.com/magento/magento2/issues/22788) -- New Shipment emails do not generate (fixed in [magento/magento2#22906](https://github.com/magento/magento2/pull/22906)) + * [#22640](https://github.com/magento/magento2/issues/22640) -- Add tax rule form checkbox design is not as per the magento admin panel checkbox design, It is showing default design (fixed in [magento/magento2#22908](https://github.com/magento/magento2/pull/22908)) + * [#21672](https://github.com/magento/magento2/issues/21672) -- Database Media Storage - Design Config fails to save transactional email logo correctly (fixed in [magento/magento2#21676](https://github.com/magento/magento2/pull/21676) and [magento/magento2#21673](https://github.com/magento/magento2/pull/21673)) + * [#18651](https://github.com/magento/magento2/issues/18651) -- Tierprice can't save float percentage value (fixed in [magento/magento2#22936](https://github.com/magento/magento2/pull/22936)) + * [#19761](https://github.com/magento/magento2/issues/19761) -- Custom import adapter data validation issue (fixed in [magento/magento2#22180](https://github.com/magento/magento2/pull/22180)) + * [#22786](https://github.com/magento/magento2/issues/22786) -- The validation for UPS configurations triggers even if UPS is disabled for checkout (fixed in [magento/magento2#22873](https://github.com/magento/magento2/pull/22873)) + * [#21852](https://github.com/magento/magento2/issues/21852) -- Random Error while waiting for package deployed (fixed in [magento/magento2#22610](https://github.com/magento/magento2/pull/22610)) + * [#22563](https://github.com/magento/magento2/issues/22563) -- Parallelised execution of static content deploy is broken on 2.3-develop (fixed in [magento/magento2#22610](https://github.com/magento/magento2/pull/22610)) + * [#22639](https://github.com/magento/magento2/issues/22639) -- Without select attribute click on add attribute it display all selected when add attribute again. (fixed in [magento/magento2#22991](https://github.com/magento/magento2/pull/22991)) + * [#21214](https://github.com/magento/magento2/issues/21214) -- Luma theme Apply Discount Code section design improvement (fixed in [magento/magento2#23009](https://github.com/magento/magento2/pull/23009)) + * [#3795](https://github.com/magento/magento2/issues/3795) -- Validation messages missing from datepicker form elements (fixed in [magento/magento2#23002](https://github.com/magento/magento2/pull/23002)) + * [#22028](https://github.com/magento/magento2/issues/22028) -- Unable to update products via csv file, when products ids from file are from wide id range (fixed in [magento/magento2#22902](https://github.com/magento/magento2/pull/22902)) + * [#22822](https://github.com/magento/magento2/issues/22822) -- [Shipping] The contact us link isn't showing on order tracking page (fixed in [magento/magento2#23019](https://github.com/magento/magento2/pull/23019)) + * [#21747](https://github.com/magento/magento2/issues/21747) -- catalog_product_flat_data for store view populated with default view data when it should be store view data (fixed in [magento/magento2#22581](https://github.com/magento/magento2/pull/22581)) + * [#22899](https://github.com/magento/magento2/issues/22899) -- Incorrect return type at getListByCustomerId in PaymentTokenManagementInterface (fixed in [magento/magento2#22915](https://github.com/magento/magento2/pull/22915)) + * [#22869](https://github.com/magento/magento2/issues/22869) -- REST: Updating a customer without store_id sets the store_id to default (fixed in [magento/magento2#22895](https://github.com/magento/magento2/pull/22895)) + * [#22380](https://github.com/magento/magento2/issues/22380) -- Checkout totals order in specific store (fixed in [magento/magento2#23058](https://github.com/magento/magento2/pull/23058)) + * [#23034](https://github.com/magento/magento2/issues/23034) -- Wrong behaviour of validation scroll (fixed in [magento/magento2#23086](https://github.com/magento/magento2/pull/23086)) + * [#22771](https://github.com/magento/magento2/issues/22771) -- Magento 2.3.0 can't change text area field height admin form using Ui component (fixed in [magento/magento2#22783](https://github.com/magento/magento2/pull/22783)) + * [#22882](https://github.com/magento/magento2/issues/22882) -- Static content deploy - Don't shows error message, just stack trace (fixed in [magento/magento2#23114](https://github.com/magento/magento2/pull/23114)) + * [#22686](https://github.com/magento/magento2/issues/22686) -- Shipment Create via API salesShipmentRepositoryV1 throw Fatal error in Admin Order -> Shipment -> View (fixed in [magento/magento2#23119](https://github.com/magento/magento2/pull/23119)) + * [#22506](https://github.com/magento/magento2/issues/22506) -- Search suggestion panel overlapping on advance reporting button (fixed in [magento/magento2#23151](https://github.com/magento/magento2/pull/23151)) + * [#23080](https://github.com/magento/magento2/issues/23080) -- Missing whitespace in mobile navigation for non-English websites (fixed in [magento/magento2#23164](https://github.com/magento/magento2/pull/23164)) + * [#21604](https://github.com/magento/magento2/issues/21604) -- Database Media Storage - Admin Product Edit page does not handle product images correctly in database storage mode (fixed in [magento/magento2#21606](https://github.com/magento/magento2/pull/21606)) + * [#23053](https://github.com/magento/magento2/issues/23053) -- Sendfriend works for products with visibility not visible individually (fixed in [magento/magento2#23121](https://github.com/magento/magento2/pull/23121)) + * [#22087](https://github.com/magento/magento2/issues/22087) -- Products Ordered Report - Not grouped by product (fixed in [magento/magento2#23252](https://github.com/magento/magento2/pull/23252)) + * [#22396](https://github.com/magento/magento2/issues/22396) -- config:set fails with JSON values (fixed in [magento/magento2#23277](https://github.com/magento/magento2/pull/23277)) + * [#22767](https://github.com/magento/magento2/issues/22767) -- Not clear logic for loading CMS Pages with setStoreId function (fixed in [magento/magento2#23149](https://github.com/magento/magento2/pull/23149)) + * [#22636](https://github.com/magento/magento2/issues/22636) -- arrow toggle not changing only showing to down It should be toggle as every where is working (fixed in [magento/magento2#23150](https://github.com/magento/magento2/pull/23150)) + * [#18337](https://github.com/magento/magento2/issues/18337) -- #search input is missing required attribute aria-expanded. (fixed in [magento/magento2#23331](https://github.com/magento/magento2/pull/23331)) + * [#23238](https://github.com/magento/magento2/issues/23238) -- Apply coupon button act like remove coupon while create new order from admin (fixed in [magento/magento2#23332](https://github.com/magento/magento2/pull/23332)) + * [#22736](https://github.com/magento/magento2/issues/22736) -- Cursor position not in right side of search keyword in search box when click on search again (Mobile issue) (fixed in [magento/magento2#23352](https://github.com/magento/magento2/pull/23352)) + * [#21671](https://github.com/magento/magento2/issues/21671) -- Database Media Storage - Transaction emails logo not used when pub/media cleared (fixed in [magento/magento2#21673](https://github.com/magento/magento2/pull/21673)) + * [#21380](https://github.com/magento/magento2/issues/21380) -- Cron schedule is being duplicated (fixed in [magento/magento2#23439](https://github.com/magento/magento2/pull/23439)) + * [#23377](https://github.com/magento/magento2/issues/23377) -- Mini cart loader not working first time magento2 (fixed in [magento/magento2#23536](https://github.com/magento/magento2/pull/23536)) + * [#22103](https://github.com/magento/magento2/issues/22103) -- Character Encoding in Plain Text Emails Fails since 2.2.8/2.3.0 due to emails no longer being sent as MIME (fixed in [magento/magento2#23537](https://github.com/magento/magento2/pull/23537)) + * [#23199](https://github.com/magento/magento2/issues/23199) -- NO sender in email header for magento 2 sales order and password change emails to customer (fixed in [magento/magento2#23537](https://github.com/magento/magento2/pull/23537)) + * [#23285](https://github.com/magento/magento2/issues/23285) -- Credit memo submit button(refund) stays disable after validation fails & unable to enable button (fixed in [magento/magento2#23566](https://github.com/magento/magento2/pull/23566)) + * [#22676](https://github.com/magento/magento2/issues/22676) -- Compare Products counter, and My Wish List counter vertical not aligned (fixed in [magento/magento2#23573](https://github.com/magento/magento2/pull/23573)) + * [#23038](https://github.com/magento/magento2/issues/23038) -- Decimal qty with Increment is with specific values are not adding in cart (fixed in [magento/magento2#23574](https://github.com/magento/magento2/pull/23574)) + * [#8298](https://github.com/magento/magento2/issues/8298) -- Mobile Menu Behavior at Incorrect Breakpoint (fixed in [magento/magento2#23547](https://github.com/magento/magento2/pull/23547)) + * [#23233](https://github.com/magento/magento2/issues/23233) -- Alert widget doesn't trigger always method on showing the message (fixed in [magento/magento2#23579](https://github.com/magento/magento2/pull/23579)) + * [#16958](https://github.com/magento/magento2/issues/16958) -- Order View Issue - This tab contains invalid data (fixed in [magento/magento2#20849](https://github.com/magento/magento2/pull/20849)) + * [#23333](https://github.com/magento/magento2/issues/23333) -- Incorrect payment method translation in order emails (fixed in [magento/magento2#23438](https://github.com/magento/magento2/pull/23438)) + * [#23467](https://github.com/magento/magento2/issues/23467) -- Phone and Zip not update if customer have no saved address (fixed in [magento/magento2#23614](https://github.com/magento/magento2/pull/23614)) + * [#23522](https://github.com/magento/magento2/issues/23522) -- UPS shipping booking and label generation gives error when shipper's street given more than 35 chars (fixed in [magento/magento2#23603](https://github.com/magento/magento2/pull/23603)) + * [#23354](https://github.com/magento/magento2/issues/23354) -- Data saving problem error showing when leave blank qty and update it (fixed in [magento/magento2#23612](https://github.com/magento/magento2/pull/23612)) + * [#22950](https://github.com/magento/magento2/issues/22950) -- Spacing issue for Gift message section in my account (fixed in [magento/magento2#23657](https://github.com/magento/magento2/pull/23657)) + * [#22940](https://github.com/magento/magento2/issues/22940) -- Reset feature does not clear the date (fixed in [magento/magento2#23658](https://github.com/magento/magento2/pull/23658)) + * [#16446](https://github.com/magento/magento2/issues/16446) -- magento 2.2.2 text swatch switches product image even if attribute feature is disabled (fixed in [magento/magento2#22510](https://github.com/magento/magento2/pull/22510)) + * [#23643](https://github.com/magento/magento2/issues/23643) -- Mime parts of email are no more encoded with quoted printable (fixed in [magento/magento2#23650](https://github.com/magento/magento2/pull/23650)) + * [#11615](https://github.com/magento/magento2/issues/11615) -- URL Rewrites vs multiple storeviews - a never ending battle (fixed in [magento/magento2#14344](https://github.com/magento/magento2/pull/14344)) +* GitHub pull requests: + * [magento/magento2#21550](https://github.com/magento/magento2/pull/21550) -- [Backport] Fixed curl adapter to properly set http version based on $http_ver argument (by @davidalger) + * [magento/magento2#22549](https://github.com/magento/magento2/pull/22549) -- [Backport] Fix #12802 - allow to override preference over CartInterface and return correct object from QuoteRepository (by @Bartlomiejsz) + * [magento/magento2#22447](https://github.com/magento/magento2/pull/22447) -- [Backport] Corrected the translation for comment tag (by @yogeshsuhagiya) + * [magento/magento2#22559](https://github.com/magento/magento2/pull/22559) -- Fix MySQL syntax error on indexation with certain attribute codes (by @Beagon) + * [magento/magento2#21748](https://github.com/magento/magento2/pull/21748) -- [Backport] Custom option type select - Allow modify list of single selection option types (by @ihor-sviziev) + * [magento/magento2#22799](https://github.com/magento/magento2/pull/22799) -- Disable Travis builds - 2.2-develop (by @okorshenko) + * [magento/magento2#22801](https://github.com/magento/magento2/pull/22801) -- [Backport] #21473: Form element validation is not triggered when validation rules (by @amol2jcommerce) + * [magento/magento2#22836](https://github.com/magento/magento2/pull/22836) -- [Backport] Fixed:#22395 (by @shikhamis11) + * [magento/magento2#22557](https://github.com/magento/magento2/pull/22557) -- [Backport] Fix the invalid currency error in credit card payment of PayPal Payflow Pro or Payments Pro (by @niravkrish) + * [magento/magento2#22907](https://github.com/magento/magento2/pull/22907) -- [Backport] 404 not found form validation url when updating quantity in cart page (by @maheshWebkul721) + * [magento/magento2#22805](https://github.com/magento/magento2/pull/22805) -- [Backport] 21842: don't cache absolute file paths in validator factory (by @david-fuehr) + * [magento/magento2#22906](https://github.com/magento/magento2/pull/22906) -- [Backport] Fixed issue #22788 (by @maheshWebkul721) + * [magento/magento2#22908](https://github.com/magento/magento2/pull/22908) -- [Backport] Fixed Issue #22640 (by @maheshWebkul721) + * [magento/magento2#21676](https://github.com/magento/magento2/pull/21676) -- [2.2][Backport] Database Media Storage - Design Config Save functions to be Database Media Storage aware (by @gwharton) + * [magento/magento2#22936](https://github.com/magento/magento2/pull/22936) -- [Backport] Tierprice can t save float percentage value 18651 (by @novikor) + * [magento/magento2#22180](https://github.com/magento/magento2/pull/22180) -- [Backport] Resolved undefined index issue for import adapter (by @amol2jcommerce) + * [magento/magento2#22873](https://github.com/magento/magento2/pull/22873) -- [Backport] #22786 Add dependency for UPS required fields to avoid validation for these fields if UPS Shipping is not active (by @serhiyzhovnir) + * [magento/magento2#22610](https://github.com/magento/magento2/pull/22610) -- [Backport] Implement Better Error Handling and Fix Waits on Null PIDs in Parallel SCD Execution (by @davidalger) + * [magento/magento2#22991](https://github.com/magento/magento2/pull/22991) -- [Backport] Fixed issue #22639: Without select attribute click on add attribute it display all selected when add attribute again. (by @maheshWebkul721) + * [magento/magento2#23009](https://github.com/magento/magento2/pull/23009) -- [Backport] - fixed-Discount-Code-improvement-21214 (by @niravkrish) + * [magento/magento2#23002](https://github.com/magento/magento2/pull/23002) -- [Backport] Fixed Validation messages missing from datepicker form elements (by @ravi-chandra3197) + * [magento/magento2#22902](https://github.com/magento/magento2/pull/22902) -- Fix for update products via csv file (fix for 22028) (by @mtwegrzycki) + * [magento/magento2#23019](https://github.com/magento/magento2/pull/23019) -- [Backport] [Shipping] Adjusting the Contact Us Xpath (by @eduard13) + * [magento/magento2#22581](https://github.com/magento/magento2/pull/22581) -- [Backport] #21747 Fix catalog_product_flat_data attribute value for store during indexer (by @maheshWebkul721) + * [magento/magento2#22915](https://github.com/magento/magento2/pull/22915) -- [Backport] #22899 Fix the issue with Incorrect return type at getListByCustomerId in PaymentTokenManagementInterface (by @serhiyzhovnir) + * [magento/magento2#22895](https://github.com/magento/magento2/pull/22895) -- [Backport] #22869 - defaulting customer storeId fix (by @Wirson) + * [magento/magento2#23058](https://github.com/magento/magento2/pull/23058) -- [Backport] Checkout totals order in specific store (by @abrarpathan19) + * [magento/magento2#23086](https://github.com/magento/magento2/pull/23086) -- [Backport] Fix wrong behavior of validation scroll (by @Den4ik) + * [magento/magento2#22783](https://github.com/magento/magento2/pull/22783) -- [Backport] #22779 Remove hardcoded height for admin textarea field (by @serhiyzhovnir) + * [magento/magento2#23114](https://github.com/magento/magento2/pull/23114) -- [Backport] Show exception message during SCD failure (by @ihor-sviziev) + * [magento/magento2#23119](https://github.com/magento/magento2/pull/23119) -- [Backport] #22686 Shipment view fixed for Fatal error. (by @milindsingh) + * [magento/magento2#23151](https://github.com/magento/magento2/pull/23151) -- [Backport] Fixed #22506: Search suggestion panel overlapping on advance reporting button (by @krishprakash) + * [magento/magento2#23158](https://github.com/magento/magento2/pull/23158) -- [Backport] Don't create a new account-nav block - use existing instead. (by @atwixfirster) + * [magento/magento2#23164](https://github.com/magento/magento2/pull/23164) -- [Backport] Fix missing whitespace in mobile navigation for non-English websites (by @speedy008) + * [magento/magento2#23163](https://github.com/magento/magento2/pull/23163) -- [Backport] Replace hardcoded CarierCode from createShippingMethod() (by @speedy008) + * [magento/magento2#22430](https://github.com/magento/magento2/pull/22430) -- [Backport] Fixed wrong url redirect when edit product review from Customer view page and product view page (by @ravi-chandra3197) + * [magento/magento2#23148](https://github.com/magento/magento2/pull/23148) -- [Backport] Customer Account Forgot Password page title fix (by @krishprakash) + * [magento/magento2#23167](https://github.com/magento/magento2/pull/23167) -- [Backport] Don't throw shipping method exception when creating quote with only virtual products in API (by @speedy008) + * [magento/magento2#21606](https://github.com/magento/magento2/pull/21606) -- [2.2][Backport] Database Media Storage - Admin Product Edit Page handles recreates images correctly when pub/media/catalog is cleared. (by @gwharton) + * [magento/magento2#23121](https://github.com/magento/magento2/pull/23121) -- [Backport] #23053 : sendfriend verifies product visibility instead of status (by @Wirson) + * [magento/magento2#23195](https://github.com/magento/magento2/pull/23195) -- Backport apply coupoun and scroll top to check. applied successfully or not (by @krnshah) + * [magento/magento2#23252](https://github.com/magento/magento2/pull/23252) -- [Backport] Fixed Issue #22087 (by @krishprakash) + * [magento/magento2#23277](https://github.com/magento/magento2/pull/23277) -- [Backport] Fixed #22396 config:set fails with JSON values (by @shikhamis11) + * [magento/magento2#23328](https://github.com/magento/magento2/pull/23328) -- [Backport] Remove fotorama.min.js (by @ihor-sviziev) + * [magento/magento2#23149](https://github.com/magento/magento2/pull/23149) -- [Backport] Fixed issue #22767: Not clear logic for loading CMS Pages with setStoreId function (by @krishprakash) + * [magento/magento2#23150](https://github.com/magento/magento2/pull/23150) -- [Backport] Issue fixed #22636 arrow toggle not changing only showing to down It should be toggle as every where is working (by @krishprakash) + * [magento/magento2#23152](https://github.com/magento/magento2/pull/23152) -- [Backport] Remove timestap from current date when saving product special price from date (by @krishprakash) + * [magento/magento2#23331](https://github.com/magento/magento2/pull/23331) -- [Backport] Fixed issue #18337 (by @amol2jcommerce) + * [magento/magento2#23332](https://github.com/magento/magento2/pull/23332) -- [Backport] Fixed #23238 Apply button act like remove button while create new order from admin (by @krishprakash) + * [magento/magento2#23352](https://github.com/magento/magento2/pull/23352) -- [Backport] fixed issue #22736 - Cursor position not in right side of search keyword in mobile (by @krishprakash) + * [magento/magento2#23375](https://github.com/magento/magento2/pull/23375) -- [Backport for #23307] Allow to define listing configuration via ui component xml (by @Den4ik) + * [magento/magento2#21673](https://github.com/magento/magento2/pull/21673) -- [2.2][Backport] Database Media Storage - Transactional Emails will now extract image from database in Database Media Storage mode (by @gwharton) + * [magento/magento2#23439](https://github.com/magento/magento2/pull/23439) -- [Backport] Added function to check against running/pending/successful cron tasks (by @ihor-sviziev) + * [magento/magento2#23536](https://github.com/magento/magento2/pull/23536) -- [Backport] #23377 Fixed Mini cart loader not working first time issue (by @krishprakash) + * [magento/magento2#23537](https://github.com/magento/magento2/pull/23537) -- [Backport-2.2] Plain Text Emails are now sent with correct MIME Encoding (by @gwharton) + * [magento/magento2#23556](https://github.com/magento/magento2/pull/23556) -- [Backport] #20234 (Improvement) Review text Should be capitalized (by @krishprakash) + * [magento/magento2#23566](https://github.com/magento/magento2/pull/23566) -- [Backport] Fixed Credit memo submit button(refund) stays disable after validation fails & unable to enable button issue. (by @krishprakash) + * [magento/magento2#23573](https://github.com/magento/magento2/pull/23573) -- [Backport] issue #22676 fixed - Compare Products counter, and My Wish List count... (by @krishprakash) + * [magento/magento2#23574](https://github.com/magento/magento2/pull/23574) -- [Backport] #23038 Decimal qty with Increment is with specific values are not adding in cart (by @krishprakash) + * [magento/magento2#23547](https://github.com/magento/magento2/pull/23547) -- [Backport] move breakpoint by -1px to make nav work correctly at viewport 768 (by @bobemoe) + * [magento/magento2#23579](https://github.com/magento/magento2/pull/23579) -- [Backport] [Ui] Calling the always action on opening and closing the modal. (by @eduard13) + * [magento/magento2#20849](https://github.com/magento/magento2/pull/20849) -- Fix truncateString (by @emilie-blackbird) + * [magento/magento2#23438](https://github.com/magento/magento2/pull/23438) -- [Backport] Fix issue with incorrect payment translation in sales emails (by @ihor-sviziev) + * [magento/magento2#23614](https://github.com/magento/magento2/pull/23614) -- [Backport] Removed editor from phone and zipcode (by @krishprakash) + * [magento/magento2#23603](https://github.com/magento/magento2/pull/23603) -- [Backport] Issue #23522 UPS shipping booking and label generation gives error when shipper's street given more than 35 chars (by @ankurvr) + * [magento/magento2#23612](https://github.com/magento/magento2/pull/23612) -- [Backport] #23354 : Data saving problem error showing when leave blank qty and update it (by @krishprakash) + * [magento/magento2#23094](https://github.com/magento/magento2/pull/23094) -- [Backport] Don't load product collection in review observer (by @Den4ik) + * [magento/magento2#23657](https://github.com/magento/magento2/pull/23657) -- [Backport] Spacing issue for Gift message section in my account (by @krishprakash) + * [magento/magento2#23658](https://github.com/magento/magento2/pull/23658) -- [Backport] [Fixed] Reset feature does not clear the date (by @krishprakash) + * [magento/magento2#22510](https://github.com/magento/magento2/pull/22510) -- [Backport] Fixed magento text swatch switches product image even if attribute feature is disabled (by @ravi-chandra3197) + * [magento/magento2#23190](https://github.com/magento/magento2/pull/23190) -- [Backport] Re-enable XML as request and response types within the SwaggerUI (by @speedy008) + * [magento/magento2#23650](https://github.com/magento/magento2/pull/23650) -- [2.2][Backport] Transfer Encoding of emails changed to QUOTED-PRINTABLE (by @gwharton) + * [magento/magento2#14344](https://github.com/magento/magento2/pull/14344) -- Fix generating product URL rewrites for anchor categories (by @mszydlo) + + +2.2.9 +============= +* GitHub issues: + * [#7967](https://github.com/magento/magento2/issues/7967) -- Problems with breadcrumbs (fixed in [magento/magento2#19760](https://github.com/magento/magento2/pull/19760)) + * [#20427](https://github.com/magento/magento2/issues/20427) -- Shipping method title overlapping on edit icon in mobile view on Checkout page (fixed in [magento/magento2#20443](https://github.com/magento/magento2/pull/20443)) + * [#20282](https://github.com/magento/magento2/issues/20282) -- Module Catalog Url Rewrite: Permanent Redirect for old URL is missed when product was imported (fixed in [magento/magento2#20737](https://github.com/magento/magento2/pull/20737)) + * [#20611](https://github.com/magento/magento2/issues/20611) -- Minicart qty input box get distorted with 3-digit (fixed in [magento/magento2#20738](https://github.com/magento/magento2/pull/20738)) + * [#20631](https://github.com/magento/magento2/issues/20631) -- Console error on checkout after changing the allowed countries from admin. (fixed in [magento/magento2#20885](https://github.com/magento/magento2/pull/20885)) + * [#20487](https://github.com/magento/magento2/issues/20487) -- On checkout page tooltip dropdown pointer not proper on tablet (fixed in [magento/magento2#20490](https://github.com/magento/magento2/pull/20490)) + * [#17926](https://github.com/magento/magento2/issues/17926) -- The ui-component field validation error not opening accordion tab that owns the field (field does not get focused) (fixed in [magento/magento2#20510](https://github.com/magento/magento2/pull/20510)) + * [#20193](https://github.com/magento/magento2/issues/20193) -- Bundle Product add to cart button misaligned on tab portrait view. (fixed in [magento/magento2#20554](https://github.com/magento/magento2/pull/20554)) + * [#20278](https://github.com/magento/magento2/issues/20278) -- Apply discount code placeholder gets cut in Tab portriat view on cart page (fixed in [magento/magento2#20586](https://github.com/magento/magento2/pull/20586)) + * [#20580](https://github.com/magento/magento2/issues/20580) -- Time fields misaligned in iPad landscape view (1024 x 768) (fixed in [magento/magento2#20602](https://github.com/magento/magento2/pull/20602)) + * [#20396](https://github.com/magento/magento2/issues/20396) -- Changing attribute from swatch to dropdown deletes swatch options for all attributes (fixed in [magento/magento2#20745](https://github.com/magento/magento2/pull/20745)) + * [#19942](https://github.com/magento/magento2/issues/19942) -- Success message is not showing when creating invoice & shipment simultaniously (fixed in [magento/magento2#20776](https://github.com/magento/magento2/pull/20776)) + * [#20723](https://github.com/magento/magento2/issues/20723) -- My account page title extra space on mobile (when not display error or success messages) (fixed in [magento/magento2#20782](https://github.com/magento/magento2/pull/20782)) + * [#20221](https://github.com/magento/magento2/issues/20221) -- add your review text is not show uniformly in Mobile view (fixed in [magento/magento2#20257](https://github.com/magento/magento2/pull/20257)) + * [#19482](https://github.com/magento/magento2/issues/19482) -- Increase product quantity with disabled Manage Stock when place order is failed (fixed in [magento/magento2#20644](https://github.com/magento/magento2/pull/20644)) + * [#9988](https://github.com/magento/magento2/issues/9988) -- Quick search by SKU not working properly (fixed in [magento/magento2#20876](https://github.com/magento/magento2/pull/20876)) + * [#20716](https://github.com/magento/magento2/issues/20716) -- Exceptions when search product with sku like "42-" (fixed in [magento/magento2#20876](https://github.com/magento/magento2/pull/20876)) + * [#13309](https://github.com/magento/magento2/issues/13309) -- Lifetime update syntax error (fixed in [magento/magento2#21078](https://github.com/magento/magento2/pull/21078)) + * [#20786](https://github.com/magento/magento2/issues/20786) -- [CMS] File upload preview style issue (fixed in [magento/magento2#21110](https://github.com/magento/magento2/pull/21110)) + * [#19328](https://github.com/magento/magento2/issues/19328) -- Success Message Icon vertically misaligned in admin panel (fixed in [magento/magento2#19333](https://github.com/magento/magento2/pull/19333)) + * [#13675](https://github.com/magento/magento2/issues/13675) -- Magento 2 :- Number of Lines in a Street Address not setting to default when you checked Use system value in Magento 2.1.7 (fixed in [magento/magento2#20566](https://github.com/magento/magento2/pull/20566)) + * [#19139](https://github.com/magento/magento2/issues/19139) -- Empty block rendering in My Account page sidebar (fixed in [magento/magento2#20845](https://github.com/magento/magento2/pull/20845)) + * [#20382](https://github.com/magento/magento2/issues/20382) -- View and Edit Cart link not aligned in middle because bellow the link a blank div (class="minicart-widgets") existing has 15px margin top, this div (class="minicart-widgets" ) should be display none or should not come if has no content, should only display if has content (fixed in [magento/magento2#21124](https://github.com/magento/magento2/pull/21124)) + * [#20497](https://github.com/magento/magento2/issues/20497) -- Product customizable options issue (fixed in [magento/magento2#20821](https://github.com/magento/magento2/pull/20821)) + * [#20402](https://github.com/magento/magento2/issues/20402) -- Schedule update from filed misalignment in 768 x 1147 resolution (fixed in [magento/magento2#20404](https://github.com/magento/magento2/pull/20404)) + * [#20240](https://github.com/magento/magento2/issues/20240) -- Admin - dropdown toggle arrow not working on closing (fixed in [magento/magento2#20616](https://github.com/magento/magento2/pull/20616)) + * [#20157](https://github.com/magento/magento2/issues/20157) -- On advanced search page Price field misaligned on mobile view (fixed in [magento/magento2#21114](https://github.com/magento/magento2/pull/21114)) + * [#19714](https://github.com/magento/magento2/issues/19714) -- Store switcher doesn't work multistore setup with different product urls (fixed in [magento/magento2#21140](https://github.com/magento/magento2/pull/21140)) + * [#20816](https://github.com/magento/magento2/issues/20816) -- Orders and Returns layout not proper (fixed in [magento/magento2#21163](https://github.com/magento/magento2/pull/21163)) + * [#6960](https://github.com/magento/magento2/issues/6960) -- Greek vat numbers cannot be validated (fixed in [magento/magento2#21169](https://github.com/magento/magento2/pull/21169)) + * [#18357](https://github.com/magento/magento2/issues/18357) -- checkout_agreement_store doesn't exist (fixed in [magento/magento2#18866](https://github.com/magento/magento2/pull/18866)) + * [#18954](https://github.com/magento/magento2/issues/18954) -- Magento 2.2.6 Terms and Conditions are Not visible in Admin (fixed in [magento/magento2#18866](https://github.com/magento/magento2/pull/18866)) + * [#20468](https://github.com/magento/magento2/issues/20468) -- On Product Page Tabings Content Misaligned on Mobile View (fixed in [magento/magento2#20476](https://github.com/magento/magento2/pull/20476)) + * [#20906](https://github.com/magento/magento2/issues/20906) -- Magento backend catalog "Cost" without currency symbol (fixed in [magento/magento2#21157](https://github.com/magento/magento2/pull/21157)) + * [#20911](https://github.com/magento/magento2/issues/20911) -- In admin login password forgot password page wrong css used to make it vertially aling middle (fixed in [magento/magento2#21162](https://github.com/magento/magento2/pull/21162)) + * [#20989](https://github.com/magento/magento2/issues/20989) -- Admin Customizable Options Dropdown sort_order issue (fixed in [magento/magento2#21159](https://github.com/magento/magento2/pull/21159)) + * [#20800](https://github.com/magento/magento2/issues/20800) -- On account my recent reviews alignment issue (fixed in [magento/magento2#21172](https://github.com/magento/magento2/pull/21172)) + * [#20555](https://github.com/magento/magento2/issues/20555) -- Meta Keywords/Meta Description are input field in product form while they are defined as textarea (fixed in [magento/magento2#21199](https://github.com/magento/magento2/pull/21199)) + * [#20492](https://github.com/magento/magento2/issues/20492) -- In Admin configuration Widget left navigation block not proper in tablet landscape view (fixed in [magento/magento2#20529](https://github.com/magento/magento2/pull/20529)) + * [#6162](https://github.com/magento/magento2/issues/6162) -- Can't set customer group when creating a new order in the admin. (fixed in [magento/magento2#21239](https://github.com/magento/magento2/pull/21239)) + * [#7974](https://github.com/magento/magento2/issues/7974) -- Can't change customer group when placing an admin order, even after MAGETWO-57077 applied (fixed in [magento/magento2#21239](https://github.com/magento/magento2/pull/21239)) + * [#21101](https://github.com/magento/magento2/issues/21101) -- Unable to open the product from sidebar's Compare Products block (fixed in [magento/magento2#21238](https://github.com/magento/magento2/pull/21238)) + * [#21144](https://github.com/magento/magento2/issues/21144) -- Can't change customer group when placing an admin order (fixed in [magento/magento2#21239](https://github.com/magento/magento2/pull/21239)) + * [#17861](https://github.com/magento/magento2/issues/17861) -- Customer Name Prefix shows white space when extra separator is addes. (fixed in [magento/magento2#21245](https://github.com/magento/magento2/pull/21245)) + * [#21070](https://github.com/magento/magento2/issues/21070) -- Luma theme my account Order Information status tabs break in tablet view (fixed in [magento/magento2#21250](https://github.com/magento/magento2/pull/21250)) + * [#20299](https://github.com/magento/magento2/issues/20299) -- Order item details label not aligned in mobile view (fixed in [magento/magento2#20539](https://github.com/magento/magento2/pull/20539) and [magento/magento2#21243](https://github.com/magento/magento2/pull/21243)) + * [#18347](https://github.com/magento/magento2/issues/18347) -- Element 'css', attribute 'as': The attribute 'as' is not allowed. (CSS preloading) (fixed in [magento/magento2#21261](https://github.com/magento/magento2/pull/21261)) + * [#18944](https://github.com/magento/magento2/issues/18944) -- Unable to open URL for downloadable product in 2.2.6 (fixed in [magento/magento2#21262](https://github.com/magento/magento2/pull/21262)) + * [#19561](https://github.com/magento/magento2/issues/19561) -- Custom option price calculation is wrong with multi currency when option price type is percentage. (fixed in [magento/magento2#21263](https://github.com/magento/magento2/pull/21263)) + * [#18158](https://github.com/magento/magento2/issues/18158) -- 2.2.6 "Special price date from" Failed to parse time string (fixed in [magento/magento2#21273](https://github.com/magento/magento2/pull/21273)) + * [#20755](https://github.com/magento/magento2/issues/20755) -- cms page top spacing issue at mobile (fixed in [magento/magento2#20781](https://github.com/magento/magento2/pull/20781)) + * [#18698](https://github.com/magento/magento2/issues/18698) -- Magento triggers and sends some of order emails exactly one month later,while the order email was not enabled then (fixed in [magento/magento2#20954](https://github.com/magento/magento2/pull/20954)) + * [#18525](https://github.com/magento/magento2/issues/18525) -- Incorrect Swager Definition for eav-data-attribute-option-interface (fixed in [magento/magento2#21164](https://github.com/magento/magento2/pull/21164)) + * [#20163](https://github.com/magento/magento2/issues/20163) -- On iPhone5 device newsletter subscription input box not contain complete text (placeholder) (fixed in [magento/magento2#20370](https://github.com/magento/magento2/pull/20370)) + * [#20760](https://github.com/magento/magento2/issues/20760) -- Admin Customer configuraion in whishlist associated product for configurable product misalign (fixed in [magento/magento2#21173](https://github.com/magento/magento2/pull/21173)) + * [#18775](https://github.com/magento/magento2/issues/18775) -- Product Advanced Pricing design issue (fixed in [magento/magento2#21229](https://github.com/magento/magento2/pull/21229)) + * [#21196](https://github.com/magento/magento2/issues/21196) -- [UI] The dropdown state doesn't change if the dropdown is expanded or not (fixed in [magento/magento2#21320](https://github.com/magento/magento2/pull/21320)) + * [#21089](https://github.com/magento/magento2/issues/21089) -- No accessible label for vault-saved credit card type (fixed in [magento/magento2#21206](https://github.com/magento/magento2/pull/21206)) + * [#19891](https://github.com/magento/magento2/issues/19891) -- product_type attribute contains incorrect value in mass import export csv after creating custom type_id attribute. actual type_id value in database gets change with newly created attribute type_id. (fixed in [magento/magento2#21208](https://github.com/magento/magento2/pull/21208)) + * [#20919](https://github.com/magento/magento2/issues/20919) -- Email label and email field not aligned from left for reorder of guest user (fixed in [magento/magento2#21241](https://github.com/magento/magento2/pull/21241)) + * [#20518](https://github.com/magento/magento2/issues/20518) -- On Bundle product radio button misalign (fixed in [magento/magento2#20743](https://github.com/magento/magento2/pull/20743)) + * [#20010](https://github.com/magento/magento2/issues/20010) -- Wrong price amount in opengraph (fixed in [magento/magento2#21202](https://github.com/magento/magento2/pull/21202)) + * [#19274](https://github.com/magento/magento2/issues/19274) -- Why is SessionManager used instead of its Interface? (fixed in [magento/magento2#21357](https://github.com/magento/magento2/pull/21357)) + * [#20380](https://github.com/magento/magento2/issues/20380) -- Get Shipping Method as object from order instance gives undefined index. (fixed in [magento/magento2#20866](https://github.com/magento/magento2/pull/20866)) + * [#8479](https://github.com/magento/magento2/issues/8479) -- Sequence of module load order should be deterministic (fixed in [magento/magento2#21423](https://github.com/magento/magento2/pull/21423)) + * [#16116](https://github.com/magento/magento2/issues/16116) -- Modules sort order in config.php is being inconsistent when no changes being made (fixed in [magento/magento2#21423](https://github.com/magento/magento2/pull/21423)) + * [#19983](https://github.com/magento/magento2/issues/19983) -- Can't work customer Image attribute programmatically (fixed in [magento/magento2#21437](https://github.com/magento/magento2/pull/21437)) + * [#11740](https://github.com/magento/magento2/issues/11740) -- Sending emails from Admin in Multi-Store Environment defaults to Primary Store (fixed in [magento/magento2#18472](https://github.com/magento/magento2/pull/18472)) + * [#14945](https://github.com/magento/magento2/issues/14945) -- Store Email Addresses are not used anymore (fixed in [magento/magento2#18472](https://github.com/magento/magento2/pull/18472)) + * [#14952](https://github.com/magento/magento2/issues/14952) -- Confirmation emails have no FROM or FROM email address 2.2.4 (fixed in [magento/magento2#18472](https://github.com/magento/magento2/pull/18472)) + * [#16355](https://github.com/magento/magento2/issues/16355) -- Magento 2.2.4 not sending from sales sender (fixed in [magento/magento2#18472](https://github.com/magento/magento2/pull/18472)) + * [#19166](https://github.com/magento/magento2/issues/19166) -- Customer related values are NULL for guests converted to customers after checkout. (fixed in [magento/magento2#21325](https://github.com/magento/magento2/pull/21325)) + * [#15059](https://github.com/magento/magento2/issues/15059) -- Cannot reorder from the first try (fixed in [magento/magento2#21513](https://github.com/magento/magento2/pull/21513)) + * [#21398](https://github.com/magento/magento2/issues/21398) -- Doesn't show any error message when customer click on Add to cart button without selecting atleast one product from recently orderred list (fixed in [magento/magento2#21538](https://github.com/magento/magento2/pull/21538)) + * [#13982](https://github.com/magento/magento2/issues/13982) -- Customer Login Block sets the title for the page when rendered (fixed in [magento/magento2#21434](https://github.com/magento/magento2/pull/21434)) + * [#21383](https://github.com/magento/magento2/issues/21383) -- As low as displays incorrect pricing on category page, tax appears to be added twice (fixed in [magento/magento2#21527](https://github.com/magento/magento2/pull/21527)) + * [#18134](https://github.com/magento/magento2/issues/18134) -- Manufacturer error when upgrading to 2.2.6 (fixed in [magento/magento2#19551](https://github.com/magento/magento2/pull/19551)) + * [#20310](https://github.com/magento/magento2/issues/20310) -- Cart section data has wrong product_price_value (fixed in [magento/magento2#21570](https://github.com/magento/magento2/pull/21570)) + * [#21077](https://github.com/magento/magento2/issues/21077) -- Tabbing issue on product detail page (fixed in [magento/magento2#21588](https://github.com/magento/magento2/pull/21588)) + * [#21425](https://github.com/magento/magento2/issues/21425) -- Date design change show not correctly value in backend (fixed in [magento/magento2#21568](https://github.com/magento/magento2/pull/21568)) + * [#21327](https://github.com/magento/magento2/issues/21327) -- Checkout Page Cancel button is not working (fixed in [magento/magento2#21569](https://github.com/magento/magento2/pull/21569)) + * [#14882](https://github.com/magento/magento2/issues/14882) -- product_types.xml doesn't allow numbers in modelInstance (fixed in [magento/magento2#21598](https://github.com/magento/magento2/pull/21598)) + * [#21294](https://github.com/magento/magento2/issues/21294) -- Cart can't be emptied if any product is out of stock (fixed in [magento/magento2#21528](https://github.com/magento/magento2/pull/21528)) + * [#20527](https://github.com/magento/magento2/issues/20527) -- [Admin] Configurable product variations table cell labels wrong position (fixed in [magento/magento2#21691](https://github.com/magento/magento2/pull/21691)) + * [#19276](https://github.com/magento/magento2/issues/19276) -- Change different product price on selecting different product swatches on category pages (fixed in [magento/magento2#21695](https://github.com/magento/magento2/pull/21695)) + * [#21493](https://github.com/magento/magento2/issues/21493) -- Setting default sorting (fixed in [magento/magento2#21694](https://github.com/magento/magento2/pull/21694)) + * [#19117](https://github.com/magento/magento2/issues/19117) -- High Database Load for Sales Rule Validation (fixed in [magento/magento2#21699](https://github.com/magento/magento2/pull/21699)) + * [#21073](https://github.com/magento/magento2/issues/21073) -- A non-numeric value encountered on mass product update when Minimum Qty Allowed in Shopping Cart is used (fixed in [magento/magento2#21080](https://github.com/magento/magento2/pull/21080)) + * [#20128](https://github.com/magento/magento2/issues/20128) -- Magento\Reports\Model\ResourceModel\Order\Collection->getDateRange() - Error in code? (fixed in [magento/magento2#21589](https://github.com/magento/magento2/pull/21589)) + * [#21278](https://github.com/magento/magento2/issues/21278) -- Sort order missing on Downloadable Product Links and Sample Columns (fixed in [magento/magento2#21662](https://github.com/magento/magento2/pull/21662)) + * [#167](https://github.com/magento/magento2/issues/167) -- Fatal error: Class 'Mage' not found (fixed in [magento/magento2#21793](https://github.com/magento/magento2/pull/21793)) + * [#21419](https://github.com/magento/magento2/issues/21419) -- Wishlist is missing review summary (fixed in [magento/magento2#21759](https://github.com/magento/magento2/pull/21759)) + * [#13612](https://github.com/magento/magento2/issues/13612) -- 1 exception(s): Exception #0 (Exception): Warning: Illegal offset type in isset or empty in /home/jewelrynest2/public_html/magento/vendor/magento/module-eav/Model/Entity/Attribute/Source/AbstractSource.php on line 76 (fixed in [magento/magento2#21802](https://github.com/magento/magento2/pull/21802)) + * [#21384](https://github.com/magento/magento2/issues/21384) -- JS minify field is not disabled in developer configuration (fixed in [magento/magento2#21800](https://github.com/magento/magento2/pull/21800)) + * [#10645](https://github.com/magento/magento2/issues/10645) -- Adding BEM class in XML via attribute tag causes class to be rewritten (fixed in [magento/magento2#21804](https://github.com/magento/magento2/pull/21804)) + * [#21648](https://github.com/magento/magento2/issues/21648) -- Checkout Agreements checkbox missing asterisk (fixed in [magento/magento2#21838](https://github.com/magento/magento2/pull/21838)) + * [#20773](https://github.com/magento/magento2/issues/20773) -- The autoloader throws an exception on class_exists (fixed in [magento/magento2#21435](https://github.com/magento/magento2/pull/21435)) + * [#20924](https://github.com/magento/magento2/issues/20924) -- Reviews ACL issue - showing Reviews menu two times under System > User Roles > Add New Role > Role Resources (fixed in [magento/magento2#21849](https://github.com/magento/magento2/pull/21849)) + * [#21062](https://github.com/magento/magento2/issues/21062) -- Static tests: forbid 'or' instead of '||' (fixed in [magento/magento2#21543](https://github.com/magento/magento2/pull/21543)) + * [#21510](https://github.com/magento/magento2/issues/21510) -- Can't access backend indexers page after creating a custom index (fixed in [magento/magento2#21576](https://github.com/magento/magento2/pull/21576)) + * [#12396](https://github.com/magento/magento2/issues/12396) -- "Total Amount" cart rule without tax (fixed in [magento/magento2#21845](https://github.com/magento/magento2/pull/21845)) + * [#21467](https://github.com/magento/magento2/issues/21467) -- Tier price of simple item not working in Bundle product (fixed in [magento/magento2#21844](https://github.com/magento/magento2/pull/21844)) + * [#21750](https://github.com/magento/magento2/issues/21750) -- Product attribute labels are translated (fixed in [magento/magento2#21864](https://github.com/magento/magento2/pull/21864)) + * [#14412](https://github.com/magento/magento2/issues/14412) -- Magento 2.2.3 TypeErrors Cannot read property 'quoteData' / 'storecode' / 'sectionLoadUrl' of undefined (fixed in [magento/magento2#21432](https://github.com/magento/magento2/pull/21432)) + * [#20825](https://github.com/magento/magento2/issues/20825) -- Missing required argument $productAvailabilityChecks of Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker. (fixed in [magento/magento2#21920](https://github.com/magento/magento2/pull/21920)) + * [#21692](https://github.com/magento/magento2/issues/21692) -- Incorrect constructor of Magento\Sales\Model\Order\Address\Validator (fixed in [magento/magento2#21719](https://github.com/magento/magento2/pull/21719)) + * [#7283](https://github.com/magento/magento2/issues/7283) -- Insignificant stores sorting issue (fixed in [magento/magento2#17371](https://github.com/magento/magento2/pull/17371)) + * [#8086](https://github.com/magento/magento2/issues/8086) -- Multiline admin field is broken (fixed in [magento/magento2#21561](https://github.com/magento/magento2/pull/21561)) + * [#18115](https://github.com/magento/magento2/issues/18115) -- Multiline field is broken (fixed in [magento/magento2#21561](https://github.com/magento/magento2/pull/21561)) + * [#21374](https://github.com/magento/magento2/issues/21374) -- Dot is not allowed when editing CMS block in-line (fixed in [magento/magento2#21939](https://github.com/magento/magento2/pull/21939)) + * [#21521](https://github.com/magento/magento2/issues/21521) -- Broken Tax Rate Search Filter - SQLSTATE[23000] (fixed in [magento/magento2#21535](https://github.com/magento/magento2/pull/21535)) + * [#18754](https://github.com/magento/magento2/issues/18754) -- Negative order amount in dashboard latest order when order is cancelled where coupon has been used (fixed in [magento/magento2#21944](https://github.com/magento/magento2/pull/21944)) + * [#21281](https://github.com/magento/magento2/issues/21281) -- Wrong order amount on dashboard on Last orders listing when order has discount and it is partially refunded (fixed in [magento/magento2#21944](https://github.com/magento/magento2/pull/21944)) + * [#19485](https://github.com/magento/magento2/issues/19485) -- DHL Shipping Quotes fail for Domestic Shipments when Content Mode is "Non Documents" (fixed in [magento/magento2#19488](https://github.com/magento/magento2/pull/19488)) + * [#20790](https://github.com/magento/magento2/issues/20790) -- Luma theme My Wish List page Edit and Remove icons consistency design improvement (fixed in [magento/magento2#21118](https://github.com/magento/magento2/pull/21118)) + * [#21734](https://github.com/magento/magento2/issues/21734) -- Error in JS validation rule (fixed in [magento/magento2#21813](https://github.com/magento/magento2/pull/21813)) + * [#20809](https://github.com/magento/magento2/issues/20809) -- Advanced Search layout not proper (fixed in [magento/magento2#21892](https://github.com/magento/magento2/pull/21892)) + * [#21805](https://github.com/magento/magento2/issues/21805) -- Filter in url rewrites table in backend isn't being remembered (fixed in [magento/magento2#22069](https://github.com/magento/magento2/pull/22069)) + * [#21499](https://github.com/magento/magento2/issues/21499) -- Cart is emptied when enter is pressed after changing product quantity (fixed in [magento/magento2#21512](https://github.com/magento/magento2/pull/21512)) + * [#14926](https://github.com/magento/magento2/issues/14926) -- "Rolled back transaction has not been completed correctly" on Magento 2.2.3 (fixed in [magento/magento2#22037](https://github.com/magento/magento2/pull/22037)) + * [#18752](https://github.com/magento/magento2/issues/18752) -- Rolled back transaction has not been completed correctly" on Magento 2.1.15 (fixed in [magento/magento2#22037](https://github.com/magento/magento2/pull/22037)) + * [#21134](https://github.com/magento/magento2/issues/21134) -- Invalid argument supplied for foreach thrown in EAV code (fixed in [magento/magento2#22086](https://github.com/magento/magento2/pull/22086)) + * [#10893](https://github.com/magento/magento2/issues/10893) -- Street fields in checkout don't have a label that's readable by a screenreader (fixed in [magento/magento2#22070](https://github.com/magento/magento2/pull/22070)) + * [#15972](https://github.com/magento/magento2/issues/15972) -- Since Magento 2.2.1, certain variables in the configuration get resolved to their actual value (fixed in [magento/magento2#22140](https://github.com/magento/magento2/pull/22140)) + * [#18580](https://github.com/magento/magento2/issues/18580) -- Currency rates not updated by crontab (fixed in [magento/magento2#18980](https://github.com/magento/magento2/pull/18980)) + * [#20614](https://github.com/magento/magento2/issues/20614) -- Minicart close button overlapping in shipping address section whenever any user open minicart in mobile view on the checkout page (fixed in [magento/magento2#20844](https://github.com/magento/magento2/pull/20844)) + * [#10790](https://github.com/magento/magento2/issues/10790) -- Tax rate + 100% discount results in negative grand total (fixed in [magento/magento2#22227](https://github.com/magento/magento2/pull/22227)) + * [#21755](https://github.com/magento/magento2/issues/21755) -- Magento should create a log entry if an observer does not implement ObserverInterface (fixed in [magento/magento2#22232](https://github.com/magento/magento2/pull/22232)) + * [#20078](https://github.com/magento/magento2/issues/20078) -- Magento Ui form validator message callback not supported (fixed in [magento/magento2#20107](https://github.com/magento/magento2/pull/20107)) + * [#20209](https://github.com/magento/magento2/issues/20209) -- errors/local.xml and error page templates are publicly accessible (fixed in [magento/magento2#21946](https://github.com/magento/magento2/pull/21946)) + * [#7906](https://github.com/magento/magento2/issues/7906) -- Fotorama Gallery too sensitive on Android Devices. (chrome) (fixed in [magento/magento2#22078](https://github.com/magento/magento2/pull/22078)) + * [#18548](https://github.com/magento/magento2/issues/18548) -- WYSIWYG-Editor Insert Image-Popup is not working correctly with multiple WYSIWYG-Editors on page (fixed in [magento/magento2#22174](https://github.com/magento/magento2/pull/22174)) + * [#21789](https://github.com/magento/magento2/issues/21789) -- [BUG] Product gallery opening by mistake (fixed in [magento/magento2#22250](https://github.com/magento/magento2/pull/22250)) + * [#22238](https://github.com/magento/magento2/issues/22238) -- Backward-incompatible change between 2.2.7 and 2.2.8 (fixed in [magento/magento2#22267](https://github.com/magento/magento2/pull/22267)) + * [#5021](https://github.com/magento/magento2/issues/5021) -- "Please specify a shipping method" Exception (fixed in [magento/magento2#21340](https://github.com/magento/magento2/pull/21340)) + * [#13902](https://github.com/magento/magento2/issues/13902) -- Carrier codes with '_' (underscores) break several payment API's (fixed in [magento/magento2#21340](https://github.com/magento/magento2/pull/21340)) + * [#11358](https://github.com/magento/magento2/issues/11358) -- Full Tax Summary display wrong numbers. (fixed in [magento/magento2#21961](https://github.com/magento/magento2/pull/21961)) + * [#19701](https://github.com/magento/magento2/issues/19701) -- Magento 2.3 Shopping Cart Taxes Missing Calc Line (fixed in [magento/magento2#21961](https://github.com/magento/magento2/pull/21961)) + * [#20366](https://github.com/magento/magento2/issues/20366) -- The parent product doesn't have configurable product options. (fixed in [magento/magento2#22295](https://github.com/magento/magento2/pull/22295)) + * [#21753](https://github.com/magento/magento2/issues/21753) -- Order Item Status to Enable Downloads is set to "Pending," but no download links are presented in "My Downloads" when logged in (fix provided) (fixed in [magento/magento2#22072](https://github.com/magento/magento2/pull/22072)) + * [#7623](https://github.com/magento/magento2/issues/7623) -- Web Setup Wizard not visible in backend (V.2.1.2) ONGOING (fixed in [magento/magento2#22369](https://github.com/magento/magento2/pull/22369)) + * [#11892](https://github.com/magento/magento2/issues/11892) -- Web Setup Wizard not visible in backend magento 2.1.9 (fixed in [magento/magento2#22369](https://github.com/magento/magento2/pull/22369)) + * [#15090](https://github.com/magento/magento2/issues/15090) -- app:config:import fails with "Please specify the admin custom URL." (fixed in [magento/magento2#22282](https://github.com/magento/magento2/pull/22282)) + * [#21868](https://github.com/magento/magento2/issues/21868) -- Method importFromArray from \Magento\Eav\Model\Entity\Collection\AbstractCollection doesn't return a working collection (fixed in [magento/magento2#22422](https://github.com/magento/magento2/pull/22422)) + * [#19909](https://github.com/magento/magento2/issues/19909) -- Not possible to use multidimensional arrays in widget parameters (fixed in [magento/magento2#22214](https://github.com/magento/magento2/pull/22214)) + * [#22152](https://github.com/magento/magento2/issues/22152) -- Click on search icon it does not working (fixed in [magento/magento2#22441](https://github.com/magento/magento2/pull/22441)) + * [#22309](https://github.com/magento/magento2/issues/22309) -- Category Update without "name" cannot be saved in scope "all" with REST API (fixed in [magento/magento2#22440](https://github.com/magento/magento2/pull/22440)) + * [#15828](https://github.com/magento/magento2/issues/15828) -- Multisite installation, default website slow (X-Magento-Vary) (fixed in [magento/magento2#22439](https://github.com/magento/magento2/pull/22439)) + * [#6715](https://github.com/magento/magento2/issues/6715) -- Few weaknesses in the code (fixed in [magento/magento2#22453](https://github.com/magento/magento2/pull/22453)) + * [#21960](https://github.com/magento/magento2/issues/21960) -- Layered Navigation: "Equalize product count" not working as expected (fixed in [magento/magento2#22453](https://github.com/magento/magento2/pull/22453)) + * [#21786](https://github.com/magento/magento2/issues/21786) -- Asynchronous email sending for the sales entities which were created with disabled email sending (fixed in [magento/magento2#22108](https://github.com/magento/magento2/pull/22108)) + * [#6272](https://github.com/magento/magento2/issues/6272) -- Changing sample for downloadable product failure (fixed in [magento/magento2#22471](https://github.com/magento/magento2/pull/22471)) + * [#21375](https://github.com/magento/magento2/issues/21375) -- Same product quantity not increment when added with guest user. (fixed in [magento/magento2#22378](https://github.com/magento/magento2/pull/22378)) + * [#22370](https://github.com/magento/magento2/issues/22370) -- Filtering ignored config values in test framework causes error (fixed in [magento/magento2#22415](https://github.com/magento/magento2/pull/22415)) + * [#21715](https://github.com/magento/magento2/issues/21715) -- Previous scrolling to invalid form element is not being canceled on hitting submit multiple times (fixed in [magento/magento2#22358](https://github.com/magento/magento2/pull/22358)) + * [#22474](https://github.com/magento/magento2/issues/22474) -- Incomplete Dependency on Backup Settings Configuration (fixed in [magento/magento2#22499](https://github.com/magento/magento2/pull/22499)) + * [#22223](https://github.com/magento/magento2/issues/22223) -- Missing/Wrong data display on downloadable report table reports>downloads in BO (fixed in [magento/magento2#22523](https://github.com/magento/magento2/pull/22523)) + * [#22299](https://github.com/magento/magento2/issues/22299) -- Cms block cache key does not contain the store id (fixed in [magento/magento2#22534](https://github.com/magento/magento2/pull/22534)) + * [#22402](https://github.com/magento/magento2/issues/22402) -- PUT /V1/products/:sku/media/:entryId does not change the file (fixed in [magento/magento2#22533](https://github.com/magento/magento2/pull/22533)) + * [#22249](https://github.com/magento/magento2/issues/22249) -- Configurable Product Gallery Images Out of Order when More than 10 images (fixed in [magento/magento2#22288](https://github.com/magento/magento2/pull/22288)) + * [#9155](https://github.com/magento/magento2/issues/9155) -- Adding product from wishlist not adding to cart showing warning message. (fixed in [magento/magento2#22536](https://github.com/magento/magento2/pull/22536)) + * [#21596](https://github.com/magento/magento2/issues/21596) -- Checkout: it is possible to leave blank Shipping Details section and get to Payment Details section by URL (fixed in [magento/magento2#22543](https://github.com/magento/magento2/pull/22543)) + * [#20917](https://github.com/magento/magento2/issues/20917) -- Alignment Issue While Editing Order Data containing Downlodable Products in Admin Section (fixed in [magento/magento2#22582](https://github.com/magento/magento2/pull/22582)) + * [#21978](https://github.com/magento/magento2/issues/21978) -- Adding product image: File doesn't exist (fixed in [magento/magento2#22579](https://github.com/magento/magento2/pull/22579)) + * [#22270](https://github.com/magento/magento2/issues/22270) -- 2.2.8 Configurable product option dropdown - price difference incorrect when catalog prices are entered excluding tax (fixed in [magento/magento2#22535](https://github.com/magento/magento2/pull/22535)) +* GitHub pull requests: + * [magento/magento2#19760](https://github.com/magento/magento2/pull/19760) -- Fix for making subcategories appear in breadcrumbs (Backport) (by @Yamaha32088) + * [magento/magento2#20443](https://github.com/magento/magento2/pull/20443) -- [Backport] Fixed-Shipping-method-title-overlapping-on-edit-icon-in-mobile (by @amol2jcommerce) + * [magento/magento2#20737](https://github.com/magento/magento2/pull/20737) -- [Backport] Fixed #20282 Module Catalog Url Rewrite: Permanent Redirect for old URL is missed when product was imported (by @shikhamis11) + * [magento/magento2#20738](https://github.com/magento/magento2/pull/20738) -- [Backport] minicart-three-digit-quantity-cutoff (by @amol2jcommerce) + * [magento/magento2#20885](https://github.com/magento/magento2/pull/20885) -- [Backport] Fixed Issue #20631 Console error on checkout after changing the allowed countries from admin (by @amol2jcommerce) + * [magento/magento2#19943](https://github.com/magento/magento2/pull/19943) -- [Backport] added config for catalog review in frontend (by @torhoehn) + * [magento/magento2#20490](https://github.com/magento/magento2/pull/20490) -- [Backport] Fixed-checkout-tooltip-dropdown (by @amol2jcommerce) + * [magento/magento2#20510](https://github.com/magento/magento2/pull/20510) -- [Backport] Fixed The ui-component field validation error not opening accordion tab that owns the field (field does not get focused) (by @mageprince) + * [magento/magento2#20554](https://github.com/magento/magento2/pull/20554) -- [Backport] Fixed-Bundle-Product-add-to-cart-button-misaligned-2.2 (by @amol2jcommerce) + * [magento/magento2#20586](https://github.com/magento/magento2/pull/20586) -- [Backport] Fixed-Apply-discount-code-placeholder-2.2 (by @amol2jcommerce) + * [magento/magento2#20640](https://github.com/magento/magento2/pull/20640) -- [Backport] issue #18349 Fixed for 2.3: Incorrect quote_item_id saved on order items during multiple address checkout (by @amol2jcommerce) + * [magento/magento2#20602](https://github.com/magento/magento2/pull/20602) -- [Backport] Time-fields-misaligned-in-iPad-landscape-view ::Time fields misaligne-2.2 (by @amol2jcommerce) + * [magento/magento2#20646](https://github.com/magento/magento2/pull/20646) -- [Backport] Avoid duplicate loading of configuration files (by @amol2jcommerce) + * [magento/magento2#20745](https://github.com/magento/magento2/pull/20745) -- [Backport] Fixes incorrect where condition when deleting swatch option, it delet... (by @amol2jcommerce) + * [magento/magento2#20776](https://github.com/magento/magento2/pull/20776) -- Fixed issue #19942 in 2.2 (by @GovindaSharma) + * [magento/magento2#20782](https://github.com/magento/magento2/pull/20782) -- [Backport] My account page title extra space on mobile 2.2 (by @amol2jcommerce) + * [magento/magento2#20257](https://github.com/magento/magento2/pull/20257) -- [Backport]changes-add-your-review-text-is-not-show-uniformly-in-Mobile-view (by @amol2jcommerce) + * [magento/magento2#20589](https://github.com/magento/magento2/pull/20589) -- [Backport] Fixed-Wishlist-alignment-issue-at-mobile-2.2 (by @amol2jcommerce) + * [magento/magento2#20644](https://github.com/magento/magento2/pull/20644) -- [Backport] 19482 increase product quantity with disabled manage stock when place order is failed (by @amol2jcommerce) + * [magento/magento2#18524](https://github.com/magento/magento2/pull/18524) -- [Backport] Added option to exclude discount amount from minimum order amount calculation (by @ccasciotti) + * [magento/magento2#20876](https://github.com/magento/magento2/pull/20876) -- [backport] Exceptions when search product with sku like "42-" (by @Nazar65) + * [magento/magento2#21078](https://github.com/magento/magento2/pull/21078) -- [Backport] Fixed - Lifetime update syntax error #13309 (by @ssp58bleuciel) + * [magento/magento2#21110](https://github.com/magento/magento2/pull/21110) -- [Backport][CMS] Improving the uploaded images styling view (by @eduard13) + * [magento/magento2#19333](https://github.com/magento/magento2/pull/19333) -- Fix issue 19328 - Success Message Icon vertically misaligned in admin panel (by @speedy008) + * [magento/magento2#21081](https://github.com/magento/magento2/pull/21081) -- [Backport] MAGETWO-95819: Customer registration fields not translated (by @tdgroot) + * [magento/magento2#20566](https://github.com/magento/magento2/pull/20566) -- [Backport] Number of Lines in a Street Address not setting to default when you checked Use system value (by @XxXgeoXxX) + * [magento/magento2#20845](https://github.com/magento/magento2/pull/20845) -- [Backport] Empty block rendering in My Account page sidebar fixed using designing changes. (by @mage2pratik) + * [magento/magento2#21123](https://github.com/magento/magento2/pull/21123) -- [Backport] Add filter for `NOT FIND_IN_SET` sql conditions (by @mageprince) + * [magento/magento2#21124](https://github.com/magento/magento2/pull/21124) -- [Backport] issue fixed #20382 (by @irajneeshgupta) + * [magento/magento2#20821](https://github.com/magento/magento2/pull/20821) -- [Backport] Fixing the styling issue on customizable options (by @eduard13) + * [magento/magento2#21155](https://github.com/magento/magento2/pull/21155) -- [Backport] [Checkout] Covering the successfully adding a valid coupon to cart by an integra... (by @eduard13) + * [magento/magento2#21156](https://github.com/magento/magento2/pull/21156) -- [Backport] [SendFriend] Covering the Send to friend by integration tests (by @eduard13) + * [magento/magento2#20404](https://github.com/magento/magento2/pull/20404) -- [Backport] Changes-Schedule-Update-Form-filed-misalign-2.2 (by @amol2jcommerce) + * [magento/magento2#20616](https://github.com/magento/magento2/pull/20616) -- [Backport] Fixed admin multiselect and select ui arrow toggle issue (by @niravkrish) + * [magento/magento2#21114](https://github.com/magento/magento2/pull/21114) -- [Backport] Fixed issue #20157 On advanced search page Price field misaligned on mobile view (by @amol2jcommerce) + * [magento/magento2#21140](https://github.com/magento/magento2/pull/21140) -- [Backport] Fixed store switcher doesn't work multistore setup with different product urls issue (by @janakbhimani) + * [magento/magento2#21160](https://github.com/magento/magento2/pull/21160) -- [Backport] Fixed redirection issue in Admin-> Content -> Schedule (by @mage2pratik) + * [magento/magento2#21161](https://github.com/magento/magento2/pull/21161) -- [Backport] Extra space from left in top message section (Notification section) (by @mage2pratik) + * [magento/magento2#21163](https://github.com/magento/magento2/pull/21163) -- [Backport] Orders-and-Returns-layout-not-proper (by @amol2jcommerce) + * [magento/magento2#21169](https://github.com/magento/magento2/pull/21169) -- [Backport] Fixes incorrect country code being used for Greek VAT numbers, should... (by @amol2jcommerce) + * [magento/magento2#18866](https://github.com/magento/magento2/pull/18866) -- [Backport] Fixes #18357 - SQL error when table prefix used. (by @gelanivishal) + * [magento/magento2#20476](https://github.com/magento/magento2/pull/20476) -- [Backport]Fixed-Product-page-tabbing-content-misalignment-in-mobile-view-2-2 ::... (by @parag2jcommerce) + * [magento/magento2#21157](https://github.com/magento/magento2/pull/21157) -- [Backport] Magento backend catalog cost without currency symbol (by @mage2pratik) + * [magento/magento2#21162](https://github.com/magento/magento2/pull/21162) -- [Backport] Fixed issue #20911 In admin login password forgot password page wrong css used to make it vertially aling middle (by @mage2pratik) + * [magento/magento2#21159](https://github.com/magento/magento2/pull/21159) -- [Backport] Solve custom option dropdown issue (by @mage2pratik) + * [magento/magento2#21168](https://github.com/magento/magento2/pull/21168) -- [Backport] Adjust table for grouped products (by @mage2pratik) + * [magento/magento2#21172](https://github.com/magento/magento2/pull/21172) -- [Backport] Fixes-for-account-my-recent-reviews-alignment-2.2 (by @amol2jcommerce) + * [magento/magento2#21198](https://github.com/magento/magento2/pull/21198) -- [Backport] added min=0 to qty field product detail page (by @amol2jcommerce) + * [magento/magento2#21199](https://github.com/magento/magento2/pull/21199) -- [Backport] Fixed issue #20555 Meta Keywords/Meta Description are input field in product form while they are defined as textarea (by @amol2jcommerce) + * [magento/magento2#20529](https://github.com/magento/magento2/pull/20529) -- [Backport] Fixed-Widget-left-navigation-block-2.2 (by @amol2jcommerce) + * [magento/magento2#21166](https://github.com/magento/magento2/pull/21166) -- [Backport] Email to a Friend form not full responsive and remove link not positi... (by @amol2jcommerce) + * [magento/magento2#21224](https://github.com/magento/magento2/pull/21224) -- [Backport] [Sales] Improves the UX by scrolling down the customer to the Recent Orders (by @eduard13) + * [magento/magento2#21238](https://github.com/magento/magento2/pull/21238) -- [Backport] [Catalog] Fixing compare block product removing action from sidebar (by @eduard13) + * [magento/magento2#21239](https://github.com/magento/magento2/pull/21239) -- [Backport] Fixed #21144 Can't change customer group when placing an admin order (by @amol2jcommerce) + * [magento/magento2#21240](https://github.com/magento/magento2/pull/21240) -- [Backport] quantity-not-center-align-on-review-order (by @amol2jcommerce) + * [magento/magento2#21245](https://github.com/magento/magento2/pull/21245) -- [Backport] Fixed #17861 Customer Name Prefix shows white space when extra separator is addes (by @mage2pratik) + * [magento/magento2#21250](https://github.com/magento/magento2/pull/21250) -- [Backport] Fixed Luma theme my account Order status tabs 21070 (by @suryakant-krish) + * [magento/magento2#20539](https://github.com/magento/magento2/pull/20539) -- [Backport] issue fixed #20299 Order item details label not aligned in mobile view (by @irajneeshgupta) + * [magento/magento2#21261](https://github.com/magento/magento2/pull/21261) -- [Backport] #18347 - Element 'css', attribute 'as': The attribute 'as' is not allowed. (CSS preloading) (by @amol2jcommerce) + * [magento/magento2#21262](https://github.com/magento/magento2/pull/21262) -- [Backport] Fixed issue Unable to open URL for downloadable product (by @amol2jcommerce) + * [magento/magento2#21263](https://github.com/magento/magento2/pull/21263) -- [Backport] Fixed Custom option price calculation is wrong with multi currency when option price type is percentage (by @amol2jcommerce) + * [magento/magento2#21273](https://github.com/magento/magento2/pull/21273) -- [Backport] Special price date from issue resolve (by @tufahu) + * [magento/magento2#21269](https://github.com/magento/magento2/pull/21269) -- [Backport] Fixed issue if there are multiple skus in catalog rule condition combination (by @mage2pratik) + * [magento/magento2#21282](https://github.com/magento/magento2/pull/21282) -- [Backport] hardcoded table name (by @keyuremipro) + * [magento/magento2#21287](https://github.com/magento/magento2/pull/21287) -- [Backport] Small refactor of getFrontName (by @mage2pratik) + * [magento/magento2#20781](https://github.com/magento/magento2/pull/20781) -- [Backport] cms-page-top-spacing-issue-2.2 (by @amol2jcommerce) + * [magento/magento2#20954](https://github.com/magento/magento2/pull/20954) -- [Backport] #18698 Fixed order email sending via order async email sending when order was created with disabled email sending (by @serhiyzhovnir) + * [magento/magento2#21164](https://github.com/magento/magento2/pull/21164) -- [Backport] Solved swagger response of product attribute option is_default (by @mage2pratik) + * [magento/magento2#21242](https://github.com/magento/magento2/pull/21242) -- [Backport] bundle-product-table-data-grouped-alignment :: Bundle product table d... (by @amol2jcommerce) + * [magento/magento2#21247](https://github.com/magento/magento2/pull/21247) -- [Backport] Assign with and, or, replaced by &&, || (by @Dharmeshvaja91) + * [magento/magento2#20370](https://github.com/magento/magento2/pull/20370) -- iPhone5-device-newsletter-subscription-#20167 (by @dipti2jcommerce) + * [magento/magento2#21173](https://github.com/magento/magento2/pull/21173) -- [Backport] fixes-customer-information-wishlist-configurable-product-alignment-2.2 (by @amol2jcommerce) + * [magento/magento2#21229](https://github.com/magento/magento2/pull/21229) -- [Backport] Fixed product advanced pricing design issue (by @mage2pratik) + * [magento/magento2#21320](https://github.com/magento/magento2/pull/21320) -- [Backport][Ui] Fixing the changing state of dropdown's icon (by @eduard13) + * [magento/magento2#21243](https://github.com/magento/magento2/pull/21243) -- [Backport] view-order-price-subtotal-alignment-not-proper-mobile (by @amol2jcommerce) + * [magento/magento2#21206](https://github.com/magento/magento2/pull/21206) -- [Backport] Add alt text to saved payment method for accessibility (by @amol2jcommerce) + * [magento/magento2#21208](https://github.com/magento/magento2/pull/21208) -- [Backport] Fixed Issue #19891 ,Added checks of type_id (by @amol2jcommerce) + * [magento/magento2#21241](https://github.com/magento/magento2/pull/21241) -- [Backport] issue fixed #20919 Email label and email field not aligned from left ... (by @amol2jcommerce) + * [magento/magento2#20743](https://github.com/magento/magento2/pull/20743) -- [Backport] bundle-product-radio-button-misalign (by @amol2jcommerce) + * [magento/magento2#20970](https://github.com/magento/magento2/pull/20970) -- [backport] admin-store-view-label-not-alignment-2 (by @amol2jcommerce) + * [magento/magento2#21207](https://github.com/magento/magento2/pull/21207) -- [Backport] disable add to cart until page load (by @amol2jcommerce) + * [magento/magento2#21202](https://github.com/magento/magento2/pull/21202) -- [Backport] Issue fix #20010 Wrong price amount in opengraph (by @mage2pratik) + * [magento/magento2#21213](https://github.com/magento/magento2/pull/21213) -- [backport] Focus not proper on configurable product swatches 2.2 (by @amol2jcommerce) + * [magento/magento2#21317](https://github.com/magento/magento2/pull/21317) -- [Backport] Minimum Qty Allowed in Shopping Cart not working on related product (by @mageprince) + * [magento/magento2#21344](https://github.com/magento/magento2/pull/21344) -- [Backport] Added translation for comment tag (by @yogeshsuhagiya) + * [magento/magento2#21357](https://github.com/magento/magento2/pull/21357) -- [Backport] Removed direct use of SessionManager class, used SessionManagerInterface instead (by @mage2pratik) + * [magento/magento2#20866](https://github.com/magento/magento2/pull/20866) -- [Backport] Issue #20380 fixed for 2.2 (by @maheshWebkul721) + * [magento/magento2#21423](https://github.com/magento/magento2/pull/21423) -- [Backport] Make the module list more deterministic (by @eduard13) + * [magento/magento2#21437](https://github.com/magento/magento2/pull/21437) -- [Backport]-issue-195196 Can't upload customer Image attribute programmatically (by @Nazar65) + * [magento/magento2#18472](https://github.com/magento/magento2/pull/18472) -- [Backport][2.2] Alternative fix for Multi Store Emails issue, Fix Async Emails issues, Fix Multiple Email issues (by @gwharton) + * [magento/magento2#21325](https://github.com/magento/magento2/pull/21325) -- [Backport]-issue-195196 Customer related values are NULL for guests converted to customers after checkout. (by @Nazar65) + * [magento/magento2#21513](https://github.com/magento/magento2/pull/21513) -- [Backport] Fixed #15059 Cannot reorder from the first try (by @shikhamis11) + * [magento/magento2#21537](https://github.com/magento/magento2/pull/21537) -- [Backport] Fixed pagination issue in admin review grid (by @dominicfernando) + * [magento/magento2#21538](https://github.com/magento/magento2/pull/21538) -- [Backport]Show error message when customer click on Add to cart button without selecting atleast one product from recently orderred list (by @mageprince) + * [magento/magento2#21434](https://github.com/magento/magento2/pull/21434) -- [Backport] 13982 customer login block sets the title for the page when rendered (by @amol2jcommerce) + * [magento/magento2#19552](https://github.com/magento/magento2/pull/19552) -- [Backport] Fix typo in SQL join when joining custom option prices for price indexer [2.2] (by @udovicic) + * [magento/magento2#21527](https://github.com/magento/magento2/pull/21527) -- [Backport] As low as displays incorrect pricing on category page, tax appears to be added twice #21383 (by @eduard13) + * [magento/magento2#19551](https://github.com/magento/magento2/pull/19551) -- [Backport] Resolved upgrade issue if manufacturer attribute missing [Magento 2.2] (by @suneet64) + * [magento/magento2#21531](https://github.com/magento/magento2/pull/21531) -- [Backport] Update details.phtml (by @amol2jcommerce) + * [magento/magento2#21532](https://github.com/magento/magento2/pull/21532) -- [Backport] Misconfigured aria-labelledby for product tabs (by @amol2jcommerce) + * [magento/magento2#21570](https://github.com/magento/magento2/pull/21570) -- [Backport] Change product_price_value in cart data section based on tax settings (by @mage2pratik) + * [magento/magento2#21588](https://github.com/magento/magento2/pull/21588) -- [Backport] Fixes for product tabbing issue (by @amol2jcommerce) + * [magento/magento2#21568](https://github.com/magento/magento2/pull/21568) -- [Backport] Fixed #21425 Date design change show not correctly value in backend (by @mage2pratik) + * [magento/magento2#21569](https://github.com/magento/magento2/pull/21569) -- [Backport] Checkout Page Cancel button is not working #21327 (by @mage2pratik) + * [magento/magento2#21598](https://github.com/magento/magento2/pull/21598) -- Back port pull 20617 (by @lisovyievhenii) + * [magento/magento2#21652](https://github.com/magento/magento2/pull/21652) -- [Backport] Fix issue with custom option file uploading (by @amol2jcommerce) + * [magento/magento2#19098](https://github.com/magento/magento2/pull/19098) -- 2.2.6 Use batches and direct queries to fix sales address upgrade (by @rikwillems) + * [magento/magento2#21528](https://github.com/magento/magento2/pull/21528) -- [Backport] Fix empty cart validation (by @wojtekn) + * [magento/magento2#21691](https://github.com/magento/magento2/pull/21691) -- [Backport][Admin] Made configurable product variations table cell label hidden (by @eduard13) + * [magento/magento2#21694](https://github.com/magento/magento2/pull/21694) -- [Backport] Setting default sorting #21493 (by @mage2pratik) + * [magento/magento2#21695](https://github.com/magento/magento2/pull/21695) -- [Backport] 19276 - Fixed price renderer issue (by @mage2pratik) + * [magento/magento2#21699](https://github.com/magento/magento2/pull/21699) -- [Backport] 20484 - Fix performance leak in salesrule collection (by @david-fuehr) + * [magento/magento2#21080](https://github.com/magento/magento2/pull/21080) -- [Backport] A non-numeric value encountered on mass product update when.. (by @Nazar65) + * [magento/magento2#21589](https://github.com/magento/magento2/pull/21589) -- [Backport] Issue fixed #20128 : Date range returns the same start and end date (by @mage2pratik) + * [magento/magento2#20412](https://github.com/magento/magento2/pull/20412) -- Remove sku from operators to validate condition (by @igor-imaginemage) + * [magento/magento2#21051](https://github.com/magento/magento2/pull/21051) -- Fix broken admin order after emptying order and readding items (by @driskell) + * [magento/magento2#21433](https://github.com/magento/magento2/pull/21433) -- [Backport] Make it possible to generate sales PDF's using the API (by @amol2jcommerce) + * [magento/magento2#21662](https://github.com/magento/magento2/pull/21662) -- [Backport] Fixed: #21278, Add sort order on downloadable links (by @mage2pratik) + * [magento/magento2#21698](https://github.com/magento/magento2/pull/21698) -- [Backport] 20818 - prevent cache drop for frontend caches on sitemap generation (by @david-fuehr) + * [magento/magento2#21759](https://github.com/magento/magento2/pull/21759) -- [Backport] Wishlist review summary (by @amol2jcommerce) + * [magento/magento2#21793](https://github.com/magento/magento2/pull/21793) -- [Backport] Fixed wrong proxing in the inventory observer (by @VitaliyBoyko) + * [magento/magento2#21800](https://github.com/magento/magento2/pull/21800) -- [Backport] Disable dropdown in JavaScript and CSS Settings in developer configur... (by @ananth-iyer) + * [magento/magento2#21802](https://github.com/magento/magento2/pull/21802) -- [Backport] #13612 Fixed-Quantity_and_stock_status when visibility set to storefront throwing exception (by @amol2jcommerce) + * [magento/magento2#21804](https://github.com/magento/magento2/pull/21804) -- [Backport] Issue #10645 - Allow BEM class via attribute tag. Public (by @amol2jcommerce) + * [magento/magento2#21838](https://github.com/magento/magento2/pull/21838) -- [Backport] Fix #21648 Checkout Agreements checkbox missing asterisk (by @amol2jcommerce) + * [magento/magento2#21037](https://github.com/magento/magento2/pull/21037) -- [Backport] Added RewriteBase directive template in .htaccess file into pub/media folder (by @ccasciotti) + * [magento/magento2#21435](https://github.com/magento/magento2/pull/21435) -- [Backport] magento/magento2#20773: Do not throw exception during autoload (by @amol2jcommerce) + * [magento/magento2#21849](https://github.com/magento/magento2/pull/21849) -- [Backport][Review] Fix Pending Reviews label, add menu for pending review (by @eduard13) + * [magento/magento2#21859](https://github.com/magento/magento2/pull/21859) -- [Backport] [Wishlist] Covering the Wishlist classes by integration and unit tests (by @eduard13) + * [magento/magento2#21872](https://github.com/magento/magento2/pull/21872) -- [Backport] [TASK] Remove translation of attribute store label in getA... (by @mage2pratik) + * [magento/magento2#21543](https://github.com/magento/magento2/pull/21543) -- [BP] static tests forbid or instead of 21062 (by @novikor) + * [magento/magento2#21576](https://github.com/magento/magento2/pull/21576) -- [Backport] Fix for issue #21510: Can't access backend indexers page after creating a custom index (by @ccasciotti) + * [magento/magento2#21661](https://github.com/magento/magento2/pull/21661) -- [backport] Improve swatch table overflow handling (by @Cyanoxide) + * [magento/magento2#21845](https://github.com/magento/magento2/pull/21845) -- [Backport] magento/magento2#12396: Total Amount cart rule without tax (by @eduard13) + * [magento/magento2#21844](https://github.com/magento/magento2/pull/21844) -- [Backport] Update price-bundle.js so that correct tier price is calculated while displaying in bundle product (by @amol2jcommerce) + * [magento/magento2#21864](https://github.com/magento/magento2/pull/21864) -- [Backport] fix #21750 remove translation of product attribute label (by @mage2pratik) + * [magento/magento2#21432](https://github.com/magento/magento2/pull/21432) -- [Backport] Checkout - Fix JS error Cannot read property 'quoteData' of undefined (by @amol2jcommerce) + * [magento/magento2#21918](https://github.com/magento/magento2/pull/21918) -- [Backport] Fix gallery full-screen triggers (by @iGerchak) + * [magento/magento2#21920](https://github.com/magento/magento2/pull/21920) -- [Backport] #20825 Missing required argument $productAvailabilityChecks of Magent... (by @amol2jcommerce) + * [magento/magento2#21719](https://github.com/magento/magento2/pull/21719) -- [Backport] Fix #21692 #21752 - logic in constructor of address validator and Locale Resolver check (by @Bartlomiejsz) + * [magento/magento2#17371](https://github.com/magento/magento2/pull/17371) -- Migrating Store Grid to UI Components (by @afirlejczyk) + * [magento/magento2#21561](https://github.com/magento/magento2/pull/21561) -- [Backport] Issue Fixed: #8086: Multiline admin field is broken (by @vivekkumarcedcoss) + * [magento/magento2#21939](https://github.com/magento/magento2/pull/21939) -- [Backport] Fixed Inline block edit identifier validation (by @hiren0241) + * [magento/magento2#21943](https://github.com/magento/magento2/pull/21943) -- [Backport] Multishipping checkout agreements now are the same as default checkout agreements (by @eduard13) + * [magento/magento2#21535](https://github.com/magento/magento2/pull/21535) -- [Backport] Fix Broken Tax Rate Search Filter Admin grid #21521 (by @tuyennn) + * [magento/magento2#21944](https://github.com/magento/magento2/pull/21944) -- [Backport] Fixed calculation of 'Total' column under "Last Orders" listing on the admin dashboard (by @eduard13) + * [magento/magento2#19488](https://github.com/magento/magento2/pull/19488) -- [Backport] Fix DHL Quotes for Domestic Shipments when Content Type is set to Non-Document (by @gwharton) + * [magento/magento2#21118](https://github.com/magento/magento2/pull/21118) -- [Backport] Fixed issue 20790 wishlist icons (by @amol2jcommerce) + * [magento/magento2#21813](https://github.com/magento/magento2/pull/21813) -- [Backport] #21734 Error in JS validation rule (by @kisroman) + * [magento/magento2#21892](https://github.com/magento/magento2/pull/21892) -- [Backport] Advanced Search layout not proper (by @amol2jcommerce) + * [magento/magento2#22039](https://github.com/magento/magento2/pull/22039) -- [Backport] Added custom_options file upload directory to .gitignore. (by @amol2jcommerce) + * [magento/magento2#22069](https://github.com/magento/magento2/pull/22069) -- [Backport] Add argument to show filter text in URL rewrite grid after click on back button (by @amol2jcommerce) + * [magento/magento2#21512](https://github.com/magento/magento2/pull/21512) -- [Backport] Fix: Cart is emptied when enter is pressed after changing product quantity (by @lfluvisotto) + * [magento/magento2#22037](https://github.com/magento/magento2/pull/22037) -- [Backport] Root exception not logged on QuoteManagement::submitQuote (by @larsroettig) + * [magento/magento2#22086](https://github.com/magento/magento2/pull/22086) -- [Backport] Fix eav form foreach error #21134 (by @wojtekn) + * [magento/magento2#21819](https://github.com/magento/magento2/pull/21819) -- [Backport] Remove all marketing get params on Varnish to minimize the cache objects (by @ihor-sviziev) + * [magento/magento2#22070](https://github.com/magento/magento2/pull/22070) -- [Backport] Populate label elements for street address fields in checkout (by @amol2jcommerce) + * [magento/magento2#20392](https://github.com/magento/magento2/pull/20392) -- Backport. Success message is not showing when creating invoice & shipment simultaniously # (by @XxXgeoXxX) + * [magento/magento2#21945](https://github.com/magento/magento2/pull/21945) -- [Backport] Trigger contentUpdate on reviews load (by @eduard13) + * [magento/magento2#22140](https://github.com/magento/magento2/pull/22140) -- Backport for Magento 2.2 - Fixes variables in configuration not being... (by @hostep) + * [magento/magento2#18980](https://github.com/magento/magento2/pull/18980) -- [Backport] Fix for currency update in crontab area (by @denispapec) + * [magento/magento2#20844](https://github.com/magento/magento2/pull/20844) -- [Backport] Fixed Minicart close button overlapping (by @mage2pratik) + * [magento/magento2#22181](https://github.com/magento/magento2/pull/22181) -- [Backport] Contact us layout in I-pad not proper (by @amol2jcommerce) + * [magento/magento2#22227](https://github.com/magento/magento2/pull/22227) -- [Backport] Fix negative subtotal when full discount applied with tax calculation #10790 (by @ilnytskyi) + * [magento/magento2#22232](https://github.com/magento/magento2/pull/22232) -- [Backport]Magento should create log if an observer not implement ObserverInterface (by @Nazar65) + * [magento/magento2#20107](https://github.com/magento/magento2/pull/20107) -- [2.2] Add support for validation message callback (by @floorz) + * [magento/magento2#21203](https://github.com/magento/magento2/pull/21203) -- [Backport] icon text showing feature (by @mage2pratik) + * [magento/magento2#21946](https://github.com/magento/magento2/pull/21946) -- [Backport] Secure errors directory (by @amol2jcommerce) + * [magento/magento2#22078](https://github.com/magento/magento2/pull/22078) -- [backport] issue - 21507 Change photo only if user swipe horizontally (by @Nazar65) + * [magento/magento2#22174](https://github.com/magento/magento2/pull/22174) -- WYSIWYG Image-Popup is not working correctly with multipleEditors (by @Nazar65) + * [magento/magento2#22250](https://github.com/magento/magento2/pull/22250) -- [Backport] Fix gallery event observer (by @Den4ik) + * [magento/magento2#22267](https://github.com/magento/magento2/pull/22267) -- magento2-22238: removed backward incompatible change from the options... (by @VitaliyBoyko) + * [magento/magento2#18443](https://github.com/magento/magento2/pull/18443) -- [Backport][2.2] Reworked gallery.phtml to move generation of gallery json strings to own block functions (by @gwharton) + * [magento/magento2#21340](https://github.com/magento/magento2/pull/21340) -- [Backport] ISSUE-5021 fixed guest checkout with custom shipping carrier with underscores (by @hostep) + * [magento/magento2#21961](https://github.com/magento/magento2/pull/21961) -- Full Tax Summary display wrong numbers -Backport (by @hiren0241) + * [magento/magento2#22295](https://github.com/magento/magento2/pull/22295) -- [Backport] Turn on edit mode for product repository when adding children (by @amol2jcommerce) + * [magento/magento2#22072](https://github.com/magento/magento2/pull/22072) -- [Backport] Bug fix for #21753 (2.2-develop) (by @crankycyclops) + * [magento/magento2#22369](https://github.com/magento/magento2/pull/22369) -- [Backport 2.2] Use correct base path to check if setup folder exists (by @JeroenVanLeusden) + * [magento/magento2#22282](https://github.com/magento/magento2/pull/22282) -- [Backport] Fixed "Please specify the admin custom URL" error on app:config:import CLI command (by @davidalger) + * [magento/magento2#22422](https://github.com/magento/magento2/pull/22422) -- Backport - Fix importFromArray by setting _isCollectionLoaded to true after import #21869 (by @slackerzz) + * [magento/magento2#22214](https://github.com/magento/magento2/pull/22214) -- [Backport] [Widget] Fixing the multidimensional array as value for the widget's parameter (by @ilnytskyi) + * [magento/magento2#22440](https://github.com/magento/magento2/pull/22440) -- [Backport] [Fixed] Category Update without "name" cannot be saved in scope "all" with REST API (by @saphaljha) + * [magento/magento2#22441](https://github.com/magento/magento2/pull/22441) -- [Backport] Fiexed 22152 - Click on search icon it does not working on admin grid sticky header (by @saphaljha) + * [magento/magento2#22451](https://github.com/magento/magento2/pull/22451) -- [Backport] Translate comment tag in DHL config settings (by @yogeshsuhagiya) + * [magento/magento2#22448](https://github.com/magento/magento2/pull/22448) -- [Backport] Translated exception message (by @yogeshsuhagiya) + * [magento/magento2#22439](https://github.com/magento/magento2/pull/22439) -- Backport Magento 2.2 Set cache id prefix on installation (by @Ctucker9233) + * [magento/magento2#22453](https://github.com/magento/magento2/pull/22453) -- [Backport]Layered Navigation: "Equalize product count" not working as expected (by @Nazar65) + * [magento/magento2#22108](https://github.com/magento/magento2/pull/22108) -- [Backport] #21786 Fix the issue with asynchronous email sending for the sales entities (by @serhiyzhovnir) + * [magento/magento2#22465](https://github.com/magento/magento2/pull/22465) -- [Backport] [Fixed] Full Tax Summary missing calculation Admin create order (by @saphaljha) + * [magento/magento2#22471](https://github.com/magento/magento2/pull/22471) -- [Backport] Fixed Changing sample for downloadable product failure (by @ravi-chandra3197) + * [magento/magento2#22473](https://github.com/magento/magento2/pull/22473) -- [Backport] Qty box visibility issue in wishlist when product is out of stock (by @niravkrish) + * [magento/magento2#22415](https://github.com/magento/magento2/pull/22415) -- filter config values on testSuiteStart (by @bcerban) + * [magento/magento2#22378](https://github.com/magento/magento2/pull/22378) -- [Backport] Same product quantity not increment when added with guest user. #21375 (by @amol2jcommerce) + * [magento/magento2#22464](https://github.com/magento/magento2/pull/22464) -- [Backport] Remove all marketing get params on Varnish to minimize the cache objects (added facebook and bronto parameter) (by @ihor-sviziev) + * [magento/magento2#22358](https://github.com/magento/magento2/pull/22358) -- [Backport] Previous scrolling to invalid form element is not being canceled on h... (by @amol2jcommerce) + * [magento/magento2#22499](https://github.com/magento/magento2/pull/22499) -- [Backport] Fixed Dependency on Backup Settings Configuration (by @keyuremipro) + * [magento/magento2#22523](https://github.com/magento/magento2/pull/22523) -- [Backport] Fixed #22223 Missing/Wrong data display on downloadable report table ... (by @shikhamis11) + * [magento/magento2#22533](https://github.com/magento/magento2/pull/22533) -- [Backport] PUT /V1/products/:sku/media/:entryId does not change the file (by @niravkrish) + * [magento/magento2#22534](https://github.com/magento/magento2/pull/22534) -- [Backport] #22299: Cms block cache key does not contain the store id (by @amol2jcommerce) + * [magento/magento2#22288](https://github.com/magento/magento2/pull/22288) -- [BACKPORT] #222249 configurable product images wrong sorting fix (by @Wirson) + * [magento/magento2#22543](https://github.com/magento/magento2/pull/22543) -- [Backport] [Fixed] Checkout Section: Shipping step is getting skipped when customer hitting direct payment step URL (by @saphaljha) + * [magento/magento2#22536](https://github.com/magento/magento2/pull/22536) -- [Bakport] Adding product from wishlist not adding to cart showing warning message. (by @niravkrish) + * [magento/magento2#22551](https://github.com/magento/magento2/pull/22551) -- [Backport] Shortening currency list in Configuration->General (replace PR #20397) (by @amol2jcommerce) + * [magento/magento2#22413](https://github.com/magento/magento2/pull/22413) -- Checkout Totals Sort Order fields can't be empty and should be a number. (by @barbanet) + * [magento/magento2#22579](https://github.com/magento/magento2/pull/22579) -- [Backport] Non existing file, when adding image to gallery with move option. Fix for #21978 (by @amol2jcommerce) + * [magento/magento2#22582](https://github.com/magento/magento2/pull/22582) -- [Backport] Alignment Issue While Editing Order Data containing Downlodable Products with "Links can be purchased separately" enabled in Admin Section (by @webkul-deepakkumar) + * [magento/magento2#22016](https://github.com/magento/magento2/pull/22016) -- Magento Catalog - fix custom option type text price conversion for mu... (by @oleksii-lisovyi) + * [magento/magento2#22535](https://github.com/magento/magento2/pull/22535) -- [Backport] Fix configurable dropdown showing tax incorrectly in 2.3-develop (by @amol2jcommerce) + +2.2.8 +============= +* GitHub issues: + * [#15196](https://github.com/magento/magento2/issues/15196) -- 2.2.4 : Magento 2 integration tests enables all modules (fixed in [magento/magento2#16361](https://github.com/magento/magento2/pull/16361)) + * [#13720](https://github.com/magento/magento2/issues/13720) -- Only 2 related products are showing in backend . (fixed in [magento/magento2#17885](https://github.com/magento/magento2/pull/17885)) + * [#14050](https://github.com/magento/magento2/issues/14050) -- Import related products issue (fixed in [magento/magento2#17885](https://github.com/magento/magento2/pull/17885)) + * [#17890](https://github.com/magento/magento2/issues/17890) -- Magento 2.2.5 Product swatches does not shows correct value for related store view (fixed in [magento/magento2#17891](https://github.com/magento/magento2/pull/17891)) + * [#17567](https://github.com/magento/magento2/issues/17567) -- Currency symbol cannot be changed back to default value from admin panel in Single-store mode (fixed in [magento/magento2#17966](https://github.com/magento/magento2/pull/17966)) + * [#5402](https://github.com/magento/magento2/issues/5402) -- Menu does not work when you change from Mobile to Desktop mode (fixed in [magento/magento2#17990](https://github.com/magento/magento2/pull/17990)) + * [#13405](https://github.com/magento/magento2/issues/13405) -- No such entity error when saving product in single-store mode if website_id <> 1 (fixed in [magento/magento2#18001](https://github.com/magento/magento2/pull/18001)) + * [#5797](https://github.com/magento/magento2/issues/5797) -- [2.1.0] module:uninstall can remove code it uses itself (fixed in [magento/magento2#18002](https://github.com/magento/magento2/pull/18002)) + * [#17780](https://github.com/magento/magento2/issues/17780) -- Module uninstall does not work with composer (fixed in [magento/magento2#18002](https://github.com/magento/magento2/pull/18002)) + * [#7557](https://github.com/magento/magento2/issues/7557) -- Backend Security key broken for controllers with frontname not equal to route ID (fixed in [magento/magento2#18018](https://github.com/magento/magento2/pull/18018)) + * [#12095](https://github.com/magento/magento2/issues/12095) -- Update 2.2.1: One or more integrations have been reset because of a change to their xml configs. (fixed in [magento/magento2#14065](https://github.com/magento/magento2/pull/14065)) + * [#17582](https://github.com/magento/magento2/issues/17582) -- ./bin/magento config:show fails with a fatal error (fixed in [magento/magento2#17993](https://github.com/magento/magento2/pull/17993)) + * [#17999](https://github.com/magento/magento2/issues/17999) -- Sitemap grid display incorrect base URL in the grid if using multiple stores (fixed in [magento/magento2#18000](https://github.com/magento/magento2/pull/18000)) + * [#9830](https://github.com/magento/magento2/issues/9830) -- Null order in Magento\Sales\Block\Order\PrintShipment.php (fixed in [magento/magento2#17998](https://github.com/magento/magento2/pull/17998)) + * [#10530](https://github.com/magento/magento2/issues/10530) -- Print order error on magento 2.1.8 (fixed in [magento/magento2#17998](https://github.com/magento/magento2/pull/17998)) + * [#10440](https://github.com/magento/magento2/issues/10440) -- Missing $debugHintsPath when sending email via command (fixed in [magento/magento2#17984](https://github.com/magento/magento2/pull/17984)) + * [#18079](https://github.com/magento/magento2/issues/18079) -- Inconsistent return type for getStoreId() (fixed in [magento/magento2#18086](https://github.com/magento/magento2/pull/18086)) + * [#18138](https://github.com/magento/magento2/issues/18138) -- WYSIWYG editor fails to parse directives of files with special characters in URL (so random files) (fixed in [magento/magento2#18215](https://github.com/magento/magento2/pull/18215)) + * [#18101](https://github.com/magento/magento2/issues/18101) -- Wrong sort order for customer groups in customer grid filter (fixed in [magento/magento2#18280](https://github.com/magento/magento2/pull/18280)) + * [#17977](https://github.com/magento/magento2/issues/17977) -- Show Method if Not Applicable for Free Shipping doesn't work. (fixed in [magento/magento2#17982](https://github.com/magento/magento2/pull/17982)) + * [#17023](https://github.com/magento/magento2/issues/17023) -- CSV Import of `sku,attribute` empties `url_key` value (fixed in [magento/magento2#17882](https://github.com/magento/magento2/pull/17882)) + * [#18330](https://github.com/magento/magento2/issues/18330) -- Checkout - Infinite loading indicator when server returned error (fixed in [magento/magento2#18369](https://github.com/magento/magento2/pull/18369)) + * [#16497](https://github.com/magento/magento2/issues/16497) -- Magento 2.2.5: Google Analytics not added to head correctly (fixed in [magento/magento2#18375](https://github.com/magento/magento2/pull/18375)) + * [#17152](https://github.com/magento/magento2/issues/17152) -- Failure of "Send Order Email Copy" spams customers, every minute, forever. (fixed in [magento/magento2#18376](https://github.com/magento/magento2/pull/18376)) + * [#18162](https://github.com/magento/magento2/issues/18162) -- Cannot edit customer using inline edit if password is expired (fixed in [magento/magento2#18414](https://github.com/magento/magento2/pull/18414)) + * [#3283](https://github.com/magento/magento2/issues/3283) -- «Yes/No» attributes should be allowed in the Layered Navigation (fixed in [magento/magento2#17823](https://github.com/magento/magento2/pull/17823)) + * [#17493](https://github.com/magento/magento2/issues/17493) -- Catalog Rule & Selected Categories with level > 3 (fixed in [magento/magento2#18175](https://github.com/magento/magento2/pull/18175)) + * [#17770](https://github.com/magento/magento2/issues/17770) -- Table rate fail when using ZIP+4 shipping address (fixed in [magento/magento2#18166](https://github.com/magento/magento2/pull/18166)) + * [#13156](https://github.com/magento/magento2/issues/13156) -- Updating attribute option data through API will set unwanted source_model on the attribute (fixed in [magento/magento2#18390](https://github.com/magento/magento2/pull/18390)) + * [#17190](https://github.com/magento/magento2/issues/17190) -- system.log rapidly increasing after Magento CE 2.2.5 update (cron logs) (fixed in [magento/magento2#18389](https://github.com/magento/magento2/pull/18389)) + * [#15085](https://github.com/magento/magento2/issues/15085) -- StockRegistryInterface :: getLowStockItems() returns StockStatusCollection instead of StockItemCollection (fixed in [magento/magento2#18427](https://github.com/magento/magento2/pull/18427)) + * [#15652](https://github.com/magento/magento2/issues/15652) -- REST API create order POST /V1/orders (fixed in [magento/magento2#15683](https://github.com/magento/magento2/pull/15683)) + * [#4942](https://github.com/magento/magento2/issues/4942) -- On editing a Bundle product from shopping cart the user defined quantities of the options are overwritten (fixed in [magento/magento2#15905](https://github.com/magento/magento2/pull/15905)) + * [#17514](https://github.com/magento/magento2/issues/17514) -- Add Australian regions (fixed in [magento/magento2#17516](https://github.com/magento/magento2/pull/17516)) + * [#12479](https://github.com/magento/magento2/issues/12479) -- Saving Customer Model directly causes loss of data (fixed in [magento/magento2#17968](https://github.com/magento/magento2/pull/17968)) + * [#9219](https://github.com/magento/magento2/issues/9219) -- Custom Product Attribute changes 'backend_type' when 'is_user_defined = 1' and get updated/saved in Admin Backend (fixed in [magento/magento2#18196](https://github.com/magento/magento2/pull/18196)) + * [#18164](https://github.com/magento/magento2/issues/18164) -- Checkout - Cannot read property 'code' of undefined (fixed in [magento/magento2#18495](https://github.com/magento/magento2/pull/18495)) + * [#14555](https://github.com/magento/magento2/issues/14555) -- Communication's component validator does not propagate exceptions, obscuring the cause of the error (fixed in [magento/magento2#18554](https://github.com/magento/magento2/pull/18554)) + * [#18477](https://github.com/magento/magento2/issues/18477) -- Set maximum Qty Allowed in Shopping Cart is 0 still allow adding to cart (fixed in [magento/magento2#18552](https://github.com/magento/magento2/pull/18552)) + * [#12070](https://github.com/magento/magento2/issues/12070) -- M2.2.0 Admin Grid column ordering/positioning not working when single store mode set On (fixed in [magento/magento2#18561](https://github.com/magento/magento2/pull/18561)) + * [#18581](https://github.com/magento/magento2/issues/18581) -- Calendar Icon aligement Issue (fixed in [magento/magento2#18593](https://github.com/magento/magento2/pull/18593)) + * [#18585](https://github.com/magento/magento2/issues/18585) -- Navigation arrows zoomed fotorama disappear (fixed in [magento/magento2#18595](https://github.com/magento/magento2/pull/18595)) + * [#12969](https://github.com/magento/magento2/issues/12969) -- processor.php getHostUrl() does not detect the server port correctly (fixed in [magento/magento2#18659](https://github.com/magento/magento2/pull/18659)) + * [#14510](https://github.com/magento/magento2/issues/14510) -- Creating custom customer attribute with default value 0 will cause not saving value for customer entity (fixed in [magento/magento2#16915](https://github.com/magento/magento2/pull/16915)) + * [#18234](https://github.com/magento/magento2/issues/18234) -- Product Import -> Upsert Category: Url Rewrites are just created for default website (fixed in [magento/magento2#18563](https://github.com/magento/magento2/pull/18563)) + * [#5929](https://github.com/magento/magento2/issues/5929) -- Saving Product does not update URL rewrite in Magento 2.1.0 (fixed in [magento/magento2#18566](https://github.com/magento/magento2/pull/18566)) + * [#18532](https://github.com/magento/magento2/issues/18532) -- Module Catalog: product "Save and Duplicate" causes getting infinite loop (fixed in [magento/magento2#18566](https://github.com/magento/magento2/pull/18566)) + * [#18131](https://github.com/magento/magento2/issues/18131) -- Entity Type ID at Join (fixed in [magento/magento2#18658](https://github.com/magento/magento2/pull/18658)) + * [#15259](https://github.com/magento/magento2/issues/15259) -- Advanced Reporting > Unable to disable without providing Industry value (fixed in [magento/magento2#15366](https://github.com/magento/magento2/pull/15366)) + * [#18094](https://github.com/magento/magento2/issues/18094) -- Should getQty() return int/float or string? (fixed in [magento/magento2#18424](https://github.com/magento/magento2/pull/18424)) + * [#18534](https://github.com/magento/magento2/issues/18534) -- Bug when 2 wysiwyg editors are on category edit page or product edit page (fixed in [magento/magento2#18535](https://github.com/magento/magento2/pull/18535)) + * [#18589](https://github.com/magento/magento2/issues/18589) -- Empty cart button does not work (fixed in [magento/magento2#18597](https://github.com/magento/magento2/pull/18597)) + * [#18268](https://github.com/magento/magento2/issues/18268) -- M2.2.6 : Special price of 0.0000 is not shown on frontend, but is calculated in cart (fixed in [magento/magento2#18604](https://github.com/magento/magento2/pull/18604)) + * [#17954](https://github.com/magento/magento2/issues/17954) -- Customer get unsubscribe to newsletter on password reset email request with Newsletter Need to Confirm Set to Yes on admin settings (fixed in [magento/magento2#18643](https://github.com/magento/magento2/pull/18643)) + * [#16939](https://github.com/magento/magento2/issues/16939) -- Incorrect configuration scope is occasionally returned when attempting to resolve a null scope id (fixed in [magento/magento2#16940](https://github.com/magento/magento2/pull/16940)) + * [#18264](https://github.com/magento/magento2/issues/18264) -- M2.2.6 : "Order by price" not working in product listing (fixed in [magento/magento2#18737](https://github.com/magento/magento2/pull/18737)) + * [#17638](https://github.com/magento/magento2/issues/17638) -- Bundle Special Prices not correctly rounded (fixed in [magento/magento2#17971](https://github.com/magento/magento2/pull/17971)) + * [#17865](https://github.com/magento/magento2/issues/17865) -- import new products via csv: products are created with empty value when strings are too long (fixed in [magento/magento2#18591](https://github.com/magento/magento2/pull/18591)) + * [#12300](https://github.com/magento/magento2/issues/12300) -- SKU values are not trimmed with the space. (fixed in [magento/magento2#18862](https://github.com/magento/magento2/pull/18862)) + * [#16572](https://github.com/magento/magento2/issues/16572) -- Trim whitespace on SKU when saving product (fixed in [magento/magento2#18862](https://github.com/magento/magento2/pull/18862)) + * [#18458](https://github.com/magento/magento2/issues/18458) -- Magento version 2.2.6 Alert widget gets close when click anywhere on screen (fixed in [magento/magento2#18865](https://github.com/magento/magento2/pull/18865)) + * [#18779](https://github.com/magento/magento2/issues/18779) -- Translation issue send-friend in sendphtml (fixed in [magento/magento2#18886](https://github.com/magento/magento2/pull/18886)) + * [#18913](https://github.com/magento/magento2/issues/18913) -- Global-search icon misaligned (fixed in [magento/magento2#18917](https://github.com/magento/magento2/pull/18917)) + * [#17488](https://github.com/magento/magento2/issues/17488) -- Authenticating a customer via REST API does not update the last logged in data (fixed in [magento/magento2#17978](https://github.com/magento/magento2/pull/17978)) + * [#4468](https://github.com/magento/magento2/issues/4468) -- Unable to insert multiple catalog product list widgets in CMS page (fixed in [magento/magento2#18874](https://github.com/magento/magento2/pull/18874)) + * [#18355](https://github.com/magento/magento2/issues/18355) -- Typo in dispatched event name (fixed in [magento/magento2#18372](https://github.com/magento/magento2/pull/18372)) + * [#17744](https://github.com/magento/magento2/issues/17744) -- Virtual-only quotes use default shipping address for estimation instead of default billing address (fixed in [magento/magento2#18863](https://github.com/magento/magento2/pull/18863)) + * [#5021](https://github.com/magento/magento2/issues/5021) -- "Please specify a shipping method" Exception (fixed in [magento/magento2#18870](https://github.com/magento/magento2/pull/18870)) + * [#17485](https://github.com/magento/magento2/issues/17485) -- Adding billing information via mine API expects costumer id (fixed in [magento/magento2#18872](https://github.com/magento/magento2/pull/18872)) + * [#13083](https://github.com/magento/magento2/issues/13083) -- OptionManagement.validateOption throws NoSuchEntityException for "0" option label (fixed in [magento/magento2#18873](https://github.com/magento/magento2/pull/18873)) + * [#18729](https://github.com/magento/magento2/issues/18729) -- Bug in "_sections.less" mixins: missing rules and incorrect default variables (fixed in [magento/magento2#18875](https://github.com/magento/magento2/pull/18875)) + * [#18555](https://github.com/magento/magento2/issues/18555) -- Magento 2.2.6 Default values are not rendering on Wishlist product edit page. (fixed in [magento/magento2#18967](https://github.com/magento/magento2/pull/18967)) + * [#18907](https://github.com/magento/magento2/issues/18907) -- Unable to select payment method according to country of the address at checkout time (fixed in [magento/magento2#18908](https://github.com/magento/magento2/pull/18908)) + * [#16684](https://github.com/magento/magento2/issues/16684) -- Default tax region/state appears in customer & order data (fixed in [magento/magento2#18857](https://github.com/magento/magento2/pull/18857)) + * [#8348](https://github.com/magento/magento2/issues/8348) -- 1 exception(s): Exception #0 (Exception): Warning: Invalid argument supplied for foreach() in NotProtectedExtension.php on line 89 (fixed in [magento/magento2#19012](https://github.com/magento/magento2/pull/19012)) + * [#18323](https://github.com/magento/magento2/issues/18323) -- Order confirmation email for guest checkout does not include download links (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#19003](https://github.com/magento/magento2/issues/19003) -- salesInvoiceOrder REST API does not make downloadable products available (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#19034](https://github.com/magento/magento2/issues/19034) -- sales_order_item_save_commit_after and sales_order_save_commit_after events will never fire for guest checkout (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#2618](https://github.com/magento/magento2/issues/2618) -- Class \Magento\Framework\Data\Form\Element\Fieldset breaks specification of the parent class \Magento\Framework\Data\Form\Element\AbstractElement by not calling the method getBeforeElementHtml (getAfterElementHtml is called) (fixed in [magento/magento2#18985](https://github.com/magento/magento2/pull/18985)) + * [#14007](https://github.com/magento/magento2/issues/14007) -- "Use in Layered Navigation: Filterable (no results)" not working for Price attribute. (fixed in [magento/magento2#19044](https://github.com/magento/magento2/pull/19044)) + * [#12399](https://github.com/magento/magento2/issues/12399) -- Exception Error in Catalog Price Rule while Backend language is not English (fixed in [magento/magento2#19074](https://github.com/magento/magento2/pull/19074)) + * [#18082](https://github.com/magento/magento2/issues/18082) -- Fatal Error when save configurable product in Magento 2.2.5 (fixed in [magento/magento2#18461](https://github.com/magento/magento2/pull/18461)) + * [#18617](https://github.com/magento/magento2/issues/18617) -- Missing Fixed Product Tax total on PDF (fixed in [magento/magento2#18649](https://github.com/magento/magento2/pull/18649)) + * [#18150](https://github.com/magento/magento2/issues/18150) -- Backups error from User Roles Permission 2.2.6 (fixed in [magento/magento2#18815](https://github.com/magento/magento2/pull/18815)) + * [#18901](https://github.com/magento/magento2/issues/18901) -- Forgot password form should not available while customer is logged in. (fixed in [magento/magento2#19089](https://github.com/magento/magento2/pull/19089)) + * [#18840](https://github.com/magento/magento2/issues/18840) -- Invalid Unit Test Annotations (fixed in [magento/magento2#19105](https://github.com/magento/magento2/pull/19105)) + * [#19060](https://github.com/magento/magento2/issues/19060) -- User created by admin cannot login (fixed in [magento/magento2#19110](https://github.com/magento/magento2/pull/19110)) + * [#14849](https://github.com/magento/magento2/issues/14849) -- In Sales Emails no translation using order.getStatusLabel() (fixed in [magento/magento2#14914](https://github.com/magento/magento2/pull/14914)) + * [#17625](https://github.com/magento/magento2/issues/17625) -- Translations done within a theme that's enabled through a category Design change aren't used (fixed in [magento/magento2#17854](https://github.com/magento/magento2/pull/17854)) + * [#17635](https://github.com/magento/magento2/issues/17635) -- addExpressionFieldToSelect has to be called after all addFieldToSelect (fixed in [magento/magento2#17915](https://github.com/magento/magento2/pull/17915)) + * [#18652](https://github.com/magento/magento2/issues/18652) -- Tierprice discount not calculated correctly if has specialprice. (fixed in [magento/magento2#18743](https://github.com/magento/magento2/pull/18743)) + * [#18939](https://github.com/magento/magento2/issues/18939) -- "Not yet calculated" for the tax in the summary section in the checkout is not translatable (fixed in [magento/magento2#18959](https://github.com/magento/magento2/pull/18959)) + * [#16434](https://github.com/magento/magento2/issues/16434) -- Bundle Product Options not showing in Customer Account - Items Ordered (fixed in [magento/magento2#17889](https://github.com/magento/magento2/pull/17889)) + * [#14020](https://github.com/magento/magento2/issues/14020) -- Cart Sales Rule with negated condition over special_price does not work for configurable products (fixed in [magento/magento2#16342](https://github.com/magento/magento2/pull/16342)) + * [#18685](https://github.com/magento/magento2/issues/18685) -- Quote Item Prices are NULL in cart related events. (fixed in [magento/magento2#18808](https://github.com/magento/magento2/pull/18808)) + * [#18956](https://github.com/magento/magento2/issues/18956) -- Import of RootCategoryId should be possbile (Magento/Store/Model/Config/Importer/Processor/Create.php) (fixed in [magento/magento2#19237](https://github.com/magento/magento2/pull/19237)) + * [#19205](https://github.com/magento/magento2/issues/19205) -- Bundle Product Option with input type is checkbox and add to cart with 3 values only 2 values added to cart (fixed in [magento/magento2#19260](https://github.com/magento/magento2/pull/19260)) + * [#6803](https://github.com/magento/magento2/issues/6803) -- Product::addImageToMediaGallery throws Exception (fixed in [magento/magento2#18951](https://github.com/magento/magento2/pull/18951)) + * [#18949](https://github.com/magento/magento2/issues/18949) -- dev/tools/grunt/configs/themes.js gets replaced after update magento (fixed in [magento/magento2#18960](https://github.com/magento/magento2/pull/18960)) + * [#19054](https://github.com/magento/magento2/issues/19054) -- Using Media Image custom attribute type could not display on frontend. (fixed in [magento/magento2#19068](https://github.com/magento/magento2/pull/19068)) + * [#19082](https://github.com/magento/magento2/issues/19082) -- Fatal error: Uncaught Error: Cannot call abstract method Magento\Framework\App\ActionInterface::execute() (fixed in [magento/magento2#19337](https://github.com/magento/magento2/pull/19337)) + * [#19263](https://github.com/magento/magento2/issues/19263) -- Broken backend popup view (fixed in [magento/magento2#19340](https://github.com/magento/magento2/pull/19340)) + * [#4136](https://github.com/magento/magento2/issues/4136) -- Widget condition with unexpected character not preventing from saving (fixed in [magento/magento2#14485](https://github.com/magento/magento2/pull/14485)) + * [#18615](https://github.com/magento/magento2/issues/18615) -- Field restriction incompatibilities between klarna_core_order and sales_order_payment last_trans_id (fixed in [magento/magento2#18621](https://github.com/magento/magento2/pull/18621)) + * [#18904](https://github.com/magento/magento2/issues/18904) -- Missing asterisk for admin required fields (fixed in [magento/magento2#18905](https://github.com/magento/magento2/pull/18905)) + * [#19286](https://github.com/magento/magento2/issues/19286) -- Wrong pager style (fixed in [magento/magento2#19296](https://github.com/magento/magento2/pull/19296)) + * [#13157](https://github.com/magento/magento2/issues/13157) -- Last Ordered Items block - bad js code (fixed in [magento/magento2#19357](https://github.com/magento/magento2/pull/19357)) + * [#17833](https://github.com/magento/magento2/issues/17833) -- Child theme does not inherit translations from parent theme (fixed in [magento/magento2#19023](https://github.com/magento/magento2/pull/19023)) + * [#18839](https://github.com/magento/magento2/issues/18839) -- can't import external http to https redirecting images by default csv import (fixed in [magento/magento2#18899](https://github.com/magento/magento2/pull/18899)) + * [#18887](https://github.com/magento/magento2/issues/18887) -- Magento backend Notifications counter round icon small cut from right side (fixed in [magento/magento2#19356](https://github.com/magento/magento2/pull/19356)) + * [#17813](https://github.com/magento/magento2/issues/17813) -- Huge "product_data_storage" in localStorage hangs the shop (fixed in [magento/magento2#19014](https://github.com/magento/magento2/pull/19014)) + * [#15505](https://github.com/magento/magento2/issues/15505) -- Interceptor class methods do not support nullable return types (fixed in [magento/magento2#19398](https://github.com/magento/magento2/pull/19398)) + * [#19172](https://github.com/magento/magento2/issues/19172) -- Newsletter subscription does not set the correct store_id if already subscribed. Not Fixed in 2.3-dev (fixed in [magento/magento2#19426](https://github.com/magento/magento2/pull/19426)) + * [#18918](https://github.com/magento/magento2/issues/18918) -- Asterisk sign display twice (fixed in [magento/magento2#18922](https://github.com/magento/magento2/pull/18922)) + * [#19127](https://github.com/magento/magento2/issues/19127) -- Cannot connect to Magento 2 market place (fixed in [magento/magento2#19239](https://github.com/magento/magento2/pull/19239)) + * [#19344](https://github.com/magento/magento2/issues/19344) -- Sample Link Issue in Downloadable product. (fixed in [magento/magento2#19431](https://github.com/magento/magento2/pull/19431)) + * [#15931](https://github.com/magento/magento2/issues/15931) -- events.xml cant have no childrens, others can [Magento 2.2.4] (fixed in [magento/magento2#19145](https://github.com/magento/magento2/pull/19145)) + * [#19418](https://github.com/magento/magento2/issues/19418) -- Cannot add additional field to Newsletter system configuration at desired position (fixed in [magento/magento2#19568](https://github.com/magento/magento2/pull/19568)) + * [#19424](https://github.com/magento/magento2/issues/19424) -- \Magento\Checkout\Observer\SalesQuoteSaveAfterObserver fails to update the checkout session quote id when applicable (fixed in [magento/magento2#19678](https://github.com/magento/magento2/pull/19678)) + * [#19796](https://github.com/magento/magento2/issues/19796) -- Sales Order invoice Update Qty's Button is misaligned (fixed in [magento/magento2#19804](https://github.com/magento/magento2/pull/19804)) + * [#19917](https://github.com/magento/magento2/issues/19917) -- allowDrug? ;-) (fixed in [magento/magento2#19949](https://github.com/magento/magento2/pull/19949)) + * [#19721](https://github.com/magento/magento2/issues/19721) -- Typo in SalesRule/Model/ResourceModel/Coupon/Usage.php (fixed in [magento/magento2#19968](https://github.com/magento/magento2/pull/19968)) + * [#8952](https://github.com/magento/magento2/issues/8952) -- You can't subscribe to newsletter if you already have an account (fixed in [magento/magento2#18912](https://github.com/magento/magento2/pull/18912)) + * [#19142](https://github.com/magento/magento2/issues/19142) -- Home page store loge should be clickable to reload page (fixed in [magento/magento2#19199](https://github.com/magento/magento2/pull/19199)) + * [#18374](https://github.com/magento/magento2/issues/18374) -- Unable to get product attribute value for store-view scope type in product collection loaded for a specific store. (fixed in [magento/magento2#19911](https://github.com/magento/magento2/pull/19911)) + * [#18941](https://github.com/magento/magento2/issues/18941) -- Calling getCurrentUrl on Store will wrongly add "___store" parameter (fixed in [magento/magento2#19945](https://github.com/magento/magento2/pull/19945)) + * [#19052](https://github.com/magento/magento2/issues/19052) -- Position order showing before the text box (fixed in [magento/magento2#19056](https://github.com/magento/magento2/pull/19056)) + * [#19285](https://github.com/magento/magento2/issues/19285) -- On Notification page Select All and Select Visible both works same (fixed in [magento/magento2#19910](https://github.com/magento/magento2/pull/19910)) + * [#19507](https://github.com/magento/magento2/issues/19507) -- Frontend Minicart dropdown alignment issue (fixed in [magento/magento2#19889](https://github.com/magento/magento2/pull/19889)) + * [#19605](https://github.com/magento/magento2/issues/19605) -- Don't static compile disabled modules (fixed in [magento/magento2#19989](https://github.com/magento/magento2/pull/19989)) + * [#19346](https://github.com/magento/magento2/issues/19346) -- Import data 2.2.6 Value for 'product_type' attribute contains incorrect value (fixed in [magento/magento2#20081](https://github.com/magento/magento2/pull/20081)) + * [#19780](https://github.com/magento/magento2/issues/19780) -- Incorrect class name on Orders and returns page. (fixed in [magento/magento2#20080](https://github.com/magento/magento2/pull/20080)) + * [#19230](https://github.com/magento/magento2/issues/19230) -- Can't Cancel Order (fixed in [magento/magento2#19423](https://github.com/magento/magento2/pull/19423)) + * [#19099](https://github.com/magento/magento2/issues/19099) -- New Link is not correctly shown as Current if contains default parts (fixed in [magento/magento2#19927](https://github.com/magento/magento2/pull/19927)) + * [#19940](https://github.com/magento/magento2/issues/19940) -- Exception undefined variable itemsOrderItemId while creating shipment through MSI (fixed in [magento/magento2#20082](https://github.com/magento/magento2/pull/20082)) + * [#19101](https://github.com/magento/magento2/issues/19101) -- API REST and Reserved Order Id (fixed in [magento/magento2#20208](https://github.com/magento/magento2/pull/20208)) + * [#20210](https://github.com/magento/magento2/issues/20210) -- Hamburger Icon was available on a page where menu was not present. Issue in responsive view (fixed in [magento/magento2#20219](https://github.com/magento/magento2/pull/20219)) + * [#16198](https://github.com/magento/magento2/issues/16198) -- Category image remain after deleted (fixed in [magento/magento2#20178](https://github.com/magento/magento2/pull/20178)) + * [#18192](https://github.com/magento/magento2/issues/18192) -- Backend issue : "ratings isn't available" website wise (fixed in [magento/magento2#20183](https://github.com/magento/magento2/pull/20183)) + * [#14937](https://github.com/magento/magento2/issues/14937) -- Javascript error thrown from uiComponent 'notification_area' if messages are malformed (fixed in [magento/magento2#20271](https://github.com/magento/magento2/pull/20271)) + * [#17819](https://github.com/magento/magento2/issues/17819) -- Wrong product url from getProductUrl when current category has not product object (fixed in [magento/magento2#20286](https://github.com/magento/magento2/pull/20286)) + * [#20296](https://github.com/magento/magento2/issues/20296) -- "@magentoDataIsolation" is used instead of "@magentoDbIsolation" in some integration tests. (fixed in [magento/magento2#20298](https://github.com/magento/magento2/pull/20298)) + * [#20158](https://github.com/magento/magento2/issues/20158) -- Store switcher not aligned proper in tab view (fixed in [magento/magento2#20325](https://github.com/magento/magento2/pull/20325)) + * [#20232](https://github.com/magento/magento2/issues/20232) -- Backend order credit card detail check box misaligned (fixed in [magento/magento2#20328](https://github.com/magento/magento2/pull/20328)) + * [#20098](https://github.com/magento/magento2/issues/20098) -- Product image failure when importing through CSV (fixed in [magento/magento2#20329](https://github.com/magento/magento2/pull/20329)) + * [#20352](https://github.com/magento/magento2/issues/20352) -- File type option value shows html content in admin order view. (fixed in [magento/magento2#20353](https://github.com/magento/magento2/pull/20353)) + * [#18170](https://github.com/magento/magento2/issues/18170) -- Unable to reset password if customer has address from not allowed country (fixed in [magento/magento2#19964](https://github.com/magento/magento2/pull/19964)) + * [#19982](https://github.com/magento/magento2/issues/19982) -- Catalogsearch Reindex (fixed in [magento/magento2#19984](https://github.com/magento/magento2/pull/19984)) + * [#9130](https://github.com/magento/magento2/issues/9130) -- If stock is bellow OutOfStock Threshold, a negative qty is displayed in Product List Page (fixed in [magento/magento2#20206](https://github.com/magento/magento2/pull/20206)) + * [#19609](https://github.com/magento/magento2/issues/19609) -- config:set --lock-config does not act on other scopes (fixed in [magento/magento2#20322](https://github.com/magento/magento2/pull/20322)) + * [#19399](https://github.com/magento/magento2/issues/19399) -- Add product customization option collapsible design issue (fixed in [magento/magento2#19400](https://github.com/magento/magento2/pull/19400)) + * [#20120](https://github.com/magento/magento2/issues/20120) -- Review Details Detailed Rating misaligned (fixed in [magento/magento2#20272](https://github.com/magento/magento2/pull/20272)) + * [#20172](https://github.com/magento/magento2/issues/20172) -- On customer login page input field are short width on tablet view (fixed in [magento/magento2#20369](https://github.com/magento/magento2/pull/20369)) + * [#19085](https://github.com/magento/magento2/issues/19085) -- Translation in tier_price.phtml not working (fixed in [magento/magento2#19377](https://github.com/magento/magento2/pull/19377)) + * [#18361](https://github.com/magento/magento2/issues/18361) -- Customer last name is encoded twice in the XML interface (fixed in [magento/magento2#18362](https://github.com/magento/magento2/pull/18362)) + * [#19887](https://github.com/magento/magento2/issues/19887) -- creating new shipment: gettting all trackers. after this commit 2307e16 (fixed in [magento/magento2#20184](https://github.com/magento/magento2/pull/20184)) + * [#19985](https://github.com/magento/magento2/issues/19985) -- Send email confirmation popup close button area overlapping to content (fixed in [magento/magento2#20541](https://github.com/magento/magento2/pull/20541)) + * [#17759](https://github.com/magento/magento2/issues/17759) -- M2.2.5 : CustomerRepository::getList() does not load custom attribute if the name is "company" (fixed in [magento/magento2#20284](https://github.com/magento/magento2/pull/20284)) + * [#19800](https://github.com/magento/magento2/issues/19800) -- Contact us : design improvement (fixed in [magento/magento2#20455](https://github.com/magento/magento2/pull/20455)) + * [#19645](https://github.com/magento/magento2/issues/19645) -- Area Frontend: Account information page checkbox alignment issue. (fixed in [magento/magento2#20457](https://github.com/magento/magento2/pull/20457)) + * [#19791](https://github.com/magento/magento2/issues/19791) -- Logo vertical misalignment. (fixed in [magento/magento2#20456](https://github.com/magento/magento2/pull/20456)) + * [#15950](https://github.com/magento/magento2/issues/15950) -- Magento2 CSV product import qty and is_in_stock not working correct (fixed in [magento/magento2#20177](https://github.com/magento/magento2/pull/20177)) + * [#19899](https://github.com/magento/magento2/issues/19899) -- Credit memo for $0 order without refunded shipping produces negative credit memo (fixed in [magento/magento2#20508](https://github.com/magento/magento2/pull/20508)) + * [#20121](https://github.com/magento/magento2/issues/20121) -- Cancel order increases stock although "Set Items' Status to be In Stock When Order is Cancelled" is set to No (fixed in [magento/magento2#20547](https://github.com/magento/magento2/pull/20547)) + * [#18027](https://github.com/magento/magento2/issues/18027) -- Cart Total is NaN in some circumstances (fixed in [magento/magento2#20638](https://github.com/magento/magento2/pull/20638)) + * [#20376](https://github.com/magento/magento2/issues/20376) -- Image gets uploaded if field is disable in Category (fixed in [magento/magento2#20636](https://github.com/magento/magento2/pull/20636)) + * [#20169](https://github.com/magento/magento2/issues/20169) -- Admin user with restricted "order create" access can "view", "cancel", etc via API (fixed in [magento/magento2#20542](https://github.com/magento/magento2/pull/20542)) + * [#20399](https://github.com/magento/magento2/issues/20399) -- On wish list page edit, remove item misalign in 640 X 767 resolution (fixed in [magento/magento2#20544](https://github.com/magento/magento2/pull/20544)) + * [#20373](https://github.com/magento/magento2/issues/20373) -- Order view invoices template not display proper on ipad (fixed in [magento/magento2#20546](https://github.com/magento/magento2/pull/20546)) + * [#18387](https://github.com/magento/magento2/issues/18387) -- catalog:images:resize fails to process all images -> Possible underlying Magento/Framework/DB/Query/Generator issue (fixed in [magento/magento2#18809](https://github.com/magento/magento2/pull/18809)) + * [#18931](https://github.com/magento/magento2/issues/18931) -- Product added to shopping cart / comparison list message not translated by default (fixed in [magento/magento2#19461](https://github.com/magento/magento2/pull/19461)) + * [#14712](https://github.com/magento/magento2/issues/14712) -- Shipping issue on PayPal Express (fixed in [magento/magento2#19655](https://github.com/magento/magento2/pull/19655)) + * [#20113](https://github.com/magento/magento2/issues/20113) -- Widget option labels are misalinged (fixed in [magento/magento2#20270](https://github.com/magento/magento2/pull/20270)) + * [#20304](https://github.com/magento/magento2/issues/20304) -- No space between step title and saved address in checkout (fixed in [magento/magento2#20418](https://github.com/magento/magento2/pull/20418)) + * [#20609](https://github.com/magento/magento2/issues/20609) -- Currency rate value not align proper in order information tab when we create creditmemo from admin (fixed in [magento/magento2#20613](https://github.com/magento/magento2/pull/20613)) + * [#20500](https://github.com/magento/magento2/issues/20500) -- Recent Order Product Title Misaligned in Sidebar (fixed in [magento/magento2#20744](https://github.com/magento/magento2/pull/20744)) + * [#20563](https://github.com/magento/magento2/issues/20563) -- Go to shipping information, Update qty & Addresses and Enter a new address button Not aligned from left and right in 767px screen size (fixed in [magento/magento2#20739](https://github.com/magento/magento2/pull/20739)) + * [#19436](https://github.com/magento/magento2/issues/19436) -- Attribute Option with zero at the bigining does not work if there is already option with the same number without the zero (REST API)) (fixed in [magento/magento2#19612](https://github.com/magento/magento2/pull/19612)) + * [#20604](https://github.com/magento/magento2/issues/20604) -- Gift option message overlap edit and remove button (fixed in [magento/magento2#20784](https://github.com/magento/magento2/pull/20784)) + * [#20137](https://github.com/magento/magento2/issues/20137) -- On checkout page apply discount button is not align with input box (fixed in [magento/magento2#20837](https://github.com/magento/magento2/pull/20837)) + * [#20624](https://github.com/magento/magento2/issues/20624) -- `\Magento\ImportExport\Block\Adminhtml\Export\Filter::_getSelectHtmlWithValue()` method overwrites self $value argument (fixed in [magento/magento2#20863](https://github.com/magento/magento2/pull/20863)) + * [#20409](https://github.com/magento/magento2/issues/20409) -- Magento\Catalog\Api\ProductRenderListInterface returns products regardless of visibility (fixed in [magento/magento2#20886](https://github.com/magento/magento2/pull/20886)) + * [#20259](https://github.com/magento/magento2/issues/20259) -- Store switcher not sliding up and down, only dropdown arrow working (fixed in [magento/magento2#20540](https://github.com/magento/magento2/pull/20540)) +* GitHub pull requests: + * [magento/magento2#16361](https://github.com/magento/magento2/pull/16361) -- Allow usage of config-global.php when running Integration Tests (by @jissereitsma) + * [magento/magento2#16422](https://github.com/magento/magento2/pull/16422) -- Replace intval() function by using direct type casting to (int) where no default value is needed (by @mhauri) + * [magento/magento2#17708](https://github.com/magento/magento2/pull/17708) -- Prevent rendering of "Ship here" button if it is not needed (by @marvinhuebner) + * [magento/magento2#17783](https://github.com/magento/magento2/pull/17783) -- Current password autocomplete for admin login (by @flancer64) + * [magento/magento2#17885](https://github.com/magento/magento2/pull/17885) -- Make sure all linked products (related, upsells, crosssells) show up ... (by @hostep) + * [magento/magento2#17891](https://github.com/magento/magento2/pull/17891) -- #17890: show correct text swatch values per store view (by @magicaner) + * [magento/magento2#17919](https://github.com/magento/magento2/pull/17919) -- [remove] rich snippet declaration on grouped product (by @AurelienLavorel) + * [magento/magento2#17945](https://github.com/magento/magento2/pull/17945) -- [2.2] return $this from setters in Analytics/ReportXml/DB/SelectBuilder.php (by @TBlindaruk) + * [magento/magento2#17966](https://github.com/magento/magento2/pull/17966) -- Fix currency symbol setting back to default #17567 (by @magently) + * [magento/magento2#17970](https://github.com/magento/magento2/pull/17970) -- Integration test for swatches types in attribute configuration added (by @rogyar) + * [magento/magento2#17990](https://github.com/magento/magento2/pull/17990) -- Menu does not work when you change from Mobile to Desktop mode #5402 (by @emanuelarcos) + * [magento/magento2#18001](https://github.com/magento/magento2/pull/18001) -- Fixes saving product in single-store mode if website_id <> 1 (by @eduard13) + * [magento/magento2#18002](https://github.com/magento/magento2/pull/18002) -- Fix module uninstall shell command and composer removal w/out regression (by @Thundar) + * [magento/magento2#18004](https://github.com/magento/magento2/pull/18004) -- [CatalogUrlRewrite] Covering the CategoryProcessUrlRewriteMovingObserver by Unit Test (by @eduard13) + * [magento/magento2#18018](https://github.com/magento/magento2/pull/18018) -- [Backport] Use route ID when creating secret keys in backend menus instead of route name #17650 (by @lfolco) + * [magento/magento2#18034](https://github.com/magento/magento2/pull/18034) -- [Backport] fix notice undefined shipment: revert locale inside loop (by @dmytro-ch) + * [magento/magento2#18127](https://github.com/magento/magento2/pull/18127) -- [Backport] typofix: ImportCollection -> ItemCollection (by @dmytro-ch) + * [magento/magento2#18137](https://github.com/magento/magento2/pull/18137) -- [2.2] Update labels section in README.md (by @sidolov) + * [magento/magento2#14065](https://github.com/magento/magento2/pull/14065) -- Correctly convert config integration api resources (by @therool) + * [magento/magento2#17679](https://github.com/magento/magento2/pull/17679) -- Update shipment collection to unserialize packages attribute after load (by @dnsv) + * [magento/magento2#17993](https://github.com/magento/magento2/pull/17993) -- fix #17582 ./bin/magento config:show fails with a fatal error (by @keyurshah070) + * [magento/magento2#18000](https://github.com/magento/magento2/pull/18000) -- Fix sitemap grid render incorrect base urls for multiple stores (by @nntoan) + * [magento/magento2#18055](https://github.com/magento/magento2/pull/18055) -- fix: reset search mini-form when we have no data / an empty response (by @DanielRuf) + * [magento/magento2#18097](https://github.com/magento/magento2/pull/18097) -- [Backport] Fix import grouped products #12853 (by @insanityinside) + * [magento/magento2#18113](https://github.com/magento/magento2/pull/18113) -- [Backport] Fixes from #15947 (by @ihor-sviziev) + * [magento/magento2#18098](https://github.com/magento/magento2/pull/18098) -- Fix shipping discount failed to apply during place order (by @torreytsui) + * [magento/magento2#18126](https://github.com/magento/magento2/pull/18126) -- [Backport] [2.2] Changed intval($val) to (int) $val, since it is faster: (by @dmytro-ch) + * [magento/magento2#17511](https://github.com/magento/magento2/pull/17511) -- Use cast types instead of xyzval() (by @sreichel) + * [magento/magento2#17998](https://github.com/magento/magento2/pull/17998) -- 9830 - Null order in Magento\Sales\Block\Order\PrintShipment.php (by @MateuszChrapek) + * [magento/magento2#17984](https://github.com/magento/magento2/pull/17984) -- Implemeted MAGETWO-81170: Missing $debugHintsPath when sending email ... (by @passtet) + * [magento/magento2#18225](https://github.com/magento/magento2/pull/18225) -- Module Catalog: fix issue with custom option price conversion for different base currency on website level (by @oleksii-lisovyi) + * [magento/magento2#16885](https://github.com/magento/magento2/pull/16885) -- [Fix] Do not modify current list of countries with require states during setup upgrade (by @jalogut) + * [magento/magento2#18086](https://github.com/magento/magento2/pull/18086) -- Cast products "getStoreId()" to int, closes #18079 (by @sreichel) + * [magento/magento2#18215](https://github.com/magento/magento2/pull/18215) -- fix wysiwyg editor not decoding base64 filenames special chars (by @adammada) + * [magento/magento2#18280](https://github.com/magento/magento2/pull/18280) -- [Backport] Change sort order for customer group options (by @dmytro-ch) + * [magento/magento2#18168](https://github.com/magento/magento2/pull/18168) -- Fixed issue with lib-line-height mixin failing when value of 'normal'... (by @CNanninga) + * [magento/magento2#18310](https://github.com/magento/magento2/pull/18310) -- [Backport] Sales: add missing unit tests for model classes (by @dmytro-ch) + * [magento/magento2#18311](https://github.com/magento/magento2/pull/18311) -- [Backport] Added integration test for gift message quote merge (by @dmytro-ch) + * [magento/magento2#17695](https://github.com/magento/magento2/pull/17695) -- ConfigurableProduct show prices in select options (by @alexeya-ven) + * [magento/magento2#17982](https://github.com/magento/magento2/pull/17982) -- add error message in else condition (by @vaibhavahalpara) + * [magento/magento2#18354](https://github.com/magento/magento2/pull/18354) -- Fix for parsing attribute options labels, when & used. (by @bartoszkubicki) + * [magento/magento2#17882](https://github.com/magento/magento2/pull/17882) -- Do not overwrite URL Key with blank value (by @josephmcdermott) + * [magento/magento2#17986](https://github.com/magento/magento2/pull/17986) -- Implemented 17964: Backend Order creation Authorizenet: If invalid cr... (by @passtet) + * [magento/magento2#18283](https://github.com/magento/magento2/pull/18283) -- [Backport] Fix for removing the dirs while creating a TAR archive (by @haroldclaus) + * [magento/magento2#18369](https://github.com/magento/magento2/pull/18369) -- [Backport] Fix throwing error by checkout error processor model (by @ihor-sviziev) + * [magento/magento2#18375](https://github.com/magento/magento2/pull/18375) -- Backport 2.2 - Fix wrong reference in google analytics module layout xml (by @sambolek) + * [magento/magento2#18377](https://github.com/magento/magento2/pull/18377) -- [Backport 2.2-develop] Refactor Mass Order Cancel code to use Interface (by @JeroenVanLeusden) + * [magento/magento2#18376](https://github.com/magento/magento2/pull/18376) -- Backport 2.2 - Fix issue 17152 - prevent email being marked as not se... (by @sambolek) + * [magento/magento2#18391](https://github.com/magento/magento2/pull/18391) -- Backport 2.2 - Allow keyboard navigation in browser on product detail... (by @hostep) + * [magento/magento2#18400](https://github.com/magento/magento2/pull/18400) -- Admin Login Form > Aliging Label (by @rafaelstz) + * [magento/magento2#18414](https://github.com/magento/magento2/pull/18414) -- [Backport] Fix the issue with customer inline edit when password is expired (by @dmytro-ch) + * [magento/magento2#18415](https://github.com/magento/magento2/pull/18415) -- [Backport] Added unit test for CRON converter plugin (by @dmytro-ch) + * [magento/magento2#18426](https://github.com/magento/magento2/pull/18426) -- [Backport] Removed unnecessary characters from comments (by @lewisvoncken) + * [magento/magento2#18428](https://github.com/magento/magento2/pull/18428) -- [Backport] small misspelling fixed (by @lewisvoncken) + * [magento/magento2#18429](https://github.com/magento/magento2/pull/18429) -- [Backport] Fix documentation grammar errors and typos in actions.js (by @lewisvoncken) + * [magento/magento2#18430](https://github.com/magento/magento2/pull/18430) -- [Backport] Fix documentation typos in registry.js (by @lewisvoncken) + * [magento/magento2#18433](https://github.com/magento/magento2/pull/18433) -- [Backport] Improve code quality subscriber new action (by @lewisvoncken) + * [magento/magento2#18432](https://github.com/magento/magento2/pull/18432) -- [Backport] Removed commented code (by @lewisvoncken) + * [magento/magento2#17823](https://github.com/magento/magento2/pull/17823) -- [FEATURE] [issue-3283] Added Filter Support for Yes/No (boolean) attr... (by @lewisvoncken) + * [magento/magento2#18175](https://github.com/magento/magento2/pull/18175) -- Fix category tree in cart price rule #17493 (by @magently) + * [magento/magento2#18166](https://github.com/magento/magento2/pull/18166) -- Fix table rate failing for zip+4 address #17770 (by @magently) + * [magento/magento2#18389](https://github.com/magento/magento2/pull/18389) -- Backport 2.2 - Introducing a dedicated cron.log file for logging cron... (by @hostep) + * [magento/magento2#18390](https://github.com/magento/magento2/pull/18390) -- Backport 2.2 - Don't set a source model on the attribute when it's no... (by @hostep) + * [magento/magento2#18422](https://github.com/magento/magento2/pull/18422) -- [BACKPORT] Replace sort callbacks to spaceship operator (by @lewisvoncken) + * [magento/magento2#18403](https://github.com/magento/magento2/pull/18403) -- Fix setup wizard page logo (by @rafaelstz) + * [magento/magento2#18425](https://github.com/magento/magento2/pull/18425) -- [Backport] Fixing Snake Case To Camel Case (by @lewisvoncken) + * [magento/magento2#18427](https://github.com/magento/magento2/pull/18427) -- [Backport] Fix wrong return type in StockRegistryInterface API (by @lewisvoncken) + * [magento/magento2#15683](https://github.com/magento/magento2/pull/15683) -- Added checks to see if the payment is available (by @michielgerritsen) + * [magento/magento2#15905](https://github.com/magento/magento2/pull/15905) -- #4942 and bundle checkbox bug (by @JosephMaxwell) + * [magento/magento2#16115](https://github.com/magento/magento2/pull/16115) -- Fix type hint of customer-data updateSectionId parameters (by @Vinai) + * [magento/magento2#17516](https://github.com/magento/magento2/pull/17516) -- Feature australian regions (by @maximbaibakov) + * [magento/magento2#18155](https://github.com/magento/magento2/pull/18155) -- Fix type hint of @message declaration as the "setWidgetParameters" method allows arrays too (by @avstudnitz) + * [magento/magento2#18401](https://github.com/magento/magento2/pull/18401) -- Admin > Footer > Aligning Proportionally (by @rafaelstz) + * [magento/magento2#17968](https://github.com/magento/magento2/pull/17968) -- Fix Customer custom attributes lost after save (by @Thundar) + * [magento/magento2#18196](https://github.com/magento/magento2/pull/18196) -- Fix for custom product attribute changing 'backend_type' when 'is_user_defined = 1' and get updated/saved in Admin Backend (by @bartoszkubicki) + * [magento/magento2#18495](https://github.com/magento/magento2/pull/18495) -- [Backport] Checkout - Fix "Cannot read property 'code' on undefined" issue (by @ihor-sviziev) + * [magento/magento2#18552](https://github.com/magento/magento2/pull/18552) -- [Backport] Added validation on maximum quantity allowed in shopping cart (by @gelanivishal) + * [magento/magento2#18554](https://github.com/magento/magento2/pull/18554) -- [Backport] throw exception InvalidArgumentException during validate scheme (by @gelanivishal) + * [magento/magento2#18556](https://github.com/magento/magento2/pull/18556) -- [Backport] Fixed typo from filed to field (by @gelanivishal) + * [magento/magento2#18559](https://github.com/magento/magento2/pull/18559) -- [Backport] Covering the AssignOrderToCustomerObserver by Unit Test (by @gelanivishal) + * [magento/magento2#18564](https://github.com/magento/magento2/pull/18564) -- [Backport] Empty option Label should always be blank even if attribute is required (by @gelanivishal) + * [magento/magento2#18561](https://github.com/magento/magento2/pull/18561) -- [2.2] added component status based filtering (by @gelanivishal) + * [magento/magento2#18569](https://github.com/magento/magento2/pull/18569) -- [Backport] Make it possible to disable report bugs link (by @gelanivishal) + * [magento/magento2#18587](https://github.com/magento/magento2/pull/18587) -- [Backport] Prevent XSS on checkout (by @dmytro-ch) + * [magento/magento2#18586](https://github.com/magento/magento2/pull/18586) -- [Backport] Added missing throw tag for exception to docblock of construct (by @dmytro-ch) + * [magento/magento2#18593](https://github.com/magento/magento2/pull/18593) -- Calendar icon in advance pricing alignment solved (by @speedy008) + * [magento/magento2#18595](https://github.com/magento/magento2/pull/18595) -- [Backport] Fix disappearing navigation arrows in fotorama zoom (by @luukschakenraad) + * [magento/magento2#18599](https://github.com/magento/magento2/pull/18599) -- [Backport] Do not use new Phrase in Link Current class (by @dmytro-ch) + * [magento/magento2#18619](https://github.com/magento/magento2/pull/18619) -- [Backport] Add required fields to templates (by @miguelbalparda) + * [magento/magento2#18656](https://github.com/magento/magento2/pull/18656) -- [Backport] Fix product details causing Validation error (by @gelanivishal) + * [magento/magento2#18657](https://github.com/magento/magento2/pull/18657) -- [Backport] Create empty modelData array to avoid undefined var error (by @gelanivishal) + * [magento/magento2#18659](https://github.com/magento/magento2/pull/18659) -- [Backport] Fix for #12969 - server port detection for errors (by @gelanivishal) + * [magento/magento2#18662](https://github.com/magento/magento2/pull/18662) -- [Backport] move hardcoded MIME types from class private to DI configuration (by @gelanivishal) + * [magento/magento2#16915](https://github.com/magento/magento2/pull/16915) -- magento/magento2#14510: Creating custom customer attribute with default value 0 will cause not saving value for customer entity. (by @swnsma) + * [magento/magento2#18563](https://github.com/magento/magento2/pull/18563) -- [Backport] Update CategoryProcessor.php (by @gelanivishal) + * [magento/magento2#18566](https://github.com/magento/magento2/pull/18566) -- Module Catalog URL Rewrite: fix issue with product URL Rewrites re-generation after changing product URL Key for product with existing url_path attribute value (by @oleksii-lisovyi) + * [magento/magento2#18670](https://github.com/magento/magento2/pull/18670) -- Remove unnecessary class import, see #18280 (by @sreichel) + * [magento/magento2#18658](https://github.com/magento/magento2/pull/18658) -- [Backport] MAGENTO-18131: Fixed EAV attributes values query (by @gelanivishal) + * [magento/magento2#15366](https://github.com/magento/magento2/pull/15366) -- 15259 : Unable to disable without providing Industry value (by @sunilit42) + * [magento/magento2#18424](https://github.com/magento/magento2/pull/18424) -- [BACKPORT] type casted $qty to float in \Magento\Catalog\Model\Produc... (by @lewisvoncken) + * [magento/magento2#18660](https://github.com/magento/magento2/pull/18660) -- [Backport] Fix of saving "clone_field" fields (by @gelanivishal) + * [magento/magento2#18758](https://github.com/magento/magento2/pull/18758) -- [Backport] Fix the typo in PHPDoc comment (by @dmytro-ch) + * [magento/magento2#18535](https://github.com/magento/magento2/pull/18535) -- Fixed issues-18534: 2 wysiwyg on catalog category edit page (by @k1las) + * [magento/magento2#18597](https://github.com/magento/magento2/pull/18597) -- [Backport] Fix empty cart button (by @luukschakenraad) + * [magento/magento2#18604](https://github.com/magento/magento2/pull/18604) -- Fixed Issue: Special price of 0.0000 is not shown on frontend, but is calculated in cart (by @maheshWebkul721) + * [magento/magento2#18643](https://github.com/magento/magento2/pull/18643) -- Fix customer unsubscribed issue (by @janakbhimani) + * [magento/magento2#18759](https://github.com/magento/magento2/pull/18759) -- [Backport] Backend: add missing unit test for ModuleService class (by @dmytro-ch) + * [magento/magento2#16940](https://github.com/magento/magento2/pull/16940) -- Resolve incorrect scope code selection when the requested scopeCode is null (by @matthew-muscat) + * [magento/magento2#18737](https://github.com/magento/magento2/pull/18737) -- [BUGFIX] GITHUB-18264 Backport of #17799 for the 2.2 branch (by @kanduvisla) + * [magento/magento2#17971](https://github.com/magento/magento2/pull/17971) -- Don't format Special Price value for Bundle Product (by @magently) + * [magento/magento2#18681](https://github.com/magento/magento2/pull/18681) -- [Backport] Set fallback values for email and _website columns to avoid 'undefined index' error in CustomerComposite.php (by @TomashKhamlai) + * [magento/magento2#18833](https://github.com/magento/magento2/pull/18833) -- [Backport] Cover \Magento\GiftMessage\Observer\SalesEventQuoteMerge with Unit test (by @vasilii-b) + * [magento/magento2#18834](https://github.com/magento/magento2/pull/18834) -- [Backport] Cover \Magento\Email\Model\Template\SenderResolver class with Unit test (by @vasilii-b) + * [magento/magento2#18835](https://github.com/magento/magento2/pull/18835) -- [Backport] Added Unit Test for WindowsSmtpConfig Plugin (by @vasilii-b) + * [magento/magento2#18876](https://github.com/magento/magento2/pull/18876) -- [Backport] Fix Useless use of Cat (by @gelanivishal) + * [magento/magento2#18591](https://github.com/magento/magento2/pull/18591) -- [Backport] Fix SKU limit in import new products (by @ravi-chandra3197) + * [magento/magento2#18862](https://github.com/magento/magento2/pull/18862) -- [Backport] Adding trimming sku value function to sku backend model. (by @gelanivishal) + * [magento/magento2#18865](https://github.com/magento/magento2/pull/18865) -- fixed issue #18458 : Alert widget gets close when click anywhere on screen #18576 (by @Shubham-Webkul) + * [magento/magento2#18886](https://github.com/magento/magento2/pull/18886) -- [Backport] fixed Translation issue send-friend in send.phtml (by @rahulwebkul) + * [magento/magento2#18917](https://github.com/magento/magento2/pull/18917) -- Fixed-Global-search icon misaligned (by @speedy008) + * [magento/magento2#17978](https://github.com/magento/magento2/pull/17978) -- #17488 Fix Authenticating a customer via REST API does not update the last logged in data (by @prakashpatel07) + * [magento/magento2#18287](https://github.com/magento/magento2/pull/18287) -- Ensure integer values are not quoted as strings (by @udovicic) + * [magento/magento2#18874](https://github.com/magento/magento2/pull/18874) -- [Backport] Fixed issue #4468 "Unable to insert multiple catalog product list wid... (by @gelanivishal) + * [magento/magento2#18372](https://github.com/magento/magento2/pull/18372) -- Resolve typo despatch event (by @neeta-wagento) + * [magento/magento2#18863](https://github.com/magento/magento2/pull/18863) -- [Backport] #17744 Adding logic to get default billing address used on Cart and Checkout (by @gelanivishal) + * [magento/magento2#18872](https://github.com/magento/magento2/pull/18872) -- [Backport] Allow set billing information via API with existing address (by @gelanivishal) + * [magento/magento2#18870](https://github.com/magento/magento2/pull/18870) -- [Backport] ISSUE-5021 - fixed place order for custom shipping methods with under... (by @gelanivishal) + * [magento/magento2#18875](https://github.com/magento/magento2/pull/18875) -- [Backport] Sections LESS mixins: fix the issue with missing rules and incorrect default variables (by @gelanivishal) + * [magento/magento2#18873](https://github.com/magento/magento2/pull/18873) -- [Backport] Prevent exception when option text converts to false (by @gelanivishal) + * [magento/magento2#18967](https://github.com/magento/magento2/pull/18967) -- fixed - Magento 2.2.6 Default values are not rendering on Wishlist product edit page (by @webkul-ratnesh) + * [magento/magento2#18908](https://github.com/magento/magento2/pull/18908) -- [Backport] fixed - Unable to select payment method according to country of the address at checkout time (by @rahulwebkul) + * [magento/magento2#18984](https://github.com/magento/magento2/pull/18984) -- [Backport] Reload cart totals when cart data changes (by @tdgroot) + * [magento/magento2#16887](https://github.com/magento/magento2/pull/16887) -- Fix blocked a frame with origin (by @iGerchak) + * [magento/magento2#18857](https://github.com/magento/magento2/pull/18857) -- Fixed - Default tax region/state appears in customer & order data #16684 (by @ssp58bleuciel) + * [magento/magento2#18964](https://github.com/magento/magento2/pull/18964) -- Backport [PR 18772] Remove unnecesary "header" block redeclaration (by @samuel27m) + * [magento/magento2#19012](https://github.com/magento/magento2/pull/19012) -- #18348 - In admin, last swatch option set to default upon save (by @RostislavS) + * [magento/magento2#19036](https://github.com/magento/magento2/pull/19036) -- magento/magento2#18323: Order confirmation email for guest checkout d... (by @swnsma) + * [magento/magento2#18985](https://github.com/magento/magento2/pull/18985) -- [Backport] Added form fieldset before html data to \Magento\Framework\Data\Form\Element\Fieldset in getElementHtml() method (by @vasilii-b) + * [magento/magento2#19002](https://github.com/magento/magento2/pull/19002) -- [Backport] Remove duplicated CSS selector (by @dmytro-ch) + * [magento/magento2#19044](https://github.com/magento/magento2/pull/19044) -- [2.2-develop] magento/magento2#14007: "Use in Layered Navigation: Filterable (no results)" property confuse for Price filter (by @vpodorozh) + * [magento/magento2#19074](https://github.com/magento/magento2/pull/19074) -- [Backport] Fix for #12399: Exception Error in Catalog Price Rule while Backend language is not English (by @Mardl) + * [magento/magento2#18461](https://github.com/magento/magento2/pull/18461) -- fix Fatal Error when save configurable product in Magento 2.2.5 #18082 (by @thiagolima-bm) + * [magento/magento2#18649](https://github.com/magento/magento2/pull/18649) -- [Backport] Issue Fixed: Missing Fixed Product Tax total on PDF (by @maheshWebkul721) + * [magento/magento2#18815](https://github.com/magento/magento2/pull/18815) -- [Backoport] Issue Fixed: Backups error from User Roles Permission 2.2.6 (by @maheshWebkul721) + * [magento/magento2#19073](https://github.com/magento/magento2/pull/19073) -- magento/magento2#19071: Password strength indicator shows No Password... (by @dimasalamatov) + * [magento/magento2#19089](https://github.com/magento/magento2/pull/19089) -- magento/magento#18901: Forgot password form should not available while customer is logged in. (by @swnsma) + * [magento/magento2#19105](https://github.com/magento/magento2/pull/19105) -- magento/magento2#18840: Invalid Unit Test Annotations. (by @swnsma) + * [magento/magento2#19110](https://github.com/magento/magento2/pull/19110) -- [Backport] Add additional check if password hash is empty in auth process (by @agorbulin) + * [magento/magento2#14914](https://github.com/magento/magento2/pull/14914) -- FIX for issue #14849 - In Sales Emails no translation using order.getStatusLabel() (by @phoenix128) + * [magento/magento2#17854](https://github.com/magento/magento2/pull/17854) -- Fix translations of category design theme not being applied (by @cezary-zeglen) + * [magento/magento2#17915](https://github.com/magento/magento2/pull/17915) -- Fix/add expresion (by @magently) + * [magento/magento2#18743](https://github.com/magento/magento2/pull/18743) -- Fixed tierprice discount not calculated correctly if has specialprice (by @gelanivishal) + * [magento/magento2#18959](https://github.com/magento/magento2/pull/18959) -- fixed js translation (by @torhoehn) + * [magento/magento2#19118](https://github.com/magento/magento2/pull/19118) -- [Backport] Add/update newsletter messages in translation file (by @arnoudhgz) + * [magento/magento2#17889](https://github.com/magento/magento2/pull/17889) -- Fixed child items showing on My Account order view (by @rogyar) + * [magento/magento2#19113](https://github.com/magento/magento2/pull/19113) -- [2.2 backport] fix cipherMethod detection for openssl 1.1.1 (by @BlackIkeEagle) + * [magento/magento2#16342](https://github.com/magento/magento2/pull/16342) -- #14020-Cart-Sales-Rule-with-negated-condition-over-special-price-does... (by @novikor) + * [magento/magento2#18808](https://github.com/magento/magento2/pull/18808) -- fixed Quote Item Prices are NULL in cart related events. #18685 (by @ashutoshwebkul) + * [magento/magento2#19216](https://github.com/magento/magento2/pull/19216) -- [Backport] Covering the \Magento\Weee observers by Unit Tests (by @eduard13) + * [magento/magento2#19217](https://github.com/magento/magento2/pull/19217) -- [Backport] Covering the CheckUserLoginBackendObserver by Unit Test (by @eduard13) + * [magento/magento2#19237](https://github.com/magento/magento2/pull/19237) -- [Backport] #18956 Fixes for set root_category_id (by @gelanivishal) + * [magento/magento2#19240](https://github.com/magento/magento2/pull/19240) -- [Backport] Add missing unit test for WishlistSettings plugin (by @gelanivishal) + * [magento/magento2#19260](https://github.com/magento/magento2/pull/19260) -- Issue #19205 Fixed: Bundle Product Option with input type is checkbox and add to cart with 3 values only 2 values added to cart. (by @maheshWebkul721) + * [magento/magento2#18642](https://github.com/magento/magento2/pull/18642) -- [Backport] Fix issue with unexpected changing of subscription status after customer saving (by @alexeya-ven) + * [magento/magento2#18951](https://github.com/magento/magento2/pull/18951) -- Magento 2.2 Fix Product::addImageToMediaGallery throws Exception (by @progreg) + * [magento/magento2#18960](https://github.com/magento/magento2/pull/18960) -- local themes should be added to git repo (by @torhoehn) + * [magento/magento2#19068](https://github.com/magento/magento2/pull/19068) -- Using Media Image custom attribute type could not display on frontend. #19054 (by @Nazar65) + * [magento/magento2#19337](https://github.com/magento/magento2/pull/19337) -- [Backport] 19082-Fatal-error-Uncaught-Error-Cannot-call-abstract-method-Magento-... (by @agorbulin) + * [magento/magento2#19336](https://github.com/magento/magento2/pull/19336) -- [Backport] small performance improvement on product listing (by @gelanivishal) + * [magento/magento2#19338](https://github.com/magento/magento2/pull/19338) -- [Backport] missing use statement in layout generator (by @gelanivishal) + * [magento/magento2#19340](https://github.com/magento/magento2/pull/19340) -- [Backport] Fix the issue: Content overlaps the close button #19263 (by @gelanivishal) + * [magento/magento2#14485](https://github.com/magento/magento2/pull/14485) -- Fix for Issue #4136, MAGETWO-53440 (by @vasilii-b) + * [magento/magento2#18621](https://github.com/magento/magento2/pull/18621) -- 18615 updates structure for last_trans_id to be varchar 255 which is ... (by @iancassidyweb) + * [magento/magento2#18905](https://github.com/magento/magento2/pull/18905) -- Fix the issue with missing asterisk for admin required fields (by @dmytro-ch) + * [magento/magento2#19296](https://github.com/magento/magento2/pull/19296) -- Fix issue 19286 - Wrong pager style (by @speedy008) + * [magento/magento2#19355](https://github.com/magento/magento2/pull/19355) -- [Backport] Changed get product way in blocks with related products (by @gelanivishal) + * [magento/magento2#19357](https://github.com/magento/magento2/pull/19357) -- [Backport] #13157 - Last Ordered Items block - bad js code (by @gelanivishal) + * [magento/magento2#19023](https://github.com/magento/magento2/pull/19023) -- [2.2 develop] [backport #19018] [issue #17833] child theme does not inherit translations from parent theme (by @vpodorozh) + * [magento/magento2#19358](https://github.com/magento/magento2/pull/19358) -- [Backport] Fix the issue with repetitive "tbody" tag for order items table (by @gelanivishal) + * [magento/magento2#19365](https://github.com/magento/magento2/pull/19365) -- Fixing a test for Magento Newsletter. (by @tiagosampaio) + * [magento/magento2#18899](https://github.com/magento/magento2/pull/18899) -- [Backport] fixed - can't import external http to https redirecting images by default csv import (by @rahulwebkul) + * [magento/magento2#19356](https://github.com/magento/magento2/pull/19356) -- [Backport] Magento backend Notifications counter round icon small cut from right side (by @gelanivishal) + * [magento/magento2#19364](https://github.com/magento/magento2/pull/19364) -- [Backport] fix: remove old code in tabs, always set tabindex to 0 when tabs are ... (by @DanielRuf) + * [magento/magento2#19374](https://github.com/magento/magento2/pull/19374) -- back-port-pull-19024 (by @agorbulin) + * [magento/magento2#19014](https://github.com/magento/magento2/pull/19014) -- [Backport] #17813 - Huge "product_data_storage" in localStorage hangs the shop (by @omiroshnichenko) + * [magento/magento2#19398](https://github.com/magento/magento2/pull/19398) -- [Backport-2.2] Code generation improvement for php 7.1 (by @swnsma) + * [magento/magento2#19422](https://github.com/magento/magento2/pull/19422) -- Fix for incorrectly escapeHtml'd JSON in commit b8f78cc6 (by @insanityinside) + * [magento/magento2#19426](https://github.com/magento/magento2/pull/19426) -- [Backport] Fixing the customer subscribing from different stores (by @eduard13) + * [magento/magento2#19427](https://github.com/magento/magento2/pull/19427) -- [Backport] Adding integration tests for wrong captcha (by @eduard13) + * [magento/magento2#18922](https://github.com/magento/magento2/pull/18922) -- Fixed 18918 Asterisk sign display twice (by @suryakant-krish) + * [magento/magento2#19239](https://github.com/magento/magento2/pull/19239) -- [Backport] Allow to read HTTP/2 response header. (by @gelanivishal) + * [magento/magento2#19430](https://github.com/magento/magento2/pull/19430) -- Fixed issue with Base Currency for website is CND when PayPal Payflow Pro is charging in USD (by @Rykh) + * [magento/magento2#19431](https://github.com/magento/magento2/pull/19431) -- [Backport] Sample Link Issue in Downloadable product in magento-2.2.6 #19344 (by @ansari-krish) + * [magento/magento2#19447](https://github.com/magento/magento2/pull/19447) -- [Backport] chore: remove unused code in admin view of catalog (by @DanielRuf) + * [magento/magento2#19145](https://github.com/magento/magento2/pull/19145) -- Add availability to leave empty config for events.xml (by @lisovyievhenii) + * [magento/magento2#19568](https://github.com/magento/magento2/pull/19568) -- [Backport] [Newsletter] #19418 Cannot add additional field to system configuration at desired position (by @vasilii-b) + * [magento/magento2#19678](https://github.com/magento/magento2/pull/19678) -- [Backport] Fix: SalesQuoteSaveAfterObserver fails to update the checkout session quote id when applicable (by @dmytro-ch) + * [magento/magento2#19668](https://github.com/magento/magento2/pull/19668) -- [Backport] style: change b to strong (a11y) (by @DanielRuf) + * [magento/magento2#19669](https://github.com/magento/magento2/pull/19669) -- [Backport] fix: remove unused params in categorySubmit invocation (by @DanielRuf) + * [magento/magento2#19804](https://github.com/magento/magento2/pull/19804) -- [Backport]Fix issue 19796 - Sales Order invoice Update Qty's Button is misaligned (by @speedy008) + * [magento/magento2#19949](https://github.com/magento/magento2/pull/19949) -- [Backport] Fixed Issue #19917 Changed allowDrug to allowDrag (by @maheshWebkul721) + * [magento/magento2#19967](https://github.com/magento/magento2/pull/19967) -- [Backport] Minor typos corrected. (by @milindsingh) + * [magento/magento2#19970](https://github.com/magento/magento2/pull/19970) -- [Backport] Typo taax -> tax (by @milindsingh) + * [magento/magento2#19968](https://github.com/magento/magento2/pull/19968) -- [Backport] Typo "customet_id" to "customer_id" fixed. (by @milindsingh) + * [magento/magento2#19972](https://github.com/magento/magento2/pull/19972) -- [Backport] Update bootstrap.js (by @milindsingh) + * [magento/magento2#19971](https://github.com/magento/magento2/pull/19971) -- [Backport] Typo corrected Update bound-nodes.js (by @milindsingh) + * [magento/magento2#18912](https://github.com/magento/magento2/pull/18912) -- [Backport] Fixed subscribe to newsletter if you already have an account issue (by @ravi-chandra3197) + * [magento/magento2#19199](https://github.com/magento/magento2/pull/19199) -- [Backport][2.2] Made logo clickable on home page (by @gwharton) + * [magento/magento2#19280](https://github.com/magento/magento2/pull/19280) -- [BackPort] resolve typos and correct variable names (by @viral-wagento) + * [magento/magento2#19690](https://github.com/magento/magento2/pull/19690) -- [Backport] Additional Cache Management title (by @thomas-blackbird) + * [magento/magento2#19693](https://github.com/magento/magento2/pull/19693) -- [Backport] Cancel expired orders using OrderManagementInterface (by @JeroenVanLeusden) + * [magento/magento2#19911](https://github.com/magento/magento2/pull/19911) -- [Backport] fixed store wise product filter issue (by @shikhamis11) + * [magento/magento2#19945](https://github.com/magento/magento2/pull/19945) -- [Backport] issue 18941 (by @Nazar65) + * [magento/magento2#19056](https://github.com/magento/magento2/pull/19056) -- Fix issue 19052- Position order showing before the text box (by @speedy008) + * [magento/magento2#19910](https://github.com/magento/magento2/pull/19910) -- [Backport] fixed Notification page Select Visible items issue (by @shikhamis11) + * [magento/magento2#19889](https://github.com/magento/magento2/pull/19889) -- [Backport]Fix issue 19507 - Frontend Minicart dropdown alignment issue (by @speedy008) + * [magento/magento2#19928](https://github.com/magento/magento2/pull/19928) -- [Backport] [Review] Integration tests for not allowed review submission (by @eduard13) + * [magento/magento2#19989](https://github.com/magento/magento2/pull/19989) -- [Backport] Fixed #19605 Don't static compile disabled modules (by @shikhamis11) + * [magento/magento2#20081](https://github.com/magento/magento2/pull/20081) -- [Backport] Fixed issue - #19346 Import data 2.2.6 Value for 'product_type' attribute contains incorrect value (by @GovindaSharma) + * [magento/magento2#20080](https://github.com/magento/magento2/pull/20080) -- [Backport] Fixed Incorrect class name on Orders and returns page. (by @shikhamis11) + * [magento/magento2#20083](https://github.com/magento/magento2/pull/20083) -- [Backport] fixed issue #19925 Close button overlapping in shipping address label whenever any user adding new shipping address in mobile view in checkout (by @GovindaSharma) + * [magento/magento2#19423](https://github.com/magento/magento2/pull/19423) -- Fixed bug, when exception occurred on order with coupons cancel, made by guest after creating of customer account. (by @Winfle) + * [magento/magento2#19927](https://github.com/magento/magento2/pull/19927) -- [Backport] [Framework] New Link is not correctly shown as Current if contains default parts (by @eduard13) + * [magento/magento2#20082](https://github.com/magento/magento2/pull/20082) -- [Backport] issue resolved:Undefined Variable $itemsOrderItemId (by @milindsingh) + * [magento/magento2#20208](https://github.com/magento/magento2/pull/20208) -- magento/magento2:#19101 - API REST and Reserved Order Id (by @saphaljha) + * [magento/magento2#20219](https://github.com/magento/magento2/pull/20219) -- Changes-Hamburger-Icon-was-available-on-a-page (by @amol2jcommerce) + * [magento/magento2#20178](https://github.com/magento/magento2/pull/20178) -- magento/magento2#16198: Category image remain after deleted. (by @p-bystritsky) + * [magento/magento2#20183](https://github.com/magento/magento2/pull/20183) -- 2.2 develop pr port 18888 (by @saphaljha) + * [magento/magento2#20185](https://github.com/magento/magento2/pull/20185) -- [Backport] Move website_name column into columnSet (by @mage2pratik) + * [magento/magento2#20271](https://github.com/magento/magento2/pull/20271) -- [Backport] Use the new json serializer which throws an error when failing (by @quisse) + * [magento/magento2#20286](https://github.com/magento/magento2/pull/20286) -- [Backport] Don't return categoryId from registry if the product doesn't belong in the current category (by @GovindaSharma) + * [magento/magento2#20298](https://github.com/magento/magento2/pull/20298) -- ISSUE-20296: "@magentoDataIsolation" is used instead of "@magentoDbIsolation" in some integration tests. (by @p-bystritsky) + * [magento/magento2#20325](https://github.com/magento/magento2/pull/20325) -- [Backport] issus fixed #20158 Store switcher not aligned proper in tab view (by @shikhamis11) + * [magento/magento2#20328](https://github.com/magento/magento2/pull/20328) -- [Backport] Fix issue 20232 : Backend order credit card detail check box misaligned (by @GovindaSharma) + * [magento/magento2#20329](https://github.com/magento/magento2/pull/20329) -- [Backport] Product image failure when importing through CSV #20098 (by @irajneeshgupta) + * [magento/magento2#20353](https://github.com/magento/magento2/pull/20353) -- Fixed#20352: displaying html content for file type option on order view admin area (by @maheshWebkul721) + * [magento/magento2#19964](https://github.com/magento/magento2/pull/19964) -- [Backport] Fix the issue with reset password when customer has address from not allowed country (by @dmytro-ch) + * [magento/magento2#19984](https://github.com/magento/magento2/pull/19984) -- [Backport] Remove unneeded, also mistyped, saveHandler from CatalogSearch indexer declaration (by @dmytro-ch) + * [magento/magento2#20206](https://github.com/magento/magento2/pull/20206) -- 9130 remove the negative qty block. (by @saphaljha) + * [magento/magento2#20322](https://github.com/magento/magento2/pull/20322) -- issue #19609 Fixed for 2.2-develop (by @maheshWebkul721) + * [magento/magento2#19400](https://github.com/magento/magento2/pull/19400) -- [Backport]Fix-issue-19399-Add product customization option collapsible design issue (by @speedy008) + * [magento/magento2#20272](https://github.com/magento/magento2/pull/20272) -- Fixed-Review-Details-Detailed-Rating-misaligned (by @amol2jcommerce) + * [magento/magento2#20369](https://github.com/magento/magento2/pull/20369) -- 'Fixes-for-customer-login-page-input-field' :: On customer login page... (by @nainesh2jcommerce) + * [magento/magento2#20375](https://github.com/magento/magento2/pull/20375) -- [Backport] [Forwardport]Fix issue 19902 - Store View label and Dropdown misaligned (by @speedy008) + * [magento/magento2#20433](https://github.com/magento/magento2/pull/20433) -- [Backport] Missing echo of php vars in widget template file - tabshoriz.phtml (by @irajneeshgupta) + * [magento/magento2#20439](https://github.com/magento/magento2/pull/20439) -- [Backport] Meassage icon is not proper aligned (by @saphaljha) + * [magento/magento2#19377](https://github.com/magento/magento2/pull/19377) -- Back port pull #19094 (by @agorbulin) + * [magento/magento2#18362](https://github.com/magento/magento2/pull/18362) -- [Backport] fix(Webapi Xml Renderer - 18361): removed the not needed ampersand re... (by @nickshatilo) + * [magento/magento2#20184](https://github.com/magento/magento2/pull/20184) -- [Backport] Fix issue 19887 creating new shipment: getting all trackers. (by @mage2pratik) + * [magento/magento2#20505](https://github.com/magento/magento2/pull/20505) -- [Backport] Added constants to unit codes to make it easier to reuse it if necessary (by @mageprince) + * [magento/magento2#20509](https://github.com/magento/magento2/pull/20509) -- [Backport] Added required error message. (by @mageprince) + * [magento/magento2#20522](https://github.com/magento/magento2/pull/20522) -- [Backport] Add useful debug info for which website has not been found (by @mageprince) + * [magento/magento2#20541](https://github.com/magento/magento2/pull/20541) -- [Backport] Issue fixed #19985 Send email confirmation popup close button area ov... (by @irajneeshgupta) + * [magento/magento2#20284](https://github.com/magento/magento2/pull/20284) -- [Backport] Fix issue causing attribute not loading when using getList (by @GovindaSharma) + * [magento/magento2#20455](https://github.com/magento/magento2/pull/20455) -- [Backport] Fixed 19800 Contact us : design improvement (by @suryakant-krish) + * [magento/magento2#20456](https://github.com/magento/magento2/pull/20456) -- [Backport] Fixed 19791: Logo vertical misalignment. (by @suryakant-krish) + * [magento/magento2#20457](https://github.com/magento/magento2/pull/20457) -- [Backport] Area Frontend: Fixed checkbox alignment account information page. (by @suryakant-krish) + * [magento/magento2#20177](https://github.com/magento/magento2/pull/20177) -- magento/magento2#15950: Magento2 CSV product import qty and is_in_stock not working correct. (by @p-bystritsky) + * [magento/magento2#20508](https://github.com/magento/magento2/pull/20508) -- [Backport] Fix negative credit memo #19899 (by @mageprince) + * [magento/magento2#20547](https://github.com/magento/magento2/pull/20547) -- [Backport] Fixed Issue #20121 Cancel order increases stock although "Set Items' Status to be In Stock When Order is Cancelled" is set to No (by @irajneeshgupta) + * [magento/magento2#20636](https://github.com/magento/magento2/pull/20636) -- [Backport] Fix issue with file uploading if an upload field is disabled (by @serhiyzhovnir) + * [magento/magento2#20638](https://github.com/magento/magento2/pull/20638) -- [Backport] Floating point overflows in checkout totals fixed (by @shikhamis11) + * [magento/magento2#20647](https://github.com/magento/magento2/pull/20647) -- [Backport] fixed Negative order amount in dashboard (by @amol2jcommerce) + * [magento/magento2#20542](https://github.com/magento/magento2/pull/20542) -- [Backport] Order API resources updated. #20169 (by @irajneeshgupta) + * [magento/magento2#20544](https://github.com/magento/magento2/pull/20544) -- [Backport] 'wishlist-page-edit-remove-item-misalign' :: On wish list page edit, ... (by @irajneeshgupta) + * [magento/magento2#20546](https://github.com/magento/magento2/pull/20546) -- [Backport] Order-view-invoices :: Order view invoices template not display prope... (by @irajneeshgupta) + * [magento/magento2#20685](https://github.com/magento/magento2/pull/20685) -- [Backport] update-button-issue-while-updating-billing-and-shipping-address (by @cmtickle) + * [magento/magento2#18809](https://github.com/magento/magento2/pull/18809) -- [Backport] catalog:images:resize total images count calculates incorrectly #18387 (by @vpodorozh) + * [magento/magento2#19461](https://github.com/magento/magento2/pull/19461) -- [Backport 2.2] issue #18931 fixed. (by @JeroenVanLeusden) + * [magento/magento2#19655](https://github.com/magento/magento2/pull/19655) -- Fixed - Shipping issue on PayPal Express #14712 (by @ssp58bleuciel) + * [magento/magento2#20285](https://github.com/magento/magento2/pull/20285) -- [Backport]#20222 Canary islands in ups carrier 2.2 (by @duckchip) + * [magento/magento2#20270](https://github.com/magento/magento2/pull/20270) -- [Backport] Fixed-Widget-option-labels-are-misalinged (by @amol2jcommerce) + * [magento/magento2#20418](https://github.com/magento/magento2/pull/20418) -- [Backport] issue fixed #20304 No space between step title and saved address in c... (by @shikhamis11) + * [magento/magento2#20613](https://github.com/magento/magento2/pull/20613) -- [Backport] admin-order-info-issue2.2 (by @dipti2jcommerce) + * [magento/magento2#20744](https://github.com/magento/magento2/pull/20744) -- [Backport] recent-order-product-title-misaligned (by @amol2jcommerce) + * [magento/magento2#20739](https://github.com/magento/magento2/pull/20739) -- [Backport] issue fixed #20563 Go to shipping information, Update qty & Addresses... (by @amol2jcommerce) + * [magento/magento2#19612](https://github.com/magento/magento2/pull/19612) -- [Backport] Fix: Attribute Option with zero at the beginning does not work if there is already option with the same number without the zero [REST API] (by @SikailoISM) + * [magento/magento2#19667](https://github.com/magento/magento2/pull/19667) -- [Backport] chore: remove old code for IE9 (by @DanielRuf) + * [magento/magento2#20642](https://github.com/magento/magento2/pull/20642) -- [Backport] magento/magento2#12194: Tier price on configurable product sorting so... (by @amol2jcommerce) + * [magento/magento2#20784](https://github.com/magento/magento2/pull/20784) -- [Backport] Gift-option-message-overlap-edit-and-remove-button-2.2 (by @ajay2jcommerce) + * [magento/magento2#20837](https://github.com/magento/magento2/pull/20837) -- [Backport] Fixed apply discount button alignment on checkout page (by @amol2jcommerce) + * [magento/magento2#20863](https://github.com/magento/magento2/pull/20863) -- [Backport] Update Filter.php fix issue #20624 (by @irajneeshgupta) + * [magento/magento2#20886](https://github.com/magento/magento2/pull/20886) -- [Backport] #20409 Fixed Unnecessary slash in namespace (by @milindsingh) + * [magento/magento2#20929](https://github.com/magento/magento2/pull/20929) -- resolve typo errors for js record.js (by @neeta-wagento) + * [magento/magento2#20540](https://github.com/magento/magento2/pull/20540) -- [Backport] issue fixed #20259 Store switcher not sliding up and down, only dropd... (by @irajneeshgupta) + +2.2.7 +============= +* GitHub issues: + * [#15009](https://github.com/magento/magento2/issues/15009) -- [2.2.4] Gallery theme variables being ignored (fixed in [magento/magento2#16594](https://github.com/magento/magento2/pull/16594)) + * [#16580](https://github.com/magento/magento2/issues/16580) -- Product gallery caption issue (fixed in [magento/magento2#16594](https://github.com/magento/magento2/pull/16594)) + * [#16243](https://github.com/magento/magento2/issues/16243) -- Integration test ProcessCronQueueObserverTest.php succeeds regardless of magento config fixture (fixed in [magento/magento2#17191](https://github.com/magento/magento2/pull/17191)) + * [#17193](https://github.com/magento/magento2/issues/17193) -- Error with translation of confirmation modal buttons (fixed in [magento/magento2#17275](https://github.com/magento/magento2/pull/17275)) + * [#13445](https://github.com/magento/magento2/issues/13445) -- "Shop By" button disabling broken on the search page (fixed in [magento/magento2#15650](https://github.com/magento/magento2/pull/15650)) + * [#16302](https://github.com/magento/magento2/issues/16302) -- JS files located outside the web/js directory (fixed in [magento/magento2#16582](https://github.com/magento/magento2/pull/16582)) + * [#16653](https://github.com/magento/magento2/issues/16653) -- Not possible to create an invoice in Magento 2.3 (fixed in [magento/magento2#16656](https://github.com/magento/magento2/pull/16656)) + * [#16655](https://github.com/magento/magento2/issues/16655) -- Block totalbar not used in invoice create and credit memo create screens (fixed in [magento/magento2#16656](https://github.com/magento/magento2/pull/16656)) + * [#12250](https://github.com/magento/magento2/issues/12250) -- View.xml is inheriting image sizes from parent (so an optional field is replaced by the value of parent) (fixed in [magento/magento2#14537](https://github.com/magento/magento2/pull/14537)) + * [#13480](https://github.com/magento/magento2/issues/13480) -- Unable to activate logs after switching from production mode to developer (fixed in [magento/magento2#15335](https://github.com/magento/magento2/pull/15335)) + * [#10687](https://github.com/magento/magento2/issues/10687) -- Product image roles randomly disappear (fixed in [magento/magento2#15606](https://github.com/magento/magento2/pull/15606)) + * [#4803](https://github.com/magento/magento2/issues/4803) -- Incorrect return value from Product Attribute Repository (fixed in [magento/magento2#15691](https://github.com/magento/magento2/pull/15691)) + * [#15028](https://github.com/magento/magento2/issues/15028) -- Configurable product addtocart with restAPI not working as expected (fixed in [magento/magento2#15720](https://github.com/magento/magento2/pull/15720)) + * [#7372](https://github.com/magento/magento2/issues/7372) -- Product images gets removed from "Images And Videos" after validation alert. (fixed in [magento/magento2#16597](https://github.com/magento/magento2/pull/16597)) + * [#13177](https://github.com/magento/magento2/issues/13177) -- Can't save attributes on a configurable product (fixed in [magento/magento2#16597](https://github.com/magento/magento2/pull/16597)) + * [#16544](https://github.com/magento/magento2/issues/16544) -- Some of JS validation rules making fields required (fixed in [magento/magento2#16724](https://github.com/magento/magento2/pull/16724)) + * [#16479](https://github.com/magento/magento2/issues/16479) -- Issue in adding the wishlist of "zero price" product. (fixed in [magento/magento2#17395](https://github.com/magento/magento2/pull/17395)) + * [#15457](https://github.com/magento/magento2/issues/15457) -- Bundle Products price range is showing expired special price from bundle options (fixed in [magento/magento2#15535](https://github.com/magento/magento2/pull/15535)) + * [#16555](https://github.com/magento/magento2/issues/16555) -- "Shipping address is not set" exception in Multishipping Checkout. (fixed in [magento/magento2#16753](https://github.com/magento/magento2/pull/16753)) + * [#17289](https://github.com/magento/magento2/issues/17289) -- Magento 2.2.5: Year-to-date dropdown in Stores>Configuration>General>Reports>Dashboard (fixed in [magento/magento2#17383](https://github.com/magento/magento2/pull/17383)) + * [#16499](https://github.com/magento/magento2/issues/16499) -- User role issue with customer group (fixed in [magento/magento2#17515](https://github.com/magento/magento2/pull/17515)) + * [#12362](https://github.com/magento/magento2/issues/12362) -- Concurrent (quick reload) requests on checkout cause cart to empty - related to session_regenerate_id (fixed in [magento/magento2#14973](https://github.com/magento/magento2/pull/14973)) + * [#6305](https://github.com/magento/magento2/issues/6305) -- Can't save Customizable options (fixed in [magento/magento2#15357](https://github.com/magento/magento2/pull/15357)) + * [#13102](https://github.com/magento/magento2/issues/13102) -- review/product/listAjax/id/{{non existent id}/ (fixed in [magento/magento2#15369](https://github.com/magento/magento2/pull/15369)) + * [#17416](https://github.com/magento/magento2/issues/17416) -- Product image zoom (magnifier) is broken in Safari (fixed in [magento/magento2#17491](https://github.com/magento/magento2/pull/17491)) + * [#17492](https://github.com/magento/magento2/issues/17492) -- "- undefined" displayed in checkout summary when shipping method name is not set (fixed in [magento/magento2#17526](https://github.com/magento/magento2/pull/17526)) + * [#15041](https://github.com/magento/magento2/issues/15041) -- Adding a new fieldset to the admin category editor changes the position of the 'General' fieldset. (fixed in [magento/magento2#17540](https://github.com/magento/magento2/pull/17540)) + * [#13948](https://github.com/magento/magento2/issues/13948) -- Sidebar shortcut to admin dashboard (Magento logo on top left) has no link in web setup wizard (fixed in [magento/magento2#17543](https://github.com/magento/magento2/pull/17543)) + * [#16929](https://github.com/magento/magento2/issues/16929) -- Incorrect displaying Product Image Watermarks on Magento 2.2.5 (fixed in [magento/magento2#17013](https://github.com/magento/magento2/pull/17013)) + * [#14819](https://github.com/magento/magento2/issues/14819) -- Custom Payment Method doesn't uncheck 'My billing and shipping address are the same' (fixed in [magento/magento2#17593](https://github.com/magento/magento2/pull/17593)) + * [#13747](https://github.com/magento/magento2/issues/13747) -- Wysiwyg > Image Uploader >Max width/height (fixed in [magento/magento2#15942](https://github.com/magento/magento2/pull/15942)) + * [#6585](https://github.com/magento/magento2/issues/6585) -- Optional PO number (fixed in [magento/magento2#14393](https://github.com/magento/magento2/pull/14393)) + * [#17648](https://github.com/magento/magento2/issues/17648) -- UI validation rule for valid time am/pm doesn't work when js is minified (fixed in [magento/magento2#17652](https://github.com/magento/magento2/pull/17652)) + * [#17700](https://github.com/magento/magento2/issues/17700) -- Message list component: the message type is always error when parameters specified (fixed in [magento/magento2#17701](https://github.com/magento/magento2/pull/17701)) + * [#16927](https://github.com/magento/magento2/issues/16927) -- 2.2.5 Swagger: With JS minification enabled, the swagger-ui-bundle.js becomes corrupted (fixed in [magento/magento2#17626](https://github.com/magento/magento2/pull/17626)) + * [#14248](https://github.com/magento/magento2/issues/14248) -- Transparent background becomes black for thumbnails of PNG into Wysiwyg editor... (fixed in [magento/magento2#16733](https://github.com/magento/magento2/pull/16733)) + * [#17715](https://github.com/magento/magento2/issues/17715) -- duplicate event in Delete operation transaction "entity_manager_delete_before" (fixed in [magento/magento2#17718](https://github.com/magento/magento2/pull/17718)) + * [#17587](https://github.com/magento/magento2/issues/17587) -- Typo in Magento\Cms\Model\Wysiwyg\Images\Storage function resizeFile($source, $keepRation = true) (fixed in [magento/magento2#17776](https://github.com/magento/magento2/pull/17776)) + * [#17851](https://github.com/magento/magento2/issues/17851) -- Wishlist icon cut on Shopping cart page in mobile view (fixed in [magento/magento2#17877](https://github.com/magento/magento2/pull/17877)) + * [#17789](https://github.com/magento/magento2/issues/17789) -- Next Page button triggered when filtering Customer grid (fixed in [magento/magento2#17870](https://github.com/magento/magento2/pull/17870)) + * [#7903](https://github.com/magento/magento2/issues/7903) -- Datepicker does not scroll (fixed in [magento/magento2#16775](https://github.com/magento/magento2/pull/16775)) +* GitHub pull requests: + * [magento/magento2#16000](https://github.com/magento/magento2/pull/16000) -- Don't force enable "Use system value" checkboxes (by @likemusic) + * [magento/magento2#16505](https://github.com/magento/magento2/pull/16505) -- admin checkout agreement controllers refactor (by @AnshuMishra17) + * [magento/magento2#16594](https://github.com/magento/magento2/pull/16594) -- Fix broken commit in #15040 that accidentally reverted previous changes. (by @gwharton) + * [magento/magento2#17127](https://github.com/magento/magento2/pull/17127) -- Allow 3rd party modules to perform actions after totals calculation (by @navarr) + * [magento/magento2#17122](https://github.com/magento/magento2/pull/17122) -- Added missing exception cause for better error handling (by @woutersamaey) + * [magento/magento2#17153](https://github.com/magento/magento2/pull/17153) -- Set proper text-aligh for the element of the Subtotal column in the Creditmemo email (by @TomashKhamlai) + * [magento/magento2#17203](https://github.com/magento/magento2/pull/17203) -- [Backport] Refactored multiples conditions which could be grouped in a single on (by @mage2pratik) + * [magento/magento2#17236](https://github.com/magento/magento2/pull/17236) -- [Backport] Fixed invalid knockoutjs data binding for Braintree PayPal (by @tiagosampaio) + * [magento/magento2#17217](https://github.com/magento/magento2/pull/17217) -- [Backport] Broken Responsive Layout on Top page (by @mage2pratik) + * [magento/magento2#17246](https://github.com/magento/magento2/pull/17246) -- [Backport] FIXED: FTP user and password strings urldecoded (by @mage2pratik) + * [magento/magento2#16788](https://github.com/magento/magento2/pull/16788) -- Replace sort callbacks to spaceship operator (by @lbajsarowicz) + * [magento/magento2#17103](https://github.com/magento/magento2/pull/17103) -- Code cleanup of lib files (by @mage2pratik) + * [magento/magento2#17191](https://github.com/magento/magento2/pull/17191) -- [Backport 2.2]Filter test result collection with the cron job code defined in the c (by @gelanivishal) + * [magento/magento2#17275](https://github.com/magento/magento2/pull/17275) -- fix #17193 Error with translation of confirmation modal buttons (by @Karlasa) + * [magento/magento2#17291](https://github.com/magento/magento2/pull/17291) -- fix: remove unused ID (by @DanielRuf) + * [magento/magento2#15650](https://github.com/magento/magento2/pull/15650) -- Fixed "Shop By" button disabling broken on the search page #13445 (by @AndreaRivadossi) + * [magento/magento2#16354](https://github.com/magento/magento2/pull/16354) -- Remove unnecessary translation of HTML tags (by @Yogeshks) + * [magento/magento2#16510](https://github.com/magento/magento2/pull/16510) -- Fix the special price expression. (by @DmitryChukhnov) + * [magento/magento2#16582](https://github.com/magento/magento2/pull/16582) -- Resolved : JS files located outside the web/js directory (by @hitesh-wagento) + * [magento/magento2#16656](https://github.com/magento/magento2/pull/16656) -- [Fix #16655] Block totalbar not used in invoice create and credit memo create screens (by @dverkade) + * [magento/magento2#16848](https://github.com/magento/magento2/pull/16848) -- Replace floatval() function by using direct type casting to (float) (by @mhauri) + * [magento/magento2#16849](https://github.com/magento/magento2/pull/16849) -- Replace strval() function by using direct type casting to (string) (by @mhauri) + * [magento/magento2#17070](https://github.com/magento/magento2/pull/17070) -- Resolved special character issue for sidebar (by @deepjoshi94) + * [magento/magento2#17066](https://github.com/magento/magento2/pull/17066) -- Update CMS Page Index (by @hryvinskyi) + * [magento/magento2#17189](https://github.com/magento/magento2/pull/17189) -- Don't add empty method to the cart summary (by @arnoudhgz) + * [magento/magento2#17250](https://github.com/magento/magento2/pull/17250) -- Maintenance: Compare products. Add unit test coverage & missed class property declaration. (by @swnsma) + * [magento/magento2#17327](https://github.com/magento/magento2/pull/17327) -- fix: add missing data-th selector for tables (by @DanielRuf) + * [magento/magento2#17365](https://github.com/magento/magento2/pull/17365) -- [Backport] Fixed some minor css issue (by @arnoudhgz) + * [magento/magento2#17368](https://github.com/magento/magento2/pull/17368) -- [Braintree] Unit tests for TransactionRefund and TransactionVoid classes (by @rogyar) + * [magento/magento2#14537](https://github.com/magento/magento2/pull/14537) -- magento/magento2#12250: View.xml is inheriting image sizes from paren (by @quisse) + * [magento/magento2#15335](https://github.com/magento/magento2/pull/15335) -- Fixed issue #13480 - Unable to activate logs after switching from production mode to developer (by @jayankaghosh) + * [magento/magento2#15606](https://github.com/magento/magento2/pull/15606) -- Fix #10687 - Product image roles disappearing (by @Scarraban) + * [magento/magento2#15691](https://github.com/magento/magento2/pull/15691) -- Fix #4803: Incorrect return value from Product Attribute Repository (by @cream-julian) + * [magento/magento2#15720](https://github.com/magento/magento2/pull/15720) -- Convert to string $option->getValue, in order to be compared with oth (by @zamboten) + * [magento/magento2#16597](https://github.com/magento/magento2/pull/16597) -- Save configurable product options after validation error (by @swnsma) + * [magento/magento2#16724](https://github.com/magento/magento2/pull/16724) -- 16544: fixed behaviour when some of JS validation rules making fields (by @VitaliyBoyko) + * [magento/magento2#16955](https://github.com/magento/magento2/pull/16955) -- fix: remove disabled attribute on region list (by @DanielRuf) + * [magento/magento2#17078](https://github.com/magento/magento2/pull/17078) -- MAGETWO-84608: Cannot perform setup:install if Redis needs a password (by @guillaumegiordana) + * [magento/magento2#17405](https://github.com/magento/magento2/pull/17405) -- [Braintree] Added unit test for instant purchase PayPal token formatter (by @rogyar) + * [magento/magento2#14397](https://github.com/magento/magento2/pull/14397) -- Allows modules with underscores in name to set custom a frontend_model in system.xml (by @bentideswell) + * [magento/magento2#16570](https://github.com/magento/magento2/pull/16570) -- [update] enhance performance on large catalog (by @AurelienLavorel) + * [magento/magento2#17101](https://github.com/magento/magento2/pull/17101) -- Refactory to Magento_Backend module class. (by @tiagosampaio) + * [magento/magento2#17395](https://github.com/magento/magento2/pull/17395) -- [Backport] Fixed add to wishlist issue on product price 0 (by @sreichel) + * [magento/magento2#17437](https://github.com/magento/magento2/pull/17437) -- Improvements in UI component MassActions (by @alexeya-ven) + * [magento/magento2#15535](https://github.com/magento/magento2/pull/15535) -- FIX for issue #15457 - Bundle Products price range is showing expired (by @phoenix128) + * [magento/magento2#15507](https://github.com/magento/magento2/pull/15507) -- fix: cache count() results for loops (by @DanielRuf) + * [magento/magento2#16753](https://github.com/magento/magento2/pull/16753) -- Fix the issue with "Shipping address is not set" exception (by @dmytro-ch) + * [magento/magento2#17454](https://github.com/magento/magento2/pull/17454) -- Braintree: test coverage (by @dmytro-ch) + * [magento/magento2#17383](https://github.com/magento/magento2/pull/17383) -- Magento 2.2.5: Year-to-date dropdown in Stores>Configuration>General>Reports>Dashboard #17289 (by @teddysie) + * [magento/magento2#15171](https://github.com/magento/magento2/pull/15171) -- AD-HOC feat (Profiler): Allow supplying complex profiler configuration (by @andrewhowdencom) + * [magento/magento2#16855](https://github.com/magento/magento2/pull/16855) -- Doesn't work if use date as condition for Catalog Price Rules (by @GlennCheng) + * [magento/magento2#13649](https://github.com/magento/magento2/pull/13649) -- Fix possible undefined index when caching config data (by @mimarcel) + * [magento/magento2#17479](https://github.com/magento/magento2/pull/17479) -- updating lib LESS docs (by @Karlasa) + * [magento/magento2#17505](https://github.com/magento/magento2/pull/17505) -- Refactor: remove some code duplication (by @arnoudhgz) + * [magento/magento2#17515](https://github.com/magento/magento2/pull/17515) -- Solution for User role issue with customer group (by @emiprotech) + * [magento/magento2#17561](https://github.com/magento/magento2/pull/17561) -- Catalog: Add unit tests for Cron classes (by @dmytro-ch) + * [magento/magento2#13133](https://github.com/magento/magento2/pull/13133) -- Magento PayPal checkout fails if email sending fails / other payment does not (by @driskell) + * [magento/magento2#14973](https://github.com/magento/magento2/pull/14973) -- Fix unstable session manager (by @elioermini) + * [magento/magento2#15357](https://github.com/magento/magento2/pull/15357) -- 6305 - Resolved product custom option title save issue (by @Madhumalak) + * [magento/magento2#15369](https://github.com/magento/magento2/pull/15369) -- Fixed review list ajax if product not exist redirect to 404 page #13102 (by @ananth747) + * [magento/magento2#16021](https://github.com/magento/magento2/pull/16021) -- Introduce Block Config Source (by @thomas-blackbird) + * [magento/magento2#17491](https://github.com/magento/magento2/pull/17491) -- [Backport] Fix incorrect image magnifier size bug in Safari (by @dannynimmo) + * [magento/magento2#17526](https://github.com/magento/magento2/pull/17526) -- Fixed undefinded shipping method name issue #17492 (by @gelanivishal) + * [magento/magento2#17540](https://github.com/magento/magento2/pull/17540) -- Fix for #15041 Adding a new fieldset to the admin category editor changes the position of the 'General' fieldset (by @vasilii-b) + * [magento/magento2#17552](https://github.com/magento/magento2/pull/17552) -- Fix proxy generation return type (by @adrian-martinez-interactiv4) + * [magento/magento2#17590](https://github.com/magento/magento2/pull/17590) -- Braintree: Add unit test for CreditCard/TokenFormatter (by @eduard13) + * [magento/magento2#16777](https://github.com/magento/magento2/pull/16777) -- Fix Translation of error message on cart for deleted bundle option. (by @swnsma) + * [magento/magento2#17527](https://github.com/magento/magento2/pull/17527) -- Refactor JS code and added JS component file (by @Yogeshks) + * [magento/magento2#17543](https://github.com/magento/magento2/pull/17543) -- Link logo in web setup wizard to back-end base URL (by @arnoudhgz) + * [magento/magento2#17575](https://github.com/magento/magento2/pull/17575) -- Translated validation error messages (by @Yogeshks) + * [magento/magento2#17013](https://github.com/magento/magento2/pull/17013) -- Fixed #16929 - Incorrect displaying Product Image Watermarks on Magento 2.2.5 (by @ronak2ram) + * [magento/magento2#17484](https://github.com/magento/magento2/pull/17484) -- Fix sending duplicate emails (by @iGerchak) + * [magento/magento2#17593](https://github.com/magento/magento2/pull/17593) -- Fixing the address checkbox being unchecked on payment step. (by @eduard13) + * [magento/magento2#15942](https://github.com/magento/magento2/pull/15942) -- Making configurable settings for MAX_IMAGE_WIDTH and MAX_IMAGE_HEIGHT (by @eduard13) + * [magento/magento2#17602](https://github.com/magento/magento2/pull/17602) -- Fix Custom Attribute Group can not translate in catalog/product page (by @GraysonChiang) + * [magento/magento2#14393](https://github.com/magento/magento2/pull/14393) -- Validate that the PO Number is set on the payment instance. (by @centerax) + * [magento/magento2#17633](https://github.com/magento/magento2/pull/17633) -- Added unit test for newsletter problem model (by @rogyar) + * [magento/magento2#17652](https://github.com/magento/magento2/pull/17652) -- Update time12h javascript validation rule to be compatible with js minify (by @markoshust) + * [magento/magento2#17678](https://github.com/magento/magento2/pull/17678) -- CMS: Add missing unit tests for model classes (by @dmytro-ch) + * [magento/magento2#17521](https://github.com/magento/magento2/pull/17521) -- Translated admin menu titles (by @Yogeshks) + * [magento/magento2#17690](https://github.com/magento/magento2/pull/17690) -- Integration test for reviews delete observer (by @rogyar) + * [magento/magento2#17693](https://github.com/magento/magento2/pull/17693) -- Review: Adding missing unit test for Observer (by @eduard13) + * [magento/magento2#17701](https://github.com/magento/magento2/pull/17701) -- Message list component fix: the message type is always error when parameters specified (by @dmytro-ch) + * [magento/magento2#17710](https://github.com/magento/magento2/pull/17710) -- Sales Rule: Add unit tests for model classes (by @dmytro-ch) + * [magento/magento2#17626](https://github.com/magento/magento2/pull/17626) -- Use '.min' in filenames of already minified js files in the Swagger module so they aren't getting minified again in production, fixes #16927 - for Magento 2.2 (by @hostep) + * [magento/magento2#16733](https://github.com/magento/magento2/pull/16733) -- Fixes black background for png images in wysiwyg editors (by @eduard13) + * [magento/magento2#17718](https://github.com/magento/magento2/pull/17718) -- ISSUE-17715: Duplicate event in Delete operation transaction "entity_manager_delete_before". (by @p-bystritsky) + * [magento/magento2#17735](https://github.com/magento/magento2/pull/17735) -- Fix translation issue (by @jignesh-baldha) + * [magento/magento2#17776](https://github.com/magento/magento2/pull/17776) -- [Backport] Changed storage.php (by @MartinAarts) + * [magento/magento2#17801](https://github.com/magento/magento2/pull/17801) -- [Search] Unit test for SynonymAnalyzer model (by @furseyev) + * [magento/magento2#17817](https://github.com/magento/magento2/pull/17817) -- Update issue templates for Magento 2 GitHub project (by @ishakhsuvarov) + * [magento/magento2#17385](https://github.com/magento/magento2/pull/17385) -- Remove leading Countrycode from EU-VAT-Numbers (by @Drischie) + * [magento/magento2#17739](https://github.com/magento/magento2/pull/17739) -- Search: Add unit test for PopularSearchTerms model (by @dmytro-ch) + * [magento/magento2#17773](https://github.com/magento/magento2/pull/17773) -- Fix for ProductLink - setterName was incorrectly being set (by @insanityinside) + * [magento/magento2#17877](https://github.com/magento/magento2/pull/17877) -- Resolved : Wishlist icon cut on Shopping cart page in mobile view #17851 (by @hitesh-wagento) + * [magento/magento2#17876](https://github.com/magento/magento2/pull/17876) -- Sales: Add unit test for validator model class (by @dmytro-ch) + * [magento/magento2#17840](https://github.com/magento/magento2/pull/17840) -- API-functional test for Search (by @rogyar) + * [magento/magento2#17870](https://github.com/magento/magento2/pull/17870) -- Fix - Next Page button triggered when filtering Customer grid (by @ronak2ram) + * [magento/magento2#16800](https://github.com/magento/magento2/pull/16800) -- [2.2-dev] Move functions.php into Framework (by @fooman) + * [magento/magento2#17872](https://github.com/magento/magento2/pull/17872) -- [Backport] Replacing deprecated methods for tests. (by @tiagosampaio) + * [magento/magento2#16775](https://github.com/magento/magento2/pull/16775) -- [Forwardport] #7903 correct the position of the datepicker when you scroll (by @hitesh-wagento) + +2.2.6 +============= +* GitHub issues: + * [#13296](https://github.com/magento/magento2/issues/13296) -- Category name with special characters brakes in url rewrites category tree (fixed in [magento/magento2#13397](https://github.com/magento/magento2/pull/13397)) + * [#4788](https://github.com/magento/magento2/issues/4788) -- Wrong sitemap product url (fixed in [magento/magento2#14338](https://github.com/magento/magento2/pull/14338)) + * [#14669](https://github.com/magento/magento2/issues/14669) -- Css class "empty" is always present on minicart dropdown (fixed in [magento/magento2#14715](https://github.com/magento/magento2/pull/14715)) + * [#4389](https://github.com/magento/magento2/issues/4389) -- Widget cache error (fixed in [magento/magento2#12764](https://github.com/magento/magento2/pull/12764)) + * [#13765](https://github.com/magento/magento2/issues/13765) -- "cart" section data gets loaded 3 times on cart page (2.2.2) (fixed in [magento/magento2#14314](https://github.com/magento/magento2/pull/14314)) + * [#1821](https://github.com/magento/magento2/issues/1821) -- CSS load order incorrect using default_head_blocks.xml (fixed in [magento/magento2#14290](https://github.com/magento/magento2/pull/14290)) + * [#14692](https://github.com/magento/magento2/issues/14692) -- 'validate-grouped-qty' validation is meaningless (fixed in [magento/magento2#14752](https://github.com/magento/magento2/pull/14752)) + * [#11396](https://github.com/magento/magento2/issues/11396) -- app:config:dump locks every configuration setting no alternative to dump specific setting only (fixed in [magento/magento2#12410](https://github.com/magento/magento2/pull/12410)) + * [#9580](https://github.com/magento/magento2/issues/9580) -- Quote Attribute trigger_recollect causes a timeout (fixed in [magento/magento2#14719](https://github.com/magento/magento2/pull/14719)) + * [#13944](https://github.com/magento/magento2/issues/13944) -- Stores -> Terms and Conditions - No Store View shown (fixed in [magento/magento2#14546](https://github.com/magento/magento2/pull/14546)) + * [#5726](https://github.com/magento/magento2/issues/5726) -- Reset Password Email Issue on Multi Store from Admin (fixed in [magento/magento2#14800](https://github.com/magento/magento2/pull/14800)) + * [#14274](https://github.com/magento/magento2/issues/14274) -- Quick search fires error (fixed in [magento/magento2#14839](https://github.com/magento/magento2/pull/14839)) + * [#7861](https://github.com/magento/magento2/issues/7861) -- Using search in Admin panel, and choosing "% in Products" returns full catalog (fixed in [magento/magento2#12735](https://github.com/magento/magento2/pull/12735)) + * [#12193](https://github.com/magento/magento2/issues/12193) -- Catalog not filtered by admin search bar (fixed in [magento/magento2#12735](https://github.com/magento/magento2/pull/12735)) + * [#5768](https://github.com/magento/magento2/issues/5768) -- Magento 2.0.7 XML sitemap is not generated by schedule (fixed in [magento/magento2#14822](https://github.com/magento/magento2/pull/14822)) + * [#14855](https://github.com/magento/magento2/issues/14855) -- Adding an * to do a customer search. (fixed in [magento/magento2#14905](https://github.com/magento/magento2/pull/14905)) + * [#14869](https://github.com/magento/magento2/issues/14869) -- M 2.2.3 price per website - wrong price at backend by a create order after update (fixed in [magento/magento2#14904](https://github.com/magento/magento2/pull/14904)) + * [#10395](https://github.com/magento/magento2/issues/10395) -- REMOTE_IP gets saved partially when using IPV6 (fixed in [magento/magento2#14976](https://github.com/magento/magento2/pull/14976)) + * [#12285](https://github.com/magento/magento2/issues/12285) -- The option false for mobile device don't work in product view page gallery (fixed in [magento/magento2#15020](https://github.com/magento/magento2/pull/15020)) + * [#15009](https://github.com/magento/magento2/issues/15009) -- [2.2.4] Gallery theme variables being ignored (fixed in [magento/magento2#15020](https://github.com/magento/magento2/pull/15020)) + * [#13460](https://github.com/magento/magento2/issues/13460) -- Allmethods config source model does not always report the full list of payment methods (fixed in [magento/magento2#15032](https://github.com/magento/magento2/pull/15032)) + * [#4301](https://github.com/magento/magento2/issues/4301) -- Hit fast twice F5 on checout page, customer loggs out automatically (fixed in [magento/magento2#14428](https://github.com/magento/magento2/pull/14428)) + * [#12362](https://github.com/magento/magento2/issues/12362) -- Concurrent (quick reload) requests on checkout cause cart to empty - related to session_regenerate_id (fixed in [magento/magento2#14428](https://github.com/magento/magento2/pull/14428)) + * [#13427](https://github.com/magento/magento2/issues/13427) -- [2.1.11] Add to cart, try to checkout, cart is empty but mini-cart has items. (fixed in [magento/magento2#14428](https://github.com/magento/magento2/pull/14428)) + * [#11354](https://github.com/magento/magento2/issues/11354) -- Merged CSS file name generation (fixed in [magento/magento2#15144](https://github.com/magento/magento2/pull/15144)) + * [#14104](https://github.com/magento/magento2/issues/14104) -- Admin Section is not visible in backend on production mode (fixed in [magento/magento2#15174](https://github.com/magento/magento2/pull/15174)) + * [#7399](https://github.com/magento/magento2/issues/7399) -- Modal UI: clickableOverlay option doesn't work (fixed in [magento/magento2#15172](https://github.com/magento/magento2/pull/15172)) + * [#14987](https://github.com/magento/magento2/issues/14987) -- Invisible breadcrumbs at product page when mageMenu widget is not used (fixed in [magento/magento2#15178](https://github.com/magento/magento2/pull/15178)) + * [#13530](https://github.com/magento/magento2/issues/13530) -- "Template file 'header.html' is not found" error while trying to save Design Configuration. (fixed in [magento/magento2#15137](https://github.com/magento/magento2/pull/15137)) + * [#14968](https://github.com/magento/magento2/issues/14968) -- Can't change the applied theme in 2.2.4 (fixed in [magento/magento2#15137](https://github.com/magento/magento2/pull/15137)) + * [#15121](https://github.com/magento/magento2/issues/15121) -- Magento 2.2.4 - Condition Category Chooser Crashes Page if Store has Several Nested Categories (fixed in [magento/magento2#15265](https://github.com/magento/magento2/pull/15265)) + * [#15334](https://github.com/magento/magento2/issues/15334) -- Purchased Order Form button should visible properly (fixed in [magento/magento2#15331](https://github.com/magento/magento2/pull/15331) and [magento/magento2#15372](https://github.com/magento/magento2/pull/15372)) + * [#15352](https://github.com/magento/magento2/issues/15352) -- Reformat the javascript code as per magento standards. (fixed in [magento/magento2#15343](https://github.com/magento/magento2/pull/15343)) + * [#15355](https://github.com/magento/magento2/issues/15355) -- Function is unnecessarily called multiple time (fixed in [magento/magento2#15346](https://github.com/magento/magento2/pull/15346)) + * [#15319](https://github.com/magento/magento2/issues/15319) -- misleading data-container in product list (fixed in [magento/magento2#15350](https://github.com/magento/magento2/pull/15350)) + * [#15354](https://github.com/magento/magento2/issues/15354) -- Refactor javascript code of button split widget call js component (fixed in [magento/magento2#15351](https://github.com/magento/magento2/pull/15351)) + * [#14941](https://github.com/magento/magento2/issues/14941) -- Unnecessary recalculation of product list pricing causes huge slowdowns (fixed in [magento/magento2#15089](https://github.com/magento/magento2/pull/15089)) + * [#14747](https://github.com/magento/magento2/issues/14747) -- Newsletter subscription confirmation message does not display after clicking link in email (fixed in [magento/magento2#15247](https://github.com/magento/magento2/pull/15247)) + * [#15037](https://github.com/magento/magento2/issues/15037) -- Product Details Page breadcrumbs cause syntax error on products containing quotes (fixed in [magento/magento2#15347](https://github.com/magento/magento2/pull/15347)) + * [#15118](https://github.com/magento/magento2/issues/15118) -- Responsive Design, Footers do not snap to bottom of screen on mobile devices (fixed in [magento/magento2#15353](https://github.com/magento/magento2/pull/15353) and [magento/magento2#17006](https://github.com/magento/magento2/pull/17006)) + * [#15192](https://github.com/magento/magento2/issues/15192) -- Module Manager module grid is not working Magento 2.2.4 (fixed in [magento/magento2#15211](https://github.com/magento/magento2/pull/15211)) + * [#13793](https://github.com/magento/magento2/issues/13793) -- Submitting search form (mini) with enter key fires event handlers bound by jquery twice (fixed in [magento/magento2#15340](https://github.com/magento/magento2/pull/15340)) + * [#15361](https://github.com/magento/magento2/issues/15361) -- Comments are not translated for Signifyd module. (fixed in [magento/magento2#15364](https://github.com/magento/magento2/pull/15364)) + * [#15356](https://github.com/magento/magento2/issues/15356) -- Refactore javascript for module URL rewrite (fixed in [magento/magento2#15422](https://github.com/magento/magento2/pull/15422)) + * [#10210](https://github.com/magento/magento2/issues/10210) -- Transport variable can not be altered in email_invoice_set_template_vars_before Event (fixed in [magento/magento2#15040](https://github.com/magento/magento2/pull/15040) and [magento/magento2#16599](https://github.com/magento/magento2/pull/16599)) + * [#4977](https://github.com/magento/magento2/issues/4977) -- Magnifier doesn't work with mode set to inner (fixed in [magento/magento2#15382](https://github.com/magento/magento2/pull/15382)) + * [#15469](https://github.com/magento/magento2/issues/15469) -- lib/web/mage/dropdowns.js fails when autoclose is set to true (fixed in [magento/magento2#15499](https://github.com/magento/magento2/pull/15499)) + * [#14153](https://github.com/magento/magento2/issues/14153) -- UI Component listing action column outside of screen when too many columns (fixed in [magento/magento2#15459](https://github.com/magento/magento2/pull/15459)) + * [#15467](https://github.com/magento/magento2/issues/15467) -- Cart does not load when Configuration product option is deleted and that option is in the cart (fixed in [magento/magento2#15468](https://github.com/magento/magento2/pull/15468)) + * [#15564](https://github.com/magento/magento2/issues/15564) -- 2.2.4 Created admin token has no access (fixed in [magento/magento2#15598](https://github.com/magento/magento2/pull/15598)) + * [#10346](https://github.com/magento/magento2/issues/10346) -- Deadlock occurs using REST API & OAuth 1.0a under high concurrency (fixed in [magento/magento2#13328](https://github.com/magento/magento2/pull/13328)) + * [#15348](https://github.com/magento/magento2/issues/15348) -- Multiple Payment Methods Enabled is giving error in console "Found 3 Elements with non - unique Id" (fixed in [magento/magento2#15349](https://github.com/magento/magento2/pull/15349)) + * [#13415](https://github.com/magento/magento2/issues/13415) -- Duplicated elements id in checkout page (fixed in [magento/magento2#15585](https://github.com/magento/magento2/pull/15585)) + * [#15590](https://github.com/magento/magento2/issues/15590) -- Typo in tests / setCateroryIds([]) (fixed in [magento/magento2#15621](https://github.com/magento/magento2/pull/15621)) + * [#7897](https://github.com/magento/magento2/issues/7897) -- Menu widget submenu alignment (fixed in [magento/magento2#15645](https://github.com/magento/magento2/pull/15645)) + * [#15565](https://github.com/magento/magento2/issues/15565) -- Getting wrong frontend-controller, when using storecodes in urls (fixed in [magento/magento2#15566](https://github.com/magento/magento2/pull/15566)) + * [#6058](https://github.com/magento/magento2/issues/6058) -- IE11 user login email validation fails if field has leading or trailing space (fixed in [magento/magento2#15365](https://github.com/magento/magento2/pull/15365) and [magento/magento2#16192](https://github.com/magento/magento2/pull/16192) and [magento/magento2#16564](https://github.com/magento/magento2/pull/16564) and [magento/magento2#16595](https://github.com/magento/magento2/pull/16595)) + * [#15210](https://github.com/magento/magento2/issues/15210) -- Advanced pricing pagination issue (fixed in [magento/magento2#15614](https://github.com/magento/magento2/pull/15614)) + * [#12221](https://github.com/magento/magento2/issues/12221) -- Google analytics pageview being triggered twice (fixed in [magento/magento2#15765](https://github.com/magento/magento2/pull/15765)) + * [#15510](https://github.com/magento/magento2/issues/15510) -- First PDF download / export after login (fixed in [magento/magento2#15539](https://github.com/magento/magento2/pull/15539)) + * [#15608](https://github.com/magento/magento2/issues/15608) -- Styling select by changing less variables in Luma theme doesn't work as expected (fixed in [magento/magento2#15734](https://github.com/magento/magento2/pull/15734)) + * [#14966](https://github.com/magento/magento2/issues/14966) -- Disabling product does not remove it from the flat index (fixed in [magento/magento2#15019](https://github.com/magento/magento2/pull/15019)) + * [#11477](https://github.com/magento/magento2/issues/11477) -- Magento REST API Schema (Swagger) is not compatible with Search Criteria (fixed in [magento/magento2#15322](https://github.com/magento/magento2/pull/15322)) + * [#14056](https://github.com/magento/magento2/issues/14056) -- Coupon API not working for guest user (fixed in [magento/magento2#15320](https://github.com/magento/magento2/pull/15320)) + * [#15660](https://github.com/magento/magento2/issues/15660) -- Wrong order amount on dashboard on Last orders listing when having more than one website with different currencies (fixed in [magento/magento2#15661](https://github.com/magento/magento2/pull/15661)) + * [#15588](https://github.com/magento/magento2/issues/15588) -- Images in XML sitemap are always linked to base store in multistore (fixed in [magento/magento2#15689](https://github.com/magento/magento2/pull/15689)) + * [#15822](https://github.com/magento/magento2/issues/15822) -- SQL Error: ambiguous column 'customer_group_id' in 'All customers' page in admin when extension attribute table is joined (fixed in [magento/magento2#15826](https://github.com/magento/magento2/pull/15826)) + * [#15323](https://github.com/magento/magento2/issues/15323) -- limiter float too generic (fixed in [magento/magento2#15878](https://github.com/magento/magento2/pull/15878)) + * [#14999](https://github.com/magento/magento2/issues/14999) -- Changing @tab-content__border variable has no effect in Blank theme (fixed in [magento/magento2#15914](https://github.com/magento/magento2/pull/15914)) + * [#15308](https://github.com/magento/magento2/issues/15308) -- extraneous margins on product list and product list items (fixed in [magento/magento2#15936](https://github.com/magento/magento2/pull/15936)) + * [#16047](https://github.com/magento/magento2/issues/16047) -- inline-block issue in name form (fixed in [magento/magento2#16048](https://github.com/magento/magento2/pull/16048)) + * [#15213](https://github.com/magento/magento2/issues/15213) -- Alignment & overlapping Issue on every Home page & category page of Hot Seller section (fixed in [magento/magento2#15893](https://github.com/magento/magento2/pull/15893)) + * [#15832](https://github.com/magento/magento2/issues/15832) -- No button-primary__font-weight (fixed in [magento/magento2#16012](https://github.com/magento/magento2/pull/16012)) + * [#13692](https://github.com/magento/magento2/issues/13692) -- In payment step of checkout I cannot unselect #billing-save-in-address-book checkbox in non-first payment method (fixed in [magento/magento2#15344](https://github.com/magento/magento2/pull/15344)) + * [#15255](https://github.com/magento/magento2/issues/15255) -- Customer who exceeded max login failures not able to login even after reset password (fixed in [magento/magento2#15534](https://github.com/magento/magento2/pull/15534)) + * [#15220](https://github.com/magento/magento2/issues/15220) -- 2.2.4: navigation dropdown caret icon missing (jQuery UI) (fixed in [magento/magento2#16082](https://github.com/magento/magento2/pull/16082)) + * [#16079](https://github.com/magento/magento2/issues/16079) -- Need information about translating issue (Magento Swatches Js) (fixed in [magento/magento2#16190](https://github.com/magento/magento2/pull/16190)) + * [#16184](https://github.com/magento/magento2/issues/16184) -- Argument 1 passed to Magento\Sales\Model\Order\Payment must be an instance of Magento\Framework\DataObject, none given (fixed in [magento/magento2#16194](https://github.com/magento/magento2/pull/16194)) + * [#8222](https://github.com/magento/magento2/issues/8222) -- Estimate Shipping and Tax Form not works due to js error in collapsible.js [proposed fix] (fixed in [magento/magento2#16213](https://github.com/magento/magento2/pull/16213)) + * [#15501](https://github.com/magento/magento2/issues/15501) -- M2.2.4 missing meta title tag and doesn't show product name if meta title is empty (fixed in [magento/magento2#15532](https://github.com/magento/magento2/pull/15532)) + * [#15627](https://github.com/magento/magento2/issues/15627) -- Product order in category changed after update to Magento 2.2.4 (fixed in [magento/magento2#15629](https://github.com/magento/magento2/pull/15629)) + * [#9307](https://github.com/magento/magento2/issues/9307) -- Color attribute taking swatch instead of Drop down option for configurable options, (fixed in [magento/magento2#12771](https://github.com/magento/magento2/pull/12771)) + * [#9923](https://github.com/magento/magento2/issues/9923) -- Upgrading to 2.1.7 changed dropdown attributes to swatches (fixed in [magento/magento2#12771](https://github.com/magento/magento2/pull/12771)) + * [#11403](https://github.com/magento/magento2/issues/11403) -- Product Attributes Not Updating on Frontend (fixed in [magento/magento2#12771](https://github.com/magento/magento2/pull/12771)) + * [#11703](https://github.com/magento/magento2/issues/11703) -- Changing Swatches to Drop-down does not remove swatches from existing products (fixed in [magento/magento2#12771](https://github.com/magento/magento2/pull/12771)) + * [#12695](https://github.com/magento/magento2/issues/12695) -- Unable to change attribute type from swatch to dropdown (fixed in [magento/magento2#12771](https://github.com/magento/magento2/pull/12771)) + * [#14895](https://github.com/magento/magento2/issues/14895) -- Change Password warning message appear two times (fixed in [magento/magento2#15774](https://github.com/magento/magento2/pull/15774)) + * [#15205](https://github.com/magento/magento2/issues/15205) -- Upgraded to Magento 2.2.4 from Magento 2.1.9 - Locale and Store Configuration issues 'Store View' Locale not being used on frontend, 'Default Config' Locale being used instead (fixed in [magento/magento2#15929](https://github.com/magento/magento2/pull/15929)) + * [#15245](https://github.com/magento/magento2/issues/15245) -- 2.2.4: Wrong home page loaded in multi store setup (fixed in [magento/magento2#15929](https://github.com/magento/magento2/pull/15929)) + * [#15345](https://github.com/magento/magento2/issues/15345) -- Template syntax in block file (fixed in [magento/magento2#15339](https://github.com/magento/magento2/pull/15339)) + * [#7379](https://github.com/magento/magento2/issues/7379) -- Calendar widget (jQuery UI DatePicker) with numberOfMonths = 2 or more (fixed in [magento/magento2#16279](https://github.com/magento/magento2/pull/16279)) + * [#16378](https://github.com/magento/magento2/issues/16378) -- Wrong placeholder for password field in the checkout page (fixed in [magento/magento2#16379](https://github.com/magento/magento2/pull/16379)) + * [#15218](https://github.com/magento/magento2/issues/15218) -- "Confirmation request" email is sent on customer's newsletter unsubscription (fixed in [magento/magento2#15464](https://github.com/magento/magento2/pull/15464)) + * [#14593](https://github.com/magento/magento2/issues/14593) -- Press Esc Key on modal generate a jquery UI error (fixed in [magento/magento2#16477](https://github.com/magento/magento2/pull/16477)) + * [#11717](https://github.com/magento/magento2/issues/11717) -- Wrong price amount on product page (fixed in [magento/magento2#15909](https://github.com/magento/magento2/pull/15909) and [magento/magento2#16590](https://github.com/magento/magento2/pull/16590)) + * [#16529](https://github.com/magento/magento2/issues/16529) -- Rewriting product listing widget block breaks its template rendering. (fixed in [magento/magento2#16530](https://github.com/magento/magento2/pull/16530)) + * [#15940](https://github.com/magento/magento2/issues/15940) -- Wrong end of month at Reports for Europe/Berlin time zone if month contains 31 day (fixed in [magento/magento2#16584](https://github.com/magento/magento2/pull/16584)) + * [#16174](https://github.com/magento/magento2/issues/16174) -- Admin tabs order not working properly (fixed in [magento/magento2#16175](https://github.com/magento/magento2/pull/16175)) + * [#16703](https://github.com/magento/magento2/issues/16703) -- User Agent Rules table headers do match content of rows. (fixed in [magento/magento2#16704](https://github.com/magento/magento2/pull/16704)) + * [#15848](https://github.com/magento/magento2/issues/15848) -- no navigation-level0-item__hover__color (fixed in [magento/magento2#16732](https://github.com/magento/magento2/pull/16732)) + * [#5067](https://github.com/magento/magento2/issues/5067) -- Custom option values do not save correctly (fixed in [magento/magento2#13569](https://github.com/magento/magento2/pull/13569)) + * [#14351](https://github.com/magento/magento2/issues/14351) -- Product import doesn't change `Enable Qty Increments` field (fixed in [magento/magento2#14379](https://github.com/magento/magento2/pull/14379)) + * [#16764](https://github.com/magento/magento2/issues/16764) -- Rating Star issue on Product detail Page. (fixed in [magento/magento2#16766](https://github.com/magento/magento2/pull/16766)) + * [#5316](https://github.com/magento/magento2/issues/5316) -- [2.1.0] HTML minification problem with php tag with a comment and no space at the end (fixed in [magento/magento2#16916](https://github.com/magento/magento2/pull/16916)) + * [#6264](https://github.com/magento/magento2/issues/6264) -- Error in Admin > products when module Reviews is disabled (fixed in [magento-partners/magento2ee#70](https://github.com/magento-partners/magento2ee/pull/70)) + * [#6504](https://github.com/magento/magento2/issues/6504) -- Magento 2.1 CE: Breadcrumbs on homepage and 404 in multistore (fixed in [magento-partners/magento2ee#72](https://github.com/magento-partners/magento2ee/pull/72)) + * [#16843](https://github.com/magento/magento2/issues/16843) -- Magento 2.2.5: Configurable Product with Only Size Options (No Color Options) Shows No Image in Cart (fixed in [magento/magento2#16863](https://github.com/magento/magento2/pull/16863)) + * [#8131](https://github.com/magento/magento2/issues/8131) -- Magento 2.1.3 - There is a bug in advanced search form regarding validation messages (fixed in [magento/magento2#16952](https://github.com/magento/magento2/pull/16952)) + * [#14476](https://github.com/magento/magento2/issues/14476) -- Mobile device style groups incorrect order in _responsive.less (fixed in [magento/magento2#16959](https://github.com/magento/magento2/pull/16959)) + * [#15393](https://github.com/magento/magento2/issues/15393) -- "Multiple Select" product attributes do not render HTML tags on the storefront view (fixed in [magento/magento2#15687](https://github.com/magento/magento2/pull/15687)) + * [#3535](https://github.com/magento/magento2/issues/3535) -- Print pdf don't delete file in var folder (fixed in [magento/magento2#16401](https://github.com/magento/magento2/pull/16401)) + * [#14517](https://github.com/magento/magento2/issues/14517) -- PDF invoices in /var folder (fixed in [magento/magento2#16401](https://github.com/magento/magento2/pull/16401)) + * [#16273](https://github.com/magento/magento2/issues/16273) -- Method $product->getUrlInStore() returning extremely long URLs, could be a bug (fixed in [magento/magento2#16468](https://github.com/magento/magento2/pull/16468)) +* GitHub pull requests: + * [magento/magento2#13397](https://github.com/magento/magento2/pull/13397) -- magento/magento2#13296: Category name with special characters brakes ... (by @vinayshah) + * [magento/magento2#14338](https://github.com/magento/magento2/pull/14338) -- Fixing wrong sitemap product url #4788 (by @DenisSaltanahmedov) + * [magento/magento2#14715](https://github.com/magento/magento2/pull/14715) -- #14669 Css class "empty" is always present on minicart dropdown (by @Karlasa) + * [magento/magento2#14716](https://github.com/magento/magento2/pull/14716) -- [2.2] Fix - minicart label fixed size issue (by @Karlasa) + * [magento/magento2#12764](https://github.com/magento/magento2/pull/12764) -- magento/magento2#4389 Widget cache error (by @AlexandrKozyr) + * [magento/magento2#14314](https://github.com/magento/magento2/pull/14314) -- magento/magento2#13765 Excess requests 'customer data' on checkout cart page were fixed (by @andrewbess) + * [magento/magento2#14538](https://github.com/magento/magento2/pull/14538) -- [Forwardport] Fix HTML tags in meta description (by @davidwindell) + * [magento/magento2#14742](https://github.com/magento/magento2/pull/14742) -- Allow multiple tabs ui_components on a page (by @FreekVandeursen) + * [magento/magento2#14751](https://github.com/magento/magento2/pull/14751) -- Deleting CMS page via webapi/cron should remove related URL rewrites (by @unicoder88) + * [magento/magento2#14290](https://github.com/magento/magento2/pull/14290) -- CSS load order incorrect using default_head_blocks.xml #1821 (by @SergeyDmitruk) + * [magento/magento2#14707](https://github.com/magento/magento2/pull/14707) -- Catalog price rule save is too slow #13378 (by @chrom) + * [magento/magento2#14752](https://github.com/magento/magento2/pull/14752) -- Fix to allow use decimals less then 1 in subproducts qty. (by @likemusic) + * [magento/magento2#14769](https://github.com/magento/magento2/pull/14769) -- [Backport] Add expanded documentation to AdapterInterface::update (by @navarr) + * [magento/magento2#14790](https://github.com/magento/magento2/pull/14790) -- Update Readme file for magento2 repository (by @sidolov) + * [magento/magento2#12410](https://github.com/magento/magento2/pull/12410) -- Add argument on app:config:dump to skip dumping all system settings. (by @jalogut) + * [magento/magento2#14719](https://github.com/magento/magento2/pull/14719) -- Fixed setting of triggerRecollection flag (by @philippsander) + * [magento/magento2#14753](https://github.com/magento/magento2/pull/14753) -- [Backport] Fix bug with retry connect and custom db port (by @julienanquetil) + * [magento/magento2#14765](https://github.com/magento/magento2/pull/14765) -- Display Wrong Data On Cart Update Page (by @nit-it) + * [magento/magento2#14546](https://github.com/magento/magento2/pull/14546) -- Fix issue #13944. Show Store Views in Terms and Conditions grid. (by @afirlejczyk) + * [magento/magento2#14726](https://github.com/magento/magento2/pull/14726) -- Fix/navigation order function (by @luke-denton-aligent) + * [magento/magento2#14800](https://github.com/magento/magento2/pull/14800) -- #5726 - Fix reset password link with appropriate customer store (by @rodrigowebjump) + * [magento/magento2#14844](https://github.com/magento/magento2/pull/14844) -- Updated readme.md file 2.2-develop (by @sidolov) + * [magento/magento2#14795](https://github.com/magento/magento2/pull/14795) -- Invoice grid shows wrong shipping & handling for partial items invoice. It shows order's shipping & handling instead if invoiced shipping& handling charge (by @ankurvr) + * [magento/magento2#14833](https://github.com/magento/magento2/pull/14833) -- Fix a non well formed numeric value encountered on Magento/Directory/... (by @bmxmale) + * [magento/magento2#14627](https://github.com/magento/magento2/pull/14627) -- Fix: Datepicker problem when using non en-US locale. (by @tao-s) + * [magento/magento2#14836](https://github.com/magento/magento2/pull/14836) -- Use index sitemap name as prefix in split sitemaps (by @jameshalsall) + * [magento/magento2#14839](https://github.com/magento/magento2/pull/14839) -- Back port pull 14301 (by @julienanquetil) + * [magento/magento2#12566](https://github.com/magento/magento2/pull/12566) -- Move isAllowed method from AccessChangeQuoteControl to separate service (by @JeroenVanLeusden) + * [magento/magento2#14699](https://github.com/magento/magento2/pull/14699) -- [2.2] Optimize ID to SKU lookup of tier prices (by @toddbc) + * [magento/magento2#14829](https://github.com/magento/magento2/pull/14829) -- Add statement to 'beforeSave' method to allow app:config:import (by @bmxmale) + * [magento/magento2#12735](https://github.com/magento/magento2/pull/12735) -- magento/magento2#12193 Catalog not filtered by admin search bar (by @hannassy) + * [magento/magento2#14822](https://github.com/magento/magento2/pull/14822) -- Add default schedule config for sitemap_generate job (by @jameshalsall) + * [magento/magento2#14876](https://github.com/magento/magento2/pull/14876) -- Changed return type of addToCartPostParams to array (by @LordZardeck) + * [magento/magento2#14892](https://github.com/magento/magento2/pull/14892) -- Corrected @param in comment block (by @Yogeshks) + * [magento/magento2#14609](https://github.com/magento/magento2/pull/14609) -- Code cleanup, add more visibility (by @thomas-blackbird) + * [magento/magento2#14891](https://github.com/magento/magento2/pull/14891) -- Fix typo in doc for updateSpecificCoupons (by @sjb9774) + * [magento/magento2#14893](https://github.com/magento/magento2/pull/14893) -- [Backport] Fix aggregations use statements and return values (by @rogyar) + * [magento/magento2#14896](https://github.com/magento/magento2/pull/14896) -- Removed extra spaces from language file (by @Yogeshks) + * [magento/magento2#14905](https://github.com/magento/magento2/pull/14905) -- FIX for issue#14855 - Adding an * to do a customer search (by @phoenix128) + * [magento/magento2#14820](https://github.com/magento/magento2/pull/14820) -- Duplicate Order Confirmation Emails for PayPal Express checkout order (by @rocketweb) + * [magento/magento2#14904](https://github.com/magento/magento2/pull/14904) -- FIX for issue#14869 - Wrong price at backend after update (by @phoenix128) + * [magento/magento2#14928](https://github.com/magento/magento2/pull/14928) -- Removed extra close tag (by @Yogeshks) + * [magento/magento2#14886](https://github.com/magento/magento2/pull/14886) -- Fixed issue products grid operations in admin cart price rule edit page for user which has no access to CatalogRule module (by @Neos2007) + * [magento/magento2#14874](https://github.com/magento/magento2/pull/14874) -- Fix infinite checkout loader when some script wasn't loaded correctly because of network error (by @vovayatsyuk) + * [magento/magento2#14923](https://github.com/magento/magento2/pull/14923) -- Move customer.account.dashboard.info.extra block to contact information (by @JeroenVanLeusden) + * [magento/magento2#14939](https://github.com/magento/magento2/pull/14939) -- Renamed "Add Block Names to Hints" config setting to represent what it actually does (by @chris-pook) + * [magento/magento2#14935](https://github.com/magento/magento2/pull/14935) -- Change 'Update'-button visibility on change qty event. (by @likemusic) + * [magento/magento2#14963](https://github.com/magento/magento2/pull/14963) -- Fixed Overlay Problems (by @ArtiDjeims) + * [magento/magento2#14946](https://github.com/magento/magento2/pull/14946) -- use "Module_Name::template/path" format instead of using template/path i... (by @Jakhotiya) + * [magento/magento2#14976](https://github.com/magento/magento2/pull/14976) -- Changed the length of the remote_ip field to store ipv6 addresses (by @georgeschiopu) + * [magento/magento2#15015](https://github.com/magento/magento2/pull/15015) -- Fix typo in less button definition (by @shochdoerfer) + * [magento/magento2#15018](https://github.com/magento/magento2/pull/15018) -- chore: upgrade Node.js to 8 (by @DanielRuf) + * [magento/magento2#15023](https://github.com/magento/magento2/pull/15023) -- Fixed typos in .less files (by @kalpmehta) + * [magento/magento2#15002](https://github.com/magento/magento2/pull/15002) -- small optimization in if-condition (by @likemusic) + * [magento/magento2#15012](https://github.com/magento/magento2/pull/15012) -- fix: set message-success in setup if we already have the latest version (by @DanielRuf) + * [magento/magento2#15017](https://github.com/magento/magento2/pull/15017) -- chore: use random_int() in some places (by @DanielRuf) + * [magento/magento2#15016](https://github.com/magento/magento2/pull/15016) -- chore: checkout last 5 commits (by @DanielRuf) + * [magento/magento2#15020](https://github.com/magento/magento2/pull/15020) -- [2.2-develop] Update Gallery Template to handle boolean config Variables (by @gwharton) + * [magento/magento2#15032](https://github.com/magento/magento2/pull/15032) -- [TASK] Fix overriding of payment methods in getPaymentMethodList (by @mash1t) + * [magento/magento2#15053](https://github.com/magento/magento2/pull/15053) -- Fixes typo (by @jee1mr) + * [magento/magento2#14967](https://github.com/magento/magento2/pull/14967) -- Format the javascript code (by @Yogeshks) + * [magento/magento2#15067](https://github.com/magento/magento2/pull/15067) -- fixed documentation about viewModels. The key in xml should be view_m... (by @Jakhotiya) + * [magento/magento2#13904](https://github.com/magento/magento2/pull/13904) -- Add a link to the cart to the success message when adding a product (by @avstudnitz) + * [magento/magento2#14428](https://github.com/magento/magento2/pull/14428) -- Fix \Magento\Checkout\Controller\Index\Index::isSecureRequest method to take care of current request being secure and also from referer, as stated in phpdoc block (by @adrian-martinez-interactiv4) + * [magento/magento2#15129](https://github.com/magento/magento2/pull/15129) -- Add missing false-check to the ConfiguredRegularPrice price-model (by @tkotosz) + * [magento/magento2#15136](https://github.com/magento/magento2/pull/15136) -- [Backport] Add concrete type hints for product and category resources (by @rogyar) + * [magento/magento2#15162](https://github.com/magento/magento2/pull/15162) -- Fixed js error when product has double quote in its name (by @vovayatsyuk) + * [magento/magento2#15173](https://github.com/magento/magento2/pull/15173) -- Removed unused class declaration & code in comment (by @Yogeshks) + * [magento/magento2#15144](https://github.com/magento/magento2/pull/15144) -- Fixed Issue #11354 Merged CSS file name generation (by @sashas777) + * [magento/magento2#15174](https://github.com/magento/magento2/pull/15174) -- Restored app:config:status command after it was accidentally removed. (by @hostep) + * [magento/magento2#12324](https://github.com/magento/magento2/pull/12324) -- Simplified \Magento\Framework\Reflection\TypeProcessor (by @joni-jones) + * [magento/magento2#13185](https://github.com/magento/magento2/pull/13185) -- Fix negative basket total due to shipping tax residue (by @torreytsui) + * [magento/magento2#14614](https://github.com/magento/magento2/pull/14614) -- Add Parent Item to order item in repository (by @JeroenVanLeusden) + * [magento/magento2#15172](https://github.com/magento/magento2/pull/15172) -- 7399-clickableOverlay-less-fix - added pointer-events rule to .modal-... (by @virtua-pmakowski) + * [magento/magento2#15178](https://github.com/magento/magento2/pull/15178) -- Removed mageMenu widget dependency from breadcrumbs component (by @vovayatsyuk) + * [magento/magento2#15197](https://github.com/magento/magento2/pull/15197) -- Fix Magento_ImportExport not supporting unicode characters in column names (by @tdgroot) + * [magento/magento2#15202](https://github.com/magento/magento2/pull/15202) -- [Backport 2.2] Fix for displaying a negative price for a custom option. (by @dverkade) + * [magento/magento2#15137](https://github.com/magento/magento2/pull/15137) -- fix: do not forcefully set area in template if it is already set, fixes #14968, fixes #13530 (by @DanielRuf) + * [magento/magento2#15256](https://github.com/magento/magento2/pull/15256) -- Fixed typo in method name (by @olmer) + * [magento/magento2#14994](https://github.com/magento/magento2/pull/14994) -- Prevent not category links in breadcrumbs at product page (by @vovayatsyuk) + * [magento/magento2#15194](https://github.com/magento/magento2/pull/15194) -- Move buttons definition to separate file (by @jissereitsma) + * [magento/magento2#15249](https://github.com/magento/magento2/pull/15249) -- Removed non-existing argument (by @Yogeshks) + * [magento/magento2#15262](https://github.com/magento/magento2/pull/15262) -- Fix \Magento\Customer\Model\Customer\NotificationStorage class (by @adrian-martinez-interactiv4) + * [magento/magento2#15058](https://github.com/magento/magento2/pull/15058) -- Add 'const' type support to layout arguments (by @IgorVitol) + * [magento/magento2#15133](https://github.com/magento/magento2/pull/15133) -- Fix outdated address data when using Braintree's "Pay with PayPal" button (by @vovayatsyuk) + * [magento/magento2#15269](https://github.com/magento/magento2/pull/15269) -- Fix typo in Image::open exception message (by @t-richards) + * [magento/magento2#15282](https://github.com/magento/magento2/pull/15282) -- [fix] typo in private method name getUniq[ue]ImageIndex (by @mhauri) + * [magento/magento2#15291](https://github.com/magento/magento2/pull/15291) -- [Backport] Fix typo in database column comment (by @VitaliyBoyko) + * [magento/magento2#15292](https://github.com/magento/magento2/pull/15292) -- Fix typo in property name (by @dmytro-ch) + * [magento/magento2#15276](https://github.com/magento/magento2/pull/15276) -- [fix] typo in method name _getCharg[e]ableOptionPrice (by @mhauri) + * [magento/magento2#15293](https://github.com/magento/magento2/pull/15293) -- Fix typos in PHPDocs and comments (by @dmytro-ch) + * [magento/magento2#15302](https://github.com/magento/magento2/pull/15302) -- Fixed typo mistake in function comment (by @NamrataChangani) + * [magento/magento2#15294](https://github.com/magento/magento2/pull/15294) -- Fix typos in variable names (by @dmytro-ch) + * [magento/magento2#15386](https://github.com/magento/magento2/pull/15386) -- [Backport-2.2] Unused variable removed (by @VitaliyBoyko) + * [magento/magento2#15265](https://github.com/magento/magento2/pull/15265) -- declare var to fix scope error (by @keithbentrup) + * [magento/magento2#15333](https://github.com/magento/magento2/pull/15333) -- Added language translation for message string (by @Yogeshks) + * [magento/magento2#15331](https://github.com/magento/magento2/pull/15331) -- set alignment purchase order form and place order button (by @neeta-wagento) + * [magento/magento2#15341](https://github.com/magento/magento2/pull/15341) -- Refactored javascript code of admin notification modal popup (by @rahul-kachhadiya) + * [magento/magento2#15343](https://github.com/magento/magento2/pull/15343) -- Format the javascript code in Tax module (by @vgelani) + * [magento/magento2#15346](https://github.com/magento/magento2/pull/15346) -- Function is unnecessarily called multiple time (by @saurabh-aureate) + * [magento/magento2#15350](https://github.com/magento/magento2/pull/15350) -- 15319 : misleading data-container in product list (by @sunilit42) + * [magento/magento2#15351](https://github.com/magento/magento2/pull/15351) -- Refactor javascript code of button split widget (by @amittiwari024) + * [magento/magento2#15362](https://github.com/magento/magento2/pull/15362) -- Removed duplicate line and added comment on variable (by @vgelani) + * [magento/magento2#15411](https://github.com/magento/magento2/pull/15411) -- [Backport-2.2] Fixed abstract.js typo (by @VitaliyBoyko) + * [magento/magento2#15089](https://github.com/magento/magento2/pull/15089) -- Fix unnecessary recalculation of product list pricing (by @JeroenVanLeusden) + * [magento/magento2#15247](https://github.com/magento/magento2/pull/15247) -- ISSUE-14747 Newsletter subscription confirmation message does not dis... (by @KaushikChavda) + * [magento/magento2#15275](https://github.com/magento/magento2/pull/15275) -- [fix] typo in method name _exportAddress[s]es (by @mhauri) + * [magento/magento2#15332](https://github.com/magento/magento2/pull/15332) -- #14063 - Wrong invoice prefix in multistore setup due to default stor... (by @sanjay-wagento) + * [magento/magento2#15336](https://github.com/magento/magento2/pull/15336) -- #12820 - Wrong annotation in _toOptionArray - magento/framework/Data/... (by @sanjay-wagento) + * [magento/magento2#15347](https://github.com/magento/magento2/pull/15347) -- Fixed breadcrumb quote issue in product page #15037 (by @jignesh-baldha) + * [magento/magento2#15372](https://github.com/magento/magento2/pull/15372) -- Fixed Purchased Order Form button should visible properly (by @vgelani) + * [magento/magento2#15353](https://github.com/magento/magento2/pull/15353) -- Responsive Design Footers bottom of screen on mobile devices #15118 (by @chirag-wagento) + * [magento/magento2#15398](https://github.com/magento/magento2/pull/15398) -- Fixed set template syntax issue (by @vgelani) + * [magento/magento2#15431](https://github.com/magento/magento2/pull/15431) -- typo correction (by @AnshuMishra17) + * [magento/magento2#15010](https://github.com/magento/magento2/pull/15010) -- [BUGFIX] Added row_id to the flat action indexer so the value isn't s... (by @lewisvoncken) + * [magento/magento2#15211](https://github.com/magento/magento2/pull/15211) -- Error 500 in Module Manager (by @flancer64) + * [magento/magento2#15258](https://github.com/magento/magento2/pull/15258) -- [backport] fixes for instant purchase module from #15257 (by @mhauri) + * [magento/magento2#15340](https://github.com/magento/magento2/pull/15340) -- Submitting search form (mini) with enter key fires event handlers bound by jquery twice (by @amjadm61) + * [magento/magento2#15364](https://github.com/magento/magento2/pull/15364) -- Added language translation for comment tag (by @Yogeshks) + * [magento/magento2#15371](https://github.com/magento/magento2/pull/15371) -- Added language translation in template files (by @rahul-kachhadiya) + * [magento/magento2#15409](https://github.com/magento/magento2/pull/15409) -- Prevent multiple add-to-cart initializations in case of ajax loaded product listing (by @vovayatsyuk) + * [magento/magento2#15422](https://github.com/magento/magento2/pull/15422) -- Refactor JavsScript for UrlRewrite module edit page (by @patelnimesh1988) + * [magento/magento2#15421](https://github.com/magento/magento2/pull/15421) -- Updated font-size variable and standardize #ToDo UI (by @vgelani) + * [magento/magento2#15435](https://github.com/magento/magento2/pull/15435) -- [Backport] Removed redundant else statement (by @rogyar) + * [magento/magento2#15460](https://github.com/magento/magento2/pull/15460) -- Improvements to the CONTRIBUTING.md document (by @RebeccaBrocton) + * [magento/magento2#15040](https://github.com/magento/magento2/pull/15040) -- [2.2-develop] Transport variable can not be altered in email_invoice_set_template_vars_before Event (by @gwharton) + * [magento/magento2#15312](https://github.com/magento/magento2/pull/15312) -- [Fix] forgot to add lowercase conversion on grouped product assignation (by @jalogut) + * [magento/magento2#15454](https://github.com/magento/magento2/pull/15454) -- Fix HTML syntax in report.phtml error template (by @abcpremium) + * [magento/magento2#15416](https://github.com/magento/magento2/pull/15416) -- Moved css from media #TODO (by @vgelani) + * [magento/magento2#15462](https://github.com/magento/magento2/pull/15462) -- Adding manners to GitHub templates 2.2 (by @dmanners) + * [magento/magento2#15511](https://github.com/magento/magento2/pull/15511) -- Fixes in config module (by @mhauri) + * [magento/magento2#15513](https://github.com/magento/magento2/pull/15513) -- Fix typos in Multishipping and User module (by @avoelkl) + * [magento/magento2#15301](https://github.com/magento/magento2/pull/15301) -- Refactor JavsScript for customer logout (by @patelnimesh1988) + * [magento/magento2#15382](https://github.com/magento/magento2/pull/15382) -- Fix for Magnifier in inside mode (by @kacperchara) + * [magento/magento2#15499](https://github.com/magento/magento2/pull/15499) -- Issue 15469: Javascript error dropdowns.js (by @brian-labelle) + * [magento/magento2#15512](https://github.com/magento/magento2/pull/15512) -- Fixes in ui module (by @mhauri) + * [magento/magento2#15515](https://github.com/magento/magento2/pull/15515) -- [fix] dynamical assigned property in webapi (by @mhauri) + * [magento/magento2#15459](https://github.com/magento/magento2/pull/15459) -- [Resolved : UI Component listing action column outside of screen when... (by @hitesh-wagento) + * [magento/magento2#15468](https://github.com/magento/magento2/pull/15468) -- Issue 15467 where a configuration sku gets deleted but is still saved... (by @jonshipman) + * [magento/magento2#15514](https://github.com/magento/magento2/pull/15514) -- Fix method name (typo) (by @avoelkl) + * [magento/magento2#15517](https://github.com/magento/magento2/pull/15517) -- Use stored value of method instead of calling same method again. (by @saurabh-aureate) + * [magento/magento2#15519](https://github.com/magento/magento2/pull/15519) -- Typo correction (by @saurabh-aureate) + * [magento/magento2#15552](https://github.com/magento/magento2/pull/15552) -- Remove extra space and format the code in translation file (by @saurabh-aureate) + * [magento/magento2#15549](https://github.com/magento/magento2/pull/15549) -- Fixed typo error (by @vgelani) + * [magento/magento2#15097](https://github.com/magento/magento2/pull/15097) -- Add field to filter to collection (by @dverkade) + * [magento/magento2#15305](https://github.com/magento/magento2/pull/15305) -- chore: remove extraneous cursor property (by @DanielRuf) + * [magento/magento2#15477](https://github.com/magento/magento2/pull/15477) -- Variant product image in sidebar wishlist block (by @kishanpatadia) + * [magento/magento2#15598](https://github.com/magento/magento2/pull/15598) -- [BUGFIX] #15564 Generated admin API token expires immediately (by @krukas) + * [magento/magento2#15602](https://github.com/magento/magento2/pull/15602) -- set correct annotation (by @sanjay-wagento) + * [magento/magento2#13328](https://github.com/magento/magento2/pull/13328) -- Add indexes to timestamp field in oauth_nonce (by @KarlDeux) + * [magento/magento2#15349](https://github.com/magento/magento2/pull/15349) -- Resolve Knockout non-unique elements id in console error (by @neeta-wagento) + * [magento/magento2#15594](https://github.com/magento/magento2/pull/15594) -- Remove extra semicolon from the files (by @saurabh-aureate) + * [magento/magento2#15585](https://github.com/magento/magento2/pull/15585) -- Fix #13415 : Duplicated elements id (by @julienanquetil) + * [magento/magento2#15615](https://github.com/magento/magento2/pull/15615) -- [Backport] Removed comma(,) from translate attribute (by @dmytro-ch) + * [magento/magento2#15621](https://github.com/magento/magento2/pull/15621) -- fix typo for setCateroryIds (by @neeta-wagento) + * [magento/magento2#15645](https://github.com/magento/magento2/pull/15645) -- [Resolved : Menu widget submenu alignment #7897] (by @hitesh-wagento) + * [magento/magento2#15566](https://github.com/magento/magento2/pull/15566) -- Fixxes #15565 (by @EliasKotlyar) + * [magento/magento2#15715](https://github.com/magento/magento2/pull/15715) -- [BACKPORT 2.2 #15695] Fixed a couple of typos (by @dverkade) + * [magento/magento2#15718](https://github.com/magento/magento2/pull/15718) -- [Backport 2.2] Fixed return type of wishlist's getImageData in DocBlock (by @rogyar) + * [magento/magento2#15365](https://github.com/magento/magento2/pull/15365) -- Trim username on customer account login page (by @dankhrapiyush) + * [magento/magento2#15485](https://github.com/magento/magento2/pull/15485) -- fix: support multiple minisearch widget instances (by @DanielRuf) + * [magento/magento2#15614](https://github.com/magento/magento2/pull/15614) -- [Backport] Fixed product tier pricing pagination issue in admin (by @dmytro-ch) + * [magento/magento2#15765](https://github.com/magento/magento2/pull/15765) -- check if order data is available to incl ec (by @torhoehn) + * [magento/magento2#12314](https://github.com/magento/magento2/pull/12314) -- Prevent layout cache corruption in edge case (by @scottsb) + * [magento/magento2#15539](https://github.com/magento/magento2/pull/15539) -- FIX fo rissue #15510 - First PDF download / export after login (by @phoenix128) + * [magento/magento2#15694](https://github.com/magento/magento2/pull/15694) -- [Backport 2.2] Fix minor issues in ui export converter classes (by @dmytro-ch) + * [magento/magento2#15734](https://github.com/magento/magento2/pull/15734) -- [Resolved : Styling select by changing less variables in Luma theme (by @hitesh-wagento) + * [magento/magento2#15782](https://github.com/magento/magento2/pull/15782) -- [Backport 2.2]Fix translations (by @VitaliyBoyko) + * [magento/magento2#15791](https://github.com/magento/magento2/pull/15791) -- Removed unused class from forms less file. (by @chirag-wagento) + * [magento/magento2#15789](https://github.com/magento/magento2/pull/15789) -- Removed unnecessary css. (by @chirag-wagento) + * [magento/magento2#15795](https://github.com/magento/magento2/pull/15795) -- Remove double semicolon from the style sheets. (by @NamrataChangani) + * [magento/magento2#15825](https://github.com/magento/magento2/pull/15825) -- Fixed set template syntax issue (by @NamrataChangani) + * [magento/magento2#15840](https://github.com/magento/magento2/pull/15840) -- [Backport] Fix for issue 911 found on MSI project - Cannot read property source_... #14 (by @chirag-wagento) + * [magento/magento2#15854](https://github.com/magento/magento2/pull/15854) -- [Backport 2.2] Fixed return type hinting in DocBlocks for Wishlist module (by @rogyar) + * [magento/magento2#15871](https://github.com/magento/magento2/pull/15871) -- chore: remove unused less import (by @DanielRuf) + * [magento/magento2#12626](https://github.com/magento/magento2/pull/12626) -- Fixed condition with usage "hack" isPostRequest method (by @pusachev) + * [magento/magento2#12935](https://github.com/magento/magento2/pull/12935) -- Add Ability To Separate Frontend / Adminhtml in New Relic (by @mpchadwick) + * [magento/magento2#15019](https://github.com/magento/magento2/pull/15019) -- [TASK] Solve issue #14966 - Disabling product does not remove it from... (by @lewisvoncken) + * [magento/magento2#15297](https://github.com/magento/magento2/pull/15297) -- Fix typo in test method's name and test result (by @dmytro-ch) + * [magento/magento2#15320](https://github.com/magento/magento2/pull/15320) -- issue/14056 - Coupon API not working for guest user (by @Hypo386) + * [magento/magento2#15322](https://github.com/magento/magento2/pull/15322) -- ISSUE-11477 - fixed Swagger response for searchCriteria (by @idziakjakub) + * [magento/magento2#15661](https://github.com/magento/magento2/pull/15661) -- Fixed Wrong order amount on dashboard on Last orders listing when having more than one website with different currencies (by @ankurvr) + * [magento/magento2#15689](https://github.com/magento/magento2/pull/15689) -- #15588 Fixed incorrect image urls in multistore xml sitemap (by @StevenGuapaBV) + * [magento/magento2#15826](https://github.com/magento/magento2/pull/15826) -- Add missing table aliases to fields mapping for Customer Group filter... (by @Radio) + * [magento/magento2#13862](https://github.com/magento/magento2/pull/13862) -- Add compare list link to success message after adding a product (by @avstudnitz) + * [magento/magento2#15888](https://github.com/magento/magento2/pull/15888) -- Correct typo correction js files (by @saurabh-aureate) + * [magento/magento2#15892](https://github.com/magento/magento2/pull/15892) -- Wrong annotation in _toOptionArray : lib\internal\Magento\Framework\D... (by @NamrataChangani) + * [magento/magento2#15891](https://github.com/magento/magento2/pull/15891) -- Remove parameter from method calling (by @saurabh-aureate) + * [magento/magento2#15878](https://github.com/magento/magento2/pull/15878) -- [Resolved : limiter float too generic] (by @hitesh-wagento) + * [magento/magento2#15907](https://github.com/magento/magento2/pull/15907) -- fixed word typo (by @ledian-hymetllari) + * [magento/magento2#15914](https://github.com/magento/magento2/pull/15914) -- [Resolved : Changing @tab-content__border variable has no effect in B... (by @hitesh-wagento) + * [magento/magento2#15936](https://github.com/magento/magento2/pull/15936) -- #15308 removed extraneous margin (by @StevenGuapaBV) + * [magento/magento2#15991](https://github.com/magento/magento2/pull/15991) -- fix for dropdown toggle icon in cart (by @Karlasa) + * [magento/magento2#16001](https://github.com/magento/magento2/pull/16001) -- Extend default config instead overwrite (by @likemusic) + * [magento/magento2#16002](https://github.com/magento/magento2/pull/16002) -- bugfix checkout page cart icon color (by @Karlasa) + * [magento/magento2#16023](https://github.com/magento/magento2/pull/16023) -- [Backport 2.2] Wishlist: Remove unnecessary parameter from invoking toHtml() method (by @rogyar) + * [magento/magento2#16048](https://github.com/magento/magento2/pull/16048) -- fix: prevent inline-block issue in name form due to space and font-size (by @DanielRuf) + * [magento/magento2#15647](https://github.com/magento/magento2/pull/15647) -- [Backport] Fixes in widget component (by @mhauri) + * [magento/magento2#15811](https://github.com/magento/magento2/pull/15811) -- [Correct code formatting] (by @hitesh-wagento) + * [magento/magento2#15893](https://github.com/magento/magento2/pull/15893) -- Solve overlapping Issue on every Home page & category page of Hot Sel... (by @chirag-wagento) + * [magento/magento2#15902](https://github.com/magento/magento2/pull/15902) -- Complete the fix for cache issue due to the currencies with no symbol (by @dmytro-ch) + * [magento/magento2#15913](https://github.com/magento/magento2/pull/15913) -- [Backport] Fixes in catalog component blocks [2.3-develop] (by @chirag-wagento) + * [magento/magento2#16012](https://github.com/magento/magento2/pull/16012) -- Fix issue #15832 (by @Karlasa) + * [magento/magento2#16010](https://github.com/magento/magento2/pull/16010) -- Small refactoring to better code readability (by @likemusic) + * [magento/magento2#16053](https://github.com/magento/magento2/pull/16053) -- Improve retrieval of first array element (by @thomas-blackbird) + * [magento/magento2#16052](https://github.com/magento/magento2/pull/16052) -- Disabling sorting in glob and scandir functions for better performance (by @lfluvisotto) + * [magento/magento2#16065](https://github.com/magento/magento2/pull/16065) -- [Backport 2.2] Added unit test for captcha string resolver (by @rogyar) + * [magento/magento2#16080](https://github.com/magento/magento2/pull/16080) -- Adding support for variadic arguments fro method in generated proxy c... (by @vgelani) + * [magento/magento2#15344](https://github.com/magento/magento2/pull/15344) -- FIXED - appended payment code to ID field to make it unique (by @rakesh-gangani) + * [magento/magento2#15534](https://github.com/magento/magento2/pull/15534) -- magento/magento2#15255 unlock customer after password reset (by @andreagaspardo) + * [magento/magento2#15604](https://github.com/magento/magento2/pull/15604) -- [Backport] Removed unused translation for comment tag (by @rogyar) + * [magento/magento2#15870](https://github.com/magento/magento2/pull/15870) -- chore: prefer woff and woff2 (by @DanielRuf) + * [magento/magento2#15272](https://github.com/magento/magento2/pull/15272) -- DOBISSUE date format changed after customer tries to register with sa... (by @KaushikChavda) + * [magento/magento2#15993](https://github.com/magento/magento2/pull/15993) -- Correct return type of methods and typo correction. (by @saurabh-aureate) + * [magento/magento2#16082](https://github.com/magento/magento2/pull/16082) -- Navigation dropdown caret icon. (by @tejash-wagento) + * [magento/magento2#16091](https://github.com/magento/magento2/pull/16091) -- Replaced @escapeNotVerified annotations (by @istiahailo) + * [magento/magento2#16144](https://github.com/magento/magento2/pull/16144) -- array_push(...) calls behaving as '$array[] = ...', $array[] = works faster than invoking functions in PHP (by @lfluvisotto) + * [magento/magento2#16160](https://github.com/magento/magento2/pull/16160) -- [Backport 2.2] Captcha: Added unit test for CheckRegisterCheckoutObserver (by @rogyar) + * [magento/magento2#16181](https://github.com/magento/magento2/pull/16181) -- Fixed syntax for before-after operators in less files. (by @NamrataChangani) + * [magento/magento2#16182](https://github.com/magento/magento2/pull/16182) -- Removed double occurrence of keywords from sentences. (by @NamrataChangani) + * [magento/magento2#16183](https://github.com/magento/magento2/pull/16183) -- Correct sentence in comment section in class file. (by @NamrataChangani) + * [magento/magento2#16190](https://github.com/magento/magento2/pull/16190) -- #16079 translation possibility for moreButtonText (by @Karlasa) + * [magento/magento2#16194](https://github.com/magento/magento2/pull/16194) -- magento/magento2#16184: Fix type error in payment void method (by @xpoback) + * [magento/magento2#16192](https://github.com/magento/magento2/pull/16192) -- Trim email address in customer account create and login form (by @dankhrapiyush) + * [magento/magento2#16206](https://github.com/magento/magento2/pull/16206) -- Declare module namespace before template path name(Magento_Sales::order/info.phtml). (by @ronak2ram) + * [magento/magento2#16211](https://github.com/magento/magento2/pull/16211) -- Setting deploy mode to production with --skip-compilation flag should not clear generated code (by @platformvaimo) + * [magento/magento2#16213](https://github.com/magento/magento2/pull/16213) -- Fix for #8222 (by @0m3r) + * [magento/magento2#16216](https://github.com/magento/magento2/pull/16216) -- 15863: [Forwardport] Refactored javascript code of admin notification modal popup (by @IvanPletnyov) + * [magento/magento2#16220](https://github.com/magento/magento2/pull/16220) -- Incorrect value NULL was passed to DataObject constructor. It caused ... (by @Jakhotiya) + * [magento/magento2#16230](https://github.com/magento/magento2/pull/16230) -- Correct spelling mistakes in Model and library files. (by @NamrataChangani) + * [magento/magento2#15521](https://github.com/magento/magento2/pull/15521) -- Move breadcrumb json configuration to viewmodel (by @diedburn) + * [magento/magento2#15532](https://github.com/magento/magento2/pull/15532) -- FIX for issue #15501 - M2.2.4 missing meta title tag and doesn't show... (by @phoenix128) + * [magento/magento2#15629](https://github.com/magento/magento2/pull/15629) -- Fix #15627: Product order in category changed in Magento 2.2.4 (by @dverkade) + * [magento/magento2#15637](https://github.com/magento/magento2/pull/15637) -- Create ability to set is_visible_on_front to order status history comment (by @markoshust) + * [magento/magento2#16141](https://github.com/magento/magento2/pull/16141) -- Fix case mismatch call (class/method) (by @lfluvisotto) + * [magento/magento2#16215](https://github.com/magento/magento2/pull/16215) -- PHPDoc (by @lfluvisotto) + * [magento/magento2#16247](https://github.com/magento/magento2/pull/16247) -- Fixed typo error (by @vgelani) + * [magento/magento2#16240](https://github.com/magento/magento2/pull/16240) -- Removed double occurrence of 'it' from sentences. (by @NamrataChangani) + * [magento/magento2#16250](https://github.com/magento/magento2/pull/16250) -- Update Israeli ZIP code mask: 7 digits instead of 5 (by @itaymesh) + * [magento/magento2#12771](https://github.com/magento/magento2/pull/12771) -- magento/magento2#12695: Unable to change attribute type from swatch (by @eugene-shab) + * [magento/magento2#15774](https://github.com/magento/magento2/pull/15774) -- [Backport] Fix issue #14895 - Change Password warning message appear two times (by @sanjay-wagento) + * [magento/magento2#15872](https://github.com/magento/magento2/pull/15872) -- Fix missing PHPDocs hinting for AdvancedPricingImportExport module (by @dmytro-ch) + * [magento/magento2#15845](https://github.com/magento/magento2/pull/15845) -- Update webapi.xml to fix typo (by @mhaack) + * [magento/magento2#15929](https://github.com/magento/magento2/pull/15929) -- Postpone instantiation of session config by using a proxy (by @fmarangi) + * [magento/magento2#16286](https://github.com/magento/magento2/pull/16286) -- Add UpdatedAtListProvider to NotSyncedDataProvider for invoice grid (by @JeroenVanLeusden) + * [magento/magento2#16300](https://github.com/magento/magento2/pull/16300) -- Captcha: Added integration test for checking admin login attempts cleanup (by @rogyar) + * [magento/magento2#16306](https://github.com/magento/magento2/pull/16306) -- Captcha: Added integration tests for checking customer login attempts cleanup (by @rogyar) + * [magento/magento2#13509](https://github.com/magento/magento2/pull/13509) -- Use constant time string comparison in FormKey validator (by @p0pr0ck5) + * [magento/magento2#15339](https://github.com/magento/magento2/pull/15339) -- Fixed set template syntax in block file (by @NamrataChangani) + * [magento/magento2#16093](https://github.com/magento/magento2/pull/16093) -- When searching for the title if search for all the segments that has ... (by @rsantellan) + * [magento/magento2#16217](https://github.com/magento/magento2/pull/16217) -- Admin controller product set save refactor (by @AnshuMishra17) + * [magento/magento2#16279](https://github.com/magento/magento2/pull/16279) -- MAGETWO-61209: Backport - Fixed issue #7379 with mage/calendar when setting `numberOfM... (by @vasilii-b) + * [magento/magento2#16333](https://github.com/magento/magento2/pull/16333) -- Add metadata title in unit test (by @slackerzz) + * [magento/magento2#16379](https://github.com/magento/magento2/pull/16379) -- [Changed password placeholder text in checkout page] (by @hitesh-wagento) + * [magento/magento2#16389](https://github.com/magento/magento2/pull/16389) -- [Backport 2.2] Use correct error message for duplicate error key in product import (by @gelanivishal) + * [magento/magento2#15464](https://github.com/magento/magento2/pull/15464) -- Fix "Confirmation request" email is sent on customer's newsletter unsubscribe action (by @nuzil) + * [magento/magento2#16009](https://github.com/magento/magento2/pull/16009) -- fix: change "My Dashboard" to "My Account", fixes #16007 (by @DanielRuf) + * [magento/magento2#16086](https://github.com/magento/magento2/pull/16086) -- Fix false cache_lifetime usage in xml layouts (by @yuriyDne) + * [magento/magento2#16372](https://github.com/magento/magento2/pull/16372) -- Wishlist update item issue (by @eduard13) + * [magento/magento2#16386](https://github.com/magento/magento2/pull/16386) -- Login with wishlist raise report after logout. (by @swnsma) + * [magento/magento2#16438](https://github.com/magento/magento2/pull/16438) -- Credit memo email template file: fixing incorrect object type error (by @JosephMaxwell) + * [magento/magento2#16458](https://github.com/magento/magento2/pull/16458) -- Add missing showInStore attributes (by @aschrammel) + * [magento/magento2#16477](https://github.com/magento/magento2/pull/16477) -- Fix for #14593 (second try #16431) (by @0m3r) + * [magento/magento2#15543](https://github.com/magento/magento2/pull/15543) -- Enhancements to module:status command (by @jissereitsma) + * [magento/magento2#16472](https://github.com/magento/magento2/pull/16472) -- Improve comment message (by @gelanivishal) + * [magento/magento2#16489](https://github.com/magento/magento2/pull/16489) -- Properly hyphenate "third-party" (by @erikhansen) + * [magento/magento2#16495](https://github.com/magento/magento2/pull/16495) -- Fixed spell issue in library (by @sanganinamrata) + * [magento/magento2#15909](https://github.com/magento/magento2/pull/15909) -- [Backport] Fix for Wrong price amount on product page (by @gelanivishal) + * [magento/magento2#16090](https://github.com/magento/magento2/pull/16090) -- Added and removed unnecessary translation for label/comment tags (by @Yogeshks) + * [magento/magento2#16393](https://github.com/magento/magento2/pull/16393) -- Rework for PR #16222 . (by @phoenix128) + * [magento/magento2#16517](https://github.com/magento/magento2/pull/16517) -- Fix responsive tables showing broken heading (by @LordZardeck) + * [magento/magento2#16524](https://github.com/magento/magento2/pull/16524) -- Clear converted file data (by @gelanivishal) + * [magento/magento2#16549](https://github.com/magento/magento2/pull/16549) -- Corrected function comment (by @sanganinamrata) + * [magento/magento2#16553](https://github.com/magento/magento2/pull/16553) -- Update mini-cart checkout translations (by @JeroenVanLeusden) + * [magento/magento2#16557](https://github.com/magento/magento2/pull/16557) -- Updated SynonymGroup.xml (by @sanganinamrata) + * [magento/magento2#16576](https://github.com/magento/magento2/pull/16576) -- [Backport] Declare module namespace before template path name (by @mageprince) + * [magento/magento2#16581](https://github.com/magento/magento2/pull/16581) -- Removed double occurrences from files. (by @sanganinamrata) + * [magento/magento2#16143](https://github.com/magento/magento2/pull/16143) -- Variable as a method parameter might be overridden by the loop (by @lfluvisotto) + * [magento/magento2#16254](https://github.com/magento/magento2/pull/16254) -- Customer group extension attributes not carried over on save (by @JosephMaxwell) + * [magento/magento2#16474](https://github.com/magento/magento2/pull/16474) -- [FIX] dev:di:info duplicates plugin info (by @Coderimus) + * [magento/magento2#16564](https://github.com/magento/magento2/pull/16564) -- Trim email address in newsletter, forgot password, checkout login and email to a friend form (by @dankhrapiyush) + * [magento/magento2#11554](https://github.com/magento/magento2/pull/11554) -- Improve attribute checking (by @FreekVandeursen) + * [magento/magento2#16271](https://github.com/magento/magento2/pull/16271) -- Covered Magento\Checkout\Model\Cart\CollectQuote by Unit Test (by @eduard13) + * [magento/magento2#16540](https://github.com/magento/magento2/pull/16540) -- Fix zero price simple failed to resolve as default (by @torreytsui) + * [magento/magento2#16530](https://github.com/magento/magento2/pull/16530) -- Fixed widget template rendering issue while rewriting widget block. (by @sanganinamrata) + * [magento/magento2#16559](https://github.com/magento/magento2/pull/16559) -- fix icon color variable naming (by @Karlasa) + * [magento/magento2#16584](https://github.com/magento/magento2/pull/16584) -- [Backport] Remove the timezone from the date when retrieving the current month from a UTC timestamp. (by @mageprince) + * [magento/magento2#16590](https://github.com/magento/magento2/pull/16590) -- Fix of invalid price for integer currencies when amount less than group size (by @vkublytskyi) + * [magento/magento2#16626](https://github.com/magento/magento2/pull/16626) -- [Backport] Fix type hints and add undefined property in Webapi [2.3-develop] (by @mageprince) + * [magento/magento2#16644](https://github.com/magento/magento2/pull/16644) -- Removed double occurrences from Magento modules. (by @sanganinamrata) + * [magento/magento2#16645](https://github.com/magento/magento2/pull/16645) -- Updated Magento_Newsletter's block file. (by @sanganinamrata) + * [magento/magento2#16646](https://github.com/magento/magento2/pull/16646) -- Corrected Magento_Framework's test xml file. (by @sanganinamrata) + * [magento/magento2#16669](https://github.com/magento/magento2/pull/16669) -- Prevent servers being slammed from many search suggestion requests (by @LordZardeck) + * [magento/magento2#16678](https://github.com/magento/magento2/pull/16678) -- Improved code and remove unnecessary space (by @ronak2ram) + * [magento/magento2#16675](https://github.com/magento/magento2/pull/16675) -- Prevent running SQL query on every item in the database when the quote is empty (by @LordZardeck) + * [magento/magento2#16689](https://github.com/magento/magento2/pull/16689) -- Added 'title' attribute to 'a' link. (by @sanganinamrata) + * [magento/magento2#16690](https://github.com/magento/magento2/pull/16690) -- Added translation function for Magento_Braintree module's template file. (by @sanganinamrata) + * [magento/magento2#16691](https://github.com/magento/magento2/pull/16691) -- Added 'title' attribute to 'img' tag in knockout template files. (by @sanganinamrata) + * [magento/magento2#16711](https://github.com/magento/magento2/pull/16711) -- Fixed typo in SynonymGroupRepositoryInterface (by @AnshuMishra17) + * [magento/magento2#15479](https://github.com/magento/magento2/pull/15479) -- Fix newsletter subscription behaviour for registered customer. (by @nuzil) + * [magento/magento2#16175](https://github.com/magento/magento2/pull/16175) -- Admin tabs order not working properly (by @tiagosampaio) + * [magento/magento2#16408](https://github.com/magento/magento2/pull/16408) -- Fixed type hints and docs for Downloadable Samples block (by @phoenix-bjoern) + * [magento/magento2#16414](https://github.com/magento/magento2/pull/16414) -- Fixing a Mistype Error (by @tiagosampaio) + * [magento/magento2#16554](https://github.com/magento/magento2/pull/16554) -- Fix docBlock for hasInvoices(), hasShipments(), hasCreditmemos() (by @nuzil) + * [magento/magento2#16566](https://github.com/magento/magento2/pull/16566) -- Smallest codestyle fix in Option/Type/Text.php (by @likemusic) + * [magento/magento2#16680](https://github.com/magento/magento2/pull/16680) -- Captcha: Added unit test for CheckGuestCheckoutObserver (by @rogyar) + * [magento/magento2#16693](https://github.com/magento/magento2/pull/16693) -- 'Allowed Countries' - get countries for scope 'default'. (by @swnsma) + * [magento/magento2#16685](https://github.com/magento/magento2/pull/16685) -- Updated security issues details (by @quisse) + * [magento/magento2#16704](https://github.com/magento/magento2/pull/16704) -- Add sort order to user agent rules table headers (by @JRhyne) + * [magento/magento2#16717](https://github.com/magento/magento2/pull/16717) -- Removed space before ending sentence. (by @sanganinamrata) + * [magento/magento2#16716](https://github.com/magento/magento2/pull/16716) -- fix _utilities.less font-size issue (by @Karlasa) + * [magento/magento2#16721](https://github.com/magento/magento2/pull/16721) -- Corrected return message from ProductRuleTest.php (by @sanganinamrata) + * [magento/magento2#16732](https://github.com/magento/magento2/pull/16732) -- Resolved : no navigation-level0-item__hover__color #15848 (by @hitesh-wagento) + * [magento/magento2#16726](https://github.com/magento/magento2/pull/16726) -- [Backport 2.3] Add spelling correction: formatedPrice to formattedPrice (by @arnoudhgz) + * [magento/magento2#13569](https://github.com/magento/magento2/pull/13569) -- Correctly save Product Custom Option values (by @JeroenVanLeusden) + * [magento/magento2#14379](https://github.com/magento/magento2/pull/14379) -- [Backport 2.2] Issue 14351: Product import doesn't change `Enable Qty Increments` field (by @simpleadm) + * [magento/magento2#16599](https://github.com/magento/magento2/pull/16599) -- Fixed backwards incompatible change to Transport variable event parameters (by @gwharton) + * [magento/magento2#16748](https://github.com/magento/magento2/pull/16748) -- Remove commented code & remove space (by @ronak2ram) + * [magento/magento2#16766](https://github.com/magento/magento2/pull/16766) -- fix #16764 Rating Star issue on Product detail Page. (by @Karlasa) + * [magento/magento2#16821](https://github.com/magento/magento2/pull/16821) -- Code improvement (by @mage2pratik) + * [magento/magento2#16831](https://github.com/magento/magento2/pull/16831) -- [Backport] Magento_Sales integration tests: fix invoice_list fixture var tags (by @ronak2ram) + * [magento/magento2#16435](https://github.com/magento/magento2/pull/16435) -- Add generated code to the psr-0 autoloader section so when optimizing... (by @hostep) + * [magento/magento2#16595](https://github.com/magento/magento2/pull/16595) -- Trim issue on customer confirmation form (by @gelanivishal) + * [magento/magento2#16845](https://github.com/magento/magento2/pull/16845) -- [Backport] Add @api annotation to Filter Group & Sort Order (by @ronak2ram) + * [magento/magento2#16861](https://github.com/magento/magento2/pull/16861) -- Add Confirm Modal Width (by @hryvinskyi) + * [magento/magento2#16872](https://github.com/magento/magento2/pull/16872) -- Remove extra spaces from Magento/Ui (by @ronak2ram) + * [magento/magento2#16873](https://github.com/magento/magento2/pull/16873) -- Improve "Invalid country code" error message on tax import (by @adampmoss) + * [magento/magento2#16916](https://github.com/magento/magento2/pull/16916) -- [Backport] Issue 5316 (by @ronak2ram) + * [magento/magento2#16579](https://github.com/magento/magento2/pull/16579) -- removed _responsive.less import from gallery.less (by @Karlasa) + * [magento/magento2#16707](https://github.com/magento/magento2/pull/16707) -- Update regex in ControllerAclTest (by @aleron75) + * [magento/magento2#16785](https://github.com/magento/magento2/pull/16785) -- Avoid undefined index warning when using uppercase reserved word (by @FreekVandeursen) + * [magento/magento2#16841](https://github.com/magento/magento2/pull/16841) -- Clean code (by @GraysonChiang) + * [magento/magento2#16840](https://github.com/magento/magento2/pull/16840) -- Log when Magento is in maintenance mode (by @Ethan3600) + * [magento/magento2#16851](https://github.com/magento/magento2/pull/16851) -- Remove direct use of object manager (by @AnshuMishra17) + * [magento/magento2#16882](https://github.com/magento/magento2/pull/16882) -- Remove duplicated string. (by @likemusic) + * [magento/magento2#16880](https://github.com/magento/magento2/pull/16880) -- Array short syntax (by @lfluvisotto) + * [magento/magento2#16889](https://github.com/magento/magento2/pull/16889) -- Microrefactoring in product gallery block helper (by @likemusic) + * [magento/magento2#16891](https://github.com/magento/magento2/pull/16891) -- Remove commented code (by @mage2pratik) + * [magento/magento2#16890](https://github.com/magento/magento2/pull/16890) -- hide cookie notice instead of reloading site (by @torhoehn) + * [magento/magento2#16899](https://github.com/magento/magento2/pull/16899) -- Fixing annotations for some methods. (by @tiagosampaio) + * [magento/magento2#16903](https://github.com/magento/magento2/pull/16903) -- Fixes white color coding standard. (by @chirag-wagento) + * [magento/magento2#16924](https://github.com/magento/magento2/pull/16924) -- Replacing Usage of Deprecated Methods for Message Manager. (by @tiagosampaio) + * [magento/magento2#16937](https://github.com/magento/magento2/pull/16937) -- Revert changing file permissions in #15144 (by @ihor-sviziev) + * [magento/magento2#16928](https://github.com/magento/magento2/pull/16928) -- Reduce lengthy code of LoginPost (by @GlennCheng) + * [magento/magento2#16978](https://github.com/magento/magento2/pull/16978) -- Wrong namespace defined in compare.phtml (by @ronak2ram) + * [magento/magento2#16977](https://github.com/magento/magento2/pull/16977) -- Removed double occurrences from Magento modules (by @mage2pratik) + * [magento/magento2#16980](https://github.com/magento/magento2/pull/16980) -- Fixed a couple of spelling mistakes (by @mage2pratik) + * [magento/magento2#17002](https://github.com/magento/magento2/pull/17002) -- [Backport] Remove unused comments from _initDiscount() function (by @mageprince) + * [magento/magento2#16560](https://github.com/magento/magento2/pull/16560) -- Admin user auth controller refactor (by @AnshuMishra17) + * [magento/magento2#16863](https://github.com/magento/magento2/pull/16863) -- Configurable Product with Only Size Options (No Color Options) Shows ... (by @ronak2ram) + * [magento/magento2#16883](https://github.com/magento/magento2/pull/16883) -- Update nginx.config.sample to exclude php5-fpm (by @sean-wcb) + * [magento/magento2#16900](https://github.com/magento/magento2/pull/16900) -- Add Clean Code (by @hryvinskyi) + * [magento/magento2#16921](https://github.com/magento/magento2/pull/16921) -- Slight Changes to Code (by @tiagosampaio) + * [magento/magento2#16946](https://github.com/magento/magento2/pull/16946) -- Delete all unused imports of lib/internal/Magento (by @osrecio) + * [magento/magento2#16965](https://github.com/magento/magento2/pull/16965) -- fix: add hasrequired notice for create account form and password forg... (by @DanielRuf) + * [magento/magento2#17019](https://github.com/magento/magento2/pull/17019) -- Fixes Black color coding standard. (by @chirag-wagento) + * [magento/magento2#16952](https://github.com/magento/magento2/pull/16952) -- Issue 8131 - Use Redirect Factory to Allow Error Message Display on Advanced Search (by @brobie) + * [magento/magento2#16959](https://github.com/magento/magento2/pull/16959) -- Resolved : Mobile device style groups incorrect order (by @tejash-wagento) + * [magento/magento2#16971](https://github.com/magento/magento2/pull/16971) -- Fix misprint ('_requesetd' > '_requested') (by @likemusic) + * [magento/magento2#16988](https://github.com/magento/magento2/pull/16988) -- Correct return type of methods (by @mage2pratik) + * [magento/magento2#16984](https://github.com/magento/magento2/pull/16984) -- Categories > Left menu > Item title space fix (by @rafaelstz) + * [magento/magento2#17006](https://github.com/magento/magento2/pull/17006) -- Adjust page-main container height for sticky footer; fixes #15118 (by @denistrator) + * [magento/magento2#17027](https://github.com/magento/magento2/pull/17027) -- Remove spaces around amount span. (by @likemusic) + * [magento/magento2#17063](https://github.com/magento/magento2/pull/17063) -- Added unit test for DB model in backup module (by @rogyar) + * [magento/magento2#17077](https://github.com/magento/magento2/pull/17077) -- Remove commented code (by @mage2pratik) + * [magento/magento2#17097](https://github.com/magento/magento2/pull/17097) -- [Backport] Magento UI - Cleanup of undefined mixins parameters and usage of "leaking" variables scope (by @mageprince) + * [magento/magento2#17099](https://github.com/magento/magento2/pull/17099) -- [Backport] Fix app/code/Magento/Backend/Block/Media/Uploader.php getConfigJson() method (by @mageprince) + * [magento/magento2#17098](https://github.com/magento/magento2/pull/17098) -- [Backport] testGetIgnoresFirstSlash method in ObjectManagerTest has lost its purpose (dummy test) (by @mageprince) + * [magento/magento2#17114](https://github.com/magento/magento2/pull/17114) -- Disable autocomplete for captcha inputs (by @denistrator) + * [magento/magento2#17129](https://github.com/magento/magento2/pull/17129) -- Update template.js (by @angelomaragna) + * [magento/magento2#17137](https://github.com/magento/magento2/pull/17137) -- GoogleAnalytics: Added unit test for order success observer (by @rogyar) + * [magento/magento2#17151](https://github.com/magento/magento2/pull/17151) -- Fixed a grammatical error on the vault tooltip (by @kreativedev) + * [magento/magento2#15687](https://github.com/magento/magento2/pull/15687) -- Fixes for #15393 (by @simonjanguapa) + * [magento/magento2#16401](https://github.com/magento/magento2/pull/16401) -- Remove PDF files after generation (by @rogyar) + * [magento/magento2#16468](https://github.com/magento/magento2/pull/16468) -- #16273: Fix bug in method getUrlInStore() of product model (by @vasilii-b) + * [magento/magento2#17124](https://github.com/magento/magento2/pull/17124) -- Using interface instead of model directly (by @woutersamaey) + * [magento/magento2#17163](https://github.com/magento/magento2/pull/17163) -- Add meta NOINDEX,NOFOLLOW to admin scope to avoid accidental crawling (by @cmtickle) + * [magento/magento2#17035](https://github.com/magento/magento2/pull/17035) -- Replaced deprecated methods. (by @tiagosampaio) + * [magento/magento2#17227](https://github.com/magento/magento2/pull/17227) -- Replaced deprecated methods. (by @tiagosampaio) + +2.2.5 +============= +* GitHub issues: + * [#7720](https://github.com/magento/magento2/issues/7720) -- Product Repository saves attribute values for existing product always on "Default Store Level" (fixed in [magento-engcom/magento2ce#967](https://github.com/magento-engcom/magento2ce/pull/967)) + * [#12186](https://github.com/magento/magento2/issues/12186) -- Custom attributes values not updated (fixed in [magento-engcom/magento2ce#967](https://github.com/magento-engcom/magento2ce/pull/967)) + * [#12395](https://github.com/magento/magento2/issues/12395) -- Custom Magento CLI command has incorrect current store id. (fixed in [magento-engcom/magento2ce#967](https://github.com/magento-engcom/magento2ce/pull/967)) + * [#12792](https://github.com/magento/magento2/issues/12792) -- [2.1.10] No order confirmation email after paying with PayPal Express (fixed in [magento/magento2#13898](https://github.com/magento/magento2/pull/13898)) + * [#13778](https://github.com/magento/magento2/issues/13778) -- Braintree Paypal Method No Order Confirmation Email Sent (fixed in [magento/magento2#13898](https://github.com/magento/magento2/pull/13898)) + * [#13556](https://github.com/magento/magento2/issues/13556) -- Sorting in Product Listing via Quantity not work (fixed in [magento/magento2#13691](https://github.com/magento/magento2/pull/13691)) + * [#13769](https://github.com/magento/magento2/issues/13769) -- Order Email Sender (fixed in [magento/magento2#13878](https://github.com/magento/magento2/pull/13878)) + * [#12405](https://github.com/magento/magento2/issues/12405) -- Magento 2.2.1 - Impossible to create a new storeview (fixed in [magento/magento2#13943](https://github.com/magento/magento2/pull/13943)) + * [#12421](https://github.com/magento/magento2/issues/12421) -- 'Requested store is not found' when trying to create a store view in the back end (fixed in [magento/magento2#13943](https://github.com/magento/magento2/pull/13943)) + * [#13804](https://github.com/magento/magento2/issues/13804) -- Invoice grid shows wrong subtotal for partial items invoice. It shows order's subtotal instead if invoiced item's subtotal (fixed in [magento/magento2#13855](https://github.com/magento/magento2/pull/13855)) + * [#7372](https://github.com/magento/magento2/issues/7372) -- Product images gets removed from "Images And Videos" after validation alert. (fixed in [magento-engcom/magento2ce#1140](https://github.com/magento-engcom/magento2ce/pull/1140)) + * [#13385](https://github.com/magento/magento2/issues/13385) -- SQL query is printed into browser in case of exception (fixed in [magento/magento2#13607](https://github.com/magento/magento2/pull/13607)) + * [#13117](https://github.com/magento/magento2/issues/13117) -- Swatch Attribute is not getting save while deleting a swatch row with empty admin scope text (fixed in [magento/magento2#13717](https://github.com/magento/magento2/pull/13717)) + * [#3483](https://github.com/magento/magento2/issues/3483) -- Default country selection issue while creating new customer from backend (fixed in [magento/magento2#13024](https://github.com/magento/magento2/pull/13024)) + * [#13231](https://github.com/magento/magento2/issues/13231) -- Default State or Province is not pre-selected in the Estimate Shipping and Tax (fixed in [magento-engcom/magento2ce#1258](https://github.com/magento-engcom/magento2ce/pull/1258)) + * [#10559](https://github.com/magento/magento2/issues/10559) -- Extending swatch functionality using javascript mixins does not work in Safari and MS Edge (fixed in [magento/magento2#12929](https://github.com/magento/magento2/pull/12929)) + * [#5463](https://github.com/magento/magento2/issues/5463) -- The ability to store passwords using different hashing algorithms is limited (fixed in [magento/magento2#13884](https://github.com/magento/magento2/pull/13884)) + * [#13988](https://github.com/magento/magento2/issues/13988) -- Mini search field looses focus after its JavaScript is initialized (fixed in [magento/magento2#13989](https://github.com/magento/magento2/pull/13989)) + * [#13820](https://github.com/magento/magento2/issues/13820) -- IE11 minicart not updating on configurable product page (ES6) (fixed in [magento/magento2#14105](https://github.com/magento/magento2/pull/14105)) + * [#14010](https://github.com/magento/magento2/issues/14010) -- Why Report Bugs link not open in new tab? (fixed in [magento/magento2#14121](https://github.com/magento/magento2/pull/14121)) + * [#12205](https://github.com/magento/magento2/issues/12205) -- Stock inventory reindex bug (fixed in [magento-engcom/magento2ce#1134](https://github.com/magento-engcom/magento2ce/pull/1134)) + * [#8168](https://github.com/magento/magento2/issues/8168) -- Configurable product on wishlist shows parent image instead variation image (fixed in [magento-engcom/magento2ce#1031](https://github.com/magento-engcom/magento2ce/pull/1031)) + * [#14138](https://github.com/magento/magento2/issues/14138) -- Outdated package after upgrade sjparkinson/static-review is abandoned (fixed in [magento/magento2#14091](https://github.com/magento/magento2/pull/14091)) + * [#14109](https://github.com/magento/magento2/issues/14109) -- `MAX_NUM_COOKIES` doesn't follow the open-closed principle (fixed in [magento/magento2#14128](https://github.com/magento/magento2/pull/14128)) + * [#13704](https://github.com/magento/magento2/issues/13704) -- Category\Collection::joinUrlRewrite should use the store set on the collection (fixed in [magento/magento2#13716](https://github.com/magento/magento2/pull/13716)) + * [#13992](https://github.com/magento/magento2/issues/13992) -- Incorrect phpdoc should be Shipment\Item not Invoice\Item (fixed in [magento/magento2#14303](https://github.com/magento/magento2/pull/14303)) + * [#14089](https://github.com/magento/magento2/issues/14089) -- Malaysian (Malaysia) missing from locale list (fixed in [magento/magento2#14306](https://github.com/magento/magento2/pull/14306)) + * [#7428](https://github.com/magento/magento2/issues/7428) -- Multiline fields in forms have no visible label (fixed in [magento/magento2#14317](https://github.com/magento/magento2/pull/14317)) + * [#10057](https://github.com/magento/magento2/issues/10057) -- Editing order with backordered items results in new order not correctly marking order items as backordered (fixed in [magento/magento2#14327](https://github.com/magento/magento2/pull/14327)) + * [#10700](https://github.com/magento/magento2/issues/10700) -- Magento 2 Admin panel show loading on each page (fixed in [magento/magento2#14361](https://github.com/magento/magento2/pull/14361)) + * [#11930](https://github.com/magento/magento2/issues/11930) -- setup:di:compile's generated cache files inaccessible by the web-server user (fixed in [magento/magento2#14361](https://github.com/magento/magento2/pull/14361)) + * [#14072](https://github.com/magento/magento2/issues/14072) -- Change zip code validation pattern for Japan (fixed in [magento/magento2#14299](https://github.com/magento/magento2/pull/14299)) + * [#7816](https://github.com/magento/magento2/issues/7816) -- Customer_account.xml file abused (fixed in [magento/magento2#14325](https://github.com/magento/magento2/pull/14325)) + * [#10650](https://github.com/magento/magento2/issues/10650) -- Cron starts when it's already running (fixed in [magento/magento2#12497](https://github.com/magento/magento2/pull/12497)) + * [#14307](https://github.com/magento/magento2/issues/14307) -- Possible to press the "Previous" button while in the first step of the installation (fixed in [magento/magento2#14309](https://github.com/magento/magento2/pull/14309)) + * [#14249](https://github.com/magento/magento2/issues/14249) -- Priduct page price is using the hardcoded digits in js (fixed in [magento/magento2#14350](https://github.com/magento/magento2/pull/14350)) + * [#13582](https://github.com/magento/magento2/issues/13582) -- Magento 2.1.11 minimum quantity validation message not showing (fixed in [magento/magento2#13942](https://github.com/magento/magento2/pull/13942)) + * [#8837](https://github.com/magento/magento2/issues/8837) -- Google Analytics code being placed in body instead of head (fixed in [magento/magento2#14293](https://github.com/magento/magento2/pull/14293)) + * [#13010](https://github.com/magento/magento2/issues/13010) -- Write a Review page works on multistore for products that are not assigned to that store (fixed in [magento/magento2#14360](https://github.com/magento/magento2/pull/14360)) + * [#14049](https://github.com/magento/magento2/issues/14049) -- Retrieve session information from another customer under /customer/section/load/sections=&update_section_id=true (fixed in [magento/magento2#14176](https://github.com/magento/magento2/pull/14176)) + * [#6879](https://github.com/magento/magento2/issues/6879) -- Unable to change country of manufacture default label value (fixed in [magento/magento2#14319](https://github.com/magento/magento2/pull/14319)) + * [#14572](https://github.com/magento/magento2/issues/14572) -- Specify the table when adding field to filter for the collection Eav/Model/ResourceModel/Entity/Attribute/Option/Collection.php (fixed in [magento/magento2#14599](https://github.com/magento/magento2/pull/14599)) + * [#9666](https://github.com/magento/magento2/issues/9666) -- Magento 2.1.6 - Invoice PDF doesn't support Thai (fixed in [magento/magento2#13016](https://github.com/magento/magento2/pull/13016)) + * [#12323](https://github.com/magento/magento2/issues/12323) -- Magento 2.1.3 - Invoice and shipment PDF doesn't support Arabic (fixed in [magento/magento2#13016](https://github.com/magento/magento2/pull/13016)) + * [#14035](https://github.com/magento/magento2/issues/14035) -- Magento REST API, wrong condition for product list category filter (fixed in [magento/magento2#14048](https://github.com/magento/magento2/pull/14048)) + * [#14465](https://github.com/magento/magento2/issues/14465) -- [Indexes] Product 'version_id' lost last 'auro_increment' value after MySQL restart. (fixed in [magento/magento2#14635](https://github.com/magento/magento2/pull/14635)) + * [#13652](https://github.com/magento/magento2/issues/13652) -- Issue in product title with special chars in mini cart (fixed in [magento/magento2#14681](https://github.com/magento/magento2/pull/14681)) +* GitHub pull requests: + * [magento/magento2#13898](https://github.com/magento/magento2/pull/13898) -- Send order email for Braintree Paypal orders (by @pmclain) + * [magento/magento2#13956](https://github.com/magento/magento2/pull/13956) -- Use event object in 'ajax:addToCart' trigger when adding a product to the cart (by @koenner01) + * [magento/magento2#13691](https://github.com/magento/magento2/pull/13691) -- Fix for Issue-13556 - Sorting in Product Listing via Quantity not work (by @nuzil) + * [magento/magento2#13878](https://github.com/magento/magento2/pull/13878) -- Issues 13769. Fix wrong info about sent email in order sender. (by @pawcioma) + * [magento/magento2#13943](https://github.com/magento/magento2/pull/13943) -- magento/magento2#12405: Impossible to create a new storeview (by @hostep) + * [magento/magento2#13173](https://github.com/magento/magento2/pull/13173) -- Performance: remove count() form the condition section of a loop (by @Coderimus) + * [magento/magento2#13855](https://github.com/magento/magento2/pull/13855) -- Invoice grid shows wrong subtotal for partial items invoice. It shows order's subtotal instead if invoiced item's subtotal (by @ankurvr) + * [magento/magento2#14011](https://github.com/magento/magento2/pull/14011) -- Added alias to block 'product.info.description' (by @chedaroo) + * [magento/magento2#14013](https://github.com/magento/magento2/pull/14013) -- Use `^1.4` for `composer/composer` (by @sandermangel) + * [magento/magento2#14026](https://github.com/magento/magento2/pull/14026) -- [FIX] Remove not used variable in template (by @Coderimus) + * [magento/magento2#14030](https://github.com/magento/magento2/pull/14030) -- [FIX] Remove not used and empty template (by @Coderimus) + * [magento/magento2#11376](https://github.com/magento/magento2/pull/11376) -- [Backport 2.2-develop] PHP Livecodetest testCodeStyle() method does not use whitelist files (by @adrian-martinez-interactiv4) + * [magento/magento2#13977](https://github.com/magento/magento2/pull/13977) -- Backport of PR-10748 for Magento 2.2: Always use https for Vimeo vide... (by @hostep) + * [magento/magento2#14028](https://github.com/magento/magento2/pull/14028) -- [FIX] small refactoring and removing not using variable from templates (by @Coderimus) + * [magento/magento2#13607](https://github.com/magento/magento2/pull/13607) -- SQL query is printed into browser in case of exception (by @shyamranpara) + * [magento/magento2#13717](https://github.com/magento/magento2/pull/13717) -- [Backport 2.2] Solve problem saving empty swatches in admin (by @enriquei4) + * [magento/magento2#13807](https://github.com/magento/magento2/pull/13807) -- [Backport 2.2] Add quoting for base path in DI compile command (by @simpleadm) + * [magento/magento2#13024](https://github.com/magento/magento2/pull/13024) -- resolved default country selection issue while creating new customer ... (by @pradeep-wagento) + * [magento/magento2#14044](https://github.com/magento/magento2/pull/14044) -- Make scope parameters of methods to save/delete config optional (by @avstudnitz) + * [magento/magento2#12929](https://github.com/magento/magento2/pull/12929) -- Issues #10559 - Extend swatch using mixins (M2.2) (by @srenon) + * [magento/magento2#13884](https://github.com/magento/magento2/pull/13884) -- #5463 - Use specified hashing algo in \Magento\Framework\Encryption\Encryptor::getHash (by @k4emic) + * [magento/magento2#13894](https://github.com/magento/magento2/pull/13894) -- Fix cache issue for currencies with no symbol (by @evgk) + * [magento/magento2#13989](https://github.com/magento/magento2/pull/13989) -- Act better on existing input focus instead of removing it (by @krzksz) + * [magento/magento2#14029](https://github.com/magento/magento2/pull/14029) -- Fix $useCache for container child blocks (by @tdgroot) + * [magento/magento2#14042](https://github.com/magento/magento2/pull/14042) -- Improve array output format for etc.php and config.php (by @avstudnitz) + * [magento/magento2#14062](https://github.com/magento/magento2/pull/14062) -- Typo in SSL port number (by @jasperzeinstra) + * [magento/magento2#14083](https://github.com/magento/magento2/pull/14083) -- Fix product attribute ordering when more than 10 attributes. (by @RandeKnight) + * [magento/magento2#14105](https://github.com/magento/magento2/pull/14105) -- magento/magento2#13820: IE11 minicart not updating on configurable pr... (by @Frodigo) + * [magento/magento2#14121](https://github.com/magento/magento2/pull/14121) -- [Backport] Open link "Report an Issue" in a new tab (by @sidolov) + * [magento/magento2#14041](https://github.com/magento/magento2/pull/14041) -- Removed unnecessary protected member variables. (by @Yogeshks) + * [magento/magento2#14106](https://github.com/magento/magento2/pull/14106) -- [FIX] several fixes for sales and tax module: not used imports, variables and legacy code (by @Coderimus) + * [magento/magento2#14136](https://github.com/magento/magento2/pull/14136) -- Added mage/translate component to customers's ajax login (by @ccasciotti) + * [magento/magento2#14154](https://github.com/magento/magento2/pull/14154) -- catalog:images:resize CLI command fix (by @nfourteen) + * [magento/magento2#14189](https://github.com/magento/magento2/pull/14189) -- fix incorrect phpdoc return type (by @EliasZ) + * [magento/magento2#11707](https://github.com/magento/magento2/pull/11707) -- UPS Option to include TAX in rate (by @gwharton) + * [magento/magento2#14156](https://github.com/magento/magento2/pull/14156) -- Add website- and storeview-code in stores admin grid (by @aschrammel) + * [magento/magento2#12893](https://github.com/magento/magento2/pull/12893) -- Improvement: Magento\Sales\Helper\Guest refactoring and bugfix (by @Coderimus) + * [magento/magento2#13653](https://github.com/magento/magento2/pull/13653) -- Update Store getConfig() to respect valid false return value (by @JeroenVanLeusden) + * [magento/magento2#14091](https://github.com/magento/magento2/pull/14091) -- Remove sjparkinson/static-review and other obsolete tools (by @orlangur) + * [magento/magento2#14128](https://github.com/magento/magento2/pull/14128) -- ISSUE-14109: Allow modification of cookies via extension (by @brideo) + * [magento/magento2#13716](https://github.com/magento/magento2/pull/13716) -- Category\Collection::joinUrlRewrite should use the store set on the collection (by @alepane21) + * [magento/magento2#14230](https://github.com/magento/magento2/pull/14230) -- Fix for broken navigation menu on IE11 (by @cstergianos) + * [magento/magento2#14306](https://github.com/magento/magento2/pull/14306) -- [#14089] Add Malaysian Locale Code (by @osrecio) + * [magento/magento2#14303](https://github.com/magento/magento2/pull/14303) -- Resolves PHPdoc issue in ticket #13992 (by @cream-julian) + * [magento/magento2#14317](https://github.com/magento/magento2/pull/14317) -- FR#7428 - Multiline fields in forms have no visible label (by @crisdiaz) + * [magento/magento2#14358](https://github.com/magento/magento2/pull/14358) -- Format code (by @mageprince) + * [magento/magento2#13414](https://github.com/magento/magento2/pull/13414) -- Add getters to product image builder (by @VincentMarmiesse) + * [magento/magento2#14308](https://github.com/magento/magento2/pull/14308) -- Added language translation, make proper sentence and removed unused delete html container (by @Yogeshks) + * [magento/magento2#14327](https://github.com/magento/magento2/pull/14327) -- magento/magento2#10057 (by @swnsma) + * [magento/magento2#14347](https://github.com/magento/magento2/pull/14347) -- [Backport 2.2] Add json and xml support to the post method in socket client (by @simpleadm) + * [magento/magento2#14361](https://github.com/magento/magento2/pull/14361) -- Removed cache backend option which explicitly set file permissions (by @xtremeperf) + * [magento/magento2#14388](https://github.com/magento/magento2/pull/14388) -- [FIX] Remove duplicated case statement (by @Coderimus) + * [magento/magento2#14060](https://github.com/magento/magento2/pull/14060) -- Disable add to cart button when redirect to cart enabled (by @ihor-sviziev) + * [magento/magento2#14299](https://github.com/magento/magento2/pull/14299) -- [#14072 2.2] Add Zip Pattern for Japan JP (by @osrecio) + * [magento/magento2#14325](https://github.com/magento/magento2/pull/14325) -- #7816: Customer_account.xml file abused (2.2) (by @mikewhitby) + * [magento/magento2#12497](https://github.com/magento/magento2/pull/12497) -- Prevent running again already running cron group (by @paveq) + * [magento/magento2#14288](https://github.com/magento/magento2/pull/14288) -- Fill visibility in AdminCreateConfigurableProductTest.xml (by @tdgroot) + * [magento/magento2#14385](https://github.com/magento/magento2/pull/14385) -- Remove improper unit test (by @orlangur) + * [magento/magento2#14309](https://github.com/magento/magento2/pull/14309) -- Disable "Back" button on the first step of the setup (by @ArjenMiedema) + * [magento/magento2#14350](https://github.com/magento/magento2/pull/14350) -- precision for price overriding by js (by @cdiacon) + * [magento/magento2#14403](https://github.com/magento/magento2/pull/14403) -- test command inside if/then clause broke before install script (by @edie-pasek) + * [magento/magento2#14440](https://github.com/magento/magento2/pull/14440) -- Removed extra backslash from comment block (by @Yogeshks) + * [magento/magento2#13942](https://github.com/magento/magento2/pull/13942) -- Issue #13582 show message for qty minAllowed, maxAllowed, qtyIncremen... (by @bordeo) + * [magento/magento2#14293](https://github.com/magento/magento2/pull/14293) -- Moved Google Analytics block code to head tag #8837 (by @KravetsAndriy) + * [magento/magento2#14439](https://github.com/magento/magento2/pull/14439) -- Update process-reviews.js (by @sanderjongsma) + * [magento/magento2#14445](https://github.com/magento/magento2/pull/14445) -- [FIX] Simplify ternary operators for catalog module (by @Coderimus) + * [magento/magento2#14455](https://github.com/magento/magento2/pull/14455) -- Fix button color on hover in email template (by @Karlasa) + * [magento/magento2#14452](https://github.com/magento/magento2/pull/14452) -- Remove else statements from \Magento\Framework\Session\SessionManager (by @adrian-martinez-interactiv4) + * [magento/magento2#14466](https://github.com/magento/magento2/pull/14466) -- Correct function return statement. (by @NamrataChangani) + * [magento/magento2#14473](https://github.com/magento/magento2/pull/14473) -- Added spanish Bolivia locale to allowedLocales list (by @JDavidVR) + * [magento/magento2#13808](https://github.com/magento/magento2/pull/13808) -- [Backport 2.2] Configurable product price options by store (by @simpleadm) + * [magento/magento2#14360](https://github.com/magento/magento2/pull/14360) -- Fix issue #13010. Check if product is assigned to current website. (by @afirlejczyk) + * [magento/magento2#14457](https://github.com/magento/magento2/pull/14457) -- [Backport 2.2] Return status in console commands (by @simpleadm) + * [magento/magento2#14498](https://github.com/magento/magento2/pull/14498) -- fix translation issue with rating stars (by @Karlasa) + * [magento/magento2#14504](https://github.com/magento/magento2/pull/14504) -- Check if store id is not null instead of empty (by @quisse) + * [magento/magento2#13629](https://github.com/magento/magento2/pull/13629) -- Fix translate issue (by @Corefix) + * [magento/magento2#13831](https://github.com/magento/magento2/pull/13831) -- Fixed comparison with 0 bug for TableRate shipping carrier (by @irs) + * [magento/magento2#14176](https://github.com/magento/magento2/pull/14176) -- Replace the existing headers with the no cache headers (by @joost-florijn-kega) + * [magento/magento2#14319](https://github.com/magento/magento2/pull/14319) -- 6879 - Unable to change country of manufacture default label value (by @MateuszChrapek) + * [magento/magento2#13257](https://github.com/magento/magento2/pull/13257) -- [FIX]: Recent orders are not filtered per store at the customer account page (by @Coderimus) + * [magento/magento2#14559](https://github.com/magento/magento2/pull/14559) -- Fix for Issue #13950 - Cache issue with configurable products related to currency-conversions (by @nuzil) + * [magento/magento2#14552](https://github.com/magento/magento2/pull/14552) -- Allow to configure min and max dates for date picker component (by @tkotosz) + * [magento/magento2#14599](https://github.com/magento/magento2/pull/14599) -- Specify the table when adding field to filter (by @PierreLeMaguer) + * [magento/magento2#13016](https://github.com/magento/magento2/pull/13016) -- Fix for sales PDFs to support more characters (by @rossmc) + * [magento/magento2#14048](https://github.com/magento/magento2/pull/14048) -- Fix for GitHub issue #14035. (by @kamilszewczyk) + * [magento/magento2#14629](https://github.com/magento/magento2/pull/14629) -- Refactor Code for Mass Order Unhold (by @AnshuMishra17) + * [magento/magento2#14635](https://github.com/magento/magento2/pull/14635) -- [Forwardport] magento/magento2#14465 Fix empty changelog tables after MySQL restart. (by @ihor-sviziev) + * [magento/magento2#14668](https://github.com/magento/magento2/pull/14668) -- Added hyphenation, cutting edge to cutting-edge. (by @surya07081995) + * [magento/magento2#14678](https://github.com/magento/magento2/pull/14678) -- Checkout page - Fix tooltip position on mobile devices (by @ihor-sviziev) + * [magento/magento2#14681](https://github.com/magento/magento2/pull/14681) -- [Backport] Fix #13652. Mini cart - fix issue in product title with special chars. (by @ihor-sviziev) + * [magento/magento2#14688](https://github.com/magento/magento2/pull/14688) -- Translate Action Label (by @net32) + * [magento/magento2#14696](https://github.com/magento/magento2/pull/14696) -- [Backport] Eliminate usage of "else" statements (by @ihor-sviziev) + +2.2.4 +============= +* GitHub issues: + * [#7691](https://github.com/magento/magento2/issues/7691) -- address with saveInAddressBook 0 are still being added to the address book for new customers (fixed in [magento/magento2#12171](https://github.com/magento/magento2/pull/12171)) + * [#9277](https://github.com/magento/magento2/issues/9277) -- Create new CLI command: enable/disable Magento Profiler (fixed in [magento/magento2#11407](https://github.com/magento/magento2/pull/11407)) + * [#11941](https://github.com/magento/magento2/issues/11941) -- Invoice for products that use qty decimal rounds down to whole number (fixed in [magento/magento2#11997](https://github.com/magento/magento2/pull/11997)) + * [#12083](https://github.com/magento/magento2/issues/12083) -- Cannot import zero (0) value into custom attribute (fixed in [magento/magento2#12283](https://github.com/magento/magento2/pull/12283)) + * [#3596](https://github.com/magento/magento2/issues/3596) -- Notice: Undefined index: value in /app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Select.php on line 72 (fixed in [magento/magento2#12296](https://github.com/magento/magento2/pull/12296)) + * [#9764](https://github.com/magento/magento2/issues/9764) -- exception message is wrong and misleading in findAccessorMethodName() of Magento\Framework\Reflection\NameFinder (fixed in [magento/magento2#12303](https://github.com/magento/magento2/pull/12303)) + * [#13214](https://github.com/magento/magento2/issues/13214) -- Not a correct displaying for Robots.txt (fixed in [magento/magento2#12310](https://github.com/magento/magento2/pull/12310)) + * [#9684](https://github.com/magento/magento2/issues/9684) -- No ACL set for integrations (fixed in [magento/magento2#12332](https://github.com/magento/magento2/pull/12332)) + * [#10438](https://github.com/magento/magento2/issues/10438) -- Potential error on order edit page when address has extension attributes (fixed in [magento/magento2#11787](https://github.com/magento/magento2/pull/11787)) + * [#11691](https://github.com/magento/magento2/issues/11691) -- Wrong return type for getAttributeText($attributeCode) (fixed in [magento/magento2#12003](https://github.com/magento/magento2/pull/12003)) + * [#12261](https://github.com/magento/magento2/issues/12261) -- Order confirmation email contains non functioning links (fixed in [magento/magento2#12308](https://github.com/magento/magento2/pull/12308)) + * [#12146](https://github.com/magento/magento2/issues/12146) -- Customer with empty "Date of Birth" cannot be saved (fixed in [magento/magento2#12302](https://github.com/magento/magento2/pull/12302)) + * [#10502](https://github.com/magento/magento2/issues/10502) -- Fatal error: Call getTranslateInline of null when generating some sitemap with errors (fixed in [magento/magento2#11320](https://github.com/magento/magento2/pull/11320)) + * [#11139](https://github.com/magento/magento2/issues/11139) -- Product Repeat Isuue after filter on category listing page. (fixed in [magento/magento2#11429](https://github.com/magento/magento2/pull/11429)) + * [#8003](https://github.com/magento/magento2/issues/8003) -- Using System Value for Base Currency Results in Config Error (fixed in [magento/magento2#11809](https://github.com/magento/magento2/pull/11809)) + * [#10347](https://github.com/magento/magento2/issues/10347) -- Wrong order tax amounts displayed when using specific tax configuration (fixed in [magento/magento2#11592](https://github.com/magento/magento2/pull/11592)) + * [#9360](https://github.com/magento/magento2/issues/9360) -- field doesn't work in system.xml for "radios" fields (fixed in [magento/magento2#11539](https://github.com/magento/magento2/pull/11539)) + * [#11792](https://github.com/magento/magento2/issues/11792) -- Can't add customizable options to product (fixed in [magento/magento2#11965](https://github.com/magento/magento2/pull/11965)) + * [#11528](https://github.com/magento/magento2/issues/11528) -- Validation prevents form closing (fixed in [magento/magento2#12048](https://github.com/magento/magento2/pull/12048)) + * [#12064](https://github.com/magento/magento2/issues/12064) -- Database Rollback not working with magento 2.1.9? (fixed in [magento/magento2#12108](https://github.com/magento/magento2/pull/12108)) + * [#9413](https://github.com/magento/magento2/issues/9413) -- Cannot remove product_list_toolbar in XML (fixed in [magento/magento2#11473](https://github.com/magento/magento2/pull/11473)) + * [#11669](https://github.com/magento/magento2/issues/11669) -- API salesRefundInvoiceV1 does no save invoice ID on credit memo (fixed in [magento/magento2#11670](https://github.com/magento/magento2/pull/11670)) + * [#11740](https://github.com/magento/magento2/issues/11740) -- Sending emails from Admin in Multi-Store Environment defaults to Primary Store (fixed in [magento/magento2#11992](https://github.com/magento/magento2/pull/11992)) + * [#9410](https://github.com/magento/magento2/issues/9410) -- Impossible to add swatch options via Service Contracts if there is no existing swatch option for attribute (fixed in [magento/magento2#12036](https://github.com/magento/magento2/pull/12036)) + * [#10707](https://github.com/magento/magento2/issues/10707) -- Create attribute option via API for swatch attribute fails (fixed in [magento/magento2#12036](https://github.com/magento/magento2/pull/12036)) + * [#10737](https://github.com/magento/magento2/issues/10737) -- Can't import attribute option over API if option is 'visual swatch' (fixed in [magento/magento2#12036](https://github.com/magento/magento2/pull/12036)) + * [#11032](https://github.com/magento/magento2/issues/11032) -- Unable to add new options to swatch attribute (fixed in [magento/magento2#12036](https://github.com/magento/magento2/pull/12036)) + * [#10128](https://github.com/magento/magento2/issues/10128) -- New Orders not being saved to order grid (fixed in [magento/magento2#12241](https://github.com/magento/magento2/pull/12241)) + * [#9515](https://github.com/magento/magento2/issues/9515) -- South Korea Zip Code Validation incorrect (fixed in [magento-engcom/magento2ce#903](https://github.com/magento-engcom/magento2ce/pull/903)) + * [#10210](https://github.com/magento/magento2/issues/10210) -- Transport variable can not be altered in email_invoice_set_template_vars_before Event (fixed in [magento/magento2#12132](https://github.com/magento/magento2/pull/12132)) + * [#11341](https://github.com/magento/magento2/issues/11341) -- Attribute category_ids issue (fixed in [magento/magento2#11389](https://github.com/magento/magento2/pull/11389)) + * [#12127](https://github.com/magento/magento2/issues/12127) -- Apostrophe in attribute option value in admin is not handled properly (fixed in [magento/magento2#12133](https://github.com/magento/magento2/pull/12133)) + * [#12058](https://github.com/magento/magento2/issues/12058) -- Can't save emoji in custom product options (fixed in [magento/magento2#12253](https://github.com/magento/magento2/pull/12253)) + * [#9742](https://github.com/magento/magento2/issues/9742) -- Default welcome message returns after being deleted (fixed in [magento/magento2#12328](https://github.com/magento/magento2/pull/12328)) + * [#9468](https://github.com/magento/magento2/issues/9468) -- REST API bundle-products/:sku/options/all always return is not authorized (fixed in [magento-engcom/magento2ce#904](https://github.com/magento-engcom/magento2ce/pull/904)) + * [#6634](https://github.com/magento/magento2/issues/6634) -- Yes/No attribute value is not shown on a product details page (fixed in [magento/magento2#12057](https://github.com/magento/magento2/pull/12057)) + * [#9961](https://github.com/magento/magento2/issues/9961) -- Unused product attributes display with value N/A or NO on storefront (fixed in [magento/magento2#12057](https://github.com/magento/magento2/pull/12057)) + * [#9931](https://github.com/magento/magento2/issues/9931) -- Empty image alt-text & missing alt attribute on product detail page (fixed in [magento/magento2#11323](https://github.com/magento/magento2/pull/11323)) + * [#11236](https://github.com/magento/magento2/issues/11236) -- Web Setup Wizard Icon Inconsistency (fixed in [magento/magento2#11388](https://github.com/magento/magento2/pull/11388)) + * [#11484](https://github.com/magento/magento2/issues/11484) -- Visual Merchandiser show prices of out of stock simple products for the associated configurable product. (fixed in [magento/magento2#11485](https://github.com/magento/magento2/pull/11485)) + * [#8255](https://github.com/magento/magento2/issues/8255) -- Export Products action doesn't consider hide_for_product_page value (fixed in [magento/magento2#11926](https://github.com/magento/magento2/pull/11926)) + * [#11509](https://github.com/magento/magento2/issues/11509) -- Psr logger debug method does not work by the default in developer mode (fixed in [magento/magento2#12207](https://github.com/magento/magento2/pull/12207)) + * [#11882](https://github.com/magento/magento2/issues/11882) -- It's not possible to enable "log to file" (debugging) in production mode (fixed in [magento/magento2#12207](https://github.com/magento/magento2/pull/12207)) + * [#9918](https://github.com/magento/magento2/issues/9918) -- Magento 2 automatically disables maintenance mode after certain actions (fixed in [magento/magento2#11052](https://github.com/magento/magento2/pull/11052)) + * [#11825](https://github.com/magento/magento2/issues/11825) -- 2.1.9 Item not added to the Wishlist if the user is not logged at the moment he click on the button to add it. (fixed in [magento/magento2#12038](https://github.com/magento/magento2/pull/12038)) + * [#11908](https://github.com/magento/magento2/issues/11908) -- Adding to wishlist doesn't work when not logged in (fixed in [magento/magento2#12038](https://github.com/magento/magento2/pull/12038)) + * [#758](https://github.com/magento/magento2/issues/758) -- Coding standards: arrays (fixed in [magento/magento2#12499](https://github.com/magento/magento2/pull/12499)) + * [#11324](https://github.com/magento/magento2/issues/11324) -- Updating a product via the REST API assigns it to all websites automatically. (fixed in [magento/magento2#11444](https://github.com/magento/magento2/pull/11444)) + * [#9633](https://github.com/magento/magento2/issues/9633) -- Web Setup Wizard 500 error when session storage is configured to use memcache (fixed in [magento/magento2#11608](https://github.com/magento/magento2/pull/11608)) + * [#6770](https://github.com/magento/magento2/issues/6770) -- M2.1.1 : Re-saving a product attribute with a different name than it's code results in an error (fixed in [magento/magento2#11617](https://github.com/magento/magento2/pull/11617)) + * [#11059](https://github.com/magento/magento2/issues/11059) -- 92 usages of expectException() with ignored $message parameter (fixed in [magento/magento2#11099](https://github.com/magento/magento2/pull/11099)) + * [#11409](https://github.com/magento/magento2/issues/11409) -- Too many password reset requests even when disabled in settings (fixed in [magento/magento2#11435](https://github.com/magento/magento2/pull/11435)) + * [#12110](https://github.com/magento/magento2/issues/12110) -- Missing cascade into attribute set deletion (fixed in [magento/magento2#12167](https://github.com/magento/magento2/pull/12167)) + * [#12268](https://github.com/magento/magento2/issues/12268) -- Gallery issues on configurable product page (fixed in [magento/magento2#12469](https://github.com/magento/magento2/pull/12469) and [magento-engcom/magento2ce#991](https://github.com/magento-engcom/magento2ce/pull/991)) + * [#12506](https://github.com/magento/magento2/issues/12506) -- Fixup typo getDispretionPath -> getDispersionPath (fixed in [magento/magento2#12507](https://github.com/magento/magento2/pull/12507)) + * [#12482](https://github.com/magento/magento2/issues/12482) -- Sitemap image links in MultiStore (fixed in [magento-engcom/magento2ce#935](https://github.com/magento-engcom/magento2ce/pull/935)) + * [#8437](https://github.com/magento/magento2/issues/8437) -- Silent error when an email template is not found (fixed in [magento-engcom/magento2ce#970](https://github.com/magento-engcom/magento2ce/pull/970)) + * [#8176](https://github.com/magento/magento2/issues/8176) -- LinkManagement::getChildren() does not include product ID's (and visibility) (fixed in [magento-engcom/magento2ce#986](https://github.com/magento-engcom/magento2ce/pull/986)) + * [#12613](https://github.com/magento/magento2/issues/12613) -- Verbiage Update Required: Product Image Watermark size Validation Message (fixed in [magento-engcom/magento2ce#985](https://github.com/magento-engcom/magento2ce/pull/985)) + * [#12180](https://github.com/magento/magento2/issues/12180) -- M2.2.1 Unable to open Address book after account creation (fixed in [magento/magento2#12220](https://github.com/magento/magento2/pull/12220)) + * [#12450](https://github.com/magento/magento2/issues/12450) -- Store not found when adding a ? to site URL. (fixed in [magento/magento2#12529](https://github.com/magento/magento2/pull/12529)) + * [#12468](https://github.com/magento/magento2/issues/12468) -- Sort by Price not working on CatalogSearch Page in Magento 2 (fixed in [magento-engcom/magento2ce#929](https://github.com/magento-engcom/magento2ce/pull/929)) + * [#7467](https://github.com/magento/magento2/issues/7467) -- File Put Contents file with empty content (fixed in [magento-engcom/magento2ce#962](https://github.com/magento-engcom/magento2ce/pull/962)) + * [#8410](https://github.com/magento/magento2/issues/8410) -- Custom Checkout Step and Shipping Step are Highlighted and Combined upon Checkout page load (fixed in [magento-engcom/magento2ce#975](https://github.com/magento-engcom/magento2ce/pull/975)) + * [#12582](https://github.com/magento/magento2/issues/12582) -- Can't remove item description from wishlist (fixed in [magento-engcom/magento2ce#981](https://github.com/magento-engcom/magento2ce/pull/981)) + * [#8862](https://github.com/magento/magento2/issues/8862) -- Can't emptying values by magento 2 api (fixed in [magento-engcom/magento2ce#916](https://github.com/magento-engcom/magento2ce/pull/916)) + * [#8011](https://github.com/magento/magento2/issues/8011) -- Strip Tags from attribute (fixed in [magento-engcom/magento2ce#968](https://github.com/magento-engcom/magento2ce/pull/968)) + * [#12526](https://github.com/magento/magento2/issues/12526) -- Currency change, Bank Transfer but checkout page shows "Your credit card will be charged for" (fixed in [magento-engcom/magento2ce#993](https://github.com/magento-engcom/magento2ce/pull/993)) + * [#12535](https://github.com/magento/magento2/issues/12535) -- Product change sku via repository (fixed in [magento-engcom/magento2ce#984](https://github.com/magento-engcom/magento2ce/pull/984)) + * [#8507](https://github.com/magento/magento2/issues/8507) -- There is invalid type in PHPDoc block of \Magento\Framework\Data\Tree::getNodeById() (fixed in [magento-engcom/magento2ce#964](https://github.com/magento-engcom/magento2ce/pull/964)) + * [#10123](https://github.com/magento/magento2/issues/10123) -- Invoice entity_model in table eav_entity_type (fixed in [magento-engcom/magento2ce#980](https://github.com/magento-engcom/magento2ce/pull/980)) + * [#9055](https://github.com/magento/magento2/issues/9055) -- Default Store is always used when retrieving sequence value's for sales entity's. (fixed in [magento/magento2#11702](https://github.com/magento/magento2/pull/11702)) + * [#8601](https://github.com/magento/magento2/issues/8601) -- Can bypass Minimum Order Amount Logic (fixed in [magento-engcom/magento2ce#963](https://github.com/magento-engcom/magento2ce/pull/963)) + * [#10797](https://github.com/magento/magento2/issues/10797) -- catalogProductTierPriceManagementV1 DELETE and POST operation wipes out media gallery selections when used on store code "all". (fixed in [magento-engcom/magento2ce#977](https://github.com/magento-engcom/magento2ce/pull/977)) + * [#12560](https://github.com/magento/magento2/issues/12560) -- Back-End issue for multi-store website: when editing Order shipping/billing address - allowed countries are selected from wrong Store View (fixed in [magento-engcom/magento2ce#982](https://github.com/magento-engcom/magento2ce/pull/982)) + * [#2907](https://github.com/magento/magento2/issues/2907) -- Integration Test Annotation magentoAppArea breaks with some valid values (fixed in [magento-engcom/magento2ce#996](https://github.com/magento-engcom/magento2ce/pull/996)) + * [#5738](https://github.com/magento/magento2/issues/5738) -- SearchCriteriaBuilder builds wrong criteria (ORDER BY part) (fixed in [magento-engcom/magento2ce#1003](https://github.com/magento-engcom/magento2ce/pull/1003)) + * [#12259](https://github.com/magento/magento2/issues/12259) -- Save and Duplicated product not working (fixed in [magento-engcom/magento2ce#983](https://github.com/magento-engcom/magento2ce/pull/983)) + * [#8204](https://github.com/magento/magento2/issues/8204) -- catalog:images:resize = getimagesize(): Read error! in vendor/magento/module-catalog/Model/Product/Image.php on line 410 if an image is 0 bytes (fixed in [magento-engcom/magento2ce#1000](https://github.com/magento-engcom/magento2ce/pull/1000)) + * [#12285](https://github.com/magento/magento2/issues/12285) -- The option false for mobile device don't work in product view page gallery (fixed in [magento-engcom/magento2ce#1006](https://github.com/magento-engcom/magento2ce/pull/1006)) + * [#12490](https://github.com/magento/magento2/issues/12490) -- I can't disable full screen gallery on mobile on magento 2.2.1 (fixed in [magento-engcom/magento2ce#1006](https://github.com/magento-engcom/magento2ce/pull/1006)) + * [#10814](https://github.com/magento/magento2/issues/10814) -- Attribute repository resets sourceModel for new attributes (fixed in [magento-engcom/magento2ce#1012](https://github.com/magento-engcom/magento2ce/pull/1012)) + * [#12632](https://github.com/magento/magento2/issues/12632) -- Magento Connect no longer exist (fixed in [magento/magento2#12633](https://github.com/magento/magento2/pull/12633)) + * [#8647](https://github.com/magento/magento2/issues/8647) -- Order of how arguments are merged in multiple di.xml-files causes unexpected results (fixed in [magento-engcom/magento2ce#995](https://github.com/magento-engcom/magento2ce/pull/995)) + * [#12378](https://github.com/magento/magento2/issues/12378) -- Regions list in Directory module for India (fixed in [magento-engcom/magento2ce#1007](https://github.com/magento-engcom/magento2ce/pull/1007)) + * [#11946](https://github.com/magento/magento2/issues/11946) -- Layer navigation showing wrong product count (fixed in [magento/magento2#12063](https://github.com/magento/magento2/pull/12063)) + * [#12452](https://github.com/magento/magento2/issues/12452) -- ACL permissions issue (fixed in [magento/magento2#12661](https://github.com/magento/magento2/pull/12661)) + * [#12660](https://github.com/magento/magento2/issues/12660) -- Invalid parameter configuration provided for $block argument upon no ACL permissions to the block (fixed in [magento/magento2#12661](https://github.com/magento/magento2/pull/12661)) + * [#12084](https://github.com/magento/magento2/issues/12084) -- Product csv import > fail on round brackets in image filename (fixed in [magento-engcom/magento2ce#1017](https://github.com/magento-engcom/magento2ce/pull/1017)) + * [#12656](https://github.com/magento/magento2/issues/12656) -- Checkout: Whitespace in front of coupon code causes "Coupon code is not valid" (fixed in [magento-engcom/magento2ce#1021](https://github.com/magento-engcom/magento2ce/pull/1021)) + * [#12667](https://github.com/magento/magento2/issues/12667) -- Incorrect partial attribute (EAV) reindex (Update by Schedule) for configurable product with childs visibility "Not Visible Individually" (fixed in [magento-engcom/magento2ce#1023](https://github.com/magento-engcom/magento2ce/pull/1023)) + * [#10743](https://github.com/magento/magento2/issues/10743) -- Magento 2 is not showing Popular Search Terms (fixed in [magento-engcom/magento2ce#1024](https://github.com/magento-engcom/magento2ce/pull/1024)) + * [#5774](https://github.com/magento/magento2/issues/5774) -- Tier price and custom options give bad results (fixed in [magento/magento2#11563](https://github.com/magento/magento2/pull/11563)) + * [#8615](https://github.com/magento/magento2/issues/8615) -- REST API unable to make requests with slash (/) in SKU (fixed in [magento-engcom/magento2ce#949](https://github.com/magento-engcom/magento2ce/pull/949)) + * [#10133](https://github.com/magento/magento2/issues/10133) -- Please add your expectations for @deprecated annotations (fixed in [magento/magento2#11070](https://github.com/magento/magento2/pull/11070)) + * [#12713](https://github.com/magento/magento2/issues/12713) -- Currency symbol overlaps entered attribute option's price while creating Configurable Product (fixed in [magento/magento2#12730](https://github.com/magento/magento2/pull/12730)) + * [#9453](https://github.com/magento/magento2/issues/9453) -- Reopened: '?SID' in URL even if disabled (fixed in [magento/magento2#12743](https://github.com/magento/magento2/pull/12743)) + * [#9720](https://github.com/magento/magento2/issues/9720) -- Menu item dependencies (dependsOnModule, dependsOnConfig) are broken (fixed in [magento/magento2#12747](https://github.com/magento/magento2/pull/12747)) + * [#6965](https://github.com/magento/magento2/issues/6965) -- \Magento\Directory\Model\PriceCurrency::format() fails without conversion rate (fixed in [magento-engcom/magento2ce#1022](https://github.com/magento-engcom/magento2ce/pull/1022)) + * [#12627](https://github.com/magento/magento2/issues/12627) -- Referer is not added to login url in checkout config (fixed in [magento/magento2#12630](https://github.com/magento/magento2/pull/12630)) + * [#12206](https://github.com/magento/magento2/issues/12206) -- Tracking link returns 404 page in admin panel (fixed in [magento/magento2#12732](https://github.com/magento/magento2/pull/12732)) + * [#6113](https://github.com/magento/magento2/issues/6113) -- Validate range-words in Form component (UI Component) (fixed in [magento/magento2#12739](https://github.com/magento/magento2/pull/12739)) + * [#12719](https://github.com/magento/magento2/issues/12719) -- Welcome message is shown with customer's first and last names after confirming account (fixed in [magento/magento2#12738](https://github.com/magento/magento2/pull/12738)) + * [#5035](https://github.com/magento/magento2/issues/5035) -- I can not to subscribe on change of all sections in Stores ->Configuration using event admin_system_config_changed_section (fixed in [magento/magento2#12758](https://github.com/magento/magento2/pull/12758)) + * [#12715](https://github.com/magento/magento2/issues/12715) -- Storefront Back to Sign in button does not work as expected (fixed in [magento/magento2#12759](https://github.com/magento/magento2/pull/12759)) + * [#11743](https://github.com/magento/magento2/issues/11743) -- AbstractPdf - ZendException font is not set (fixed in [magento-engcom/magento2ce#1016](https://github.com/magento-engcom/magento2ce/pull/1016)) + * [#7241](https://github.com/magento/magento2/issues/7241) -- No option to start with blank option for prefix and suffix in checkout. (fixed in [magento/magento2#11462](https://github.com/magento/magento2/pull/11462)) + * [#5188](https://github.com/magento/magento2/issues/5188) -- Error generating URN-catalog when blank one exists (fixed in [magento/magento2#11686](https://github.com/magento/magento2/pull/11686)) + * [#11936](https://github.com/magento/magento2/issues/11936) -- required attribute set id filter on attribute group repository getList (fixed in [magento/magento2#12105](https://github.com/magento/magento2/pull/12105)) + * [#12625](https://github.com/magento/magento2/issues/12625) -- when saving a page in magento 2.2.1, 'Modified' date field is not getting updated (fixed in [magento/magento2#12636](https://github.com/magento/magento2/pull/12636)) + * [#11953](https://github.com/magento/magento2/issues/11953) -- Product configuration creator does not warn about invalid SKUs (fixed in [magento/magento2#12737](https://github.com/magento/magento2/pull/12737)) + * [#12439](https://github.com/magento/magento2/issues/12439) -- Newsletter subscription success email not sent after confirmation (fixed in [magento/magento2#12751](https://github.com/magento/magento2/pull/12751)) + * [#8830](https://github.com/magento/magento2/issues/8830) -- Can`t delete row in dynamicRows component (fixed in [magento-engcom/magento2ce#921](https://github.com/magento-engcom/magento2ce/pull/921)) + * [#12712](https://github.com/magento/magento2/issues/12712) -- Latest Google Chrome Browser issue with duplicate #email (fixed in [magento-engcom/magento2ce#1036](https://github.com/magento-engcom/magento2ce/pull/1036)) + * [#6916](https://github.com/magento/magento2/issues/6916) -- Update Bundle Product without changes in bundle items (fixed in [magento/magento2#12734](https://github.com/magento/magento2/pull/12734)) + * [#12374](https://github.com/magento/magento2/issues/12374) -- Model hasDataChanges always true (fixed in [magento/magento2#12736](https://github.com/magento/magento2/pull/12736)) + * [#11885](https://github.com/magento/magento2/issues/11885) -- Magento 2.2 Paypal Can't Accept Checkout Agreements Before Routing to PayPal (fixed in [magento/magento2#12401](https://github.com/magento/magento2/pull/12401)) + * [#12844](https://github.com/magento/magento2/issues/12844) -- "Cannot instantiate interface Magento\Framework\Interception\ObjectManager\ConfigInterface" error in integration tests (fixed in [magento/magento2#12845](https://github.com/magento/magento2/pull/12845)) + * [#12294](https://github.com/magento/magento2/issues/12294) -- Bug: Adding Custom Attribute - The value of Admin scope can't be empty (fixed in [magento/magento2#12755](https://github.com/magento/magento2/pull/12755)) + * [#12900](https://github.com/magento/magento2/issues/12900) -- Braintree "Place Order" button is disabled after failed validation (fixed in [magento/magento2#12902](https://github.com/magento/magento2/pull/12902)) + * [#12555](https://github.com/magento/magento2/issues/12555) -- Naming collision in Javascript ui registry (backend) (fixed in [magento/magento2#12945](https://github.com/magento/magento2/pull/12945)) + * [#4292](https://github.com/magento/magento2/issues/4292) -- Why can't one switch back to default mode ? (fixed in [magento/magento2#12752](https://github.com/magento/magento2/pull/12752)) + * [#2156](https://github.com/magento/magento2/issues/2156) -- Why does \Magento\Translation\Model\Js\DataProvider use \Magento\Framework\Phrase\Renderer\Translate, not \Magento\Framework\Phrase\Renderer\Composite? (fixed in [magento/magento2#12953](https://github.com/magento/magento2/pull/12953)) + * [#7441](https://github.com/magento/magento2/issues/7441) -- Configurable attribute options are not sorted (fixed in [magento/magento2#12963](https://github.com/magento/magento2/pull/12963)) + * [#10869](https://github.com/magento/magento2/issues/10869) -- field lengths differ across many tables (fixed in [magento/magento2#13015](https://github.com/magento/magento2/pull/13015)) + * [#12446](https://github.com/magento/magento2/issues/12446) -- Remove /home from the sitemap.xml (fixed in [magento/magento2#12649](https://github.com/magento/magento2/pull/12649)) + * [#12894](https://github.com/magento/magento2/issues/12894) -- Can't remove State is required for all countries (fixed in [magento/magento2#12917](https://github.com/magento/magento2/pull/12917)) + * [#12393](https://github.com/magento/magento2/issues/12393) -- Attribute with "Catalog Input Type for Store Owner" equal "Fixed Product Tax" for Multi-store (fixed in [magento/magento2#13019](https://github.com/magento/magento2/pull/13019)) + * [#9036](https://github.com/magento/magento2/issues/9036) -- Database backup doesn't include triggers (fixed in [magento/magento2#11369](https://github.com/magento/magento2/pull/11369)) + * [#12209](https://github.com/magento/magento2/issues/12209) -- Substitution payment method - Incorrect message (fixed in [magento/magento2#12731](https://github.com/magento/magento2/pull/12731)) + * [#10415](https://github.com/magento/magento2/issues/10415) -- Customer First and Last names not being trimmed of leading and trailing spaces on save. (fixed in [magento/magento2#12964](https://github.com/magento/magento2/pull/12964)) + * [#12601](https://github.com/magento/magento2/issues/12601) -- A space between the category page and the main footer when applying specific settings (fixed in [magento/magento2#13026](https://github.com/magento/magento2/pull/13026)) + * [#12320](https://github.com/magento/magento2/issues/12320) -- Newsletter subscribe button title wrapped (fixed in [magento/magento2#13041](https://github.com/magento/magento2/pull/13041) and [magento/magento2#13029](https://github.com/magento/magento2/pull/13029)) + * [#11796](https://github.com/magento/magento2/issues/11796) -- Magento2.2.0 home page product grid issues (fixed in [magento/magento2#13081](https://github.com/magento/magento2/pull/13081)) + * [#12828](https://github.com/magento/magento2/issues/12828) -- Uncaught Error: Script error for: trackingCode error on every frontend page (fixed in [magento/magento2#13061](https://github.com/magento/magento2/pull/13061)) + * [#5129](https://github.com/magento/magento2/issues/5129) -- Product details page zoom issue when dropdown menu have overlap area with it. (fixed in [magento/magento2#13084](https://github.com/magento/magento2/pull/13084)) + * [#6486](https://github.com/magento/magento2/issues/6486) -- Unable to save certain product properties via Rest API (fixed in [magento-engcom/magento2ce#1018](https://github.com/magento-engcom/magento2ce/pull/1018)) + * [#9969](https://github.com/magento/magento2/issues/9969) -- Cancel order and restore quote methods increase stocks twice (fixed in [magento/magento2#12668](https://github.com/magento/magento2/pull/12668)) + * [#12221](https://github.com/magento/magento2/issues/12221) -- Google analytics pageview being triggered twice (fixed in [magento/magento2#13034](https://github.com/magento/magento2/pull/13034)) + * [#12705](https://github.com/magento/magento2/issues/12705) -- Integrity constraint violation error after reordering product with custom options (fixed in [magento/magento2#13036](https://github.com/magento/magento2/pull/13036)) + * [#12876](https://github.com/magento/magento2/issues/12876) -- Multiple newsletter confirmation emails sent (fixed in [magento/magento2#13044](https://github.com/magento/magento2/pull/13044)) + * [#8114](https://github.com/magento/magento2/issues/8114) -- "Save Block"-button on Add New Block silently ignores clicks if the content is empty. (fixed in [magento-engcom/magento2ce#1032](https://github.com/magento-engcom/magento2ce/pull/1032)) + * [#8453](https://github.com/magento/magento2/issues/8453) -- Price outlining in Invoice PDF (fixed in [magento-engcom/magento2ce#1216](https://github.com/magento-engcom/magento2ce/pull/1216)) + * [#12967](https://github.com/magento/magento2/issues/12967) -- Undeclared dependency magento/zendframework1 by magento/framework (fixed in [magento/magento2#12990](https://github.com/magento/magento2/pull/12990)) + * [#12787](https://github.com/magento/magento2/issues/12787) -- Newsletter\Model\Subscriber::loadByEmail() does not use MySQL index (fixed in [magento/magento2#13033](https://github.com/magento/magento2/pull/13033)) + * [#12877](https://github.com/magento/magento2/issues/12877) -- [2.2.1] Magento Database Backup Command Fails (Fix included) (fixed in [magento/magento2#13066](https://github.com/magento/magento2/pull/13066)) + * [#5550](https://github.com/magento/magento2/issues/5550) -- Incorrect language on swatch error (fixed in [magento-engcom/magento2ce#1117](https://github.com/magento-engcom/magento2ce/pull/1117)) + * [#11828](https://github.com/magento/magento2/issues/11828) -- Visual Swatches not showing swatch color in admin (fixed in [magento/magento2#13101](https://github.com/magento/magento2/pull/13101)) + * [#13095](https://github.com/magento/magento2/issues/13095) -- No locale for Swedish (Finland) (fixed in [magento-engcom/magento2ce#1207](https://github.com/magento-engcom/magento2ce/pull/1207)) + * [#11428](https://github.com/magento/magento2/issues/11428) -- Cart Price Rule Label is not working (fixed in [magento/magento2#13141](https://github.com/magento/magento2/pull/13141)) + * [#11497](https://github.com/magento/magento2/issues/11497) -- Discount Rule does not show Default Rule Label (fixed in [magento/magento2#13141](https://github.com/magento/magento2/pull/13141)) + * [#12430](https://github.com/magento/magento2/issues/12430) -- While assigning prices to configurable products, prices aren's readable when using custom price symbol. (fixed in [magento/magento2#13025](https://github.com/magento/magento2/pull/13025)) + * [#12322](https://github.com/magento/magento2/issues/12322) -- Bug with CDATA in XML layout update (fixed in [magento-engcom/magento2ce#1163](https://github.com/magento-engcom/magento2ce/pull/1163)) + * [#12714](https://github.com/magento/magento2/issues/12714) -- Extra records are in exported CSV file for order (fixed in [magento/magento2#13208](https://github.com/magento/magento2/pull/13208)) + * [#8624](https://github.com/magento/magento2/issues/8624) -- Stock status not coming back after qty update (fixed in [magento-engcom/magento2ce#955](https://github.com/magento-engcom/magento2ce/pull/955)) + * [#11897](https://github.com/magento/magento2/issues/11897) -- Catalog product list widget not working with multiple sku (fixed in [magento-engcom/magento2ce#1050](https://github.com/magento-engcom/magento2ce/pull/1050)) + * [#12147](https://github.com/magento/magento2/issues/12147) -- The function "isUsingStaticUrlsAllowed" (configuration setting "cms/wysiwyg/use_static_urls_in_catalog") doesn't have any effect with the WYSIWYG editor image insertion (fixed in [magento-engcom/magento2ce#1215](https://github.com/magento-engcom/magento2ce/pull/1215)) + * [#12819](https://github.com/magento/magento2/issues/12819) -- CartTotalRepository cannot handle extension attributes in quote addresses in 2.2.2 (fixed in [magento-engcom/magento2ce#1186](https://github.com/magento-engcom/magento2ce/pull/1186)) + * [#12993](https://github.com/magento/magento2/issues/12993) -- Type error in Cart/Totals (fixed in [magento-engcom/magento2ce#1186](https://github.com/magento-engcom/magento2ce/pull/1186)) + * [#12342](https://github.com/magento/magento2/issues/12342) -- JSTestDriver removal (fixed in [magento/magento2#12406](https://github.com/magento/magento2/pull/12406)) + * [#13126](https://github.com/magento/magento2/issues/13126) -- 2.2.2 - Duplicating Bundle Product Removes Bundle Options From Original Product (fixed in [magento-engcom/magento2ce#1217](https://github.com/magento-engcom/magento2ce/pull/1217)) + * [#7768](https://github.com/magento/magento2/issues/7768) -- Adding 'is_saleable' attribute to sort of product collection causes exception and adding 'is_salable' has no effect (fixed in [magento-engcom/magento2ce#1045](https://github.com/magento-engcom/magento2ce/pull/1045)) + * [#12231](https://github.com/magento/magento2/issues/12231) -- New Cart Rule : Small styles issue because of styles-old.css (fixed in [magento-engcom/magento2ce#1146](https://github.com/magento-engcom/magento2ce/pull/1146)) + * [#5697](https://github.com/magento/magento2/issues/5697) -- [2.1.0] Misleading feedback when sending tracking information email (fixed in [magento-engcom/magento2ce#1245](https://github.com/magento-engcom/magento2ce/pull/1245)) + * [#7213](https://github.com/magento/magento2/issues/7213) -- WEBAPI: PHP session is always started 2.1.2 (fixed in [magento-engcom/magento2ce#1247](https://github.com/magento-engcom/magento2ce/pull/1247)) + * [#5948](https://github.com/magento/magento2/issues/5948) -- Magento 2 configurable product selection stock issue (fixed in [magento/magento2#12936](https://github.com/magento/magento2/pull/12936)) + * [#10661](https://github.com/magento/magento2/issues/10661) -- Opacity png watermark became white box on product images (fixed in [magento/magento2#11060](https://github.com/magento/magento2/pull/11060)) + * [#13327](https://github.com/magento/magento2/issues/13327) -- Menu ui-state-active not removed from previous opened menu item (fixed in [magento/magento2#13341](https://github.com/magento/magento2/pull/13341)) + * [#8621](https://github.com/magento/magento2/issues/8621) -- M2.1 Multishipping Checkout step New Address - Old State is saved when country is changed (fixed in [magento/magento2#13364](https://github.com/magento/magento2/pull/13364)) + * [#7760](https://github.com/magento/magento2/issues/7760) -- M2.1.2 : Shipment Tracking REST API should throw an error if order doesn't exist. (fixed in [magento-engcom/magento2ce#1162](https://github.com/magento-engcom/magento2ce/pull/1162)) + * [#7849](https://github.com/magento/magento2/issues/7849) -- M2.x.x Translation Missing in Checkout for Tax (fixed in [magento-engcom/magento2ce#1147](https://github.com/magento-engcom/magento2ce/pull/1147)) + * [#12860](https://github.com/magento/magento2/issues/12860) -- Sort by Product Name doesn't work with Ancor and available filters (fixed in [magento-engcom/magento2ce#1192](https://github.com/magento-engcom/magento2ce/pull/1192)) + * [#7848](https://github.com/magento/magento2/issues/7848) -- M2.1.x : Require Customer To Be Logged In To Checkout (fixed in [magento-engcom/magento2ce#1148](https://github.com/magento-engcom/magento2ce/pull/1148)) + * [#11527](https://github.com/magento/magento2/issues/11527) -- Notification messages not disappearing after being displayed (fixed in [magento-engcom/magento2ce#1111](https://github.com/magento-engcom/magento2ce/pull/1111)) + * [#7698](https://github.com/magento/magento2/issues/7698) -- Admin Global Search was build in a hurry (fixed in [magento-engcom/magento2ce#1167](https://github.com/magento-engcom/magento2ce/pull/1167)) + * [#12574](https://github.com/magento/magento2/issues/12574) -- ConfigurationTest fails when installing via composer (fixed in [magento-engcom/magento2ce#1161](https://github.com/magento-engcom/magento2ce/pull/1161)) + * [#11798](https://github.com/magento/magento2/issues/11798) -- Magento 2.1.9 - Refunding / Credit Memo Total Value is not updated (fixed in [magento-engcom/magento2ce#1185](https://github.com/magento-engcom/magento2ce/pull/1185)) + * [#13497](https://github.com/magento/magento2/issues/13497) -- Method getUrl in Magento\Catalog\Model\Product\Attribute\Frontend returns image url with double slash (fixed in [magento/magento2#13498](https://github.com/magento/magento2/pull/13498)) + * [#12081](https://github.com/magento/magento2/issues/12081) -- Magento 2.2.0: Translations for 'Item in Cart' missing in mini cart. (fixed in [magento/magento2#13528](https://github.com/magento/magento2/pull/13528)) + * [#11252](https://github.com/magento/magento2/issues/11252) -- Custom attribute - File not allowing uploads (fixed in [magento/magento2#13563](https://github.com/magento/magento2/pull/13563)) + * [#12817](https://github.com/magento/magento2/issues/12817) -- Coupon code with canceled order (fixed in [magento-engcom/magento2ce#1095](https://github.com/magento-engcom/magento2ce/pull/1095)) + * [#11963](https://github.com/magento/magento2/issues/11963) -- Magento 2.2 language switching not working on catalog and Product Pages (fixed in [magento-engcom/magento2ce#1143](https://github.com/magento-engcom/magento2ce/pull/1143)) + * [#12791](https://github.com/magento/magento2/issues/12791) -- Customer & Product Tax class wrongly styled (fixed in [magento/magento2#13643](https://github.com/magento/magento2/pull/13643)) + * [#13429](https://github.com/magento/magento2/issues/13429) -- Magento 2.2.2 password reset strength meter (fixed in [magento/magento2#13761](https://github.com/magento/magento2/pull/13761)) + * [#13760](https://github.com/magento/magento2/issues/13760) -- Remove deprecated Brazilian currencies in the setup process (fixed in [magento/magento2#13770](https://github.com/magento/magento2/pull/13770)) + * [#5451](https://github.com/magento/magento2/issues/5451) -- Rating titles with whitespace results in broken ID attributes (fixed in [magento-engcom/magento2ce#1119](https://github.com/magento-engcom/magento2ce/pull/1119)) + * [#8035](https://github.com/magento/magento2/issues/8035) -- Join extension attributes are not added to Order results (REST api) (fixed in [magento-engcom/magento2ce#1168](https://github.com/magento-engcom/magento2ce/pull/1168)) + * [#13595](https://github.com/magento/magento2/issues/13595) -- loadCache for Block Magento\Theme\Block\Html\Footer dont work (fixed in [magento/magento2#13762](https://github.com/magento/magento2/pull/13762)) + * [#10595](https://github.com/magento/magento2/issues/10595) -- Low Stock Report Grid Empty (fixed in [magento/magento2#13682](https://github.com/magento/magento2/pull/13682)) + * [#13315](https://github.com/magento/magento2/issues/13315) -- Mobile "Payment Methods" step looks bad on mobile (fixed in [magento/magento2#13777](https://github.com/magento/magento2/pull/13777)) + * [#13791](https://github.com/magento/magento2/issues/13791) -- Submitting search form (mini) with empty value throws error on preventDefault (fixed in [magento/magento2#13811](https://github.com/magento/magento2/pull/13811)) + * [#12711](https://github.com/magento/magento2/issues/12711) -- Default Welcome message is broken on storefront with enabled translate-inline (fixed in [magento/magento2#13038](https://github.com/magento/magento2/pull/13038)) + * [#5863](https://github.com/magento/magento2/issues/5863) -- URL Rewrite issues occur very often /catalog/product/view/id/711/s/product-name/category/16/ (fixed in [magento/magento2#13567](https://github.com/magento/magento2/pull/13567)) + * [#8227](https://github.com/magento/magento2/issues/8227) -- After upgrade to 2.1.3 url rewrite problem multi store (fixed in [magento/magento2#13567](https://github.com/magento/magento2/pull/13567)) + * [#8957](https://github.com/magento/magento2/issues/8957) -- Permanent Redirect for old URL missing via API (fixed in [magento/magento2#13567](https://github.com/magento/magento2/pull/13567)) + * [#10073](https://github.com/magento/magento2/issues/10073) -- Magento don't create product redirect if URL key on store view level was changed. (fixed in [magento/magento2#13567](https://github.com/magento/magento2/pull/13567)) + * [#13240](https://github.com/magento/magento2/issues/13240) -- Permanent 301 redirect is not generated when product url changes on storeview scope (fixed in [magento/magento2#13567](https://github.com/magento/magento2/pull/13567)) + * [#13768](https://github.com/magento/magento2/issues/13768) -- Expired backend password - Attention: Something went wrong (fixed in [magento/magento2#13787](https://github.com/magento/magento2/pull/13787)) + * [#4454](https://github.com/magento/magento2/issues/4454) -- CMS Page with in layout update xml (fixed in [magento/magento2#13817](https://github.com/magento/magento2/pull/13817)) + * [#13350](https://github.com/magento/magento2/issues/13350) -- Magento 2.2 Encoding Issue -> Google Analytics (fixed in [magento/magento2#13844](https://github.com/magento/magento2/pull/13844)) + * [#13827](https://github.com/magento/magento2/issues/13827) -- Google Analytics character encoding issue ( \u0020 ) (fixed in [magento/magento2#13844](https://github.com/magento/magento2/pull/13844)) + * [#7765](https://github.com/magento/magento2/issues/7765) -- Filter block on category is still present also mode is to just show "static block" (fixed in [magento-engcom/magento2ce#1159](https://github.com/magento-engcom/magento2ce/pull/1159)) + * [#11512](https://github.com/magento/magento2/issues/11512) -- Incorrect use of 503 status code (fixed in [magento/magento2#11513](https://github.com/magento/magento2/pull/11513)) + * [#12889](https://github.com/magento/magento2/issues/12889) -- Wrong shipping fee in backend with multiple store views (fixed in [magento-engcom/magento2ce#1132](https://github.com/magento-engcom/magento2ce/pull/1132)) + * [#13216](https://github.com/magento/magento2/issues/13216) -- `quoteAddressToFormAddressData` mutates the argument (fixed in [magento/magento2#13217](https://github.com/magento/magento2/pull/13217)) + * [#13631](https://github.com/magento/magento2/issues/13631) -- Totals sort order is not respected in customer account order view (fixed in [magento/magento2#13641](https://github.com/magento/magento2/pull/13641)) + * [#7515](https://github.com/magento/magento2/issues/7515) -- Error when submit customer/account/editPost form and session expired (fixed in [magento-engcom/magento2ce#1187](https://github.com/magento-engcom/magento2ce/pull/1187)) + * [#12404](https://github.com/magento/magento2/issues/12404) -- Output of setup:static-content:deploy contains red color, should be a friendlier color (fixed in [magento/magento2#13709](https://github.com/magento/magento2/pull/13709)) + * [#13006](https://github.com/magento/magento2/issues/13006) -- Drop down values are not showing in catalog product grid magento2 (fixed in [magento/magento2#13861](https://github.com/magento/magento2/pull/13861)) + * [#13899](https://github.com/magento/magento2/issues/13899) -- Postal code (zip code) for Canada should allow postal codes without space (fixed in [magento/magento2#13930](https://github.com/magento/magento2/pull/13930)) +* GitHub pull requests: + * [magento/magento2#12171](https://github.com/magento/magento2/pull/12171) -- 7691: address with saveInAddressBook 0 are still being added to the address book for new customers(backport to 2.2) (by @RomaKis) + * [magento/magento2#12239](https://github.com/magento/magento2/pull/12239) -- Fixed php notice when invalid ui_component config is used (by @vovayatsyuk) + * [magento/magento2#11407](https://github.com/magento/magento2/pull/11407) -- Added CLI command to enable and disable the Profiler (by @peterjaap) + * [magento/magento2#12257](https://github.com/magento/magento2/pull/12257) -- Phpdoc improvements (by @KarlDeux) + * [magento/magento2#11997](https://github.com/magento/magento2/pull/11997) -- 11941: Invoice for products that use qty decimal rounds down to whole number. (by @nmalevanec) + * [magento/magento2#12283](https://github.com/magento/magento2/pull/12283) -- magento/magento2#12083: Cannot import zero (0) value into custom attribute (by @p-bystritsky) + * [magento/magento2#12296](https://github.com/magento/magento2/pull/12296) -- Issue: 3596. Resolve Notice with undefined index 'value' (by @madonzy) + * [magento/magento2#12303](https://github.com/magento/magento2/pull/12303) -- 9764: exception message is wrong and misleading in findAccessorMethodName() of Magento\Framework\Reflection\NameFinder (by @RomaKis) + * [magento/magento2#12304](https://github.com/magento/magento2/pull/12304) -- Handle empty or incorrect lines in a language CSV (by @FreekVandeursen) + * [magento/magento2#12276](https://github.com/magento/magento2/pull/12276) -- Webshop throws an exception when sharing wishlist with RSS enabled (by @mediactbv) + * [magento/magento2#12310](https://github.com/magento/magento2/pull/12310) -- Fix robots.txt content type to 'text/plain' (by @tufahu) + * [magento/magento2#12332](https://github.com/magento/magento2/pull/12332) -- 9684: No ACL set for integrations (by @RomaKis) + * [magento/magento2#11787](https://github.com/magento/magento2/pull/11787) -- Fix #10438: Potential error on order edit page when address has extension attributes (by @joni-jones) + * [magento/magento2#12003](https://github.com/magento/magento2/pull/12003) -- magento/magento2#11691: Wrong return type for getAttributeText($attributeCode) (by @p-bystritsky) + * [magento/magento2#12308](https://github.com/magento/magento2/pull/12308) -- 12261: Order confirmation email contains non functioning links #12261 (by @RomaKis) + * [magento/magento2#12302](https://github.com/magento/magento2/pull/12302) -- Fixed 'Non-numeric value' warning on account create/save when DOB field is visible (by @vovayatsyuk) + * [magento/magento2#11320](https://github.com/magento/magento2/pull/11320) -- Fix email not sent when sitemap generation has errors (by @marinagociu) + * [magento/magento2#11429](https://github.com/magento/magento2/pull/11429) -- Magento 2.2.0 A solution for Product Repeat Issue after filter on category listing page. (by @mayankzalavadia) + * [magento/magento2#11550](https://github.com/magento/magento2/pull/11550) -- Even existing credit memos should be refundable if their state is open (by @ajpevers) + * [magento/magento2#11809](https://github.com/magento/magento2/pull/11809) -- 8003: Using System Value for Base Currency Results in Config Error. (by @nmalevanec) + * [magento/magento2#11592](https://github.com/magento/magento2/pull/11592) -- Fix issue #10347 - Wrong order tax amounts displayed when using specific tax configuration (2.2-develop) (by @PieterCappelle) + * [magento/magento2#11539](https://github.com/magento/magento2/pull/11539) -- Fix depends field not working for radio elements (by @jahvi) + * [magento/magento2#11846](https://github.com/magento/magento2/pull/11846) -- Fixed a js bug where ui_component labels have the wrong sort order. (by @deiserh) + * [magento/magento2#11965](https://github.com/magento/magento2/pull/11965) -- 11792: Can't add customizable options to product (by @RomaKis) + * [magento/magento2#12048](https://github.com/magento/magento2/pull/12048) -- #11528 can't save customizable options (by @luismiguelyangehuaman) + * [magento/magento2#12108](https://github.com/magento/magento2/pull/12108) -- 12064: Database Rollback not working with magento 2.1.9? (by @RomaKis) + * [magento/magento2#12387](https://github.com/magento/magento2/pull/12387) -- Update CAPTCHA labels to reflect the symbols in the CAPTCHA image (by @RhodriOwainDavies) + * [magento/magento2#12120](https://github.com/magento/magento2/pull/12120) -- Update AbstractBackend.php (by @hewersonfreitas) + * [magento/magento2#12154](https://github.com/magento/magento2/pull/12154) -- Add link to issue gates wiki page in the labels section of the readme (by @dmanners) + * [magento/magento2#11422](https://github.com/magento/magento2/pull/11422) -- [Backport 2.2] Translate order getCreatedAtFormatted() to store locale (by @JeroenVanLeusden) + * [magento/magento2#11473](https://github.com/magento/magento2/pull/11473) -- Fix for remove 'product_list_toolbar' block from layout in XML #9413 (by @mariuscris) + * [magento/magento2#11670](https://github.com/magento/magento2/pull/11670) -- save invoice ID on credit memo when using API method salesRefundInvoiceV1 (by @ajpevers) + * [magento/magento2#11992](https://github.com/magento/magento2/pull/11992) -- 11740: Sending emails from Admin in Multi-Store Environment defaults to Primary Store (by @RomaKis) + * [magento/magento2#12036](https://github.com/magento/magento2/pull/12036) -- Add swatch option: Prevent loosing data and default value if data is not populated via adminhtml (by @gomencal) + * [magento/magento2#12227](https://github.com/magento/magento2/pull/12227) -- Shipping method fixtures not compatible with getShippingMethod(true) in OrderCreateTest (by @andrew-garside-temando) + * [magento/magento2#12241](https://github.com/magento/magento2/pull/12241) -- 10128: New Orders not being saved to order grid (by @RomaKis) + * [magento/magento2#12132](https://github.com/magento/magento2/pull/12132) -- 10210: Transport variable can not be altered in email_invoice_set_template_vars_before Event (backport MAGETWO-69482 to 2.2) (by @RomaKis) + * [magento/magento2#11389](https://github.com/magento/magento2/pull/11389) -- Attribute category_ids issue (by @manuelson) + * [magento/magento2#12133](https://github.com/magento/magento2/pull/12133) -- Fix for issue 12127: Single quotation marks are now decoded properly in admin attribute option input fields (by @erfanimani) + * [magento/magento2#12253](https://github.com/magento/magento2/pull/12253) -- New validation: 3bytes characters filter (4 bytes characters cannot be stored using UTF8) (by @KarlDeux) + * [magento/magento2#12328](https://github.com/magento/magento2/pull/12328) -- 9742: Default welcome message returns after being deleted #9742 (by @RomaKis) + * [magento/magento2#12057](https://github.com/magento/magento2/pull/12057) -- [Backport] magento/magento2#9961: Unused product attributes display with value N/A or NO on storefront. (by @p-bystritsky) + * [magento/magento2#12441](https://github.com/magento/magento2/pull/12441) -- Add command "app:config:status" to check if "app:config:import" needed (by @jalogut) + * [magento/magento2#12443](https://github.com/magento/magento2/pull/12443) -- Fixed missing 'size' and 'type' props on a third-party category images [Backport 2.2] (by @vovayatsyuk) + * [magento/magento2#12495](https://github.com/magento/magento2/pull/12495) -- Fixed invalid parameter type in phpdoc block in Topmenu class (by @vovayatsyuk) + * [magento/magento2#11323](https://github.com/magento/magento2/pull/11323) -- Defaulting missing alt-text for a product to use the product name. (by @brobie) + * [magento/magento2#11388](https://github.com/magento/magento2/pull/11388) -- Fix #11236: Web Setup Wizard Icon Inconsistency (by @dverkade) + * [magento/magento2#11485](https://github.com/magento/magento2/pull/11485) -- do the stock check on default level because the stock on website leve... (by @joost-florijn-kega) + * [magento/magento2#11926](https://github.com/magento/magento2/pull/11926) -- 8255: Export Products action doesn't consider hide_for_product_page value. (by @nmalevanec) + * [magento/magento2#12207](https://github.com/magento/magento2/pull/12207) -- 11882: It's not possible to enable "log to file" (debugging) in production mode. Psr logger debug method does not work by the default in developer mode. (by @nmalevanec) + * [magento/magento2#11052](https://github.com/magento/magento2/pull/11052) -- Keep maintenance mode on if it was previously enabled (by @jokeputs) + * [magento/magento2#12038](https://github.com/magento/magento2/pull/12038) -- #11825: Generate new FormKey and replace for oldRequestParams Wishlist (by @osrecio) + * [magento/magento2#12161](https://github.com/magento/magento2/pull/12161) -- Fix delay initialization options for customized JQuery UI menu widget (by @scazz010) + * [magento/magento2#12466](https://github.com/magento/magento2/pull/12466) -- Category page X-Magento-Tags headers contains product cache identities even which category display mode is set to "Static block only" (by @atishgoswami) + * [magento/magento2#12515](https://github.com/magento/magento2/pull/12515) -- The left and the right parts of assignment are equal (by @lfluvisotto) + * [magento/magento2#12499](https://github.com/magento/magento2/pull/12499) -- Format generated config files using the short array syntax (by @cykirsch) + * [magento/magento2#12513](https://github.com/magento/magento2/pull/12513) -- Duplicate array key (by @lfluvisotto) + * [magento/magento2#12516](https://github.com/magento/magento2/pull/12516) -- Case mismatch (by @lfluvisotto) + * [magento/magento2#11444](https://github.com/magento/magento2/pull/11444) -- [Backport 2.2-develop] #11324 REST API - Only associate automatically product with all websites when creating product in All Store Views scope (by @adrian-martinez-interactiv4) + * [magento/magento2#11608](https://github.com/magento/magento2/pull/11608) -- Fix for issue 9633 500 error on setup wizard with memcache (by @sylink) + * [magento/magento2#11617](https://github.com/magento/magento2/pull/11617) -- Re saving product attribute (by @raumatbel) + * [magento/magento2#12359](https://github.com/magento/magento2/pull/12359) -- Add a --no-update option to sampledata:deploy and sampledata:remove commands (by @schmengler) + * [magento/magento2#12530](https://github.com/magento/magento2/pull/12530) -- Added correction for og:type content value (by @atishgoswami) + * [magento/magento2#11099](https://github.com/magento/magento2/pull/11099) -- Fix syntax of expectException() calls (by @schmengler) + * [magento/magento2#11435](https://github.com/magento/magento2/pull/11435) -- [Backport 2.2-develop] #11409: Too many password reset requests even when disabled in settings (by @adrian-martinez-interactiv4) + * [magento/magento2#12122](https://github.com/magento/magento2/pull/12122) -- [2.2] - Add command to view mview state and queue (by @convenient) + * [magento/magento2#12167](https://github.com/magento/magento2/pull/12167) -- 12110: Missing cascade into attribute set deletion. (by @nmalevanec) + * [magento/magento2#12469](https://github.com/magento/magento2/pull/12469) -- Added namespace to product videos fotorama events (by @roma84) + * [magento/magento2#12507](https://github.com/magento/magento2/pull/12507) -- Issue 12506: Fixup typo getDispretionPath -> getDispersionPath (by @PascalBrouwers) + * [magento/magento2#12539](https://github.com/magento/magento2/pull/12539) -- Trying to get data from non existent products (by @angelo983) + * [magento/magento2#12541](https://github.com/magento/magento2/pull/12541) -- [Backport 2.2-develop] Fix swagger-ui on instances of Magento running on a non-standard port (by @JeroenVanLeusden) + * [magento/magento2#12220](https://github.com/magento/magento2/pull/12220) -- 12180 Remove unnecessary use operator for Context, causes 503 error i... (by @chris-pook) + * [magento/magento2#12477](https://github.com/magento/magento2/pull/12477) -- NewRelic: Disables Module Deployments, Creates new Deploy Marker Command (by @fooman) + * [magento/magento2#12529](https://github.com/magento/magento2/pull/12529) -- #12450: Set Current Store from Store Code if isUseStoreInUrl (by @osrecio) + * [magento/magento2#12606](https://github.com/magento/magento2/pull/12606) -- Fix error loading theme configuration on PHP 7.2 (by @Alanaktion) + * [magento/magento2#12610](https://github.com/magento/magento2/pull/12610) -- Update CrontabManager.php (by @WaPoNe) + * [magento/magento2#12639](https://github.com/magento/magento2/pull/12639) -- Remove @escapeNotVerified from documentation (by @mzeis) + * [magento/magento2#11702](https://github.com/magento/magento2/pull/11702) -- Fix getReservedOrderId() to use current store instead of default store (by @tdgroot) + * [magento/magento2#12633](https://github.com/magento/magento2/pull/12633) -- Magento Connect no longer exist (by @miguelbalparda) + * [magento/magento2#12063](https://github.com/magento/magento2/pull/12063) -- 11946: Layer navigation showing wrong product count (by @RomaKis) + * [magento/magento2#12661](https://github.com/magento/magento2/pull/12661) -- [2.2-develop] Fixes #12660 invalid parameter configuration provided for argument (by @Tomasz-Silpion) + * [magento/magento2#11563](https://github.com/magento/magento2/pull/11563) -- Add price calculation improvement for product option value price (by @marinagociu) + * [magento/magento2#12666](https://github.com/magento/magento2/pull/12666) -- Fix incorrect DHL Product codes (by @gwharton) + * [magento/magento2#12723](https://github.com/magento/magento2/pull/12723) -- [2.2 Backport] Create CODE_OF_CONDUCT.md (by @ishakhsuvarov) + * [magento/magento2#11070](https://github.com/magento/magento2/pull/11070) -- Remove deprecation without alternative (by @schmengler) + * [magento/magento2#12730](https://github.com/magento/magento2/pull/12730) -- 12713 (by @EfremovaVI) + * [magento/magento2#12743](https://github.com/magento/magento2/pull/12743) -- #9453 - ported down c2e5d77a9516c8305585e819c2f0a0629648cc14 (by @strell) + * [magento/magento2#12747](https://github.com/magento/magento2/pull/12747) -- magento/magento2#9720 Menu item dependencies (dependsOnModule, depend... (by @hannassy) + * [magento/magento2#12767](https://github.com/magento/magento2/pull/12767) -- magento/magento2#12699: Multiselect Attribute is not saved (by @awarche) + * [magento/magento2#12786](https://github.com/magento/magento2/pull/12786) -- Fix typo in SINGLE_PRODUCT_LAYOUT_HANLDE (by @aschrammel) + * [magento/magento2#12630](https://github.com/magento/magento2/pull/12630) -- Add customer login url from Customer Url model to checkout config so ... (by @quisse) + * [magento/magento2#12732](https://github.com/magento/magento2/pull/12732) -- Fix issue when tracking link returns 404 page in admin panel (by @ihor-sviziev) + * [magento/magento2#12739](https://github.com/magento/magento2/pull/12739) -- magento/magento2#6113: Validate range-words in Form component (UI Component) (by @Zamoroka) + * [magento/magento2#12738](https://github.com/magento/magento2/pull/12738) -- magento/magento2#12719: Use full name in welcome message (by @xpoback) + * [magento/magento2#12758](https://github.com/magento/magento2/pull/12758) -- magento/magento2#5035 Cannot subscribe to events with a number in name (by @Mobecls) + * [magento/magento2#12759](https://github.com/magento/magento2/pull/12759) -- Fix Back to Sign in url on confirmation form (by @StasKozar) + * [magento/magento2#12810](https://github.com/magento/magento2/pull/12810) -- Stop the profiler when returning early in \Magento\Eav\Model\Config::getAttribute (by @nicka101) + * [magento/magento2#12826](https://github.com/magento/magento2/pull/12826) -- Fix PhpDoc to show correct parameter types (by @FreekVandeursen) + * [magento/magento2#11462](https://github.com/magento/magento2/pull/11462) -- #7241: Always add empty option for prefix and/or suffix if optional (by @avstudnitz) + * [magento/magento2#11686](https://github.com/magento/magento2/pull/11686) -- Fix error when generating urn catalog for empty misc.xml (by @tdgroot) + * [magento/magento2#11878](https://github.com/magento/magento2/pull/11878) -- [BUGFIX] Made method public so a plugin is possible. (by @dheesbeen) + * [magento/magento2#12105](https://github.com/magento/magento2/pull/12105) -- #11936:required attribute set id filter on attribute group repository getList (by @tzyganu) + * [magento/magento2#12636](https://github.com/magento/magento2/pull/12636) -- #12625: Add Current Date to update_time Field for Block and Pages (by @osrecio) + * [magento/magento2#12737](https://github.com/magento/magento2/pull/12737) -- magento/magento2#11953: Product configuration creator does not warn about invalid SKUs (by @Zamoroka) + * [magento/magento2#12751](https://github.com/magento/magento2/pull/12751) -- magento/magento2#12439 Newsletter subscription success email not sent... (by @Styopchik) + * [magento/magento2#12884](https://github.com/magento/magento2/pull/12884) -- [Backport 2.2] Update functional.suite.dist.yml to handle a custom backend name (by @scribam) + * [magento/magento2#12734](https://github.com/magento/magento2/pull/12734) -- #6916 Fix notice during Update Bundle Product without changes (by @dzianis-yurevich) + * [magento/magento2#12859](https://github.com/magento/magento2/pull/12859) -- Throw ValidationException for invalid xml (by @pmclain) + * [magento/magento2#12875](https://github.com/magento/magento2/pull/12875) -- Add more parameters to ajax:addToCart (by @srenon) + * [magento/magento2#12736](https://github.com/magento/magento2/pull/12736) -- Issues/12374 (by @virtual97) + * [magento/magento2#12401](https://github.com/magento/magento2/pull/12401) -- Correctly set payment information when using paypal (by @therool) + * [magento/magento2#12768](https://github.com/magento/magento2/pull/12768) -- magento/magento2: Missing ext-bcmath dependency added (by @Mobecls) + * [magento/magento2#12845](https://github.com/magento/magento2/pull/12845) -- Add missing preference for ObjectManager\ConfigInterface in integrati... (by @schmengler) + * [magento/magento2#12857](https://github.com/magento/magento2/pull/12857) -- Update progress.phtml (by @jonashrem) + * [magento/magento2#12887](https://github.com/magento/magento2/pull/12887) -- Remove unused if statement in order invoice save (by @JeroenVanLeusden) + * [magento/magento2#12931](https://github.com/magento/magento2/pull/12931) -- Display scroll bar of admin store switcher in OSX computers. (by @jalogut) + * [magento/magento2#12946](https://github.com/magento/magento2/pull/12946) -- Respect "Learn More Link" in Recently Viewed Products widget options (by @JeroenVanLeusden) + * [magento/magento2#12951](https://github.com/magento/magento2/pull/12951) -- [Bug] Correctly construct Magento\Framework\Phrase (by @punkstar) + * [magento/magento2#12755](https://github.com/magento/magento2/pull/12755) -- magento/magento2#12294: Bug: Adding Custom Attribute - The value of A... (by @virtual97) + * [magento/magento2#12902](https://github.com/magento/magento2/pull/12902) -- Fix #12900: Braintree "Place Order" button is disabled after failed validation (by @joni-jones) + * [magento/magento2#12945](https://github.com/magento/magento2/pull/12945) -- Naming collision in Javascript ui registry (backend) to 2.2 (by @VladimirZaets) + * [magento/magento2#12521](https://github.com/magento/magento2/pull/12521) -- Match flexible static file version in nginx sample config (by @scottsb) + * [magento/magento2#12752](https://github.com/magento/magento2/pull/12752) -- magento/magento2#4292: Ability to sitch to default mode (by @Etty) + * [magento/magento2#12953](https://github.com/magento/magento2/pull/12953) -- [Backport to 2.2-develop] Fix #2156 Js\Dataprovider uses the RendererInterface. (by @dverkade) + * [magento/magento2#12963](https://github.com/magento/magento2/pull/12963) -- Sort configurable attribute options by sort_order (by @wardcapp) + * [magento/magento2#12862](https://github.com/magento/magento2/pull/12862) -- Change _getHtml to append class rather than overwrite for children (by @jonshipman) + * [magento/magento2#13015](https://github.com/magento/magento2/pull/13015) -- [Backport to 2.2-develop] The quote address fields length expanded in the database (by @dverkade) + * [magento/magento2#13027](https://github.com/magento/magento2/pull/13027) -- Change of copyright year from 2017 to 2018. (by @bhargavmehta) + * [magento/magento2#12649](https://github.com/magento/magento2/pull/12649) -- #12446: Add GetUtilityPageIdentifiers for Manage Custom Pages to be excluded ... (by @osrecio) + * [magento/magento2#12917](https://github.com/magento/magento2/pull/12917) -- Fix issue 12894: Can't remove State is required for all countries (by @vasilii-b) + * [magento/magento2#12922](https://github.com/magento/magento2/pull/12922) -- Handle multiple errors in customer address validation when shown in adminhtml customer edit page (by @adrian-martinez-interactiv4) + * [magento/magento2#13019](https://github.com/magento/magento2/pull/13019) -- [Backport to 2.2-develop] Attribute with "Catalog Input Type for Store Owner" equal "Fixed Product Tax" for Multi-store (by @dverkade) + * [magento/magento2#13052](https://github.com/magento/magento2/pull/13052) -- Make "top destinations" config field configurable on store level (by @avstudnitz) + * [magento/magento2#12901](https://github.com/magento/magento2/pull/12901) -- FIX: remove not used count() from templates (by @Coderimus) + * [magento/magento2#13050](https://github.com/magento/magento2/pull/13050) -- Updated cron documentation URL to 2.2 (by @robbie-thompson) + * [magento/magento2#11369](https://github.com/magento/magento2/pull/11369) -- Database backup doesn't include triggers #9036 (by @denisristic) + * [magento/magento2#12731](https://github.com/magento/magento2/pull/12731) -- magento/magento2#12209: Substitution payment method - Incorrect message (by @Zamoroka) + * [magento/magento2#12964](https://github.com/magento/magento2/pull/12964) -- Add trim filter to first, middle and lastname. (by @wardcapp) + * [magento/magento2#12985](https://github.com/magento/magento2/pull/12985) -- Fix jumping content on page reload in admin area (by @avoelkl) + * [magento/magento2#13026](https://github.com/magento/magento2/pull/13026) -- Feature space between category page (by @sanjay-wagento) + * [magento/magento2#13041](https://github.com/magento/magento2/pull/13041) -- Solution For Newsletter subscribe button title wrapped (by @monaemipro) + * [magento/magento2#13051](https://github.com/magento/magento2/pull/13051) -- Fix JS error on cart from postcode validation when 'US' is deselected as an allowed country (by @codekipple) + * [magento/magento2#13076](https://github.com/magento/magento2/pull/13076) -- Fix issues caused by using continue in loops (by @ihor-sviziev) + * [magento/magento2#13029](https://github.com/magento/magento2/pull/13029) -- Newsletter Label is broking on chinese Language like 订阅 (by @dasharath-wagento) + * [magento/magento2#12965](https://github.com/magento/magento2/pull/12965) -- Fix vault_payment_token install script type where column defaults were not set (by @helloitsluke) + * [magento/magento2#13030](https://github.com/magento/magento2/pull/13030) -- Resolved Checkout-Payment-Wrong promo code cancelled issue (by @chiragp-wagento) + * [magento/magento2#13039](https://github.com/magento/magento2/pull/13039) -- Feature minimum order amount notice issue (by @neeta-wagento) + * [magento/magento2#13061](https://github.com/magento/magento2/pull/13061) -- Fix for requireJS loading issues (for ad blockers) (by @Yonn-Trimoreau) + * [magento/magento2#13081](https://github.com/magento/magento2/pull/13081) -- Fix for #11796 Magento2.2.0 home page product grid issues (by @punitv) + * [magento/magento2#13084](https://github.com/magento/magento2/pull/13084) -- Fixed magnifier issue. (by @mayankzalavadia) + * [magento/magento2#12668](https://github.com/magento/magento2/pull/12668) -- Fix for reverting stock twice for cancelled orders (by @dverkade) + * [magento/magento2#13034](https://github.com/magento/magento2/pull/13034) -- Magento 2.2 Develop fix for #12221 Google Analytics Pageview Triggered twice (by @bhargavmehta) + * [magento/magento2#13036](https://github.com/magento/magento2/pull/13036) -- magento/magento2#12705: Integrity constraint violation error after re... (by @vinayshah) + * [magento/magento2#13044](https://github.com/magento/magento2/pull/13044) -- Fix Newsletter Subscribe Workflow (by @torhoehn) + * [magento/magento2#13161](https://github.com/magento/magento2/pull/13161) -- Updated README file to take resources from 2.2 instead of 2.0. (by @bhargavmehta) + * [magento/magento2#12990](https://github.com/magento/magento2/pull/12990) -- [2.2.x] Fix undeclared dependency magento/zendframework1 by magento/framework (by @ihor-sviziev) + * [magento/magento2#12998](https://github.com/magento/magento2/pull/12998) -- Make customer name link to customer dashboard (by @srenon) + * [magento/magento2#13033](https://github.com/magento/magento2/pull/13033) -- Newsletter\Model\Subscriber::loadByEmail() does not use MySQL index (by @devamitbera) + * [magento/magento2#13066](https://github.com/magento/magento2/pull/13066) -- Fix for #12877 as per @azeemism (by @jagritijoshi) + * [magento/magento2#13086](https://github.com/magento/magento2/pull/13086) -- Add failsafe to items.phtml (by @samgranger) + * [magento/magento2#13169](https://github.com/magento/magento2/pull/13169) -- Optimization: magento/module-eav is_null change to strict comparison ... (by @Coderimus) + * [magento/magento2#13170](https://github.com/magento/magento2/pull/13170) -- Optimization: magento/module-tax is_null change to strict comparison (by @Coderimus) + * [magento/magento2#13155](https://github.com/magento/magento2/pull/13155) -- Optimization: module-sales is_null change to strict comparison instead (by @Coderimus) + * [magento/magento2#13171](https://github.com/magento/magento2/pull/13171) -- Optimization: magento/module-catalog is_null change to strict comparison (by @Coderimus) + * [magento/magento2#13174](https://github.com/magento/magento2/pull/13174) -- Fix: remove TestObserver class (by @Coderimus) + * [magento/magento2#12807](https://github.com/magento/magento2/pull/12807) -- Reorder adding of page layout handles (by @aschrammel) + * [magento/magento2#13101](https://github.com/magento/magento2/pull/13101) -- 11828 Fix issue with swatch colour block not showing in admin panel once colour selected (PHP7.1.x issue). (by @chris-pook) + * [magento/magento2#13082](https://github.com/magento/magento2/pull/13082) -- Fix Magento_Checkout address formatting (by @nfourteen) + * [magento/magento2#13141](https://github.com/magento/magento2/pull/13141) -- Fix missing discount label in checkout (by @ihor-sviziev) + * [magento/magento2#13025](https://github.com/magento/magento2/pull/13025) -- fixed issue prices aren't readable when using custom price symbol (by @pradeep-wagento) + * [magento/magento2#13208](https://github.com/magento/magento2/pull/13208) -- #12714 - pass parameter for export button url (by @sanjay-wagento) + * [magento/magento2#12406](https://github.com/magento/magento2/pull/12406) -- Issue/12342/js test driver removal (by @KarlDeux) + * [magento/magento2#13310](https://github.com/magento/magento2/pull/13310) -- Add the domReady! statement (by @arnoudhgz) + * [magento/magento2#13324](https://github.com/magento/magento2/pull/13324) -- Alignement Array assignement (by @Nolwennig) + * [magento/magento2#12936](https://github.com/magento/magento2/pull/12936) -- FIX: out-of-stock options for configurable product visible on frontend as sellable (by @Coderimus) + * [magento/magento2#11060](https://github.com/magento/magento2/pull/11060) -- Handle transparncy correctly for watermark (by @elzekool) + * [magento/magento2#13408](https://github.com/magento/magento2/pull/13408) -- Translate time zone label according to current locale in Stores > Configuration > Advanced Reporting (by @adrian-martinez-interactiv4) + * [magento/magento2#12650](https://github.com/magento/magento2/pull/12650) -- Add fallback for Product_links position attribute if not set in request (by @mohammedsalem) + * [magento/magento2#13341](https://github.com/magento/magento2/pull/13341) -- Bugfix/13327 ui active state not removed from previous menu item (by @arnoudhgz) + * [magento/magento2#13364](https://github.com/magento/magento2/pull/13364) -- [Backport 2.2] In checkout->multishipping-> new addres clean region when select country without dropdown for states (by @enriquei4) + * [magento/magento2#13373](https://github.com/magento/magento2/pull/13373) -- Edited doc block of the walk method in a Collection (by @ByteCreation) + * [magento/magento2#13436](https://github.com/magento/magento2/pull/13436) -- Product Link Save Handler - Remove not used constructor dependency (by @ihor-sviziev) + * [magento/magento2#13449](https://github.com/magento/magento2/pull/13449) -- Fix default discount tax calculation in double (by @VincentMarmiesse) + * [magento/magento2#13450](https://github.com/magento/magento2/pull/13450) -- Removed each function usage (by @ihor-sviziev) + * [magento/magento2#13485](https://github.com/magento/magento2/pull/13485) -- Update code formatting in Swagger Block (by @JeroenVanLeusden) + * [magento/magento2#13132](https://github.com/magento/magento2/pull/13132) -- Update the Emogrifier dependency to ^2.0.0 (by @oliverklee) + * [magento/magento2#13494](https://github.com/magento/magento2/pull/13494) -- Fixing of Problem with updating stock item qty and stock status (by @nuzil) + * [magento/magento2#13498](https://github.com/magento/magento2/pull/13498) -- issue #13497 - Method getUrl in Magento\Catalog\Model\Product\Attribu... (by @igortregub) + * [magento/magento2#13040](https://github.com/magento/magento2/pull/13040) -- magento/magento2#: Customer Login/Logout Issue (by @vinayshah) + * [magento/magento2#13462](https://github.com/magento/magento2/pull/13462) -- Switch updatecart qty input validators to dynamic instead of hardcoding (by @gil--) + * [magento/magento2#13528](https://github.com/magento/magento2/pull/13528) -- Fix for #12081: missing translations in the js-translations.json (by @mattijv) + * [magento/magento2#13563](https://github.com/magento/magento2/pull/13563) -- magento/magento2#11252: fix adminhtml file attribute edit form (by @Mkennethsmith) + * [magento/magento2#13551](https://github.com/magento/magento2/pull/13551) -- Fix json encoded attribute backend type to not encode attribute value multiple times (by @tkotosz) + * [magento/magento2#12843](https://github.com/magento/magento2/pull/12843) -- Display a more meaningful error message in case of misspelt module name (by @JanisE) + * [magento/magento2#13438](https://github.com/magento/magento2/pull/13438) -- Product image builder - Override attributes when builder used multiple times (by @ihor-sviziev) + * [magento/magento2#13596](https://github.com/magento/magento2/pull/13596) -- Fix adding values to system variable collection (by @mszydlo) + * [magento/magento2#13614](https://github.com/magento/magento2/pull/13614) -- Show redirect_to_base config in store scope (by @JeroenVanLeusden) + * [magento/magento2#11504](https://github.com/magento/magento2/pull/11504) -- Add MagentoStyle as Console Input/output helper object... (by @wesleywmd) + * [magento/magento2#13587](https://github.com/magento/magento2/pull/13587) -- Show maintenance IP-address without commas (by @barryvdh) + * [magento/magento2#13679](https://github.com/magento/magento2/pull/13679) -- Update StorageInterface.php (by @davidangel) + * [magento/magento2#13663](https://github.com/magento/magento2/pull/13663) -- Refactoring: remove unuseful temporary variable (by @real34) + * [magento/magento2#13698](https://github.com/magento/magento2/pull/13698) -- [Travis Test Fix] Add MagentoStyle as Console Input/output (by @magento-engcom-team) + * [magento/magento2#13586](https://github.com/magento/magento2/pull/13586) -- Add option to add IP address to existing list (by @barryvdh) + * [magento/magento2#13643](https://github.com/magento/magento2/pull/13643) -- Fixes #12791 - Use a selector to only select the correct tax rate sel... (by @hostep) + * [magento/magento2#13661](https://github.com/magento/magento2/pull/13661) -- Typo (address not addres) (by @srenon) + * [magento/magento2#13678](https://github.com/magento/magento2/pull/13678) -- Add RewriteBase directive template in .htaccess file into pub/static folder (by @ccasciotti) + * [magento/magento2#13740](https://github.com/magento/magento2/pull/13740) -- Display a more meaningful error message in case of misspelt module name unit test. (by @nmalevanec) + * [magento/magento2#13742](https://github.com/magento/magento2/pull/13742) -- Fix adding values to system variable collection unit test. (by @nmalevanec) + * [magento/magento2#13761](https://github.com/magento/magento2/pull/13761) -- Fix bug Magento 2.2.2 password reset strength meter #13429 (by @aoldoni) + * [magento/magento2#13759](https://github.com/magento/magento2/pull/13759) -- Add ObserverInterface to the api (by @fooman) + * [magento/magento2#13770](https://github.com/magento/magento2/pull/13770) -- Remove not-allowed currencies from the currencies dropdown in Setup (by @r-martins) + * [magento/magento2#12749](https://github.com/magento/magento2/pull/12749) -- Grid filtration doesn't work for mysql special characters (by @laconica-sergey) + * [magento/magento2#13280](https://github.com/magento/magento2/pull/13280) -- Add option "lock-config" for shell command "config:set" (by @avstudnitz) + * [magento/magento2#13584](https://github.com/magento/magento2/pull/13584) -- Ensure DeploymentConfig Reader always returns an array (by @barryvdh) + * [magento/magento2#13680](https://github.com/magento/magento2/pull/13680) -- Cast handling fee to float (by @schmengler) + * [magento/magento2#13762](https://github.com/magento/magento2/pull/13762) -- Remove forced setting of cache_lifetime to false in constructor and set default cache_lifetime to 3600 (by @zolat) + * [magento/magento2#12564](https://github.com/magento/magento2/pull/12564) -- Add visibility and status filter to category product grid (by @peterjaap) + * [magento/magento2#13682](https://github.com/magento/magento2/pull/13682) -- [Backport-2.2] of PR-#10935 Fix LowStock report in All Websites view (by @gwharton) + * [magento/magento2#13700](https://github.com/magento/magento2/pull/13700) -- Fix faulty admin spinner animation (by @RNanoware) + * [magento/magento2#13777](https://github.com/magento/magento2/pull/13777) -- Fix #13315. Mobile 'Payments methods' step looks bad on mobile (by @Frodigo) + * [magento/magento2#13811](https://github.com/magento/magento2/pull/13811) -- Added missing event parameter for proxy function on the search form submit (by @koenner01) + * [magento/magento2#13816](https://github.com/magento/magento2/pull/13816) -- Add @api annotation to block argument marker interface (by @Vinai) + * [magento/magento2#13830](https://github.com/magento/magento2/pull/13830) -- Minicart should require dropdownDialog (by @amenk) + * [magento/magento2#13038](https://github.com/magento/magento2/pull/13038) -- Default Welcome message is broken on storefront with enabled translate-inline (by @pareshpansuriya) + * [magento/magento2#13567](https://github.com/magento/magento2/pull/13567) -- Add integration tests for product urls rewrite generation (by @adrien-louis-r) + * [magento/magento2#13787](https://github.com/magento/magento2/pull/13787) -- Issue-13768 Fixed error messages on admin user account page after redirect for force password change (by @nuzil) + * [magento/magento2#13817](https://github.com/magento/magento2/pull/13817) -- Allow changing head and body element through xml layout updates (by @cedricziel) + * [magento/magento2#13828](https://github.com/magento/magento2/pull/13828) -- Inconsistent Redirect in Admin Notification Controller (by @chickenland) + * [magento/magento2#13844](https://github.com/magento/magento2/pull/13844) -- Fix issue 13827 (by @julienanquetil) + * [magento/magento2#13897](https://github.com/magento/magento2/pull/13897) -- Fix typo in securityCheckers array (by @pmclain) + * [magento/magento2#13796](https://github.com/magento/magento2/pull/13796) -- Save CMS Block using repository (by @JeroenVanLeusden) + * [magento/magento2#13814](https://github.com/magento/magento2/pull/13814) -- Load CMS Page using repository in save action (by @JeroenVanLeusden) + * [magento/magento2#11513](https://github.com/magento/magento2/pull/11513) -- Modify Report processor to return 500 (by @andrewhowdencom) + * [magento/magento2#13914](https://github.com/magento/magento2/pull/13914) -- Pass Expected Data Type in backgroundColor Call (2.2) (by @northernco) + * [magento/magento2#13217](https://github.com/magento/magento2/pull/13217) -- Fix JS address converter function from mutating its argument (by @vaaralav) + * [magento/magento2#13641](https://github.com/magento/magento2/pull/13641) -- Add missing implementation for applySortOrder() (by @schmengler) + * [magento/magento2#13709](https://github.com/magento/magento2/pull/13709) -- Changes static content deploy log levels verbosity (by @hostep) + * [magento/magento2#13750](https://github.com/magento/magento2/pull/13750) -- Less clean up (by @Karlasa) + * [magento/magento2#13861](https://github.com/magento/magento2/pull/13861) -- Solved this issue : Drop down values are not showing in catalog produ... (by @hiren-wagento) + * [magento/magento2#13930](https://github.com/magento/magento2/pull/13930) -- #13899 Solve Canada Zip Code pattern (by @tadeobarranco) + * [magento/magento2#13966](https://github.com/magento/magento2/pull/13966) -- Setup Lists - Make allowedCurrencies property private (by @ihor-sviziev) + 2.2.2 ============= * GitHub issues: @@ -98,7 +2328,7 @@ * [#10628](https://github.com/magento/magento2/issues/10628) -- Color attribute swatches are not visible if sorting is enabled (fixed in [#12077](https://github.com/magento/magento2/pull/12077)) * [#8022](https://github.com/magento/magento2/issues/8022) -- Uncaught Error: Call to a member function addItem() on array in app/code/Magento/Sales/Model/Order/Shipment.php (fixed in [#12173](https://github.com/magento/magento2/pull/12173)) * GitHub pull requests: - * [#11240](https://github.com/magento/magento2/pull/11240) -- Virtual Theme load: Check for null to actually reach the code that handles this case to t… (by @leptoquark1) + * [#11240](https://github.com/magento/magento2/pull/11240) -- Virtual Theme load: Check for null to actually reach the code that handles this case to t... (by @leptoquark1) * [#11261](https://github.com/magento/magento2/pull/11261) -- Prevent invoice cancelation multiple times 2.2-develop [Backport] (by @osrecio) * [#11342](https://github.com/magento/magento2/pull/11342) -- ADDED $sortByPostion flag to getChildren() (by @denisristic) * [#11351](https://github.com/magento/magento2/pull/11351) -- Fix the wrong input format of Customer date of birth (by @manuelson) @@ -112,11 +2342,11 @@ * [#11199](https://github.com/magento/magento2/pull/11199) -- Add db-prefix from env conf when command admin:user:create is executed (by @osrecio) * [#11299](https://github.com/magento/magento2/pull/11299) -- Update Guest.php (by @lano-vargas) * [#11381](https://github.com/magento/magento2/pull/11381) -- Save region correctly to save sales address from admin (by @raumatbel) - * [#11460](https://github.com/magento/magento2/pull/11460) -- [ISSUE-11140][BUGFIX] Skip store code admin from being detected in ca… (by @diglin) + * [#11460](https://github.com/magento/magento2/pull/11460) -- [ISSUE-11140][BUGFIX] Skip store code admin from being detected in ca... (by @diglin) * [#11505](https://github.com/magento/magento2/pull/11505) -- [Backport-2.2] Retain additional cron history by default (by @mpchadwick) * [#11437](https://github.com/magento/magento2/pull/11437) -- Add `confirmation` and `lock_expires ` to customer export csv - Fix issue 10765 (by @convenient) * [#11486](https://github.com/magento/magento2/pull/11486) -- [Backport 2.2]Add VAT number to email source variables (by @JeroenVanLeusden) - * [#11495](https://github.com/magento/magento2/pull/11495) -- MAGETWO-75743: Fix for #9783 Multiple parameters in widget.… (by @diazwatson) + * [#11495](https://github.com/magento/magento2/pull/11495) -- MAGETWO-75743: Fix for #9783 Multiple parameters in widget.... (by @diazwatson) * [#11500](https://github.com/magento/magento2/pull/11500) -- MAGETWO-81245: Handling all setProductsFilter items in array as arguments (by @kirmorozov) * [#11555](https://github.com/magento/magento2/pull/11555) -- Travis CI functional tests maintenance for 2.2-develop (by @ishakhsuvarov) * [#11235](https://github.com/magento/magento2/pull/11235) -- [2.2-develop] Add static test to detect blocks without name attribute (by @ihor-sviziev) @@ -134,13 +2364,13 @@ * [#11675](https://github.com/magento/magento2/pull/11675) -- MAGETWO-77672: in system.xml translate phrase not work, if comment starts from new line. (by @nmalevanec) * [#11673](https://github.com/magento/magento2/pull/11673) -- [BACKPORT 2.2] [TASK] Removed Typo in Paypal TestCase didgit => digit (by @lewisvoncken) * [#11704](https://github.com/magento/magento2/pull/11704) -- [Backport 2.2-develop] Travis: surround variable TRAVIS_BRANCH with double-quotes instead of single-quotes (by @adrian-martinez-interactiv4) - * [#11677](https://github.com/magento/magento2/pull/11677) -- [BACKPORT 2.2] [TASK] Moved Customer Groups Menu Item from Other sett… (by @lewisvoncken) + * [#11677](https://github.com/magento/magento2/pull/11677) -- [BACKPORT 2.2] [TASK] Moved Customer Groups Menu Item from Other sett... (by @lewisvoncken) * [#11676](https://github.com/magento/magento2/pull/11676) -- #7915: customer objects are equal to eachother after observing event customer_save_after_data_object (by @RomaKis) * [#11250](https://github.com/magento/magento2/pull/11250) -- Fixing #10275 keyboard submit of adminhtml suggest form. (by @romainruaud) * [#11421](https://github.com/magento/magento2/pull/11421) -- FIX #11022 in 2.2-develop: Filter Groups of search criteria parameter have not been included for further processing (by @davidverholen) * [#11440](https://github.com/magento/magento2/pull/11440) -- Add missing translations in Magento_UI (by @JeroenVanLeusden) * [#11643](https://github.com/magento/magento2/pull/11643) -- Fixed ability to set field config from layout xml #11302 [backport 2.2] (by @vovayatsyuk) - * [#11637](https://github.com/magento/magento2/pull/11637) -- MAGETWO-81311: Check the length of the array before attempting to sli… (by @briscoda) + * [#11637](https://github.com/magento/magento2/pull/11637) -- MAGETWO-81311: Check the length of the array before attempting to sli... (by @briscoda) * [#11635](https://github.com/magento/magento2/pull/11635) -- Coupon codes not showing in invoice (by @crissanclick) * [#11690](https://github.com/magento/magento2/pull/11690) -- Add a health check to the NGINX configuration sample (by @andrewhowdencom) * [#11710](https://github.com/magento/magento2/pull/11710) -- Allow coupon code with special charater to be applied to order in checkout (by @gabrielqs-redstage) @@ -177,7 +2407,7 @@ * [#11817](https://github.com/magento/magento2/pull/11817) -- GITHUB-8970: Cannot assign products to categories not under tree root. (by @p-bystritsky) * [#11405](https://github.com/magento/magento2/pull/11405) -- Allow setting of http response status code in a Redirection (by @gabrielqs-redstage) * [#11858](https://github.com/magento/magento2/pull/11858) -- #11697 Theme: Added html node to page xml root, cause validation error (by @adrian-martinez-interactiv4) - * [#11869](https://github.com/magento/magento2/pull/11869) -- Resolve Error While Trying To Load Quote Item Collection Using Magent… (by @neeta-wagento) + * [#11869](https://github.com/magento/magento2/pull/11869) -- Resolve Error While Trying To Load Quote Item Collection Using Magent... (by @neeta-wagento) * [#11889](https://github.com/magento/magento2/pull/11889) -- Save background color correctly in images. [backport 2.2] (by @raumatbel) * [#11917](https://github.com/magento/magento2/pull/11917) -- [BACKPORT 2.2] [TASK] Add resetPassword call to the webapi (by @lewisvoncken) * [#11949](https://github.com/magento/magento2/pull/11949) -- 11868: "Add Products" button has been duplicated after the customer group was changed. (by @nmalevanec) @@ -189,13 +2419,13 @@ * [#12013](https://github.com/magento/magento2/pull/12013) -- Add validation for number of street lines (by @crissanclick) * [#11785](https://github.com/magento/magento2/pull/11785) -- fix #8846: avoid duplicated attribute option values (by @gomencal) * [#11993](https://github.com/magento/magento2/pull/11993) -- 11700: "Something Went Wrong" error for limited access admin user (by @RomaKis) - * [#12018](https://github.com/magento/magento2/pull/12018) -- Magento 2.2.0 Solution for Cross-sell product placeholder image size … (by @emiprotech) + * [#12018](https://github.com/magento/magento2/pull/12018) -- Magento 2.2.0 Solution for Cross-sell product placeholder image size ... (by @emiprotech) * [#11556](https://github.com/magento/magento2/pull/11556) -- Fix #10583: Checkout place order exception when using a new address (by @joni-jones) * [#11879](https://github.com/magento/magento2/pull/11879) -- #4004: Newsletter Subscriber create-date not set, and change_status_at broken (by @nemesis-back) * [#11588](https://github.com/magento/magento2/pull/11588) -- Fix Issue #7225 - Remove hardcoding of apply_to when saving attributes (by @MartinPeverelli) * [#11958](https://github.com/magento/magento2/pull/11958) -- 11197: Blank page at the checkout 'shipping' step[backport]. (by @nmalevanec) * [#12091](https://github.com/magento/magento2/pull/12091) -- Fix "Undefined variable: responseAjax" notice when trying to save a shipment package (by @lazyguru) - * [#11461](https://github.com/magento/magento2/pull/11461) -- [ISSUE-10811][BUGFIX] Update .htaccess.sample to replace FollowSymLin… (by @diglin) + * [#11461](https://github.com/magento/magento2/pull/11461) -- [ISSUE-10811][BUGFIX] Update .htaccess.sample to replace FollowSymLin... (by @diglin) * [#11719](https://github.com/magento/magento2/pull/11719) -- 10920: Sku => Entity_id relations are fetched inefficiently when inserting attributes values during product import. (by @nmalevanec) * [#11722](https://github.com/magento/magento2/pull/11722) -- 6802: Magento\Search\Helper\getSuggestUrl() not used in search template. (by @nmalevanec) * [#11857](https://github.com/magento/magento2/pull/11857) -- CMS Page - CMS Page - Force validate layout update xml in production mode when saving CMS Page - Handle layout update xml validation exceptions (by @adrian-martinez-interactiv4) @@ -207,7 +2437,7 @@ * [#12082](https://github.com/magento/magento2/pull/12082) -- Products in cart report error when we have grouped or bundle product (by @mihaifaget) * [#12131](https://github.com/magento/magento2/pull/12131) -- [Backport 2.2] Close PayPal popup window in case of rejected request #10820 (by @vovayatsyuk) * [#12139](https://github.com/magento/magento2/pull/12139) -- 9768: Admin dashboard Most Viewed Products Tab only gives default attribute set's products(backport for 2.2) (by @RomaKis) - * [#11914](https://github.com/magento/magento2/pull/11914) -- [BACKPORT 2.2] [BUGFIX] All UI input fields should have maxlength of 255 because of V… (by @lewisvoncken) + * [#11914](https://github.com/magento/magento2/pull/11914) -- [BACKPORT 2.2] [BUGFIX] All UI input fields should have maxlength of 255 because of V... (by @lewisvoncken) * [#11944](https://github.com/magento/magento2/pull/11944) -- Report Handled Exceptions To New Relic (by @mpchadwick) * [#12144](https://github.com/magento/magento2/pull/12144) -- Removed FileClassScannerTest dependency to "Magento_Catalog" (by @wexo-team) * [#11459](https://github.com/magento/magento2/pull/11459) -- close #10810 Migrates Apache Access Syntax to 2.4 on Apache >= 2.4 (by @jonashrem) @@ -257,8 +2487,8 @@ * [#11054](https://github.com/magento/magento2/pull/11054) -- Add dev:tests:run parameter to pass arguments to phpunit (by @schmengler) * [#11056](https://github.com/magento/magento2/pull/11056) -- Do not disable maintenance mode after running a backup. (by @stevenvdp) * [#11058](https://github.com/magento/magento2/pull/11058) -- Escape html before replace new line with break (by @Quinten) - * [#11063](https://github.com/magento/magento2/pull/11063) -- 6712 Remove additional margin for footer links widget; prevents layou… (by @fragdochkarl) - * [#11064](https://github.com/magento/magento2/pull/11064) -- Show different message if DB module version is higher than code modul… (by @schmengler) + * [#11063](https://github.com/magento/magento2/pull/11063) -- 6712 Remove additional margin for footer links widget; prevents layou... (by @fragdochkarl) + * [#11064](https://github.com/magento/magento2/pull/11064) -- Show different message if DB module version is higher than code modul... (by @schmengler) * [#11076](https://github.com/magento/magento2/pull/11076) -- Backport to 2.2 of #10824: add name for order items grid default renderer block (by @gsomoza) * [#11048](https://github.com/magento/magento2/pull/11048) -- Fix #10417 (by @PieterCappelle) * [#11049](https://github.com/magento/magento2/pull/11049) -- Vague error message for invalid url_key for category (by @avdb) @@ -467,9 +2697,9 @@ * [#8336](https://github.com/magento/magento2/pull/8336) -- fixing time format on admin sales order grid (by @magexo) * [#8327](https://github.com/magento/magento2/pull/8327) -- Change order of parameters passed to LogicException in AbstractTemplate.php (by @bery) * [#8307](https://github.com/magento/magento2/pull/8307) -- Allow digits in communication class type definition (by @cmuench) - * [#8354](https://github.com/magento/magento2/pull/8354) -- Display correctly "Add" button label for the block class \Magento\Con… (by @diglin) + * [#8354](https://github.com/magento/magento2/pull/8354) -- Display correctly "Add" button label for the block class \Magento\Con... (by @diglin) * [#8246](https://github.com/magento/magento2/pull/8246) -- Fixes #7723 - saving multi select field in UI component form (by @Zefiryn) - * [#8353](https://github.com/magento/magento2/pull/8353) -- Replace into the layout adminhtml_order_shipment_new.xml block alias … (by @diglin) + * [#8353](https://github.com/magento/magento2/pull/8353) -- Replace into the layout adminhtml_order_shipment_new.xml block alias ... (by @diglin) * [#8395](https://github.com/magento/magento2/pull/8395) -- Added "editPost" action for customer sections.xml (by @rossluk) * [#8383](https://github.com/magento/magento2/pull/8383) -- Issue/8382 (by @PascalBrouwers) * [#8151](https://github.com/magento/magento2/pull/8151) -- Remove "<2.7" constraint on symfony/console (by @nicolas-grekas) @@ -600,7 +2830,7 @@ * [#6677](https://github.com/magento/magento2/pull/6677) -- Update addCategoriesFilter to return $this (by @maciekpaprocki) * [#6894](https://github.com/magento/magento2/pull/6894) -- Fixed phpseclib\Net\SFTP constants used in write() method (by @federivo) * [#7117](https://github.com/magento/magento2/pull/7117) -- Add de_LU and fr_LU languages for Luxembourg (by @ajpevers) - * [#8915](https://github.com/magento/magento2/pull/8915) -- magento/magetno2#8676: I can not translate title attribute in xml fil… (by @DanijelPotocki) + * [#8915](https://github.com/magento/magento2/pull/8915) -- magento/magetno2#8676: I can not translate title attribute in xml fil... (by @DanijelPotocki) * [#5446](https://github.com/magento/magento2/pull/5446) -- Fix Rest Api - GET /V1/configurable-products/{sku}/children not giving ID in response (by @k-andrew) * [#6321](https://github.com/magento/magento2/pull/6321) -- Add missing return in resolveShippingRates (by @GordonLesti) * [#6707](https://github.com/magento/magento2/pull/6707) -- Issue/6706 (by @PascalBrouwers) @@ -638,10 +2868,10 @@ * [#6811](https://github.com/magento/magento2/pull/6811) -- Unable to save subscription checkbox on Admin customer save (by @rich1990) * [#6839](https://github.com/magento/magento2/pull/6839) -- Changed constructor to use an interface (by @dverkade) * [#6912](https://github.com/magento/magento2/pull/6912) -- Changed module readme text (by @dverkade) - * [#7262](https://github.com/magento/magento2/pull/7262) -- Replace boolean cast to be able to disable frame, aspect ratio, trans… (by @joost-florijn-kega) + * [#7262](https://github.com/magento/magento2/pull/7262) -- Replace boolean cast to be able to disable frame, aspect ratio, trans... (by @joost-florijn-kega) * [#7762](https://github.com/magento/magento2/pull/7762) -- change getId() to getPaymentId() (by @HirokazuNishi) - * [#8769](https://github.com/magento/magento2/pull/8769) -- magento/magento2#7860: Invalid comment for the method __order in Mage… (by @mcspronko) - * [#8917](https://github.com/magento/magento2/pull/8917) -- imagento/magento2#8515: Downloadable product is available for downloa… (by @nazarpadalka) + * [#8769](https://github.com/magento/magento2/pull/8769) -- magento/magento2#7860: Invalid comment for the method __order in Mage... (by @mcspronko) + * [#8917](https://github.com/magento/magento2/pull/8917) -- imagento/magento2#8515: Downloadable product is available for downloa... (by @nazarpadalka) * [#8908](https://github.com/magento/magento2/pull/8908) -- magento/magento2#8871: Typo in Pull Request Template (by @tomislavsantek) * [#8989](https://github.com/magento/magento2/pull/8989) -- Remove redundant check in if-condition (by @FabianLauer) * [#8953](https://github.com/magento/magento2/pull/8953) -- Log level for caught exception (by @flancer64) @@ -782,7 +3012,7 @@ * [#9091](https://github.com/magento/magento2/pull/9091) -- ESLint errors fix (by @Igloczek) * [#9285](https://github.com/magento/magento2/pull/9285) -- Replace Zend_Log with Psr\Log\LoggerInterface (by @tdgroot) * [#9380](https://github.com/magento/magento2/pull/9380) -- Removed unnecessary code and namespaces from import validators (by @ccasciotti) - * [#9540](https://github.com/magento/magento2/pull/9540) -- Removed workaround for old Webkit bug in the TinyMCE editor for selec… (by @hostep) + * [#9540](https://github.com/magento/magento2/pull/9540) -- Removed workaround for old Webkit bug in the TinyMCE editor for selec... (by @hostep) * [#9549](https://github.com/magento/magento2/pull/9549) -- Selects correct stores value option (by @Corefix) * [#9574](https://github.com/magento/magento2/pull/9574) -- no need to create customer once u got the quote object (by @sivajik34) * [#9618](https://github.com/magento/magento2/pull/9618) -- Flip the property assignments for _logger and _fetchStrategy in __wakeup (by @cykirsch) @@ -2893,7 +5123,7 @@ Tests: * [#686](https://github.com/magento/magento2/issues/686) -- Product save validation errors in the admin don't hide the overlay * [#702](https://github.com/magento/magento2/issues/702) -- Base table or view not found * [#652](https://github.com/magento/magento2/issues/652) -- Multishipping checkout not to change the Billing address js issue - * [#648](https://github.com/magento/magento2/issues/648) -- An equal (=) sign in the hash of the product page to to break the tabs functionality + * [#648](https://github.com/magento/magento2/issues/648) -- An equal (=) sign in the hash of the product page to break the tabs functionality * Service Contracts: * Refactored usage of new API of the Customer module * Implemented Service Contracts for the Sales module diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2839ac5ee9d32..0000000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,36 +0,0 @@ -# Contributing to Magento 2 code - -Contributions to the Magento 2 codebase are done using the fork & pull model. -This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes (hence the phrase “pull request”). - -Contributions can take the form of new components/features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations or just good suggestions. - -The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor for two weeks, the issue is closed. - - -## Contribution requirements - -1. Contributions must adhere to [Magento coding standards](http://devdocs.magento.com/guides/v2.0/coding-standards/bk-coding-standards.html). -2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request to be merged quickly and without additional clarification requests. -3. Commits must be accompanied by meaningful commit messages. -4. PRs which include bug fixing, must be accompanied with step-by-step description of how to reproduce the bug. -3. PRs which include new logic or new features must be submitted along with: -* Unit/integration test coverage (we will be releasing more information on writing test coverage in the near future). -* Proposed [documentation](http://devdocs.magento.com) update. Documentation contributions can be submitted [here](https://github.com/magento/devdocs). -4. For large features or changes, please [open an issue](https://github.com/magento/magento2/issues) and discuss first. This may prevent duplicate or unnecessary effort, and it may gain you some additional contributors. -5. All automated tests are passed successfully (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). - -## Contribution process - -If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). By doing that, you will be able to collaborate with the Magento 2 development team, “fork” the Magento 2 project and be able to easily send “pull requests”. - -1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. -2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. -3. Create and test your work. -4. Fork the Magento 2 repository according to [Fork a repository instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow [Create a pull request instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#pull_request). -5. Once your contribution is received, Magento 2 development team will review the contribution and collaborate with you as needed to improve the quality of the contribution. - -## Code of Conduct - -Please note that this project is released with a Contributor Code of Conduct. We expect you to agree to its terms when participating in this project. -The full text is available in the repository [Wiki](https://github.com/magento/magento2/wiki/Magento-Code-of-Conduct). diff --git a/COPYING.txt b/COPYING.txt index d2cbcd01539dd..040bdd5f3ce72 100644 --- a/COPYING.txt +++ b/COPYING.txt @@ -1,4 +1,4 @@ -Copyright © 2013-2017 Magento, Inc. +Copyright © 2013-present Magento, Inc. Each Magento source file included in this distribution is licensed under OSL 3.0 or the Magento Enterprise Edition (MEE) license diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 3ac68076d4353..0000000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ - - - -### Preconditions - - -1. -2. - -### Steps to reproduce - -1. -2. -3. - -### Expected result - -1. - -### Actual result - -1. [Screenshot, logs] - - diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index d1f01ba9f2640..0000000000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,20 +0,0 @@ - - -### Description - - -### Fixed Issues (if relevant) - -1. magento/magento2#: Issue title -2. ... - -### Manual testing scenarios - -1. ... -2. ... - -### Contribution checklist - - [ ] Pull request has a meaningful description of its purpose - - [ ] All commits are accompanied by meaningful commit messages - - [ ] All new or changed code is covered with unit/integration tests (if applicable) - - [ ] All automated tests passed successfully (all builds on Travis CI are green) diff --git a/README.md b/README.md index 9b1aa1b7b3e28..5a200505ec576 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=develop)](https://travis-ci.org/magento/magento2) +[![Open Source Helpers](https://www.codetriage.com/magento/magento2/badges/users.svg)](https://www.codetriage.com/magento/magento2) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/magento/magento2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/magento-2/localized.png)](https://crowdin.com/project/magento-2)

Welcome

-Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting edge, feature-rich eCommerce solution that gets results. +Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting-edge, feature-rich eCommerce solution that gets results. ## Magento system requirements -[Magento system requirements](http://devdocs.magento.com/magento-system-requirements.html) +[Magento system requirements](http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements2.html) ## Install Magento To install Magento, see either: -* [Magento DevBox](https://magento.com/tech-resources/download), the easiest way to get started with Magento. -* [Installation guide](http://devdocs.magento.com/guides/v2.0/install-gde/bk-install-guide.html) +* [Installation guide](http://devdocs.magento.com/guides/v2.2/install-gde/bk-install-guide.html)

Contributing to the Magento 2 code base

Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions. @@ -22,28 +21,31 @@ To learn about issues, click [here][2]. To open an issue, click [here][3]. To suggest documentation improvements, click [here][4]. -[1]: -[2]: +[1]: +[2]: [3]: [4]: -

Labels applied by the Magento team

+

Community Maintainers

+The members of this team have been recognized for their outstanding commitment to maintaining and improving Magento. Magento has granted them permission to accept, merge, and reject pull requests, as well as review issues, and thanks these Community Maintainers for their valuable contributions. + + + + -| Label | Description | -| ------------- |-------------| -| ![DOC](http://devdocs.magento.com/common/images/github_DOC.png) | Affects Documentation domain. | -| ![PROD](http://devdocs.magento.com/common/images/github_PROD.png) | Affects the Product team (mostly feature requests or business logic change). | -| ![TECH](http://devdocs.magento.com/common/images/github_TECH.png) | Affects Architect Group (mostly to make decisions around technology changes). | -| ![accept](http://devdocs.magento.com/common/images/github_accept.png) | The pull request has been accepted and will be merged into mainline code. | -| ![reject](http://devdocs.magento.com/common/images/github_reject.png) | The pull request has been rejected and will not be merged into mainline code. Possible reasons can include but are not limited to: issue has already been fixed in another code contribution, or there is an issue with the code contribution. | -| ![bug report](http://devdocs.magento.com/common/images/github_bug.png) | The Magento Team has confirmed that this issue contains the minimum required information to reproduce. | -| ![acknowledged](http://devdocs.magento.com/common/images/gitHub_acknowledged.png) | The Magento Team has validated the issue and an internal ticket has been created. | -| ![acknowledged](http://devdocs.magento.com/common/images/github_inProgress.png) | The internal ticket is currently in progress, fix is scheduled to be delivered. | -| ![acknowledged](http://devdocs.magento.com/common/images/github_needsUpdate.png) | The Magento Team needs additional information from the reporter to properly prioritize and process the issue or pull request. | +

Top Contributors

+Magento is thankful for any contribution that can improve our code base, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. + + + + +

Labels applied by the Magento team

+We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more. +Please review the Code Contributions guide for detailed information on labels used in Magento 2 repositories.

Reporting security issues

-To report security vulnerabilities in Magento software or web sites, please e-mail security@magento.com. Please do not report security issues using GitHub. Be sure to encrypt your e-mail with our encryption key if it includes sensitive information. Learn more about reporting security issues here. +To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account there to submit and follow-up your issue. Learn more about reporting security issues here. Stay up-to-date on the latest security news and patches for Magento by signing up for Security Alert Notifications. diff --git a/app/bootstrap.php b/app/bootstrap.php index 6701a9f4dd51e..4a923cd0c910b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -8,18 +8,19 @@ * Environment initialization */ error_reporting(E_ALL); +stream_wrapper_unregister('phar'); #ini_set('display_errors', 1); /* PHP version validation */ if (!defined('PHP_VERSION_ID') || !(PHP_VERSION_ID === 70002 || PHP_VERSION_ID === 70004 || PHP_VERSION_ID >= 70006)) { if (PHP_SAPI == 'cli') { echo 'Magento supports 7.0.2, 7.0.4, and 7.0.6 or later. ' . - 'Please read http://devdocs.magento.com/guides/v1.0/install-gde/system-requirements.html'; + 'Please read http://devdocs.magento.com/guides/v2.2/install-gde/system-requirements.html'; } else { echo <<

Magento supports PHP 7.0.2, 7.0.4, and 7.0.6 or later. Please read - + Magento System Requirements. HTML; @@ -31,8 +32,6 @@ // Sets default autoload mappings, may be overridden in Bootstrap::create \Magento\Framework\App\Bootstrap::populateAutoloader(BP, []); -require_once BP . '/app/functions.php'; - /* Custom umask value may be provided in optional mage_umask file in root */ $umaskFile = BP . '/magento_umask'; $mask = file_exists($umaskFile) ? octdec(file_get_contents($umaskFile)) : 002; @@ -49,12 +48,21 @@ unset($_SERVER['ORIG_PATH_INFO']); } -if (!empty($_SERVER['MAGE_PROFILER']) +if ( + (!empty($_SERVER['MAGE_PROFILER']) || file_exists(BP . '/var/profiler.flag')) && isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false ) { - \Magento\Framework\Profiler::applyConfig( - $_SERVER['MAGE_PROFILER'], + $profilerConfig = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER']) + ? $_SERVER['MAGE_PROFILER'] + : trim(file_get_contents(BP . '/var/profiler.flag')); + + if ($profilerConfig) { + $profilerConfig = json_decode($profilerConfig, true) ?: $profilerConfig; + } + + Magento\Framework\Profiler::applyConfig( + $profilerConfig, BP, !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' ); diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php index 6f0e42bdcbef1..e515fb3ccae6c 100644 --- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php +++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php @@ -8,6 +8,9 @@ namespace Magento\AdminNotification\Block\Grid\Renderer; +/** + * Renderer class for action in the admin notifications grid. + */ class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -37,19 +40,23 @@ public function __construct( */ public function render(\Magento\Framework\DataObject $row) { - $readDetailsHtml = $row->getUrl() ? '' . + $readDetailsHtml = $row->getUrl() ? '' . __('Read Details') . '' : ''; - $markAsReadHtml = !$row->getIsRead() ? '' . __( - 'Mark as Read' - ) . '' : ''; + $markAsReadHtml = !$row->getIsRead() ? '' . __( + 'Mark as Read' + ) . '' : ''; $encodedUrl = $this->_urlHelper->getEncodedUrl(); return sprintf( - '%s%s%s', + '%s%s%s', $readDetailsHtml, $markAsReadHtml, $this->getUrl( diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php index 79f69ab5da88d..6b5e0681139cf 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsRead.php @@ -28,11 +28,11 @@ public function execute() )->markAsRead( $notificationId ); - $this->messageManager->addSuccess(__('The message has been marked as Read.')); + $this->messageManager->addSuccessMessage(__('The message has been marked as Read.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("We couldn't mark the notification as Read because of an error.") ); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php index 9e61b8ff4b83c..9ae4a7cdac0b9 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassMarkAsRead.php @@ -23,7 +23,7 @@ public function execute() { $ids = $this->getRequest()->getParam('notification'); if (!is_array($ids)) { - $this->messageManager->addError(__('Please select messages.')); + $this->messageManager->addErrorMessage(__('Please select messages.')); } else { try { foreach ($ids as $id) { @@ -32,13 +32,13 @@ public function execute() $model->setIsRead(1)->save(); } } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been marked as Read.', count($ids)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("We couldn't mark the notification as Read because of an error.") ); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php index 6c0dfd1db7d16..f4cafa09c7e45 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php @@ -23,7 +23,7 @@ public function execute() { $ids = $this->getRequest()->getParam('notification'); if (!is_array($ids)) { - $this->messageManager->addError(__('Please select messages.')); + $this->messageManager->addErrorMessage(__('Please select messages.')); } else { try { foreach ($ids as $id) { @@ -32,13 +32,14 @@ public function execute() $model->setIsRemove(1)->save(); } } - $this->messageManager->addSuccess(__('Total of %1 record(s) have been removed.', count($ids))); + $this->messageManager->addSuccessMessage(__('Total of %1 record(s) have been removed.', count($ids))); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("We couldn't remove the messages because of an error.")); + $this->messageManager + ->addExceptionMessage($e, __("We couldn't remove the messages because of an error.")); } } - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); + $this->_redirect('adminhtml/*/'); } } diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php index 17f911339cb61..bec101fc27d48 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/Remove.php @@ -31,11 +31,12 @@ public function execute() try { $model->setIsRemove(1)->save(); - $this->messageManager->addSuccess(__('The message has been removed.')); + $this->messageManager->addSuccessMessage(__('The message has been removed.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("We couldn't remove the messages because of an error.")); + $this->messageManager + ->addExceptionMessage($e, __("We couldn't remove the messages because of an error.")); } $this->_redirect('adminhtml/*/'); diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php index c332440276083..6088afbc2e1a4 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php @@ -59,8 +59,10 @@ public function execute() if (empty($result)) { $result[] = [ 'severity' => (string)\Magento\Framework\Notification\MessageInterface::SEVERITY_NOTICE, - 'text' => 'You have viewed and resolved all recent system notices. ' - . 'Please refresh the web page to clear the notice alert.', + 'text' => __( + 'You have viewed and resolved all recent system notices. ' + . 'Please refresh the web page to clear the notice alert.' + ) ]; } $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); diff --git a/app/code/Magento/AdminNotification/Model/Feed.php b/app/code/Magento/AdminNotification/Model/Feed.php index 1766425fb19b1..5a4f7d5ddd390 100644 --- a/app/code/Magento/AdminNotification/Model/Feed.php +++ b/app/code/Magento/AdminNotification/Model/Feed.php @@ -25,6 +25,11 @@ class Feed extends \Magento\Framework\Model\AbstractModel const XML_LAST_UPDATE_PATH = 'system/adminnotification/last_update'; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * Feed url * @@ -77,6 +82,7 @@ class Feed extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\Escaper|null $escaper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -90,7 +96,8 @@ public function __construct( \Magento\Framework\UrlInterface $urlBuilder, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Framework\Escaper $escaper = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->_backendConfig = $backendConfig; @@ -99,12 +106,16 @@ public function __construct( $this->_deploymentConfig = $deploymentConfig; $this->productMetadata = $productMetadata; $this->urlBuilder = $urlBuilder; + $this->escaper = $escaper ?? \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Escaper::class + ); } /** * Init model * * @return void + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ protected function _construct() { @@ -255,6 +266,6 @@ public function getFeedXml() */ private function escapeString(\SimpleXMLElement $data) { - return htmlspecialchars((string)$data); + return $this->escaper->escapeHtml((string)$data); } } diff --git a/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AdminNotification/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/app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/AdminNotification/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AdminNotification/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/app/code/Magento/AdminNotification/Test/Mftf/README.md b/app/code/Magento/AdminNotification/Test/Mftf/README.md new file mode 100644 index 0000000000000..33f88ba74200a --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Admin Notification Functional Tests + +The Functional Test Module for **Magento Admin Notification** module. diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml new file mode 100644 index 0000000000000..8a73968edb9a6 --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml @@ -0,0 +1,15 @@ + + + + +

+ + +
+ diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php index 2fbfc43aa8775..f49911c3e7a93 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/CacheOutdatedTest.php @@ -62,6 +62,9 @@ public function testGetIdentity($expectedSum, $cacheTypes) $this->assertEquals($expectedSum, $this->_messageModel->getIdentity()); } + /** + * @return array + */ public function getIdentityDataProvider() { $cacheTypeMock1 = $this->createPartialMock(\stdClass::class, ['getCacheType']); @@ -95,6 +98,9 @@ public function testIsDisplayed($expected, $allowed, $cacheTypes) $this->assertEquals($expected, $this->_messageModel->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { $cacheTypesMock = $this->createPartialMock(\stdClass::class, ['getCacheType']); diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php index 2c259db868851..b490efd8e9683 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/Media/Synchronization/ErrorTest.php @@ -72,6 +72,9 @@ public function testIsDisplayed($expectedFirstRun, $data) $this->assertEquals($expectedFirstRun, $model->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { return [ diff --git a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php index 1e71570a5e30b..c6f61fee862ba 100644 --- a/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php +++ b/app/code/Magento/AdminNotification/Test/Unit/Model/System/Message/SecurityTest.php @@ -76,6 +76,9 @@ public function testIsDisplayed($expectedResult, $cached, $response) $this->assertEquals($expectedResult, $this->_messageModel->isDisplayed()); } + /** + * @return array + */ public function isDisplayedDataProvider() { return [ diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index 59a3845cbd4b7..7cf7b350549f8 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-admin-notification", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/module-media-storage": "100.2.*", @@ -11,7 +11,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml index fbed5c0960b73..04d700b9f90ce 100644 --- a/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml +++ b/app/code/Magento/AdminNotification/etc/adminhtml/menu.xml @@ -7,6 +7,6 @@ --> - + diff --git a/app/code/Magento/AdminNotification/i18n/en_US.csv b/app/code/Magento/AdminNotification/i18n/en_US.csv index 16c5abb9db0d2..db5a4c9254814 100644 --- a/app/code/Magento/AdminNotification/i18n/en_US.csv +++ b/app/code/Magento/AdminNotification/i18n/en_US.csv @@ -48,3 +48,4 @@ Severity,Severity "Date Added","Date Added" Message,Message Actions,Actions +"You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert.","You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert." diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml index 3f79e803ccca2..6f403d8fbd36b 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> -getHeaderText() ?>" + "title": "escapeHtmlAttr($block->getHeaderText()) ?>" } }'>
  • - getNoticeMessageText() ?>
    - getReadDetailsText() ?> + escapeHtml($block->getNoticeMessageText()) ?>
    + escapeHtml($block->getReadDetailsText()) ?>
  • diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml index 01d6fdcb29571..60e3d63473596 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages.phtml @@ -4,41 +4,39 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\AdminNotification\Block\System\Messages */ ?> - getLastCritical();?> -
    +
    - +
    • - getText() ?> + escapeHtml($lastCritical->getText()) ?>
    - + escapeHtml(__('System Messages:')) ?> - getCriticalCount()): ?> + getCriticalCount()) : ?> - + - getMajorCount()): ?> - - + getMajorCount()) : ?> + +
    diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml index a97293547e132..6673ad7a18b38 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml @@ -4,35 +4,25 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup */ ?> - -' ; + return $result . $this->_getInputValueElement($row) . '
    '; } return $this->_getValue($row); } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php index ff0399e4f507f..03566bce3fc34 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php @@ -68,10 +68,7 @@ public function __construct( $this->_storeManager = $storeManager; $this->_currencyLocator = $currencyLocator; $this->_localeCurrency = $localeCurrency; - $defaultBaseCurrencyCode = $this->_scopeConfig->getValue( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' - ); + $defaultBaseCurrencyCode = $currencyLocator->getDefaultCurrency($this->_request); $this->_defaultBaseCurrency = $currencyFactory->create()->load($defaultBaseCurrencyCode); } @@ -85,7 +82,7 @@ public function render(\Magento\Framework\DataObject $row) { if ($data = (string)$this->_getValue($row)) { $currency_code = $this->_getCurrencyCode($row); - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $sign = (bool)(int)$this->getColumn()->getShowNumberSign() && $data > 0 ? '+' : ''; $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currency_code)->toCurrency($data); @@ -121,10 +118,10 @@ protected function _getCurrencyCode($row) protected function _getRate($row) { if ($rate = $this->getColumn()->getRate()) { - return floatval($rate); + return (float)$rate; } if ($rate = $row->getData($this->getColumn()->getRateField())) { - return floatval($rate); + return (float)$rate; } return $this->_defaultBaseCurrency->getRate($this->_getCurrencyCode($row)); } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php index 320713f8b57c4..a611e91f32f00 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Massaction.php @@ -65,7 +65,7 @@ public function render(\Magento\Framework\DataObject $row) */ protected function _getCheckboxHtml($value, $checked) { - $id = 'id_' . rand(0, 999); + $id = 'id_' . random_int(0, 999); $html = '
    @@ -65,15 +65,7 @@
    -
    - -
    +
    diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php index 0b3a938255de1..46db8a9907341 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -17,12 +17,10 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/checkbox.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; '; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index 304b3a5cf34ed..629f08dc75106 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -17,12 +17,10 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/multi.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; '; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php index e011ab36e8029..1519b3a67ac97 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php @@ -17,7 +17,7 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/radio.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/radio.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php index f1206db359b5c..502dfa32044a3 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php @@ -17,7 +17,7 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/select.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/select.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php index 5911bf5c393b5..0fe8c38cc4992 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab; /** diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php index 15808c9dd170d..0e21e566d5e75 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php @@ -142,7 +142,6 @@ public function getExtendedElement($switchAttributeCode) [ 'name' => "product[{$switchAttributeCode}]", 'values' => $this->getOptions(), - 'value' => $switchAttributeCode, 'class' => 'required-entry next-toinput', 'no_span' => true, 'disabled' => $this->isDisabledField(), diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php index f124740a766ab..8be512a3e6348 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php @@ -20,7 +20,7 @@ class Bundle extends \Magento\Backend\Block\Widget implements \Magento\Backend\B /** * @var string */ - protected $_template = 'product/edit/bundle.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle.phtml'; /** * Core registry diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php index 13c5dcc81afb3..19da6bc6244e5 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php @@ -26,7 +26,7 @@ class Option extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option.phtml'; /** * Core registry diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php index 5b73c22b5781a..cf4814d3cd778 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php @@ -15,7 +15,7 @@ class Search extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option/search.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option/search.phtml'; /** * @return void diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php index 353808dc66a72..cf88f9b93d32f 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php @@ -15,7 +15,7 @@ class Selection extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option/selection.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option/selection.phtml'; /** * Catalog data diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index 4cb087df0e1a6..23fc2026ab111 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -95,9 +95,8 @@ public function getChildren($item) if (isset($itemsArray[$item->getOrderItem()->getId()])) { return $itemsArray[$item->getOrderItem()->getId()]; - } else { - return null; } + return null; } /** @@ -219,9 +218,8 @@ public function getOrderItem() { if ($this->getItem() instanceof \Magento\Sales\Model\Order\Item) { return $this->getItem(); - } else { - return $this->getItem()->getOrderItem(); } + return $this->getItem()->getOrderItem(); } /** diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php index 6cb103fc86789..fc8706ce54d06 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -7,6 +7,7 @@ use Magento\Bundle\Model\Option; use Magento\Catalog\Model\Product; +use Magento\Framework\DataObject; /** * Catalog bundle product info block @@ -56,6 +57,11 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView */ private $catalogRuleProcessor; + /** + * @var array + */ + private $optionsPosition = []; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils @@ -161,7 +167,7 @@ public function getJsonConfig() $defaultValues = []; $preConfiguredFlag = $currentProduct->hasPreconfiguredValues(); - /** @var \Magento\Framework\DataObject|null $preConfiguredValues */ + /** @var DataObject|null $preConfiguredValues */ $preConfiguredValues = $preConfiguredFlag ? $currentProduct->getPreconfiguredValues() : null; $position = 0; @@ -172,6 +178,7 @@ public function getJsonConfig() } $optionId = $optionItem->getId(); $options[$optionId] = $this->getOptionItemData($optionItem, $currentProduct, $position); + $this->optionsPosition[$position] = $optionId; // Add attribute default value (if set) if ($preConfiguredFlag) { @@ -179,12 +186,13 @@ public function getJsonConfig() if ($configValue) { $defaultValues[$optionId] = $configValue; } + $options = $this->processOptions($optionId, $options, $preConfiguredValues); } $position++; } $config = $this->getConfigData($currentProduct, $options); - $configObj = new \Magento\Framework\DataObject( + $configObj = new DataObject( [ 'config' => $config, ] @@ -228,18 +236,23 @@ private function getSelectionItemData(Product $product, Product $selection) $qty = ($selection->getSelectionQty() * 1) ?: '1'; $optionPriceAmount = $product->getPriceInfo() - ->getPrice('bundle_option') + ->getPrice(\Magento\Bundle\Pricing\Price\BundleOptionPrice::PRICE_CODE) ->getOptionSelectionAmount($selection); $finalPrice = $optionPriceAmount->getValue(); $basePrice = $optionPriceAmount->getBaseAmount(); + $oldPrice = $product->getPriceInfo() + ->getPrice(\Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::PRICE_CODE) + ->getOptionSelectionAmount($selection) + ->getValue(); + $selection = [ 'qty' => $qty, 'customQty' => $selection->getSelectionCanChangeQty(), 'optionId' => $selection->getId(), 'prices' => [ 'oldPrice' => [ - 'amount' => $basePrice + 'amount' => $oldPrice ], 'basePrice' => [ 'amount' => $basePrice @@ -363,6 +376,7 @@ private function getConfigData(Product $product, array $options) $config = [ 'options' => $options, 'selected' => $this->selectedOptions, + 'positions' => $this->optionsPosition, 'bundleId' => $product->getId(), 'priceFormat' => $this->localeFormat->getPriceFormat(), 'prices' => [ @@ -381,4 +395,30 @@ private function getConfigData(Product $product, array $options) ]; return $config; } + + /** + * Set preconfigured quantities and selections to options. + * + * @param string $optionId + * @param array $options + * @param DataObject $preConfiguredValues + * @return array + */ + private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues) + { + $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? []; + $selections = $options[$optionId]['selections']; + array_walk($selections, function (&$selection, $selectionId) use ($preConfiguredQtys) { + if (is_array($preConfiguredQtys) && isset($preConfiguredQtys[$selectionId])) { + $selection['qty'] = $preConfiguredQtys[$selectionId]; + } else { + if ((int)$preConfiguredQtys > 0) { + $selection['qty'] = $preConfiguredQtys; + } + } + }); + $options[$optionId]['selections'] = $selections; + + return $options; + } } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php index dd62676612250..0b346e08fcd9f 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Block\Catalog\Product\View\Type\Bundle; /** @@ -93,7 +91,7 @@ public function __construct( */ public function showSingle() { - if (is_null($this->_showSingle)) { + if ($this->_showSingle === null) { $option = $this->getOption(); $selections = $option->getSelections(); @@ -169,7 +167,9 @@ protected function _getSelectedOptions() */ protected function assignSelection(\Magento\Bundle\Model\Option $option, $selectionId) { - if ($selectionId && $option->getSelectionById($selectionId)) { + if (is_array($selectionId)) { + $this->_selectedOptions = $selectionId; + } else if ($selectionId && $option->getSelectionById($selectionId)) { $this->_selectedOptions = $selectionId; } elseif (!$option->getRequired()) { $this->_selectedOptions = 'None'; @@ -191,9 +191,8 @@ public function isSelected($selection) return in_array($selection->getSelectionId(), $selectedOptions); } elseif ($selectedOptions == 'None') { return false; - } else { - return $selection->getIsDefault() && $selection->isSaleable(); } + return $selection->getIsDefault() && $selection->isSaleable(); } /** @@ -238,7 +237,10 @@ public function getProduct() public function getSelectionQtyTitlePrice($selection, $includeContainer = true) { $this->setFormatProduct($selection); - $priceTitle = '' . $selection->getSelectionQty() * 1 . ' x ' . $this->escapeHtml($selection->getName()) . ''; + $priceTitle = '' + . $selection->getSelectionQty() * 1 + . ' x ' . $this->escapeHtml($selection->getName()) + . ''; $priceTitle .= '   ' . ($includeContainer ? '' : '') . '+' . $this->renderPriceString($selection, $includeContainer) . ($includeContainer ? '' : ''); diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php index 8ca0cf8a5159e..83730d4eae2bd 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php @@ -16,5 +16,5 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/checkbox.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/checkbox.phtml'; } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php index 3319db8cff1d5..79e94a18a789e 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php @@ -16,7 +16,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/multi.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/multi.phtml'; /** * @inheritdoc diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php index 84a619dafab52..07c113bd8e4bb 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php @@ -16,5 +16,5 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/radio.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/radio.phtml'; } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php index d7f1cf41057a8..63f0d35bda0f0 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php @@ -16,5 +16,5 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/select.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/select.phtml'; } diff --git a/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php new file mode 100644 index 0000000000000..058b3a981b52f --- /dev/null +++ b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php @@ -0,0 +1,63 @@ +layout = $layout; + } + + /** + * Format tier price string + * + * @param Product $selection + * @param array $arguments + * @return string + */ + public function renderTierPrice(Product $selection, array $arguments = []): string + { + if (!array_key_exists('zone', $arguments)) { + $arguments['zone'] = Render::ZONE_ITEM_OPTION; + } + + $priceHtml = ''; + + /** @var Render $priceRender */ + $priceRender = $this->layout->getBlock('product.price.render.default'); + if ($priceRender !== false) { + $priceHtml = $priceRender->render( + TierPrice::PRICE_CODE, + $selection, + $arguments + ); + } + + return $priceHtml; + } +} diff --git a/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php index a29c93fc4e139..003ddba86ad75 100644 --- a/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Sales/Order/Items/Renderer.php @@ -142,9 +142,8 @@ public function getValueHtml($item) if ($attributes = $this->getSelectionAttributes($item)) { return sprintf('%d', $attributes['qty']) . ' x ' . $this->escapeHtml($item->getName()) . " " . $this->getOrder()->formatPrice($attributes['price']); - } else { - return $this->escapeHtml($item->getName()); } + return $this->escapeHtml($item->getName()); } /** @@ -179,9 +178,8 @@ public function getChildren($item) if (isset($itemsArray[$item->getOrderItem()->getId()])) { return $itemsArray[$item->getOrderItem()->getId()]; - } else { - return null; } + return null; } /** diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php index 6688648a3c4fd..3c9eac68eb9e4 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Bundle.php @@ -105,8 +105,13 @@ public function afterInitialize( if ($result['bundle_options'] && !$compositeReadonly) { $product->setBundleOptionsData($result['bundle_options']); } + $this->processBundleOptionsData($product); $this->processDynamicOptionsData($product); + } elseif (!$compositeReadonly) { + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions([]); + $product->setExtensionAttributes($extension); } $affectProductSelections = (bool)$this->request->getPost('affect_bundle_product_selections'); @@ -127,7 +132,7 @@ protected function processBundleOptionsData(\Magento\Catalog\Model\Product $prod } $options = []; foreach ($bundleOptionsData as $key => $optionData) { - if ((bool)$optionData['delete']) { + if (!empty($optionData['delete'])) { continue; } diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php index 9940344b5b61c..39fcbdc62102f 100644 --- a/app/code/Magento/Bundle/Model/OptionRepository.php +++ b/app/code/Magento/Bundle/Model/OptionRepository.php @@ -277,10 +277,11 @@ protected function updateOptionSelection( * @param string $sku * @return \Magento\Catalog\Api\Data\ProductInterface * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getProduct($sku) { - $product = $this->productRepository->get($sku, true); + $product = $this->productRepository->get($sku, true, null, true); if ($product->getTypeId() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) { throw new InputException(__('Only implemented for bundle product')); } diff --git a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php index f3c0548f76e5d..1914d5b5146c3 100644 --- a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php +++ b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php @@ -29,8 +29,7 @@ public function aroundValidate( && $object->getPriceType() == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC ) { return true; - } else { - return $proceed($object); } + return $proceed($object); } } diff --git a/app/code/Magento/Bundle/Model/Plugin/UpdatePriceInQuoteItemOptions.php b/app/code/Magento/Bundle/Model/Plugin/UpdatePriceInQuoteItemOptions.php new file mode 100644 index 0000000000000..ab56874786500 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Plugin/UpdatePriceInQuoteItemOptions.php @@ -0,0 +1,55 @@ +serializer = $serializer; + } + + /** + * Update price on quote item options level + * + * @param OrigQuoteItem $subject + * @param AbstractItem $result + * @return AbstractItem + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterCalcRowTotal(OrigQuoteItem $subject, AbstractItem $result): AbstractItem + { + $bundleAttributes = $result->getProduct()->getCustomOption('bundle_selection_attributes'); + if ($bundleAttributes !== null) { + $actualAmount = $result->getPrice() * $result->getQty(); + $parsedValue = $this->serializer->unserialize($bundleAttributes->getValue()); + if (is_array($parsedValue) && array_key_exists('price', $parsedValue)) { + $parsedValue['price'] = $actualAmount; + } + $bundleAttributes->setValue($this->serializer->serialize($parsedValue)); + } + + return $result; + } +} diff --git a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php index 61559df4d2cf6..20e4828835d06 100644 --- a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php +++ b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php @@ -27,7 +27,17 @@ public function build(Product $product, Product $duplicate) $bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: []; $duplicatedBundleOptions = []; foreach ($bundleOptions as $key => $bundleOption) { - $duplicatedBundleOptions[$key] = clone $bundleOption; + $duplicatedBundleOption = clone $bundleOption; + /** + * Set option and selection ids to 'null' in order to create new option(selection) for duplicated product, + * but not modifying existing one, which led to lost of option(selection) in original product. + */ + $productLinks = $duplicatedBundleOption->getProductLinks() ?: []; + foreach ($productLinks as $productLink) { + $productLink->setSelectionId(null); + } + $duplicatedBundleOption->setOptionId(null); + $duplicatedBundleOptions[$key] = $duplicatedBundleOption; } $duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions); } diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index 00b6b2d7a3f5a..c9ed97fada966 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -11,8 +11,11 @@ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; /** + * Bundle product type price model + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price extends \Magento\Catalog\Model\Product\Type\Price @@ -180,9 +183,9 @@ protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product /** * Get product final price * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float */ public function getFinalPrice($qty, $product) { @@ -207,9 +210,9 @@ public function getFinalPrice($qty, $product) * Returns final price of a child product * * @param \Magento\Catalog\Model\Product $product - * @param float $productQty + * @param float $productQty * @param \Magento\Catalog\Model\Product $childProduct - * @param float $childProductQty + * @param float $childProductQty * @return float */ public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) @@ -220,10 +223,10 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP /** * Retrieve Price considering tier price * - * @param \Magento\Catalog\Model\Product $product - * @param string|null $which - * @param bool|null $includeTax - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $product + * @param string|null $which + * @param bool|null $includeTax + * @param bool $takeTierPrice * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -402,8 +405,8 @@ public function getOptions($product) * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float|null $selectionQty - * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity + * @param float|null $selectionQty + * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity * @return float * * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice() @@ -418,7 +421,7 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $qty + * @param float $qty * @return float */ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) @@ -427,15 +430,14 @@ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qt } /** - * Calculate final price of selection - * with take into account tier price + * Calculate final price of selection with take into account tier price * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $bundleQty - * @param float $selectionQty - * @param bool $multiplyQty - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $bundleProduct + * @param \Magento\Catalog\Model\Product $selectionProduct + * @param float $bundleQty + * @param float $selectionQty + * @param bool $multiplyQty + * @param bool $takeTierPrice * @return float */ public function getSelectionFinalTotalPrice( @@ -454,7 +456,11 @@ public function getSelectionFinalTotalPrice( } if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) { - $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1); + $totalQty = $bundleQty * $selectionQty; + if (!$takeTierPrice || $totalQty === 0) { + $totalQty = 1; + } + $price = $selectionProduct->getFinalPrice($totalQty); } else { if ($selectionProduct->getSelectionPriceType()) { // percent @@ -485,10 +491,10 @@ public function getSelectionFinalTotalPrice( /** * Apply tier price for bundle * - * @param \Magento\Catalog\Model\Product $product - * @param float $qty - * @param float $finalPrice - * @return float + * @param \Magento\Catalog\Model\Product $product + * @param float $qty + * @param float $finalPrice + * @return float */ protected function _applyTierPrice($product, $qty, $finalPrice) { @@ -509,9 +515,9 @@ protected function _applyTierPrice($product, $qty, $finalPrice) /** * Get product tier price by qty * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float|array + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -605,11 +611,11 @@ public function getTierPrice($qty, $product) /** * Calculate and apply special price * - * @param float $finalPrice - * @param float $specialPrice + * @param float $finalPrice + * @param float $specialPrice * @param string $specialPriceFrom * @param string $specialPriceTo - * @param mixed $store + * @param mixed $store * @return float */ public function calculateSpecialPrice( @@ -634,7 +640,7 @@ public function calculateSpecialPrice( * * @param /Magento/Catalog/Model/Product $bundleProduct * @param float|string $price - * @param int $bundleQty + * @param int $bundleQty * @return float */ public function getLowestPrice($bundleProduct, $price, $bundleQty = 1) diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index de11df62cbb05..dbd07f188f90b 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -5,9 +5,9 @@ */ namespace Magento\Bundle\Model\Product; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\ExtensionInterface; @@ -50,51 +50,50 @@ public function __construct( } /** + * Perform action on Bundle product relation/extension attribute. + * * @param object $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @return ProductInterface|object * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entity, $arguments = []) { /** @var \Magento\Bundle\Api\Data\OptionInterface[] $options */ - $options = $entity->getExtensionAttributes()->getBundleProductOptions() ?: []; + $bundleProductOptions = $entity->getExtensionAttributes()->getBundleProductOptions() ?: []; - if ($entity->getTypeId() !== 'bundle' || empty($options)) { + if ($entity->getTypeId() !== Type::TYPE_CODE || empty($bundleProductOptions)) { return $entity; } - if (!$entity->getCopyFromView()) { - $updatedOptions = []; - $oldOptions = $this->optionRepository->getList($entity->getSku()); - - $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $existingBundleProductOptions = $this->optionRepository->getList($entity->getSku()); - $productId = $entity->getData($metadata->getLinkField()); + $existingOptionsIds = !empty($existingBundleProductOptions) + ? $this->getOptionIds($existingBundleProductOptions) + : []; + $optionIds = !empty($bundleProductOptions) + ? $this->getOptionIds($bundleProductOptions) + : []; - foreach ($options as $option) { - $updatedOptions[$option->getOptionId()][$productId] = (bool)$option->getOptionId(); - } + $options = $bundleProductOptions ?: []; - foreach ($oldOptions as $option) { - if (!isset($updatedOptions[$option->getOptionId()][$productId])) { - $option->setParentId($productId); - $this->removeOptionLinks($entity->getSku(), $option); - $this->optionRepository->delete($option); - } - } - } - - foreach ($options as $option) { - $this->optionRepository->save($entity, $option); + if (!$entity->getCopyFromView()) { + $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); + + $newOptionsIds = array_diff($optionIds, $existingOptionsIds); + $this->saveOptions($entity, $options, $newOptionsIds); + } else { + //save only labels and not selections + product links + $this->saveOptions($entity, $options); + $entity->setCopyFromView(false); } - $entity->setCopyFromView(false); - return $entity; } /** + * Remove option product links. + * * @param string $entitySku * @param \Magento\Bundle\Api\Data\OptionInterface $option * @return void @@ -108,4 +107,67 @@ protected function removeOptionLinks($entitySku, $option) } } } + + /** + * Perform save for all options entities + * + * @param object $entity + * @param array $options + * @param array $newOptionsIds + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @return void + */ + private function saveOptions($entity, array $options, array $newOptionsIds = []) + { + foreach ($options as $option) { + if (in_array($option->getOptionId(), $newOptionsIds, true)) { + $option->setOptionId(null); + } + $this->optionRepository->save($entity, $option); + } + } + + /** + * Get options ids from array of the options entities + * + * @param array $options + * @return array + */ + private function getOptionIds(array $options) + { + $optionIds = []; + + if (empty($options)) { + return $optionIds; + } + + /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ + foreach ($options as $option) { + if ($option->getOptionId()) { + $optionIds[] = $option->getOptionId(); + } + } + return $optionIds; + } + + /** + * Removes old options that no longer exists. + * + * @param ProductInterface $entity + * @param array $existingOptionsIds + * @param array $optionIds + * @return void + */ + private function processRemovedOptions(ProductInterface $entity, array $existingOptionsIds, array $optionIds) + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $parentId = $entity->getData($metadata->getLinkField()); + foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) { + $option = $this->optionRepository->get($entity->getSku(), $optionId); + $option->setParentId($parentId); + $this->removeOptionLinks($entity->getSku(), $option); + $this->optionRepository->delete($option); + } + } } diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index bd261d287273e..2490c82d84469 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Model\Product; -use Magento\Framework\App\ObjectManager; +use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; +use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; /** * Bundle Type Model @@ -309,8 +308,11 @@ public function getSku($product) $selectionIds = $this->serializer->unserialize($customOption->getValue()); if (!empty($selectionIds)) { $selections = $this->getSelectionsByIds($selectionIds, $product); - foreach ($selections->getItems() as $selection) { - $skuParts[] = $selection->getSku(); + foreach ($selectionIds as $selectionId) { + $entity = $selections->getItemByColumnValue('selection_id', $selectionId); + if (isset($entity) && $entity->getEntityId()) { + $skuParts[] = $entity->getSku(); + } } } } @@ -486,7 +488,9 @@ public function getSelectionsCollection($optionIds, $product) \Magento\Catalog\Api\Data\ProductInterface::class ); - $selectionsCollection = $this->_bundleCollection->create() + /** @var Selections $selectionsCollection */ + $selectionsCollection = $this->_bundleCollection->create(); + $selectionsCollection ->addAttributeToSelect($this->_config->getProductAttributes()) ->addAttributeToSelect('tax_class_id') //used for calculation item taxes in Bundle with Dynamic Price ->setFlag('product_children', true) @@ -533,12 +537,12 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option, foreach ($selections as $selection) { if ($selection->getProductId() == $optionProduct->getId()) { - foreach ($options as &$option) { - if ($option->getCode() == 'selection_qty_' . $selection->getSelectionId()) { + foreach ($options as $quoteItemOption) { + if ($quoteItemOption->getCode() == 'selection_qty_' . $selection->getSelectionId()) { if ($optionUpdateFlag) { - $option->setValue(intval($option->getValue())); + $quoteItemOption->setValue((int)$quoteItemOption->getValue()); } else { - $option->setValue($value); + $quoteItemOption->setValue($value); } } } @@ -558,7 +562,7 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option, */ public function prepareQuoteItemQty($qty, $product) { - return intval($qty); + return (int)$qty; } /** @@ -587,6 +591,7 @@ public function isSalable($product) foreach ($this->getOptionsCollection($product) as $option) { $hasSalable = false; + /** @var Selections $selectionsCollection */ $selectionsCollection = $this->_bundleCollection->create(); $selectionsCollection->addAttributeToSelect('status'); $selectionsCollection->addQuantityFilter(); @@ -708,7 +713,7 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $selections = $this->mergeSelectionsWithOptions($options, $selections); } - if (count($selections) > 0 || !$isStrictProcessMode) { + if ((is_array($selections) && count($selections) > 0) || !$isStrictProcessMode) { $uniqueKey = [$product->getId()]; $selectionIds = []; $qtys = $buyRequest->getBundleOptionQty(); @@ -734,7 +739,7 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $price = $product->getPriceModel() ->getSelectionFinalTotalPrice($product, $selection, 0, $qty); $attributes = [ - 'price' => $this->priceCurrency->convert($price), + 'price' => $price, 'qty' => $qty, 'option_label' => $selection->getOption() ->getTitle(), @@ -814,11 +819,11 @@ private function recursiveIntval(array $array) private function multiToFlatArray(array $array) { $flatArray = []; - foreach ($array as $key => $value) { + foreach ($array as $value) { if (is_array($value)) { $flatArray = array_merge($flatArray, $this->multiToFlatArray($value)); } else { - $flatArray[$key] = $value; + $flatArray[] = $value; } } @@ -855,8 +860,9 @@ public function getSelectionsByIds($selectionIds, $product) if (!$usedSelections || $usedSelectionsIds !== $selectionIds) { $storeId = $product->getStoreId(); - $usedSelections = $this->_bundleCollection - ->create() + /** @var Selections $usedSelections */ + $usedSelections = $this->_bundleCollection->create(); + $usedSelections ->addAttributeToSelect('*') ->setFlag('product_children', true) ->addStoreFilter($this->getStoreFilter($product)) @@ -897,8 +903,7 @@ public function getOptionsByIds($optionIds, $product) $usedOptions = $product->getData($this->_keyUsedOptions); $usedOptionsIds = $product->getData($this->_keyUsedOptionsIds); - if ( - !$usedOptions + if (!$usedOptions || $this->serializer->serialize($usedOptionsIds) != $this->serializer->serialize($optionIds) ) { $usedOptions = $this->_bundleOption @@ -1008,11 +1013,8 @@ public function shakeSelections($firstItem, $secondItem) $secondItem->getPosition(), $secondItem->getSelectionId(), ]; - if ($aPosition == $bPosition) { - return 0; - } else { - return $aPosition < $bPosition ? -1 : 1; - } + + return $aPosition <=> $bPosition; } /** @@ -1261,7 +1263,9 @@ protected function checkIsAllRequiredOptions($product, $isStrictProcessMode, $op if (!$product->getSkipCheckRequiredOption() && $isStrictProcessMode) { foreach ($optionsCollection->getItems() as $option) { if ($option->getRequired() && !isset($options[$option->getId()])) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please select all required options.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Please select all required options.') + ); } } } @@ -1323,8 +1327,9 @@ protected function checkIsResult($_result) protected function mergeSelectionsWithOptions($options, $selections) { foreach ($options as $option) { - if ($option->getRequired() && count($option->getSelections()) == 1) { - $selections = array_merge($selections, $option->getSelections()); + $optionSelections = $option->getSelections(); + if ($option->getRequired() && is_array($optionSelections) && count($optionSelections) == 1) { + $selections = array_merge($selections, $optionSelections); } else { $selections = []; break; diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 401374db86fef..e42ab1c672604 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -6,20 +6,170 @@ namespace Magento\Bundle\Model\ResourceModel\Indexer; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; +use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\JoinAttributeProcessor; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Catalog\Model\Product\Attribute\Source\Status; /** * Bundle products Price indexer resource model * - * @author Magento Core Team + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Price extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice +class Price implements DimensionalIndexerInterface { /** - * @inheritdoc + * @var IndexTableStructureFactory */ - protected function reindex($entityIds = null) + private $indexTableStructureFactory; + + /** + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var bool + */ + private $fullReindexAction; + + /** + * @var string + */ + private $connectionName; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * Mapping between dimensions and field in database + * + * @var array + */ + private $dimensionToFieldMapper = [ + WebsiteDimensionProvider::DIMENSION_NAME => 'pw.website_id', + CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id', + ]; + + /** + * @var BasePriceModifier + */ + private $basePriceModifier; + + /** + * @var JoinAttributeProcessor + */ + private $joinAttributeProcessor; + + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + + /** + * @var \Magento\Framework\Module\Manager + */ + private $moduleManager; + + /** + * @param IndexTableStructureFactory $indexTableStructureFactory + * @param TableMaintainer $tableMaintainer + * @param MetadataPool $metadataPool + * @param \Magento\Framework\App\ResourceConnection $resource + * @param BasePriceModifier $basePriceModifier + * @param JoinAttributeProcessor $joinAttributeProcessor + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Module\Manager $moduleManager + * @param bool $fullReindexAction + * @param string $connectionName + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + IndexTableStructureFactory $indexTableStructureFactory, + TableMaintainer $tableMaintainer, + MetadataPool $metadataPool, + \Magento\Framework\App\ResourceConnection $resource, + BasePriceModifier $basePriceModifier, + JoinAttributeProcessor $joinAttributeProcessor, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Module\Manager $moduleManager, + $fullReindexAction = false, + $connectionName = 'indexer' + ) { + $this->indexTableStructureFactory = $indexTableStructureFactory; + $this->tableMaintainer = $tableMaintainer; + $this->connectionName = $connectionName; + $this->metadataPool = $metadataPool; + $this->resource = $resource; + $this->fullReindexAction = $fullReindexAction; + $this->basePriceModifier = $basePriceModifier; + $this->joinAttributeProcessor = $joinAttributeProcessor; + $this->eventManager = $eventManager; + $this->moduleManager = $moduleManager; + } + + /** + * {@inheritdoc} + * + * @throws \Exception + */ + public function executeByDimensions(array $dimensions, \Traversable $entityIds) { - $this->_prepareBundlePrice($entityIds); + $this->tableMaintainer->createMainTmpTable($dimensions); + + $temporaryPriceTable = $this->indexTableStructureFactory->create([ + 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'price', + 'finalPriceField' => 'final_price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', + ]); + + $entityIds = iterator_to_array($entityIds); + + $this->prepareTierPriceIndex($dimensions, $entityIds); + + $this->prepareBundlePriceTable(); + + $this->prepareBundlePriceByType( + \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, + $dimensions, + $entityIds + ); + + $this->prepareBundlePriceByType( + \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, + $dimensions, + $entityIds + ); + + $this->calculateBundleOptionPrice($temporaryPriceTable, $dimensions); + + $this->basePriceModifier->modifyPrice($temporaryPriceTable, $entityIds); } /** @@ -27,9 +177,9 @@ protected function reindex($entityIds = null) * * @return string */ - protected function _getBundlePriceTable() + private function getBundlePriceTable() { - return $this->tableStrategy->getTableName('catalog_product_index_price_bundle'); + return $this->getTable('catalog_product_index_price_bundle_tmp'); } /** @@ -37,9 +187,9 @@ protected function _getBundlePriceTable() * * @return string */ - protected function _getBundleSelectionTable() + private function getBundleSelectionTable() { - return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_sel'); + return $this->getTable('catalog_product_index_price_bundle_sel_tmp'); } /** @@ -47,9 +197,9 @@ protected function _getBundleSelectionTable() * * @return string */ - protected function _getBundleOptionTable() + private function getBundleOptionTable() { - return $this->tableStrategy->getTableName('catalog_product_index_price_bundle_opt'); + return $this->getTable('catalog_product_index_price_bundle_opt_tmp'); } /** @@ -57,9 +207,9 @@ protected function _getBundleOptionTable() * * @return $this */ - protected function _prepareBundlePriceTable() + private function prepareBundlePriceTable() { - $this->getConnection()->delete($this->_getBundlePriceTable()); + $this->getConnection()->delete($this->getBundlePriceTable()); return $this; } @@ -68,9 +218,9 @@ protected function _prepareBundlePriceTable() * * @return $this */ - protected function _prepareBundleSelectionTable() + private function prepareBundleSelectionTable() { - $this->getConnection()->delete($this->_getBundleSelectionTable()); + $this->getConnection()->delete($this->getBundleSelectionTable()); return $this; } @@ -79,61 +229,68 @@ protected function _prepareBundleSelectionTable() * * @return $this */ - protected function _prepareBundleOptionTable() + private function prepareBundleOptionTable() { - $this->getConnection()->delete($this->_getBundleOptionTable()); + $this->getConnection()->delete($this->getBundleOptionTable()); return $this; } /** * Prepare temporary price index data for bundle products by price type * + * @param array $dimensions * @param int $priceType * @param int|array $entityIds the entity ids limitation - * @return $this + * @return void + * @throws \Exception * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function _prepareBundlePriceByType($priceType, $entityIds = null) + private function prepareBundlePriceByType($priceType, array $dimensions, $entityIds = null) { $connection = $this->getConnection(); - $table = $this->_getBundlePriceTable(); - $select = $connection->select()->from( ['e' => $this->getTable('catalog_product_entity')], ['entity_id'] - )->join( + )->joinInner( ['cg' => $this->getTable('customer_group')], - '', + array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions) + ? sprintf( + '%s = %s', + $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME], + $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue() + ) : '', ['customer_group_id'] - ); - $this->_addWebsiteJoinToSelect($select, true); - $this->_addProductWebsiteJoinToSelect($select, 'cw.website_id', "e.entity_id"); - $select->columns( - 'website_id', - 'cw' - )->join( - ['cwd' => $this->_getWebsiteDateTable()], - 'cw.website_id = cwd.website_id', + )->joinInner( + ['pw' => $this->getTable('catalog_product_website')], + 'pw.product_id = e.entity_id', + ['pw.website_id'] + )->joinInner( + ['cwd' => $this->getTable('catalog_product_index_website')], + 'pw.website_id = cwd.website_id', [] - )->joinLeft( - ['tp' => $this->_getTierPriceIndexTable()], - 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' . + ); + $select->joinLeft( + ['tp' => $this->getTable('catalog_product_index_tier_price')], + 'tp.entity_id = e.entity_id AND tp.website_id = pw.website_id' . ' AND tp.customer_group_id = cg.customer_group_id', [] )->where( 'e.type_id=?', - $this->getTypeId() + \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE ); - // add enable products limitation - $statusCond = $connection->quoteInto( - '=?', - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED - ); - $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); - $this->_addAttributeToSelect($select, 'status', "e.$linkField", 'cs.store_id', $statusCond, true); + foreach ($dimensions as $dimension) { + if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) { + throw new \LogicException( + 'Provided dimension is not valid for Price indexer: ' . $dimension->getName() + ); + } + $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); + } + + $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED); if ($this->moduleManager->isEnabled('Magento_Tax')) { - $taxClassId = $this->_addAttributeToSelect($select, 'tax_class_id', "e.$linkField", 'cs.store_id'); + $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id'); } else { $taxClassId = new \Zend_Db_Expr('0'); } @@ -146,59 +303,49 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) ); } - $priceTypeCond = $connection->quoteInto('=?', $priceType); - $this->_addAttributeToSelect($select, 'price_type', "e.$linkField", 'cs.store_id', $priceTypeCond); - - $price = $this->_addAttributeToSelect($select, 'price', "e.$linkField", 'cs.store_id'); - $specialPrice = $this->_addAttributeToSelect($select, 'special_price', "e.$linkField", 'cs.store_id'); - $specialFrom = $this->_addAttributeToSelect($select, 'special_from_date', "e.$linkField", 'cs.store_id'); - $specialTo = $this->_addAttributeToSelect($select, 'special_to_date', "e.$linkField", 'cs.store_id'); - $curentDate = new \Zend_Db_Expr('cwd.website_date'); - - $specialExpr = $connection->getCheckSql( - $connection->getCheckSql( - $specialFrom . ' IS NULL', - '1', - $connection->getCheckSql($specialFrom . ' <= ' . $curentDate, '1', '0') - ) . " > 0 AND " . $connection->getCheckSql( - $specialTo . ' IS NULL', - '1', - $connection->getCheckSql($specialTo . ' >= ' . $curentDate, '1', '0') - ) . " > 0 AND {$specialPrice} > 0 AND {$specialPrice} < 100 ", - $specialPrice, - '0' - ); + $this->joinAttributeProcessor->process($select, 'price_type', $priceType); + + $price = $this->joinAttributeProcessor->process($select, 'price'); + $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price'); + $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date'); + $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date'); + $currentDate = new \Zend_Db_Expr('cwd.website_date'); - $tierExpr = new \Zend_Db_Expr("tp.min_price"); + $specialFromDate = $connection->getDatePartSql($specialFrom); + $specialToDate = $connection->getDatePartSql($specialTo); + $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; + $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}"; + $specialExpr = "{$specialPrice} IS NOT NULL AND {$specialPrice} > 0 AND {$specialPrice} < 100" + . " AND {$specialFromExpr} AND {$specialToExpr}"; + $tierExpr = new \Zend_Db_Expr('tp.min_price'); if ($priceType == \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED) { - $finalPrice = $connection->getCheckSql( - $specialExpr . ' > 0', - 'ROUND(' . $price . ' * (' . $specialExpr . ' / 100), 4)', - $price + $specialPriceExpr = $connection->getCheckSql( + $specialExpr, + 'ROUND(' . $price . ' * (' . $specialPrice . ' / 100), 4)', + 'NULL' ); $tierPrice = $connection->getCheckSql( $tierExpr . ' IS NOT NULL', - 'ROUND(' . $price . ' - ' . '(' . $price . ' * (' . $tierExpr . ' / 100)), 4)', + 'ROUND((1 - ' . $tierExpr . ' / 100) * ' . $price . ', 4)', 'NULL' ); - - $finalPrice = $connection->getCheckSql( - "{$tierPrice} < {$finalPrice}", - $tierPrice, - $finalPrice - ); + $finalPrice = $connection->getLeastSql([ + $price, + $connection->getIfNullSql($specialPriceExpr, $price), + $connection->getIfNullSql($tierPrice, $price), + ]); } else { - $finalPrice = new \Zend_Db_Expr("0"); + $finalPrice = new \Zend_Db_Expr('0'); $tierPrice = $connection->getCheckSql($tierExpr . ' IS NOT NULL', '0', 'NULL'); } $select->columns( [ 'price_type' => new \Zend_Db_Expr($priceType), - 'special_price' => $specialExpr, + 'special_price' => $connection->getCheckSql($specialExpr, $specialPrice, '0'), 'tier_percent' => $tierExpr, - 'orig_price' => $connection->getCheckSql($price . ' IS NULL', '0', $price), + 'orig_price' => $connection->getIfNullSql($price, '0'), 'price' => $finalPrice, 'min_price' => $finalPrice, 'max_price' => $finalPrice, @@ -214,107 +361,75 @@ protected function _prepareBundlePriceByType($priceType, $entityIds = null) /** * Add additional external limitation */ - $this->_eventManager->dispatch( + $this->eventManager->dispatch( 'catalog_product_prepare_index_select', [ 'select' => $select, 'entity_field' => new \Zend_Db_Expr('e.entity_id'), - 'website_field' => new \Zend_Db_Expr('cw.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'website_field' => new \Zend_Db_Expr('pw.website_id'), + 'store_field' => new \Zend_Db_Expr('cwd.default_store_id') ] ); - $query = $select->insertFromSelect($table); + $query = $select->insertFromSelect($this->getBundlePriceTable()); $connection->query($query); - - return $this; } /** * Calculate fixed bundle product selections price * - * @return $this + * @param IndexTableStructure $priceTable + * @param array $dimensions + * + * @return void + * @throws \Exception */ - protected function _calculateBundleOptionPrice() + private function calculateBundleOptionPrice($priceTable, $dimensions) { $connection = $this->getConnection(); - $this->_prepareBundleSelectionTable(); - $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED); - $this->_calculateBundleSelectionPrice(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC); + $this->prepareBundleSelectionTable(); + $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED); + $this->calculateBundleSelectionPrice($dimensions, \Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC); - $this->_prepareBundleOptionTable(); + $this->prepareBundleOptionTable(); $select = $connection->select()->from( - ['i' => $this->_getBundleSelectionTable()], + $this->getBundleSelectionTable(), ['entity_id', 'customer_group_id', 'website_id', 'option_id'] )->group( - ['entity_id', 'customer_group_id', 'website_id', 'option_id', 'is_required', 'group_type'] - )->columns( - [ - 'min_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.price)', '0'), - 'alt_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.price)', '0'), - 'max_price' => $connection->getCheckSql('i.group_type = 1', 'SUM(i.price)', 'MAX(i.price)'), - 'tier_price' => $connection->getCheckSql('i.is_required = 1', 'MIN(i.tier_price)', '0'), - 'alt_tier_price' => $connection->getCheckSql('i.is_required = 0', 'MIN(i.tier_price)', '0'), - ] - ); - - $query = $select->insertFromSelect($this->_getBundleOptionTable()); - $connection->query($query); - - $this->_prepareDefaultFinalPriceTable(); - - $minPrice = new \Zend_Db_Expr( - $connection->getCheckSql('SUM(io.min_price) = 0', 'MIN(io.alt_price)', 'SUM(io.min_price)') . ' + i.price' - ); - $maxPrice = new \Zend_Db_Expr("SUM(io.max_price) + i.price"); - $tierPrice = $connection->getCheckSql( - 'MIN(i.tier_percent) IS NOT NULL', - $connection->getCheckSql( - 'SUM(io.tier_price) = 0', - 'SUM(io.alt_tier_price)', - 'SUM(io.tier_price)' - ) . ' + MIN(i.tier_price)', - 'NULL' + ['entity_id', 'customer_group_id', 'website_id', 'option_id'] ); - - $select = $connection->select()->from( - ['io' => $this->_getBundleOptionTable()], - ['entity_id', 'customer_group_id', 'website_id'] - )->join( - ['i' => $this->_getBundlePriceTable()], - 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . - ' AND i.website_id = io.website_id', - [] - )->group( - ['io.entity_id', 'io.customer_group_id', 'io.website_id', 'i.tax_class_id', 'i.orig_price', 'i.price'] - )->columns( + $minPrice = $connection->getCheckSql('is_required = 1', 'price', 'NULL'); + $tierPrice = $connection->getCheckSql('is_required = 1', 'tier_price', 'NULL'); + $select->columns( [ - 'i.tax_class_id', - 'orig_price' => 'i.orig_price', - 'price' => 'i.price', - 'min_price' => $minPrice, - 'max_price' => $maxPrice, - 'tier_price' => $tierPrice, - 'base_tier' => 'MIN(i.base_tier)', + 'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'), + 'alt_price' => new \Zend_Db_Expr('MIN(price)'), + 'max_price' => $connection->getCheckSql('group_type = 0', 'MAX(price)', 'SUM(price)'), + 'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'), + 'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'), ] ); - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable()); + $query = $select->insertFromSelect($this->getBundleOptionTable()); $connection->query($query); - return $this; + $this->getConnection()->delete($priceTable->getTableName()); + $this->applyBundlePrice($priceTable); + $this->applyBundleOptionPrice($priceTable); } /** * Calculate bundle product selections price by product type * + * @param array $dimensions * @param int $priceType - * @return $this + * @return void + * @throws \Exception * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function _calculateBundleSelectionPrice($priceType) + private function calculateBundleSelectionPrice($dimensions, $priceType) { $connection = $this->getConnection(); @@ -348,38 +463,39 @@ protected function _calculateBundleSelectionPrice($priceType) 'ROUND(i.base_tier - (i.base_tier * (' . $selectionPriceValue . ' / 100)),4)', $connection->getCheckSql( 'i.tier_percent > 0', - 'ROUND(' . - $selectionPriceValue . - ' - (' . - $selectionPriceValue . - ' * (i.tier_percent / 100)),4)', + 'ROUND((1 - i.tier_percent / 100) * ' . $selectionPriceValue . ',4)', $selectionPriceValue ) ) . ' * bs.selection_qty', 'NULL' ); - $priceExpr = new \Zend_Db_Expr( - $connection->getCheckSql("{$tierExpr} < {$priceExpr}", $tierExpr, $priceExpr) - ); + $priceExpr = $connection->getLeastSql([ + $priceExpr, + $connection->getIfNullSql($tierExpr, $priceExpr), + ]); } else { - $priceExpr = new \Zend_Db_Expr( - $connection->getCheckSql( - 'i.special_price > 0 AND i.special_price < 100', - 'ROUND(idx.min_price * (i.special_price / 100), 4)', - 'idx.min_price' - ) . ' * bs.selection_qty' + $price = 'idx.min_price * bs.selection_qty'; + $specialExpr = $connection->getCheckSql( + 'i.special_price > 0 AND i.special_price < 100', + 'ROUND(' . $price . ' * (i.special_price / 100), 4)', + $price ); $tierExpr = $connection->getCheckSql( - 'i.base_tier IS NOT NULL', - 'ROUND(idx.min_price * (i.base_tier / 100), 4)* bs.selection_qty', + 'i.tier_percent IS NOT NULL', + 'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)', 'NULL' ); + $priceExpr = $connection->getLeastSql([ + $specialExpr, + $connection->getIfNullSql($tierExpr, $price), + ]); } - $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); $select = $connection->select()->from( - ['i' => $this->_getBundlePriceTable()], + ['i' => $this->getBundlePriceTable()], ['entity_id', 'customer_group_id', 'website_id'] )->join( ['parent_product' => $this->getTable('catalog_product_entity')], @@ -398,7 +514,7 @@ protected function _calculateBundleSelectionPrice($priceType) 'bs.selection_id = bsp.selection_id AND bsp.website_id = i.website_id', [''] )->join( - ['idx' => $this->getIdxTable()], + ['idx' => $this->getMainTable($dimensions)], 'bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id' . ' AND i.website_id = idx.website_id', [] @@ -418,49 +534,26 @@ protected function _calculateBundleSelectionPrice($priceType) ] ); - $query = $select->insertFromSelect($this->_getBundleSelectionTable()); + $query = $select->insertFromSelect($this->getBundleSelectionTable()); $connection->query($query); - - return $this; - } - - /** - * Prepare temporary index price for bundle products - * - * @param int|array $entityIds the entity ids limitation - * @return $this - */ - protected function _prepareBundlePrice($entityIds = null) - { - if (!$this->hasEntity() && empty($entityIds)) { - return $this; - } - $this->_prepareTierPriceIndex($entityIds); - $this->_prepareBundlePriceTable(); - $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_FIXED, $entityIds); - $this->_prepareBundlePriceByType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC, $entityIds); - - $this->_calculateBundleOptionPrice(); - $this->_applyCustomOption(); - - $this->_movePriceDataToIndexTable(); - - return $this; } /** * Prepare percentage tier price for bundle products * - * @param int|array $entityIds - * @return $this + * @param array $dimensions + * @param array $entityIds + * @return void + * @throws \Exception */ - protected function _prepareTierPriceIndex($entityIds = null) + private function prepareTierPriceIndex($dimensions, $entityIds) { $connection = $this->getConnection(); - $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); // remove index by bundle products $select = $connection->select()->from( - ['i' => $this->_getTierPriceIndexTable()], + ['i' => $this->getTable('catalog_product_index_tier_price')], null )->join( ['e' => $this->getTable('catalog_product_entity')], @@ -468,7 +561,7 @@ protected function _prepareTierPriceIndex($entityIds = null) [] )->where( 'e.type_id=?', - $this->getTypeId() + \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE ); $query = $select->deleteFromSelect('i'); $connection->query($query); @@ -485,27 +578,140 @@ protected function _prepareTierPriceIndex($entityIds = null) 'tp.all_groups = 1 OR (tp.all_groups = 0 AND tp.customer_group_id = cg.customer_group_id)', ['customer_group_id'] )->join( - ['cw' => $this->getTable('store_website')], - 'tp.website_id = 0 OR tp.website_id = cw.website_id', + ['pw' => $this->getTable('store_website')], + 'tp.website_id = 0 OR tp.website_id = pw.website_id', ['website_id'] )->where( - 'cw.website_id != 0' + 'pw.website_id != 0' )->where( 'e.type_id=?', - $this->getTypeId() + \Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice::PRODUCT_TYPE )->columns( new \Zend_Db_Expr('MIN(tp.value)') )->group( - ['e.entity_id', 'cg.customer_group_id', 'cw.website_id'] + ['e.entity_id', 'cg.customer_group_id', 'pw.website_id'] ); if (!empty($entityIds)) { $select->where('e.entity_id IN(?)', $entityIds); } + foreach ($dimensions as $dimension) { + if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) { + throw new \LogicException( + 'Provided dimension is not valid for Price indexer: ' . $dimension->getName() + ); + } + $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); + } - $query = $select->insertFromSelect($this->_getTierPriceIndexTable()); + $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price')); $connection->query($query); + } - return $this; + /** + * @param IndexTableStructure $priceTable + */ + private function applyBundlePrice($priceTable) + { + $select = $this->getConnection()->select(); + $select->from( + $this->getBundlePriceTable(), + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'tax_class_id', + 'orig_price', + 'price', + 'min_price', + 'max_price', + 'tier_price', + ] + ); + + $query = $select->insertFromSelect($priceTable->getTableName()); + $this->getConnection()->query($query); + } + + /** + * @param IndexTableStructure $priceTable + */ + private function applyBundleOptionPrice($priceTable) + { + $connection = $this->getConnection(); + + $subSelect = $connection->select()->from( + $this->getBundleOptionTable(), + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'min_price' => new \Zend_Db_Expr('SUM(min_price)'), + 'alt_price' => new \Zend_Db_Expr('MIN(alt_price)'), + 'max_price' => new \Zend_Db_Expr('SUM(max_price)'), + 'tier_price' => new \Zend_Db_Expr('SUM(tier_price)'), + 'alt_tier_price' => new \Zend_Db_Expr('MIN(alt_tier_price)'), + ] + )->group( + ['entity_id', 'customer_group_id', 'website_id'] + ); + + $minPrice = 'i.min_price + ' . $connection->getIfNullSql('io.min_price', '0'); + $tierPrice = 'i.tier_price + ' . $connection->getIfNullSql('io.tier_price', '0'); + $select = $connection->select()->join( + ['io' => $subSelect], + 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . + ' AND i.website_id = io.website_id', + [] + )->columns( + [ + 'min_price' => $connection->getCheckSql("{$minPrice} = 0", 'io.alt_price', $minPrice), + 'max_price' => new \Zend_Db_Expr('io.max_price + i.max_price'), + 'tier_price' => $connection->getCheckSql("{$tierPrice} = 0", 'io.alt_tier_price', $tierPrice), + ] + ); + + $query = $select->crossUpdateFromSelect(['i' => $priceTable->getTableName()]); + $connection->query($query); + } + + /** + * Get main table + * + * @param array $dimensions + * @return string + */ + private function getMainTable($dimensions) + { + if ($this->fullReindexAction) { + return $this->tableMaintainer->getMainReplicaTable($dimensions); + } + return $this->tableMaintainer->getMainTable($dimensions); + } + + /** + * Get connection + * + * return \Magento\Framework\DB\Adapter\AdapterInterface + * @throws \DomainException + */ + private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface + { + if ($this->connection === null) { + $this->connection = $this->resource->getConnection($this->connectionName); + } + + return $this->connection; + } + + /** + * Get table + * + * @param string $tableName + * @return string + */ + private function getTable($tableName) + { + return $this->resource->getTableName($tableName, $this->connectionName); } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option.php b/app/code/Magento/Bundle/Model/ResourceModel/Option.php index 2ad7e57f522d6..6ba281cba10b4 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option.php @@ -86,7 +86,6 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) 'store_id = ? OR store_id = 0' => $object->getStoreId(), 'parent_product_id = ?' => $object->getParentId() ]; - $connection = $this->getConnection(); $connection->delete($this->getTable('catalog_product_bundle_option_value'), $condition); @@ -99,12 +98,10 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); /** - * also saving default value if this store view scope + * also saving default fallback value */ - - if ($object->getStoreId()) { - $data->setStoreId(0); - $data->setTitle($object->getDefaultTitle()); + if (0 !== (int)$object->getStoreId()) { + $data->setStoreId(0)->setTitle($object->getDefaultTitle()); $connection->insert($this->getTable('catalog_product_bundle_option_value'), $data->getData()); } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php index e701b4cf9cc1d..5efbab94c9227 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/Collection.php @@ -5,6 +5,8 @@ */ namespace Magento\Bundle\Model\ResourceModel\Option; +use Magento\Catalog\Model\Product\Attribute\Source\Status; + /** * Bundle Options Resource Collection * @api @@ -138,12 +140,10 @@ public function setPositionOrder() /** * Append selection to options - * stripBefore - indicates to reload - * appendAll - indicates do we need to filter by saleable and required custom options * * @param \Magento\Bundle\Model\ResourceModel\Selection\Collection $selectionsCollection - * @param bool $stripBefore - * @param bool $appendAll + * @param bool $stripBefore indicates to reload + * @param bool $appendAll indicates do we need to filter by saleable and required custom options * @return \Magento\Framework\DataObject[] */ public function appendSelections($selectionsCollection, $stripBefore = false, $appendAll = true) @@ -156,7 +156,9 @@ public function appendSelections($selectionsCollection, $stripBefore = false, $a foreach ($selectionsCollection->getItems() as $key => $selection) { $option = $this->getItemById($selection->getOptionId()); if ($option) { - if ($appendAll || $selection->isSalable() && !$selection->getRequiredOptions()) { + if ($appendAll || + ((int) $selection->getStatus()) === Status::STATUS_ENABLED && !$selection->getRequiredOptions() + ) { $selection->setOption($option); $option->addSelection($selection); } else { diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 0216812199b50..5b88288ff72ca 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -5,10 +5,8 @@ */ namespace Magento\Bundle\Model\ResourceModel\Selection; -use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\DataObject; use Magento\Framework\DB\Select; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\Framework\App\ObjectManager; @@ -45,6 +43,95 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $websiteScopePriceJoined = false; + /** + * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item + */ + private $stockItem; + + /** + * Collection constructor. + * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Framework\App\ResourceConnection $resource + * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory + * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory + * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection + * @param ProductLimitationFactory|null $productLimitationFactory + * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|null $tableMaintainer + * @param \Magento\CatalogInventory\Model\ResourceModel\Stock\Item|null $stockItem + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\Data\Collection\EntityFactory $entityFactory, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\App\ResourceConnection $resource, + \Magento\Eav\Model\EntityFactory $eavEntityFactory, + \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, + \Magento\Framework\Validator\UniversalFactory $universalFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Module\Manager $moduleManager, + \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, + \Magento\Catalog\Model\ResourceModel\Url $catalogUrl, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Stdlib\DateTime $dateTime, + \Magento\Customer\Api\GroupManagementInterface $groupManagement, + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ProductLimitationFactory $productLimitationFactory = null, + \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer $tableMaintainer = null, + \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $stockItem = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $eavConfig, + $resource, + $eavEntityFactory, + $resourceHelper, + $universalFactory, + $storeManager, + $moduleManager, + $catalogProductFlatState, + $scopeConfig, + $productOptionFactory, + $catalogUrl, + $localeDate, + $customerSession, + $dateTime, + $groupManagement, + $connection, + $productLimitationFactory, + $metadataPool, + $tableMaintainer + ); + + $this->stockItem = $stockItem + ?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); + } + /** * Initialize collection * @@ -64,13 +151,7 @@ protected function _construct() */ public function _afterLoad() { - parent::_afterLoad(); - if ($this->getStoreId() && $this->_items) { - foreach ($this->_items as $item) { - $item->setStoreId($this->getStoreId()); - } - } - return $this; + return parent::_afterLoad(); } /** @@ -163,22 +244,38 @@ public function setPositionOrder() } /** - * Add filtering of product then havent enoght stock + * Add filtering of products that have 0 items left. * * @return $this * @since 100.2.0 */ public function addQuantityFilter() { + $manageStockExpr = $this->stockItem->getManageStockExpr('stock_item'); + $backordersExpr = $this->stockItem->getBackordersExpr('stock_item'); + $minQtyExpr = $this->getConnection()->getCheckSql( + 'selection.selection_can_change_qty', + $this->stockItem->getMinSaleQtyExpr('stock_item'), + 'selection.selection_qty' + ); + + $where = $manageStockExpr . ' = 0'; + $where .= ' OR (' + . 'stock_item.is_in_stock = ' . \Magento\CatalogInventory\Model\Stock::STOCK_IN_STOCK + . ' AND (' + . $backordersExpr . ' != ' . \Magento\CatalogInventory\Model\Stock::BACKORDERS_NO + . ' OR ' + . $minQtyExpr . ' <= stock_item.qty' + . ')' + . ')'; + $this->getSelect() ->joinInner( - ['stock' => $this->getTable('cataloginventory_stock_status')], - 'selection.product_id = stock.product_id', + ['stock_item' => $this->stockItem->getMainTable()], + 'selection.product_id = stock_item.product_id', [] - ) - ->where( - '(selection.selection_can_change_qty or selection.selection_qty <= stock.qty) and stock.stock_status' - ); + )->where($where); + return $this; } @@ -253,7 +350,10 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false) } /** + * Get Catalog Rule Processor. + * * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor + * * @deprecated 100.2.0 */ private function getCatalogRuleProcessor() diff --git a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php index 2f81308f67f50..30e37e54a21db 100644 --- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php +++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/AbstractItems.php @@ -92,9 +92,8 @@ public function getChildren($item) if (isset($itemsArray[$item->getOrderItem()->getId()])) { return $itemsArray[$item->getOrderItem()->getId()]; - } else { - return null; } + return null; } /** @@ -244,9 +243,8 @@ public function getOrderItem() { if ($this->getItem() instanceof \Magento\Sales\Model\Order\Item) { return $this->getItem(); - } else { - return $this->getItem()->getOrderItem(); } + return $this->getItem()->getOrderItem(); } /** diff --git a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php index e9a1a8d276a15..1827c2249dda3 100644 --- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php +++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php @@ -10,8 +10,6 @@ /** * Order invoice pdf default items renderer - * - * @codingStandardsIgnoreFile */ class Invoice extends AbstractItems { diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php index 9d035aece57bc..2f0a99072594b 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php @@ -271,9 +271,8 @@ public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectio { if ($bundleProduct->getPriceType() == Price::PRICE_TYPE_FIXED) { return $this->calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude); - } else { - return $this->calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude); } + return $this->calculateDynamicBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude); } /** @@ -282,7 +281,7 @@ public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectio * @param float $basePriceValue * @param Product $bundleProduct * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList - * @param null|bool|string|arrayy $exclude + * @param null|bool|string|array $exclude * @return \Magento\Framework\Pricing\Amount\AmountInterface */ protected function calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude) diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php index 56c403ad9960c..297c4659cb877 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php @@ -61,8 +61,8 @@ public function getPriceList(Product $bundleProduct, $searchMin, $useRegularPric if (!$useRegularPrice) { $selectionsCollection->addAttributeToSelect('special_price'); - $selectionsCollection->addAttributeToSelect('special_price_from'); - $selectionsCollection->addAttributeToSelect('special_price_to'); + $selectionsCollection->addAttributeToSelect('special_from_date'); + $selectionsCollection->addAttributeToSelect('special_to_date'); $selectionsCollection->addAttributeToSelect('tax_class_id'); } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php index 995572636e759..241902f6bba61 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptionPrice.php @@ -10,7 +10,7 @@ use Magento\Framework\Pricing\Price\AbstractPrice; /** - * Bundle option price model + * Bundle option price model with final price */ class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterface { @@ -26,6 +26,7 @@ class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterf /** * @var BundleSelectionFactory + * @deprecated */ protected $selectionFactory; @@ -34,23 +35,32 @@ class BundleOptionPrice extends AbstractPrice implements BundleOptionPriceInterf */ protected $maximalPrice; + /** + * @var \Magento\Bundle\Pricing\Price\BundleOptions + */ + private $bundleOptions; + /** * @param Product $saleableItem * @param float $quantity * @param BundleCalculatorInterface $calculator * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param BundleSelectionFactory $bundleSelectionFactory + * @param BundleOptions|null $bundleOptions */ public function __construct( Product $saleableItem, $quantity, BundleCalculatorInterface $calculator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - BundleSelectionFactory $bundleSelectionFactory + BundleSelectionFactory $bundleSelectionFactory, + BundleOptions $bundleOptions = null ) { $this->selectionFactory = $bundleSelectionFactory; parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->product->setQty($this->quantity); + $this->bundleOptions = $bundleOptions ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Bundle\Pricing\Price\BundleOptions::class); } /** @@ -59,7 +69,7 @@ public function __construct( public function getValue() { if (null === $this->value) { - $this->value = $this->calculateOptions(); + $this->value = $this->bundleOptions->calculateOptions($this->product); } return $this->value; } @@ -68,11 +78,12 @@ public function getValue() * Getter for maximal price of options * * @return bool|float + * @deprecated */ public function getMaxValue() { if (null === $this->maximalPrice) { - $this->maximalPrice = $this->calculateOptions(false); + $this->maximalPrice = $this->bundleOptions->calculateOptions($this->product, false); } return $this->maximalPrice; } @@ -84,21 +95,7 @@ public function getMaxValue() */ public function getOptions() { - $bundleProduct = $this->product; - /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ - $typeInstance = $bundleProduct->getTypeInstance(); - $typeInstance->setStoreFilter($bundleProduct->getStoreId(), $bundleProduct); - - /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionCollection */ - $optionCollection = $typeInstance->getOptionsCollection($bundleProduct); - - $selectionCollection = $typeInstance->getSelectionsCollection( - $typeInstance->getOptionsIds($bundleProduct), - $bundleProduct - ); - - $priceOptions = $optionCollection->appendSelections($selectionCollection, true, false); - return $priceOptions; + return $this->bundleOptions->getOptions($this->product); } /** @@ -109,22 +106,11 @@ public function getOptions() */ public function getOptionSelectionAmount($selection) { - $cacheKey = implode( - '_', - [ - $this->product->getId(), - $selection->getOptionId(), - $selection->getSelectionId() - ] + return $this->bundleOptions->getOptionSelectionAmount( + $this->product, + $selection, + false ); - - if (!isset($this->optionSelecionAmountCache[$cacheKey])) { - $selectionPrice = $this->selectionFactory - ->create($this->product, $selection, $selection->getSelectionQty()); - $this->optionSelecionAmountCache[$cacheKey] = $selectionPrice->getAmount(); - } - - return $this->optionSelecionAmountCache[$cacheKey]; } /** @@ -135,18 +121,7 @@ public function getOptionSelectionAmount($selection) */ protected function calculateOptions($searchMin = true) { - $priceList = []; - /* @var $option \Magento\Bundle\Model\Option */ - foreach ($this->getOptions() as $option) { - if ($searchMin && !$option->getRequired()) { - continue; - } - $selectionPriceList = $this->calculator->createSelectionPriceList($option, $this->product); - $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin); - $priceList = array_merge($priceList, $selectionPriceList); - } - $amount = $this->calculator->calculateBundleAmount(0., $this->product, $priceList); - return $amount->getValue(); + return $this->bundleOptions->calculateOptions($this->product, $searchMin); } /** diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php new file mode 100644 index 0000000000000..d611619cf2dc5 --- /dev/null +++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptionRegularPrice.php @@ -0,0 +1,98 @@ +product->setQty($this->quantity); + $this->bundleOptions = $bundleOptions; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + if (null === $this->value) { + $this->value = $this->bundleOptions->calculateOptions($this->product); + } + return $this->value; + } + + /** + * Get Options with attached Selections collection + * + * @return \Magento\Bundle\Model\ResourceModel\Option\Collection + */ + public function getOptions() + { + return $this->bundleOptions->getOptions($this->product); + } + + /** + * Get selection amount + * + * @param \Magento\Bundle\Model\Selection $selection + * @return \Magento\Framework\Pricing\Amount\AmountInterface + */ + public function getOptionSelectionAmount($selection) + { + return $this->bundleOptions->getOptionSelectionAmount( + $this->product, + $selection, + true + ); + } + + /** + * Get minimal amount of bundle price with options + * + * @return \Magento\Framework\Pricing\Amount\AmountInterface + */ + public function getAmount() + { + return $this->calculator->getOptionsAmount($this->product); + } +} diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php new file mode 100644 index 0000000000000..3e7a41d993e7f --- /dev/null +++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php @@ -0,0 +1,129 @@ +calculator = $calculator; + $this->selectionFactory = $bundleSelectionFactory; + } + + /** + * Get Options with attached Selections collection + * + * @param \Magento\Framework\Pricing\SaleableInterface $bundleProduct + * @return \Magento\Bundle\Model\ResourceModel\Option\Collection + */ + public function getOptions(\Magento\Framework\Pricing\SaleableInterface $bundleProduct) + { + /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ + $typeInstance = $bundleProduct->getTypeInstance(); + $typeInstance->setStoreFilter($bundleProduct->getStoreId(), $bundleProduct); + + /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionCollection */ + $optionCollection = $typeInstance->getOptionsCollection($bundleProduct); + + /** @var \Magento\Bundle\Model\ResourceModel\Selection\Collection $selectionCollection */ + $selectionCollection = $typeInstance->getSelectionsCollection( + $typeInstance->getOptionsIds($bundleProduct), + $bundleProduct + ); + + $priceOptions = $optionCollection->appendSelections($selectionCollection, true, false); + return $priceOptions; + } + + /** + * Calculate maximal or minimal options value + * + * @param \Magento\Framework\Pricing\SaleableInterface $bundleProduct + * @param bool $searchMin + * @return float + */ + public function calculateOptions( + \Magento\Framework\Pricing\SaleableInterface $bundleProduct, + bool $searchMin = true + ) { + $priceList = []; + /* @var $option \Magento\Bundle\Model\Option */ + foreach ($this->getOptions($bundleProduct) as $option) { + if ($searchMin && !$option->getRequired()) { + continue; + } + /** @var \Magento\Bundle\Pricing\Price\BundleSelectionPrice $selectionPriceList */ + $selectionPriceList = $this->calculator->createSelectionPriceList($option, $bundleProduct); + $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin); + $priceList = array_merge($priceList, $selectionPriceList); + } + $amount = $this->calculator->calculateBundleAmount(0., $bundleProduct, $priceList); + return $amount->getValue(); + } + + /** + * Get selection amount + * + * @param \Magento\Catalog\Model\Product $bundleProduct + * @param \Magento\Bundle\Model\Selection $selection + * @param bool $useRegularPrice + * @return \Magento\Framework\Pricing\Amount\AmountInterface + */ + public function getOptionSelectionAmount( + \Magento\Catalog\Model\Product $bundleProduct, + $selection, + bool $useRegularPrice = false + ) { + $cacheKey = implode( + '_', + [ + $bundleProduct->getId(), + $selection->getOptionId(), + $selection->getSelectionId(), + $useRegularPrice ? 1 : 0 + ] + ); + + if (!isset($this->optionSelectionAmountCache[$cacheKey])) { + $selectionPrice = $this->selectionFactory + ->create( + $bundleProduct, + $selection, + $selection->getSelectionQty(), + ['useRegularPrice' => $useRegularPrice] + ); + $this->optionSelectionAmountCache[$cacheKey] = $selectionPrice->getAmount(); + } + + return $this->optionSelectionAmountCache[$cacheKey]; + } +} diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php index 034b735764011..184f8b1e85eaa 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php @@ -52,7 +52,7 @@ public function getAmount() if ($this->product->getPriceType() == Price::PRICE_TYPE_FIXED) { /** @var \Magento\Catalog\Pricing\Price\CustomOptionPrice $customOptionPrice */ $customOptionPrice = $this->priceInfo->getPrice(CustomOptionPrice::PRICE_CODE); - $price += $customOptionPrice->getCustomOptionRange(true); + $price += $customOptionPrice->getCustomOptionRange(true, $this->getPriceCode()); } $this->amount[$this->getValue()] = $this->calculator->getMinRegularAmount($price, $this->product); } @@ -71,7 +71,7 @@ public function getMaximalPrice() if ($this->product->getPriceType() == Price::PRICE_TYPE_FIXED) { /** @var \Magento\Catalog\Pricing\Price\CustomOptionPrice $customOptionPrice */ $customOptionPrice = $this->priceInfo->getPrice(CustomOptionPrice::PRICE_CODE); - $price += $customOptionPrice->getCustomOptionRange(false); + $price += $customOptionPrice->getCustomOptionRange(false, $this->getPriceCode()); } $this->maximalPrice = $this->calculator->getMaxRegularAmount($price, $this->product); } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php index 927b8fbff8d85..a28d721cc9a4e 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php @@ -54,7 +54,7 @@ public function create( ) { $arguments['bundleProduct'] = $bundleProduct; $arguments['saleableItem'] = $selection; - $arguments['quantity'] = $quantity ? floatval($quantity) : 1.; + $arguments['quantity'] = $quantity ? (float)$quantity : 1.; return $this->objectManager->create(self::SELECTION_CLASS_DEFAULT, $arguments); } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php index 71c1b5c5e98cb..b98a9d05240b1 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionPrice.php @@ -103,7 +103,10 @@ public function getValue() return $this->value; } $product = $this->selection; - $bundleSelectionKey = 'bundle-selection-value-' . $product->getSelectionId(); + $bundleSelectionKey = 'bundle-selection-' + . ($this->useRegularPrice ? 'regular-' : '') + . 'value-' + . $product->getSelectionId(); if ($product->hasData($bundleSelectionKey)) { return $product->getData($bundleSelectionKey); } @@ -128,7 +131,8 @@ public function getValue() 'catalog_product_get_final_price', ['product' => $product, 'qty' => $this->bundleProduct->getQty()] ); - $value = $product->getData('final_price') * ($selectionPriceValue / 100); + $price = $this->useRegularPrice ? $product->getData('price') : $product->getData('final_price'); + $value = $price * ($selectionPriceValue / 100); } else { // calculate price for selection type fixed $value = $this->priceCurrency->convert($selectionPriceValue); @@ -150,7 +154,10 @@ public function getValue() public function getAmount() { $product = $this->selection; - $bundleSelectionKey = 'bundle-selection-amount-' . $product->getSelectionId(); + $bundleSelectionKey = 'bundle-selection' + . ($this->useRegularPrice ? 'regular-' : '') + . '-amount-' + . $product->getSelectionId(); if ($product->hasData($bundleSelectionKey)) { return $product->getData($bundleSelectionKey); } @@ -177,8 +184,7 @@ public function getProduct() { if ($this->bundleProduct->getPriceType() == Price::PRICE_TYPE_DYNAMIC) { return parent::getProduct(); - } else { - return $this->bundleProduct; } + return $this->bundleProduct; } } diff --git a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php index 274ea95474120..8effab864868a 100644 --- a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php @@ -15,7 +15,6 @@ /** * Configured price model * @api - * @since 100.0.2 */ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPriceInterface { @@ -41,6 +40,11 @@ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPrice */ private $serializer; + /** + * @var \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection + */ + private $configuredPriceSelection; + /** * @param Product $saleableItem * @param float $quantity @@ -48,6 +52,7 @@ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPrice * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param ItemInterface $item * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection|null $configuredPriceSelection */ public function __construct( Product $saleableItem, @@ -55,11 +60,15 @@ public function __construct( BundleCalculatorInterface $calculator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, ItemInterface $item = null, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection $configuredPriceSelection = null ) { $this->item = $item; $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->configuredPriceSelection = $configuredPriceSelection + ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\Pricing\Price\ConfiguredPriceSelection::class); parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); } @@ -84,13 +93,14 @@ public function getOptions() $bundleOptions = []; /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ $typeInstance = $bundleProduct->getTypeInstance(); - - // get bundle options - $optionsQuoteItemOption = $this->item->getOptionByCode('bundle_option_ids'); - $bundleOptionsIds = $optionsQuoteItemOption - ? $this->serializer->unserialize($optionsQuoteItemOption->getValue()) - : []; - + $bundleOptionsIds = []; + if ($this->item) { + // get bundle options + $optionsQuoteItemOption = $this->item->getOptionByCode('bundle_option_ids'); + if ($optionsQuoteItemOption && $optionsQuoteItemOption->getValue()) { + $bundleOptionsIds = $this->serializer->unserialize($optionsQuoteItemOption->getValue()); + } + } if ($bundleOptionsIds) { /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ $optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $bundleProduct); @@ -113,13 +123,7 @@ public function getOptions() */ public function getConfiguredAmount($baseValue = 0.) { - $selectionPriceList = []; - foreach ($this->getOptions() as $option) { - $selectionPriceList = array_merge( - $selectionPriceList, - $this->calculator->createSelectionPriceList($option, $this->product) - ); - } + $selectionPriceList = $this->configuredPriceSelection->getSelectionPriceList($this); return $this->calculator->calculateBundleAmount( $baseValue, $this->product, @@ -140,9 +144,8 @@ public function getValue() $this->priceInfo ->getPrice(BundleDiscountPrice::PRICE_CODE) ->calculateDiscount($configuredOptionsAmount); - } else { - return parent::getValue(); } + return parent::getValue(); } /** diff --git a/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php new file mode 100644 index 0000000000000..07da42f791e4e --- /dev/null +++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredRegularPrice.php @@ -0,0 +1,30 @@ +calculator->createSelectionPriceList($option, $this->product, true); + } +} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml new file mode 100644 index 0000000000000..2444776065f7e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + 4.99 + + + 2.89 + + + 7.33 + + + 18.25 + + + + {{productName}} + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4.99 + + + 2.89 + + + 7.33 + + + 18.25 + + + + {{productName}} + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + 20 + + + + + {{productName}} + + + + + Drop-down Option + + + + Radio Buttons Option + + + + Checkbox Option + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml new file mode 100644 index 0000000000000..c600e80f7265f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/BundleProductsOnAdminActionGroup.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml new file mode 100644 index 0000000000000..06d7cccb3623f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml new file mode 100644 index 0000000000000..7123a573bc2e1 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleLinkData.xml @@ -0,0 +1,20 @@ + + + + + + + + 1 + 0 + 1.11 + 1 + 1 + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml new file mode 100644 index 0000000000000..7af276bcede7c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleOptionData.xml @@ -0,0 +1,39 @@ + + + + + + bundle-option-dropdown + true + select + 0 + + + + bundle-option-radio + true + radio + 1 + + + + bundle-option-checkbox + true + checkbox + 3 + + + + bundle-option-multipleselect + true + multi + 4 + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml new file mode 100644 index 0000000000000..5a4f9827cd57c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductData.xml @@ -0,0 +1,31 @@ + + + + + + bundle + 4 + BundleOption + checkbox + 10 + BundleProduct + BundleProduct + 1 + 4 + bundle + bundleproduct + 4 + TestOption + 10 + 20 + Drop-down + EavStockItem + CustomAttributeCategoryIds + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml new file mode 100644 index 0000000000000..380b5b8959025 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml @@ -0,0 +1,26 @@ + + + + + price_type + 0 + + + price_type + 1 + + + price_view + 1 + + + price_view + 0 + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml new file mode 100644 index 0000000000000..ecdc4da3a75b6 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -0,0 +1,55 @@ + + + + + + Api Bundle Product + api-bundle-product + bundle + 4 + 4 + 1 + api-bundle-product + EavStockItem + ApiProductDescription + ApiProductShortDescription + CustomAttributeDynamicPrice + CustomAttributePriceView + + + Api Bundle Product + api-bundle-product + bundle + 4 + 4 + 1 + api-bundle-product + CustomAttributeCategoryIds + EavStockItem + ApiProductDescription + ApiProductShortDescription + CustomAttributeDynamicPrice + CustomAttributePriceViewRange + + + Api Fixed Bundle Product + api-fixed-bundle-product + bundle + 4 + 1.23 + 4 + 1 + api-fixed-bundle-product + EavStockItem + ApiProductDescription + ApiProductShortDescription + CustomAttributeFixPrice + CustomAttributePriceView + + diff --git a/app/code/Magento/Bundle/Test/Mftf/LICENSE.txt b/app/code/Magento/Bundle/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Bundle/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/app/code/Magento/Bundle/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Bundle/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Bundle/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/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml new file mode 100644 index 0000000000000..ca39253aa54a0 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_link-meta.xml @@ -0,0 +1,24 @@ + + + + + + application/json + + string + integer + integer + integer + boolean + number + integer + integer + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml new file mode 100644 index 0000000000000..c912ea5eac41a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Metadata/bundle_option-meta.xml @@ -0,0 +1,21 @@ + + + + + + application/json + + string + boolean + string + integer + string + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..f0048e2fc95d4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml new file mode 100644 index 0000000000000..89cc6a6d5d2fe --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Page/StorefrontProductPage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Bundle/Test/Mftf/README.md b/app/code/Magento/Bundle/Test/Mftf/README.md new file mode 100644 index 0000000000000..8e8da0c15fa56 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Bundle Functional Tests + +The Functional Test Module for **Magento Bundle** module. diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml new file mode 100644 index 0000000000000..8f60227926099 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -0,0 +1,23 @@ + + + + +
    + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml new file mode 100644 index 0000000000000..06ed27f4e970c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormSection.xml @@ -0,0 +1,25 @@ + + + + +
    + + + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml new file mode 100644 index 0000000000000..a5c70c24e3d9b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -0,0 +1,23 @@ + + + + +
    + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml new file mode 100644 index 0000000000000..3d5dc61d88a87 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..b1acc97cc0261 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml new file mode 100644 index 0000000000000..6f26c31a67427 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml @@ -0,0 +1,65 @@ + + + + + + + + + + <description value="You should be able to create a Bundle Product in the Magento Admin"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77417"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct1"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createPreReqSimpleProduct2"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct1" stepKey="deletePreReqSimpleProduct1"/> + <deleteData createDataKey="createPreReqSimpleProduct2" stepKey="deletePreReqSimpleProduct2"/> + <actionGroup ref="DeleteProductOnProductsGridPageByName" stepKey="DeleteProductOnProductsGridPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + </after> + <!--Step1. Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin" /> + + <!--Step2. Navigate to the Products>Inventory>Catalog--> + <!--Step3. Click on "+" dropdown and select Bundle Product type--> + <actionGroup ref="OpenNewBundleProductPage" stepKey="OpenNewBundleProductPage"/> + + <!--Step4. Fill in all data according to data set--> + <actionGroup ref="CreateBundleProductForTwoSimpleProducts" stepKey="CreateBundleProductForTwoSimpleProducts"> + <argument name="bundleProduct" value="BundleProduct"/> + <argument name="simpleProductFirst" value="$$createPreReqSimpleProduct1$$"/> + <argument name="simpleProductSecond" value="$$createPreReqSimpleProduct2$$"/> + </actionGroup> + + <!--Step5. Save product--> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="SaveProductOnProductPageOnAdmin"/> + + <!--Step6. Verify created bundle product in the Magento Admin--> + <actionGroup ref="CheckVisibilityOfProductOnProductsGridPageByName" stepKey="CheckVisibilityOfProductOnProductsGridPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!--Step6. Verify created bundle product on Front End--> + <actionGroup ref="CheckVisibilityOfProductOnCategoryPageByName" stepKey="CheckVisibilityOfProductOnPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml new file mode 100644 index 0000000000000..f0c370bc2d515 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CurrencyChangingBundleProductInCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="MAGETWO-90381: Bundle product price doubled when switching currency"/> + <title value="Work of currency changing with a bundle product added to the cart"/> + <description value="User should be able change the currency and add one more product in cart and get right price in previous currency"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96305"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login" /> + <createData entity="CurrencySettingWithEuroAndUSD" stepKey="configureCurrencyOptions"/> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct1"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createPreReqSimpleProduct2"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct1" stepKey="deletePreReqSimpleProduct1"/> + <deleteData createDataKey="createPreReqSimpleProduct2" stepKey="deletePreReqSimpleProduct2"/> + <createData entity="DefaultCurrencySetting" stepKey="restoreCurrencyOptions"/> + <!-- Delete the bundled product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProductOnProductsGridPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <!--Clear Configs--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Navigate to the Products>Inventory>Catalog --> + <!-- Click on "+" dropdown and select Bundle Product type --> + <actionGroup ref="OpenNewBundleProductPage" stepKey="openNewBundleProductPage"/> + <!-- Add Option, a "Radio Buttons" type option --> + <actionGroup ref="CreateBundleProductForTwoSimpleProductsWithRadioTypeOptions" stepKey="addBundleOptionWithTwoProducts2"> + <argument name="bundleProduct" value="BundleProduct"/> + <argument name="simpleProductFirst" value="$$createPreReqSimpleProduct1$$"/> + <argument name="simpleProductSecond" value="$$createPreReqSimpleProduct2$$"/> + </actionGroup> + <!-- Save product --> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProductOnProductPageOnAdmin"/> + <!-- Go to storefront BundleProduct --> + <amOnPage url="{{StorefrontProductPage.url(BundleProduct.name)}}" stepKey="goToStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPage"/> + <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct1ToCartAndChangeCurrencyToEuro"> + <argument name="product" value="$$createPreReqSimpleProduct1$$"/> + <argument name="currency" value="EUR - Euro"/> + </actionGroup> + <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct2ToCartAndChangeCurrencyToUSD"> + <argument name="product" value="$$createPreReqSimpleProduct1$$"/> + <argument name="currency" value="USD - US Dollar"/> + </actionGroup> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <waitForPageLoad stepKey="waitForMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" userInput="$4,000.00" stepKey="seeCartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml new file mode 100644 index 0000000000000..46ac1cdae93c0 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontAddBundleProductWithZeroPriceToShoppingCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="Add Bundle product with zero price to shopping cart"/> + <title value="Add Bundle product with zero price to shopping cart"/> + <description value="Add Bundle product with zero price to shopping cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-83535"/> + <group value="bundle"/> + </annotations> + <before> + <!--Enable freeShipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + <!--Create simple with zero price product--> + <createData entity="ApiProductWithDescription" stepKey="apiSimple"> + <field key="price">0</field> + </createData> + <!--Create Bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="apiBundleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <!--Create Attribute--> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="apiBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink"> + <requiredEntity createDataKey="apiBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="apiSimple"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <deleteData createDataKey="apiSimple" stepKey="deleteSimple"/> + <deleteData createDataKey="apiBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open category page--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <!--Add bundle product to cart--> + <actionGroup ref="StorefrontAddCategoryBundleProductToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="product" value="$$apiBundleProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!--Place order--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!--Check subtotal in created order--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$0.00" stepKey="checkSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml new file mode 100644 index 0000000000000..89423c2c63658 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckBundleProductOptionTierPrices"> + <annotations> + <features value="Bundle"/> + <stories value="View bundle products"/> + <title value="Check tier prices for bundle options"/> + <testCaseId value="MAGETWO-99047"/> + <useCaseId value="MAGETWO-96898"/> + <group value="catalog"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create Dynamic Bundle product --> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + + <!-- Add tier prices to simple products --> + <!-- Simple product 1 --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct1"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct1"> + <argument name="website" value="All Websites [USD]"/> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <!-- Simple product 2 --> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct2CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct2"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct2"> + <argument name="website" value="All Websites [USD]"/> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="7"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="25"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutAsAdmin"/> + + <!-- Run reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2CreateBundleProduct" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSubCategoryCreateBundleProduct" stepKey="deleteSubCategory"/> + </after> + + <!-- Go to storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--"Drop-down" type option--> + <!-- Check Tier Prices for product 1 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct1"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="DropDownTierPriceTextProduct1"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct2"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct2"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="dropDownTierPriceTextProduct2"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">dropDownTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Radio Buttons" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Checkbox" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct2</actualResult> + </assertContains> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml new file mode 100644 index 0000000000000..e65a4dc99902b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -0,0 +1,125 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontEditBundleProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle products list on Storefront"/> + <title value="Customer should be able to change chosen options for Bundle Product when clicking Edit button in Shopping Cart page"/> + <description value="Customer should be able to change chosen options for Bundle Product when clicking Edit button in Shopping Cart page"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-77523"/> + <group value="bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> + <createData entity="WeeeConfigDisable" stepKey="disableFPT"/> + <createData entity="SimpleProduct3" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct3" stepKey="simpleProduct2"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + + <!-- Create a bundle product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> + <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateBundleProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillBundleProductNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Add two bundle items --> + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <scrollTo selector="{{AdminProductFormBundleSection.bundledItems}}" stepKey="scrollToBundleItems"/> + <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption3"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> + <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> + <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanelSection.firstCheckbox}}" stepKey="selectFirstGridRow"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanelSection.firstCheckbox}}" stepKey="selectFirstGridRow2"/> + <click selector="{{AdminAddProductsToOptionPanelSection.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductBundle"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="assertSuccess"/> + + <!-- Go to the storefront bundled product page --> + <amOnPage url="/{{BundleProduct.urlKey}}.html" stepKey="visitStoreFrontBundle"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="customizeAndAddToCart"/> + <waitForPageLoad stepKey="waitCustomizableOptionsPopUp"/> + + <!-- add two products to the shopping cart, each with one different option --> + <click selector="{{StorefrontBundledSection.bundleOption('1','1')}}" stepKey="selectFirstBundleOption"/> + <waitForPageLoad stepKey="waitForPriceUpdate"/> + <see selector="{{StorefrontBundledSection.configuredPrice}}" userInput="1,230.00" stepKey="seeSinglePrice"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="addFirstItemToCart"/> + <waitForPageLoad stepKey="waitForElementAdded"/> + + <click selector="{{StorefrontBundledSection.bundleOption('1','1')}}" stepKey="unselectFirstBundleOption"/> + <click selector="{{StorefrontBundledSection.bundleOption('1','2')}}" stepKey="selectSecondBundleOption"/> + <waitForPageLoad stepKey="waitForPriceUpdate2"/> + <see selector="{{StorefrontBundledSection.configuredPrice}}" userInput="1,230.00" stepKey="seeSinglePrice2"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="addSecondItemToCart"/> + <waitForPageLoad stepKey="waitForElementAdded2"/> + + <!-- Go to the shopping cart page and edit the first product --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="onPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.total}}" stepKey="waitForInfoDropdown"/> + <waitForPageLoad stepKey="waitForCartPageLoad3"/> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.total}}" stepKey="grabTotalBefore"/> + <click selector="{{CheckoutCartProductSection.editItemParametersButton('1')}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForStorefront2"/> + + <!-- Check second one option to choose both of the options on the storefront --> + <click selector="{{StorefrontBundledSection.bundleOption('1','2')}}" stepKey="selectSecondBundleOption2"/> + + <waitForPageLoad stepKey="waitForPriceUpdate3"/> + <see selector="{{StorefrontBundledSection.configuredPrice}}" userInput="2,460.00" stepKey="seeDoublePrice"/> + + <click selector="{{StorefrontBundledSection.updateCart}}" stepKey="addFirstItemToCart2"/> + <waitForPageLoad stepKey="waitForElementAdded3"/> + + <!-- Go to the shopping cart page --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="onPageShoppingCart2"/> + <waitForPageLoad stepKey="waitForCartPageLoad2"/> + + <!-- Assert that the options are both there and the proce no longer matches --> + <see selector="{{CheckoutCartProductSection.itemOptionsBlock('2')}}" userInput="$$simpleProduct1.sku$$" stepKey="assertBothOptions"/> + <see selector="{{CheckoutCartProductSection.itemOptionsBlock('2')}}" userInput="$$simpleProduct2.sku$$" stepKey="assertBothOptions2"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.total}}" stepKey="waitForInfoDropdown2"/> + <waitForPageLoad stepKey="waitForCartPageLoad4"/> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.total}}" stepKey="grabTotalAfter"/> + <assertNotEquals expected="{$grabTotalBefore}" expectedType="string" actual="{$grabTotalAfter}" actualType="string" stepKey="assertNotEquals"/> + + <!-- Delete the bundled product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundle"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml new file mode 100644 index 0000000000000..8ced50e26a448 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml @@ -0,0 +1,242 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest"> + <annotations> + <features value="Bundle"/> + <title value="Verify dynamic bundle product prices for combination of options"/> + <description value="Verify prices for various configurations of Dynamic Bundle product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78827"/> + <group value="bundle"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + + <!--Create 5 simple product--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">4.99</field> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">2.89</field> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"> + <field key="price">7.33</field> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"> + <field key="price">18.25</field> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct5"> + <field key="price">10.00</field> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + + <!--Add special price to simple product--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct5.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="addSpecialPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Create Bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">false</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct3"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct4"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct4"/> + </createData> + + <!--Create Bundle product 2--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct2"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createBundleOption2_1"> + <requiredEntity createDataKey="createBundleProduct2"/> + </createData> + <createData entity="RadioButtonsOption" stepKey="createBundleOption2_2"> + <requiredEntity createDataKey="createBundleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct5"> + <requiredEntity createDataKey="createBundleProduct2"/> + <requiredEntity createDataKey="createBundleOption2_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + <field key="qty">2</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct6"> + <requiredEntity createDataKey="createBundleProduct2"/> + <requiredEntity createDataKey="createBundleOption2_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct7"> + <requiredEntity createDataKey="createBundleProduct2"/> + <requiredEntity createDataKey="createBundleOption2_2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct8"> + <requiredEntity createDataKey="createBundleProduct2"/> + <requiredEntity createDataKey="createBundleOption2_2"/> + <requiredEntity createDataKey="simpleProduct4"/> + <field key="qty">5</field> + </createData> + + <!--Create Bundle product 3--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct3"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="MultipleSelectOption" stepKey="createBundleOption3_1"> + <requiredEntity createDataKey="createBundleProduct3"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createBundleOption3_2"> + <requiredEntity createDataKey="createBundleProduct3"/> + <field key="required">false</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct9"> + <requiredEntity createDataKey="createBundleProduct3"/> + <requiredEntity createDataKey="createBundleOption3_1"/> + <requiredEntity createDataKey="simpleProduct4"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct10"> + <requiredEntity createDataKey="createBundleProduct3"/> + <requiredEntity createDataKey="createBundleOption3_1"/> + <requiredEntity createDataKey="simpleProduct5"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct11"> + <requiredEntity createDataKey="createBundleProduct3"/> + <requiredEntity createDataKey="createBundleOption3_2"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct12"> + <requiredEntity createDataKey="createBundleProduct3"/> + <requiredEntity createDataKey="createBundleOption3_2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + + <!-- navigate to the tax configuration page --> + <amOnPage url="{{AdminTaxConfigurationPage.url}}" stepKey="goToAdminTaxPage"/> + <waitForPageLoad stepKey="waitForTaxConfigLoad"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationSettingsOpened}}" visible="false" stepKey="openCalculationSettingsTab"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationAlgorithmInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithmDisabled}}" visible="true" stepKey="clickCalculationMethodBasedCheckBox"/> + <selectOption userInput="Total" selector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" stepKey="fillCalculationMethodBased"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationBasedInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationBasedDisabled}}" visible="true" stepKey="clickTaxCalculationBasedCheckBox"/> + <selectOption userInput="Shipping Origin" selector="{{AdminConfigureTaxSection.taxCalculationBased}}" stepKey="fillTaxCalculationBased"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationPricesInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationPricesDisabled}}" visible="true" stepKey="clickCalculationPricesCheckBox"/> + <selectOption userInput="Excluding Tax" selector="{{AdminConfigureTaxSection.taxCalculationPrices}}" stepKey="clickCalculationPrices"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxPriceDisplaySettings}}" dependentSelector="{{AdminConfigureTaxSection.taxPriceDisplaySettingsOpened}}" visible="false" stepKey="openPriceDisplaySettings"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxDisplayProductPricesInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxDisplayProductPricesDisabled}}" visible="true" stepKey="clickDisplayProductPricesCheckBox"/> + <selectOption userInput="Excluding Tax" selector="{{AdminConfigureTaxSection.taxDisplayProductPrices}}" stepKey="clickDisplayProductPrices"/> + + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> + <waitForPageLoad stepKey="waitForTaxSaved"/> + <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <!-- navigate to the tax configuration page --> + <amOnPage url="{{AdminTaxConfigurationPage.url}}" stepKey="goToAdminTaxPage"/> + <waitForPageLoad stepKey="waitForTaxConfigLoad"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationSettingsOpened}}" visible="false" stepKey="openCalculationSettingsTab"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationAlgorithmInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithmDisabled}}" visible="true" stepKey="clickCalculationMethodBasedCheckBox"/> + <selectOption userInput="Total" selector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" stepKey="fillCalculationMethodBased"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationBasedInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationBasedDisabled}}" visible="true" stepKey="clickTaxCalculationBasedCheckBox"/> + <selectOption userInput="Shipping Address" selector="{{AdminConfigureTaxSection.taxCalculationBased}}" stepKey="fillTaxCalculationBased"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationPricesInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationPricesDisabled}}" visible="true" stepKey="clickCalculationPricesCheckBox"/> + <selectOption userInput="Excluding Tax" selector="{{AdminConfigureTaxSection.taxCalculationPrices}}" stepKey="clickCalculationPrices"/> + + <conditionalClick selector="{{AdminConfigureTaxSection.taxPriceDisplaySettings}}" dependentSelector="{{AdminConfigureTaxSection.taxPriceDisplaySettingsOpened}}" visible="false" stepKey="openPriceDisplaySettings"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxDisplayProductPricesInherit}}" dependentSelector="{{AdminConfigureTaxSection.taxDisplayProductPricesDisabled}}" visible="true" stepKey="clickDisplayProductPricesCheckBox"/> + <selectOption userInput="Excluding Tax" selector="{{AdminConfigureTaxSection.taxDisplayProductPrices}}" stepKey="clickDisplayProductPrices"/> + + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> + <waitForPageLoad stepKey="waitForTaxSaved"/> + <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> + + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory1"/> + + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> + + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createBundleProduct2" stepKey="deleteBundleProduct2"/> + <deleteData createDataKey="createBundleProduct3" stepKey="deleteBundleProduct3"/> + </after> + + <!-- Go to storefront category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + + <see userInput="From $7.33" selector="{{StorefrontCategoryProductSection.priceFromByProductId($$createBundleProduct.id$$)}}" stepKey="seePriceFromInCategoryBundle1"/> + <see userInput="To $33.46" selector="{{StorefrontCategoryProductSection.priceToByProductId($$createBundleProduct.id$$)}}" stepKey="seePriceToInCategoryBundle1"/> + + <see userInput="From $10.22" selector="{{StorefrontCategoryProductSection.priceFromByProductId($$createBundleProduct2.id$$)}}" stepKey="seePriceFromInCategoryBundle2"/> + <see userInput="To $101.23" selector="{{StorefrontCategoryProductSection.priceToByProductId($$createBundleProduct2.id$$)}}" stepKey="seePriceToInCategoryBundle2"/> + + <see userInput="From $8.00 Regular Price $10.00" selector="{{StorefrontCategoryProductSection.priceFromByProductId($$createBundleProduct3.id$$)}}" stepKey="seePriceFromInCategoryBundle3"/> + <see userInput="To $33.58 Regular Price $35.58" selector="{{StorefrontCategoryProductSection.priceToByProductId($$createBundleProduct3.id$$)}}" stepKey="seePriceToInCategoryBundle3"/> + + <!-- Go to storefront product pages --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleProduct.custom_attributes[url_key]$$)}}" stepKey="onPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="From $7.33" selector="{{StorefrontProductInfoMainSection.priceFrom}}" stepKey="seePriceFromBundle1"/> + <see userInput="To $33.46" selector="{{StorefrontProductInfoMainSection.priceTo}}" stepKey="seePriceToBundle1"/> + + <amOnPage url="{{StorefrontProductPage.url($$createBundleProduct2.custom_attributes[url_key]$$)}}" stepKey="onPageBundle2"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <see userInput="From $10.22" selector="{{StorefrontProductInfoMainSection.priceFrom}}" stepKey="seePriceFromBundle2"/> + <see userInput="To $101.23" selector="{{StorefrontProductInfoMainSection.priceTo}}" stepKey="seePriceToBundle2"/> + + <amOnPage url="{{StorefrontProductPage.url($$createBundleProduct3.custom_attributes[url_key]$$)}}" stepKey="onPageBundle3"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <see userInput="From $8.00 Regular Price $10.00" selector="{{StorefrontProductInfoMainSection.priceFrom}}" stepKey="seePriceFromBundle3"/> + <see userInput="To $33.58 Regular Price $35.58" selector="{{StorefrontProductInfoMainSection.priceTo}}" stepKey="seePriceToBundle3"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php index 414b460a1b81d..473fbbd035b00 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/Items/RendererTest.php @@ -46,6 +46,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass) $this->assertSame(null, $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenEmptyItemsDataProvider() { return [ @@ -97,6 +100,9 @@ public function testGetChildren($parentItem) $this->assertSame([2 => $this->orderItem], $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenDataProvider() { return [ @@ -116,6 +122,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isShipmentSeparately()); } + /** + * @return array + */ public function isShipmentSeparatelyWithoutItemDataProvider() { return [ @@ -145,6 +154,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare $this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem)); } + /** + * @return array + */ public function isShipmentSeparatelyWithItemDataProvider() { return [ @@ -166,6 +178,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isChildCalculated()); } + /** + * @return array + */ public function isChildCalculatedWithoutItemDataProvider() { return [ @@ -195,6 +210,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI $this->assertSame($result, $this->model->isChildCalculated($this->orderItem)); } + /** + * @return array + */ public function isChildCalculatedWithItemDataProvider() { return [ @@ -257,6 +275,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result) $this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem)); } + /** + * @return array + */ public function canShowPriceInfoDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php index 95dcb48f84be1..5d8cabdd8c1b9 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Sales/Order/View/Items/RendererTest.php @@ -41,6 +41,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isShipmentSeparately()); } + /** + * @return array + */ public function isShipmentSeparatelyWithoutItemDataProvider() { return [ @@ -70,6 +73,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare $this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem)); } + /** + * @return array + */ public function isShipmentSeparatelyWithItemDataProvider() { return [ @@ -91,6 +97,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isChildCalculated()); } + /** + * @return array + */ public function isChildCalculatedWithoutItemDataProvider() { return [ @@ -120,6 +129,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI $this->assertSame($result, $this->model->isChildCalculated($this->orderItem)); } + /** + * @return array + */ public function isChildCalculatedWithItemDataProvider() { return [ @@ -151,6 +163,9 @@ public function testGetSelectionAttributesWithBundle() $this->assertEquals($unserializedResult, $this->model->getSelectionAttributes($this->orderItem)); } + /** + * @return array + */ public function getSelectionAttributesDataProvider() { return [ @@ -184,6 +199,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result) $this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem)); } + /** + * @return array + */ public function canShowPriceInfoDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/Bundle/OptionTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/Bundle/OptionTest.php index 5e0349e88e095..45b5c8507204b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/Bundle/OptionTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/Bundle/OptionTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Test\Unit\Block\Catalog\Product\View\Type\Bundle; class OptionTest extends \PHPUnit\Framework\TestCase @@ -62,12 +60,10 @@ public function testSetOption() $selectionId = 315; $this->product->expects($this->atLeastOnce()) ->method('hasPreconfiguredValues') - ->will($this->returnValue(true)); + ->willReturn(true); $this->product->expects($this->atLeastOnce()) ->method('getPreconfiguredValues') - ->will($this->returnValue( - new \Magento\Framework\DataObject(['bundle_option' => [15 => 315, 16 => 316]])) - ); + ->willReturn(new \Magento\Framework\DataObject(['bundle_option' => [15 => 315, 16 => 316]])); $option = $this->createMock(\Magento\Bundle\Model\Option::class); $option->expects($this->any())->method('getId')->will($this->returnValue(15)); diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php index 97e8098b8181e..bda1c32d4d66e 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php @@ -244,12 +244,14 @@ public function testGetJsonConfigFixedPriceBundle() ), ] ); + $bundleOptionPriceMock = $this->getAmountPriceMock( + $baseAmount, + $regularPriceMock, + [['item' => $selections[0], 'value' => $basePriceValue, 'base_amount' => 321321]] + ); $prices = [ - 'bundle_option' => $this->getAmountPriceMock( - $baseAmount, - $regularPriceMock, - [['item' => $selections[0], 'value' => $basePriceValue, 'base_amount' => 321321]] - ), + 'bundle_option' => $bundleOptionPriceMock, + 'bundle_option_regular_price' => $bundleOptionPriceMock, \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE => $finalPriceMock, \Magento\Catalog\Pricing\Price\RegularPrice::PRICE_CODE => $regularPriceMock, ]; @@ -278,6 +280,7 @@ public function testGetJsonConfigFixedPriceBundle() $this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']); + $this->assertEquals([1], $jsonConfig['positions']); } /** @@ -328,6 +331,11 @@ private function updateBundleBlock($options, $priceInfo, $priceType) ->will($this->returnArgument(0)); } + /** + * @param $price + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getPriceInfoMock($price) { $priceInfoMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) @@ -352,6 +360,11 @@ private function getPriceInfoMock($price) return $priceInfoMock; } + /** + * @param $prices + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getPriceMock($prices) { $methods = []; diff --git a/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php new file mode 100644 index 0000000000000..d335554ef373c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Block\DataProviders; + +use Magento\Bundle\Block\DataProviders\OptionPriceRenderer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Pricing\Render; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test additional data for bundle options + */ +class OptionPriceRendererTest extends TestCase +{ + /** + * @var LayoutInterface|MockObject + */ + private $layoutMock; + + /** + * @var OptionPriceRenderer + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->layoutMock = $this->createMock( + LayoutInterface::class + ); + + $this->renderer = $objectManager->getObject( + OptionPriceRenderer::class, + ['layout' => $this->layoutMock] + ); + } + + /** + * Test to render Tier price html + */ + public function testRenderTierPrice() + { + $expectedHtml = 'tier price html'; + $expectedArguments = ['zone' => Render::ZONE_ITEM_OPTION]; + + $productMock = $this->createMock(Product::class); + + $priceRenderer = $this->createPartialMock(BlockInterface::class, ['toHtml', 'render']); + $priceRenderer->expects($this->once()) + ->method('render') + ->with('tier_price', $productMock, $expectedArguments) + ->willReturn($expectedHtml); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderer); + + $this->assertEquals( + $expectedHtml, + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } + + /** + * Test to render Tier price html when render block is not exists + */ + public function testRenderTierPriceNotExist() + { + $productMock = $this->createMock(Product::class); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn(false); + + $this->assertEquals( + '', + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php index d79afdddfb7ae..2f5dcef391063 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Sales/Order/Items/RendererTest.php @@ -47,6 +47,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass) $this->assertSame(null, $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenEmptyItemsDataProvider() { return [ @@ -96,6 +99,9 @@ public function testGetChildren($parentItem) $this->assertSame([2 => $this->orderItem], $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenDataProvider() { return [ @@ -115,6 +121,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isShipmentSeparately()); } + /** + * @return array + */ public function isShipmentSeparatelyWithoutItemDataProvider() { return [ @@ -144,6 +153,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare $this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem)); } + /** + * @return array + */ public function isShipmentSeparatelyWithItemDataProvider() { return [ @@ -165,6 +177,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isChildCalculated()); } + /** + * @return array + */ public function isChildCalculatedWithoutItemDataProvider() { return [ @@ -194,6 +209,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI $this->assertSame($result, $this->model->isChildCalculated($this->orderItem)); } + /** + * @return array + */ public function isChildCalculatedWithItemDataProvider() { return [ @@ -238,6 +256,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result) $this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem)); } + /** + * @return array + */ public function canShowPriceInfoDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php index c4f05e9f32461..58fae96ea2ca6 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/LinkManagementTest.php @@ -5,11 +5,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Test\Unit\Model; use Magento\Bundle\Model\LinkManagement; +use Magento\Bundle\Model\ResourceModel\Selection\Collection; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** @@ -51,7 +50,7 @@ class LinkManagementTest extends \PHPUnit\Framework\TestCase protected $optionCollection; /** - * @var \Magento\Bundle\Model\ResourceModel\Selection\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ protected $selectionCollection; @@ -135,8 +134,7 @@ protected function setUp() ->setMethods(['appendSelections']) ->disableOriginalConstructor() ->getMock(); - $this->selectionCollection = $this->getMockBuilder( - \Magento\Bundle\Model\ResourceModel\Selection\Collection::class) + $this->selectionCollection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) @@ -150,9 +148,18 @@ protected function setUp() ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->bundleSelectionMock = $this->createPartialMock(\Magento\Bundle\Model\SelectionFactory::class, ['create']); - $this->bundleFactoryMock = $this->createPartialMock(\Magento\Bundle\Model\ResourceModel\BundleFactory::class, ['create']); - $this->optionCollectionFactoryMock = $this->createPartialMock(\Magento\Bundle\Model\ResourceModel\Option\CollectionFactory::class, ['create']); + $this->bundleSelectionMock = $this->createPartialMock( + \Magento\Bundle\Model\SelectionFactory::class, + ['create'] + ); + $this->bundleFactoryMock = $this->createPartialMock( + \Magento\Bundle\Model\ResourceModel\BundleFactory::class, + ['create'] + ); + $this->optionCollectionFactoryMock = $this->createPartialMock( + \Magento\Bundle\Model\ResourceModel\Option\CollectionFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() @@ -193,9 +200,9 @@ public function testGetChildren() $this->getOptions(); $this->productRepository->expects($this->any())->method('get')->with($this->equalTo($productSku)) - ->will($this->returnValue($this->product)); + ->willReturn($this->product); - $this->product->expects($this->once())->method('getTypeId')->will($this->returnValue('bundle')); + $this->product->expects($this->once())->method('getTypeId')->willReturn('bundle'); $this->productType->expects($this->once())->method('setStoreFilter')->with( $this->equalTo($this->storeId), @@ -203,13 +210,13 @@ public function testGetChildren() ); $this->productType->expects($this->once())->method('getSelectionsCollection') ->with($this->equalTo($this->optionIds), $this->equalTo($this->product)) - ->will($this->returnValue($this->selectionCollection)); + ->willReturn($this->selectionCollection); $this->productType->expects($this->once())->method('getOptionsIds')->with($this->equalTo($this->product)) - ->will($this->returnValue($this->optionIds)); + ->willReturn($this->optionIds); $this->optionCollection->expects($this->once())->method('appendSelections') ->with($this->equalTo($this->selectionCollection)) - ->will($this->returnValue([$this->option])); + ->willReturn([$this->option]); $this->option->expects($this->any())->method('getSelections')->willReturn([$this->product]); $this->product->expects($this->any())->method('getData')->willReturn([]); @@ -236,9 +243,9 @@ public function testGetChildrenWithOptionId() $this->getOptions(); $this->productRepository->expects($this->any())->method('get')->with($this->equalTo($productSku)) - ->will($this->returnValue($this->product)); + ->willReturn($this->product); - $this->product->expects($this->once())->method('getTypeId')->will($this->returnValue('bundle')); + $this->product->expects($this->once())->method('getTypeId')->willReturn('bundle'); $this->productType->expects($this->once())->method('setStoreFilter')->with( $this->equalTo($this->storeId), @@ -246,15 +253,15 @@ public function testGetChildrenWithOptionId() ); $this->productType->expects($this->once())->method('getSelectionsCollection') ->with($this->equalTo($this->optionIds), $this->equalTo($this->product)) - ->will($this->returnValue($this->selectionCollection)); + ->willReturn($this->selectionCollection); $this->productType->expects($this->once())->method('getOptionsIds')->with($this->equalTo($this->product)) - ->will($this->returnValue($this->optionIds)); + ->willReturn($this->optionIds); $this->optionCollection->expects($this->once())->method('appendSelections') ->with($this->equalTo($this->selectionCollection)) - ->will($this->returnValue([$this->option])); + ->willReturn([$this->option]); - $this->option->expects($this->any())->method('getOptionId')->will($this->returnValue(10)); + $this->option->expects($this->any())->method('getOptionId')->willReturn(10); $this->option->expects($this->once())->method('getSelections')->willReturn([1, 2]); $this->dataObjectHelperMock->expects($this->never())->method('populateWithArray'); @@ -270,9 +277,9 @@ public function testGetChildrenException() $productSku = 'productSku'; $this->productRepository->expects($this->once())->method('get')->with($this->equalTo($productSku)) - ->will($this->returnValue($this->product)); + ->willReturn($this->product); - $this->product->expects($this->once())->method('getTypeId')->will($this->returnValue('simple')); + $this->product->expects($this->once())->method('getTypeId')->willReturn('simple'); $this->assertEquals([$this->link], $this->model->getChildren($productSku)); } @@ -283,12 +290,12 @@ public function testGetChildrenException() public function testAddChildToNotBundleProduct() { $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE - )); + ); $this->model->addChild($productMock, 1, $productLink); } @@ -298,36 +305,37 @@ public function testAddChildToNotBundleProduct() public function testAddChildNonExistingOption() { $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $emptyOption = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)->disableOriginalConstructor() ->setMethods(['getId', '__wakeup']) ->getMock(); $emptyOption->expects($this->once()) ->method('getId') - ->will($this->returnValue(null)); + ->willReturn(null); $optionsCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); $optionsCollectionMock->expects($this->once()) ->method('setIdFilter') ->with($this->equalTo(1)) - ->will($this->returnSelf()); + ->willReturnSelf(); $optionsCollectionMock->expects($this->once()) ->method('getFirstItem') - ->will($this->returnValue($emptyOption)); + ->willReturn($emptyOption); - $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( - $this->returnValue($optionsCollectionMock) - ); + $this->optionCollectionFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($optionsCollectionMock); $this->model->addChild($productMock, 1, $productLink); } @@ -338,52 +346,53 @@ public function testAddChildNonExistingOption() public function testAddChildLinkedProductIsComposite() { $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn($this->linkField); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $productMock->expects($this->any()) ->method('getData') ->with($this->linkField) ->willReturn($this->linkField); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue(13)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(true)); + $linkedProductMock->expects($this->any())->method('getId')->willReturn(13); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(true); $this->productRepository ->expects($this->once()) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)->disableOriginalConstructor() ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->willReturn(1); $optionsCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); $optionsCollectionMock->expects($this->once()) ->method('setIdFilter') ->with($this->equalTo('1')) - ->will($this->returnSelf()); + ->willReturnSelf(); $optionsCollectionMock->expects($this->once()) ->method('getFirstItem') - ->will($this->returnValue($option)); - $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( - $this->returnValue($optionsCollectionMock) - ); + ->willReturn($option); + $this->optionCollectionFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($optionsCollectionMock); $bundle = $this->createMock(\Magento\Bundle\Model\ResourceModel\Bundle::class); $bundle->expects($this->once())->method('getSelectionsData')->with($this->linkField)->willReturn([]); - $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); + $this->bundleFactoryMock->expects($this->once())->method('create')->willReturn($bundle); $this->model->addChild($productMock, 1, $productLink); } @@ -396,9 +405,9 @@ public function testAddChildProductAlreadyExistsInOption() ->setMethods(['getSku', 'getOptionId', 'getSelectionId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); - $productLink->expects($this->any())->method('getSelectionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); + $productLink->expects($this->any())->method('getSelectionId')->willReturn(1); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn($this->linkField); $productMock = $this->createPartialMock( @@ -413,37 +422,38 @@ public function testAddChildProductAlreadyExistsInOption() ->method('getData') ->with($this->linkField) ->willReturn($this->linkField); - $productMock->expects($this->any())->method('getCopyFromView')->will($this->returnValue(false)); + $productMock->expects($this->any())->method('getCopyFromView')->willReturn(false); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getEntityId')->will($this->returnValue(13)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->any())->method('getEntityId')->willReturn(13); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->once()) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)->disableOriginalConstructor() ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->willReturn(1); $optionsCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); $optionsCollectionMock->expects($this->once()) ->method('setIdFilter') ->with($this->equalTo(1)) - ->will($this->returnSelf()); + ->willReturnSelf(); $optionsCollectionMock->expects($this->once()) ->method('getFirstItem') - ->will($this->returnValue($option)); - $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( - $this->returnValue($optionsCollectionMock) - ); + ->willReturn($option); + $this->optionCollectionFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($optionsCollectionMock); $selections = [ ['option_id' => 1, 'product_id' => 12, 'parent_product_id' => 'product_id'], @@ -452,8 +462,8 @@ public function testAddChildProductAlreadyExistsInOption() $bundle = $this->createMock(\Magento\Bundle\Model\ResourceModel\Bundle::class); $bundle->expects($this->once())->method('getSelectionsData') ->with($this->linkField) - ->will($this->returnValue($selections)); - $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); + ->willReturn($selections); + $this->bundleFactoryMock->expects($this->once())->method('create')->willReturn($bundle); $this->model->addChild($productMock, 1, $productLink); } @@ -466,49 +476,50 @@ public function testAddChildCouldNotSave() ->setMethods(['getSku', 'getOptionId', 'getSelectionId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); - $productLink->expects($this->any())->method('getSelectionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); + $productLink->expects($this->any())->method('getSelectionId')->willReturn(1); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn($this->linkField); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $productMock->expects($this->any()) ->method('getData') ->with($this->linkField) ->willReturn($this->linkField); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue(13)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->any())->method('getId')->willReturn(13); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->once()) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)->disableOriginalConstructor() ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->willReturn(1); $optionsCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); $optionsCollectionMock->expects($this->once()) ->method('setIdFilter') ->with($this->equalTo(1)) - ->will($this->returnSelf()); + ->willReturnSelf(); $optionsCollectionMock->expects($this->once()) ->method('getFirstItem') - ->will($this->returnValue($option)); - $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( - $this->returnValue($optionsCollectionMock) - ); + ->willReturn($option); + $this->optionCollectionFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($optionsCollectionMock); $selections = [ ['option_id' => 1, 'product_id' => 11], @@ -517,19 +528,17 @@ public function testAddChildCouldNotSave() $bundle = $this->createMock(\Magento\Bundle\Model\ResourceModel\Bundle::class); $bundle->expects($this->once())->method('getSelectionsData') ->with($this->linkField) - ->will($this->returnValue($selections)); - $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); + ->willReturn($selections); + $this->bundleFactoryMock->expects($this->once())->method('create')->willReturn($bundle); $selection = $this->createPartialMock(\Magento\Bundle\Model\Selection::class, ['save']); $selection->expects($this->once())->method('save') - ->will( - $this->returnCallback( - function () { - throw new \Exception('message'); - } - ) + ->willReturnCallback( + function () { + throw new \Exception('message'); + } ); - $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection); $this->model->addChild($productMock, 1, $productLink); } @@ -539,49 +548,50 @@ public function testAddChild() ->setMethods(['getSku', 'getOptionId', 'getSelectionId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue(1)); - $productLink->expects($this->any())->method('getSelectionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getOptionId')->willReturn(1); + $productLink->expects($this->any())->method('getSelectionId')->willReturn(1); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn($this->linkField); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $productMock->expects($this->any()) ->method('getData') ->with($this->linkField) ->willReturn($this->linkField); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue(13)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->any())->method('getId')->willReturn(13); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->once()) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)->disableOriginalConstructor() ->setMethods(['getId', '__wakeup']) ->getMock(); - $option->expects($this->once())->method('getId')->will($this->returnValue(1)); + $option->expects($this->once())->method('getId')->willReturn(1); $optionsCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); $optionsCollectionMock->expects($this->once()) ->method('setIdFilter') ->with($this->equalTo(1)) - ->will($this->returnSelf()); + ->willReturnSelf(); $optionsCollectionMock->expects($this->once()) ->method('getFirstItem') - ->will($this->returnValue($option)); - $this->optionCollectionFactoryMock->expects($this->any())->method('create')->will( - $this->returnValue($optionsCollectionMock) - ); + ->willReturn($option); + $this->optionCollectionFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($optionsCollectionMock); $selections = [ ['option_id' => 1, 'product_id' => 11], @@ -590,13 +600,13 @@ public function testAddChild() $bundle = $this->createMock(\Magento\Bundle\Model\ResourceModel\Bundle::class); $bundle->expects($this->once())->method('getSelectionsData') ->with($this->linkField) - ->will($this->returnValue($selections)); - $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); + ->willReturn($selections); + $this->bundleFactoryMock->expects($this->once())->method('create')->willReturn($bundle); $selection = $this->createPartialMock(\Magento\Bundle\Model\Selection::class, ['save', 'getId']); $selection->expects($this->once())->method('save'); - $selection->expects($this->once())->method('getId')->will($this->returnValue(42)); - $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $selection->expects($this->once())->method('getId')->willReturn(42); + $this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection); $result = $this->model->addChild($productMock, 1, $productLink); $this->assertEquals(42, $result); } @@ -619,44 +629,44 @@ public function testSaveChild() ->setMethods(['getSku', 'getOptionId', 'getSelectionId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); - $productLink->expects($this->any())->method('getOptionId')->will($this->returnValue($optionId)); - $productLink->expects($this->any())->method('getPosition')->will($this->returnValue($position)); - $productLink->expects($this->any())->method('getQty')->will($this->returnValue($qty)); - $productLink->expects($this->any())->method('getPriceType')->will($this->returnValue($priceType)); - $productLink->expects($this->any())->method('getPrice')->will($this->returnValue($price)); - $productLink->expects($this->any())->method('getCanChangeQuantity')->will($this->returnValue($canChangeQuantity)); - $productLink->expects($this->any())->method('getIsDefault')->will($this->returnValue($isDefault)); - $productLink->expects($this->any())->method('getSelectionId')->will($this->returnValue($optionId)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getId')->willReturn($id); + $productLink->expects($this->any())->method('getOptionId')->willReturn($optionId); + $productLink->expects($this->any())->method('getPosition')->willReturn($position); + $productLink->expects($this->any())->method('getQty')->willReturn($qty); + $productLink->expects($this->any())->method('getPriceType')->willReturn($priceType); + $productLink->expects($this->any())->method('getPrice')->willReturn($price); + $productLink->expects($this->any())->method('getCanChangeQuantity')->willReturn($canChangeQuantity); + $productLink->expects($this->any())->method('getIsDefault')->willReturn($isDefault); + $productLink->expects($this->any())->method('getSelectionId')->willReturn($optionId); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn($this->linkField); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $productMock->expects($this->any()) ->method('getData') ->with($this->linkField) ->willReturn($parentProductId); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue($linkProductId)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->any())->method('getId')->willReturn($linkProductId); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->at(0)) ->method('get') ->with($bundleProductSku) - ->will($this->returnValue($productMock)); + ->willReturn($productMock); $this->productRepository ->expects($this->at(1)) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $selection = $this->createPartialMock(\Magento\Bundle\Model\Selection::class, [ 'save', @@ -673,8 +683,8 @@ public function testSaveChild() 'setIsDefault' ]); $selection->expects($this->once())->method('save'); - $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); - $selection->expects($this->any())->method('getId')->will($this->returnValue($id)); + $selection->expects($this->once())->method('load')->with($id)->willReturnSelf(); + $selection->expects($this->any())->method('getId')->willReturn($id); $selection->expects($this->once())->method('setProductId')->with($linkProductId); $selection->expects($this->once())->method('setParentProductId')->with($parentProductId); $selection->expects($this->once())->method('setOptionId')->with($optionId); @@ -685,7 +695,7 @@ public function testSaveChild() $selection->expects($this->once())->method('setSelectionCanChangeQty')->with($canChangeQuantity); $selection->expects($this->once())->method('setIsDefault')->with($isDefault); - $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection); $this->assertTrue($this->model->saveChild($bundleProductSku, $productLink)); } @@ -702,34 +712,34 @@ public function testSaveChildFailedToSave() ->setMethods(['getSku', 'getOptionId', 'getSelectionId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue('linked_product_sku')); - $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); - $productLink->expects($this->any())->method('getSelectionId')->will($this->returnValue(1)); + $productLink->expects($this->any())->method('getSku')->willReturn('linked_product_sku'); + $productLink->expects($this->any())->method('getId')->willReturn($id); + $productLink->expects($this->any())->method('getSelectionId')->willReturn(1); $bundleProductSku = 'bundleProductSku'; $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); - $productMock->expects($this->any())->method('getId')->will($this->returnValue($parentProductId)); + ); + $productMock->expects($this->any())->method('getId')->willReturn($parentProductId); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->any())->method('getId')->will($this->returnValue($linkProductId)); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->any())->method('getId')->willReturn($linkProductId); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->at(0)) ->method('get') ->with($bundleProductSku) - ->will($this->returnValue($productMock)); + ->willReturn($productMock); $this->productRepository ->expects($this->at(1)) ->method('get') ->with('linked_product_sku') - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $store->expects($this->any())->method('getId')->will($this->returnValue(0)); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($store); + $store->expects($this->any())->method('getId')->willReturn(0); $selection = $this->createPartialMock(\Magento\Bundle\Model\Selection::class, [ 'save', @@ -748,11 +758,11 @@ public function testSaveChildFailedToSave() ]); $mockException = $this->createMock(\Exception::class); $selection->expects($this->once())->method('save')->will($this->throwException($mockException)); - $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); - $selection->expects($this->any())->method('getId')->will($this->returnValue($id)); + $selection->expects($this->once())->method('load')->with($id)->willReturnSelf(); + $selection->expects($this->any())->method('getId')->willReturn($id); $selection->expects($this->once())->method('setProductId')->with($linkProductId); - $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection); $this->model->saveChild($bundleProductSku, $productLink); } @@ -764,26 +774,26 @@ public function testSaveChildWithoutId() $bundleProductSku = "bundleSku"; $linkedProductSku = 'simple'; $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getId')->will($this->returnValue(null)); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + $productLink->expects($this->any())->method('getId')->willReturn(null); + $productLink->expects($this->any())->method('getSku')->willReturn($linkedProductSku); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->at(0)) ->method('get') ->with($bundleProductSku) - ->will($this->returnValue($productMock)); + ->willReturn($productMock); $this->productRepository ->expects($this->at(1)) ->method('get') ->with($linkedProductSku) - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $this->model->saveChild($bundleProductSku, $productLink); } @@ -798,35 +808,35 @@ public function testSaveChildWithInvalidId() $linkedProductSku = 'simple'; $bundleProductSku = "bundleProductSku"; $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + $productLink->expects($this->any())->method('getId')->willReturn($id); + $productLink->expects($this->any())->method('getSku')->willReturn($linkedProductSku); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(false)); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(false); $this->productRepository ->expects($this->at(0)) ->method('get') ->with($bundleProductSku) - ->will($this->returnValue($productMock)); + ->willReturn($productMock); $this->productRepository ->expects($this->at(1)) ->method('get') ->with($linkedProductSku) - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $selection = $this->createPartialMock(\Magento\Bundle\Model\Selection::class, [ 'getId', 'load', ]); - $selection->expects($this->once())->method('load')->with($id)->will($this->returnSelf()); - $selection->expects($this->any())->method('getId')->will($this->returnValue(null)); + $selection->expects($this->once())->method('load')->with($id)->willReturnSelf(); + $selection->expects($this->any())->method('getId')->willReturn(null); - $this->bundleSelectionMock->expects($this->once())->method('create')->will($this->returnValue($selection)); + $this->bundleSelectionMock->expects($this->once())->method('create')->willReturn($selection); $this->model->saveChild($bundleProductSku, $productLink); } @@ -840,26 +850,26 @@ public function testSaveChildWithCompositeProductLink() $id = 12; $linkedProductSku = 'simple'; $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + $productLink->expects($this->any())->method('getId')->willReturn($id); + $productLink->expects($this->any())->method('getSku')->willReturn($linkedProductSku); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE - )); + ); $linkedProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $linkedProductMock->expects($this->once())->method('isComposite')->will($this->returnValue(true)); + $linkedProductMock->expects($this->once())->method('isComposite')->willReturn(true); $this->productRepository ->expects($this->at(0)) ->method('get') ->with($bundleProductSku) - ->will($this->returnValue($productMock)); + ->willReturn($productMock); $this->productRepository ->expects($this->at(1)) ->method('get') ->with($linkedProductSku) - ->will($this->returnValue($linkedProductMock)); + ->willReturn($linkedProductMock); $this->model->saveChild($bundleProductSku, $productLink); } @@ -874,13 +884,13 @@ public function testSaveChildWithSimpleProduct() $bundleProductSku = "bundleProductSku"; $productLink = $this->createMock(\Magento\Bundle\Api\Data\LinkInterface::class); - $productLink->expects($this->any())->method('getId')->will($this->returnValue($id)); - $productLink->expects($this->any())->method('getSku')->will($this->returnValue($linkedProductSku)); + $productLink->expects($this->any())->method('getId')->willReturn($id); + $productLink->expects($this->any())->method('getSku')->willReturn($linkedProductSku); $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $productMock->expects($this->once())->method('getTypeId')->will($this->returnValue( + $productMock->expects($this->once())->method('getTypeId')->willReturn( \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE - )); + ); $this->productRepository->expects($this->once())->method('get')->with($bundleProductSku) ->willReturn($productMock); @@ -890,9 +900,9 @@ public function testSaveChildWithSimpleProduct() public function testRemoveChild() { - $this->productRepository->expects($this->any())->method('get')->will($this->returnValue($this->product)); + $this->productRepository->expects($this->any())->method('get')->willReturn($this->product); $bundle = $this->createMock(\Magento\Bundle\Model\ResourceModel\Bundle::class); - $this->bundleFactoryMock->expects($this->once())->method('create')->will($this->returnValue($bundle)); + $this->bundleFactoryMock->expects($this->once())->method('create')->willReturn($bundle); $productSku = 'productSku'; $optionId = 1; $productId = 1; @@ -901,7 +911,7 @@ public function testRemoveChild() $this->product ->expects($this->any()) ->method('getTypeId') - ->will($this->returnValue(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE)); + ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); $this->getRemoveOptions(); @@ -909,12 +919,12 @@ public function testRemoveChild() ->setMethods(['getSku', 'getOptionId', 'getSelectionId', 'getProductId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); - $selection->expects($this->any())->method('getSku')->will($this->returnValue($childSku)); - $selection->expects($this->any())->method('getOptionId')->will($this->returnValue($optionId)); - $selection->expects($this->any())->method('getSelectionId')->will($this->returnValue(55)); + $selection->expects($this->any())->method('getSku')->willReturn($childSku); + $selection->expects($this->any())->method('getOptionId')->willReturn($optionId); + $selection->expects($this->any())->method('getSelectionId')->willReturn(55); $selection->expects($this->any())->method('getProductId')->willReturn($productId); - $this->option->expects($this->any())->method('getSelections')->will($this->returnValue([$selection])); + $this->option->expects($this->any())->method('getSelections')->willReturn([$selection]); $this->metadataMock->expects($this->any())->method('getLinkField')->willReturn($this->linkField); $this->product->expects($this->any()) ->method('getData') @@ -932,14 +942,14 @@ public function testRemoveChild() */ public function testRemoveChildForbidden() { - $this->productRepository->expects($this->any())->method('get')->will($this->returnValue($this->product)); + $this->productRepository->expects($this->any())->method('get')->willReturn($this->product); $productSku = 'productSku'; $optionId = 1; $childSku = 'childSku'; $this->product ->expects($this->any()) ->method('getTypeId') - ->will($this->returnValue(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)); + ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE); $this->model->removeChild($productSku, $optionId, $childSku); } @@ -948,7 +958,7 @@ public function testRemoveChildForbidden() */ public function testRemoveChildInvalidOptionId() { - $this->productRepository->expects($this->any())->method('get')->will($this->returnValue($this->product)); + $this->productRepository->expects($this->any())->method('get')->willReturn($this->product); $productSku = 'productSku'; $optionId = 1; $childSku = 'childSku'; @@ -956,7 +966,7 @@ public function testRemoveChildInvalidOptionId() $this->product ->expects($this->any()) ->method('getTypeId') - ->will($this->returnValue(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE)); + ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); $this->getRemoveOptions(); @@ -964,12 +974,12 @@ public function testRemoveChildInvalidOptionId() ->setMethods(['getSku', 'getOptionId', 'getSelectionId', 'getProductId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); - $selection->expects($this->any())->method('getSku')->will($this->returnValue($childSku)); - $selection->expects($this->any())->method('getOptionId')->will($this->returnValue($optionId + 1)); - $selection->expects($this->any())->method('getSelectionId')->will($this->returnValue(55)); - $selection->expects($this->any())->method('getProductId')->will($this->returnValue(1)); + $selection->expects($this->any())->method('getSku')->willReturn($childSku); + $selection->expects($this->any())->method('getOptionId')->willReturn($optionId + 1); + $selection->expects($this->any())->method('getSelectionId')->willReturn(55); + $selection->expects($this->any())->method('getProductId')->willReturn(1); - $this->option->expects($this->any())->method('getSelections')->will($this->returnValue([$selection])); + $this->option->expects($this->any())->method('getSelections')->willReturn([$selection]); $this->model->removeChild($productSku, $optionId, $childSku); } @@ -978,7 +988,7 @@ public function testRemoveChildInvalidOptionId() */ public function testRemoveChildInvalidChildSku() { - $this->productRepository->expects($this->any())->method('get')->will($this->returnValue($this->product)); + $this->productRepository->expects($this->any())->method('get')->willReturn($this->product); $productSku = 'productSku'; $optionId = 1; $childSku = 'childSku'; @@ -986,7 +996,7 @@ public function testRemoveChildInvalidChildSku() $this->product ->expects($this->any()) ->method('getTypeId') - ->will($this->returnValue(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE)); + ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); $this->getRemoveOptions(); @@ -994,45 +1004,45 @@ public function testRemoveChildInvalidChildSku() ->setMethods(['getSku', 'getOptionId', 'getSelectionId', 'getProductId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); - $selection->expects($this->any())->method('getSku')->will($this->returnValue($childSku . '_invalid')); - $selection->expects($this->any())->method('getOptionId')->will($this->returnValue($optionId)); - $selection->expects($this->any())->method('getSelectionId')->will($this->returnValue(55)); - $selection->expects($this->any())->method('getProductId')->will($this->returnValue(1)); + $selection->expects($this->any())->method('getSku')->willReturn($childSku . '_invalid'); + $selection->expects($this->any())->method('getOptionId')->willReturn($optionId); + $selection->expects($this->any())->method('getSelectionId')->willReturn(55); + $selection->expects($this->any())->method('getProductId')->willReturn(1); - $this->option->expects($this->any())->method('getSelections')->will($this->returnValue([$selection])); + $this->option->expects($this->any())->method('getSelections')->willReturn([$selection]); $this->model->removeChild($productSku, $optionId, $childSku); } private function getOptions() { - $this->product->expects($this->any())->method('getTypeInstance')->will($this->returnValue($this->productType)); - $this->product->expects($this->once())->method('getStoreId')->will($this->returnValue($this->storeId)); + $this->product->expects($this->any())->method('getTypeInstance')->willReturn($this->productType); + $this->product->expects($this->once())->method('getStoreId')->willReturn($this->storeId); $this->productType->expects($this->once())->method('setStoreFilter') ->with($this->equalTo($this->storeId), $this->equalTo($this->product)); $this->productType->expects($this->once())->method('getOptionsCollection') ->with($this->equalTo($this->product)) - ->will($this->returnValue($this->optionCollection)); + ->willReturn($this->optionCollection); } public function getRemoveOptions() { - $this->product->expects($this->any())->method('getTypeInstance')->will($this->returnValue($this->productType)); - $this->product->expects($this->once())->method('getStoreId')->will($this->returnValue(1)); + $this->product->expects($this->any())->method('getTypeInstance')->willReturn($this->productType); + $this->product->expects($this->once())->method('getStoreId')->willReturn(1); $this->productType->expects($this->once())->method('setStoreFilter'); $this->productType->expects($this->once())->method('getOptionsCollection') ->with($this->equalTo($this->product)) - ->will($this->returnValue($this->optionCollection)); + ->willReturn($this->optionCollection); $this->productType->expects($this->once())->method('getOptionsIds')->with($this->equalTo($this->product)) - ->will($this->returnValue([1, 2, 3])); + ->willReturn([1, 2, 3]); $this->productType->expects($this->once())->method('getSelectionsCollection') - ->will($this->returnValue([])); + ->willReturn([]); $this->optionCollection->expects($this->any())->method('appendSelections') ->with($this->equalTo([]), true) - ->will($this->returnValue([$this->option])); + ->willReturn([$this->option]); } } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php index 45d0208c1d575..d438dc2e9b216 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php @@ -5,8 +5,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Test\Unit\Model; use Magento\Bundle\Model\OptionRepository; @@ -89,7 +87,10 @@ protected function setUp() $this->dataObjectHelperMock = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) ->disableOriginalConstructor() ->getMock(); - $this->optionResourceMock = $this->createPartialMock(\Magento\Bundle\Model\ResourceModel\Option::class, ['delete', '__wakeup', 'save', 'removeOptionSelections']); + $this->optionResourceMock = $this->createPartialMock( + \Magento\Bundle\Model\ResourceModel\Option::class, + ['delete', '__wakeup', 'save', 'removeOptionSelections'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->linkManagementMock = $this->createMock(\Magento\Bundle\Api\ProductLinkManagementInterface::class); $this->optionListMock = $this->createMock(\Magento\Bundle\Model\Product\OptionList::class); @@ -166,7 +167,10 @@ public function testGet() $optionId = 100; $optionData = ['title' => 'option title']; - $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku']); + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku'] + ); $productMock->expects($this->once()) ->method('getTypeId') ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); @@ -292,7 +296,6 @@ public function testSaveExistingOption() ); $optionCollectionMock->expects($this->once())->method('getFirstItem')->willReturn($optionMock); - $metadataMock = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class); $metadataMock->expects($this->once())->method('getLinkField')->willReturn('product_option'); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/UpdatePriceInQuoteItemOptionsTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/UpdatePriceInQuoteItemOptionsTest.php new file mode 100644 index 0000000000000..0405a22773c37 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/UpdatePriceInQuoteItemOptionsTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Bundle\Test\Unit\Model\Plugin; + +use Magento\Bundle\Model\Plugin\UpdatePriceInQuoteItemOptions; +use Magento\Catalog\Model\Product; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option; + +/** + * Test for Magento\Bundle\Model\Plugin\UpdatePriceInQuoteItemOptions class. + */ +class UpdatePriceInQuoteItemOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializerMock; + + /** + * @var QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private $subjectMock; + + /** + * @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultMock; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var Option|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteItemOptionMock; + + /** + * @var UpdatePriceInQuoteItemOptions + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->serializerMock = $this->createMock(SerializerInterface::class); + $this->subjectMock = $this->createMock(QuoteItem::class); + $this->resultMock = $this->createMock(AbstractItem::class); + $this->productMock = $this->createMock(Product::class); + $this->quoteItemOptionMock = $this->createMock(Option::class); + + $this->model = new UpdatePriceInQuoteItemOptions($this->serializerMock); + } + + /** + * @return void + */ + public function testAfterCalcRowTotalWithBundleOption() + { + $bundleAttributeValue = '{"price":100,"qty":1,"option_label":"option1","option_id":"1"}'; + $parsedValue = [ + 'price' => 100, + 'qty' => 1, + 'option_label' => 'option1', + 'option_id' => "1", + ]; + + $this->resultMock->expects($this->once()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->productMock->expects($this->once()) + ->method('getCustomOption') + ->with('bundle_selection_attributes') + ->willReturn($this->quoteItemOptionMock); + $this->resultMock->expects($this->once()) + ->method('getPrice') + ->willReturn(100); + $this->resultMock->expects($this->once()) + ->method('getQty') + ->willReturn(1); + $this->quoteItemOptionMock->expects($this->once()) + ->method('getValue') + ->willReturn($bundleAttributeValue); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->with($bundleAttributeValue) + ->willReturn($parsedValue); + $this->serializerMock->expects($this->once()) + ->method('serialize') + ->with($parsedValue) + ->willReturn($bundleAttributeValue); + + $this->model->afterCalcRowTotal($this->subjectMock, $this->resultMock); + } + + /** + * @return void + */ + public function testAfterCalcRowTotalWithoutBundleOption() + { + $this->resultMock->expects($this->once()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->productMock->expects($this->once()) + ->method('getCustomOption') + ->with('bundle_selection_attributes') + ->willReturn(null); + $this->resultMock->expects($this->never()) + ->method('getPrice'); + $this->resultMock->expects($this->never()) + ->method('getQty'); + $this->quoteItemOptionMock->expects($this->never()) + ->method('getValue'); + $this->serializerMock->expects($this->never()) + ->method('unserialize'); + $this->serializerMock->expects($this->never()) + ->method('serialize'); + + $this->model->afterCalcRowTotal($this->subjectMock, $this->resultMock); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php index 831098cc44c38..4df60d07d98ef 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor; use Magento\Bundle\Api\Data\BundleOptionInterface; +use Magento\Bundle\Model\Link; use Magento\Bundle\Model\Product\CopyConstructor\Bundle; use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; @@ -45,6 +46,7 @@ public function testBuildNegative() */ public function testBuildPositive() { + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); @@ -60,18 +62,42 @@ public function testBuildPositive() ->method('getExtensionAttributes') ->willReturn($extensionAttributesProduct); + $productLink = $this->getMockBuilder(Link::class) + ->setMethods(['setSelectionId']) + ->disableOriginalConstructor() + ->getMock(); + $productLink->expects($this->exactly(2)) + ->method('setSelectionId') + ->with($this->identicalTo(null)); + $firstOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $firstOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $firstOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); + $secondOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $secondOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $secondOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); $bundleOptions = [ - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(), - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass() + $firstOption, + $secondOption ]; $extensionAttributesProduct->expects($this->once()) ->method('getBundleProductOptions') ->willReturn($bundleOptions); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $duplicate */ $duplicate = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php new file mode 100644 index 0000000000000..41e6c2c160e0a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php @@ -0,0 +1,279 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Unit\Model\Product; + +use Magento\Bundle\Api\Data\LinkInterface; +use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Api\ProductOptionRepositoryInterface; +use Magento\Bundle\Model\Product\SaveHandler; +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Unit tests for \Magento\Bundle\Model\Product\SaveHandler class. + */ +class SaveHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductInterface|MockObject + */ + private $productMock; + + /** + * @var ProductExtensionInterface|MockObject + */ + private $productExtensionMock; + + /** + * @var OptionInterface|MockObject + */ + private $optionMock; + + /** + * @var ProductOptionRepositoryInterface|MockObject + */ + private $optionRepositoryMock; + + /** + * @var ProductLinkManagementInterface|MockObject + */ + private $productLinkManagementMock; + + /** + * @var LinkInterface|MockObject + */ + private $linkMock; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPoolMock; + + /** + * @var EntityMetadataInterface|MockObject + */ + private $metadataMock; + + /** + * @var SaveHandler + */ + private $saveHandler; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->productMock = $this->getMockBuilder(ProductInterface::class) + ->setMethods( + [ + 'getExtensionAttributes', + 'getCopyFromView', + 'getData', + 'getTypeId', + 'getSku', + ] + ) + ->getMockForAbstractClass(); + $this->productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['getBundleProductOptions']) + ->getMockForAbstractClass(); + $this->optionMock = $this->getMockBuilder(OptionInterface::class) + ->setMethods( + [ + 'setParentId', + 'getId', + 'getOptionId', + ] + ) + ->getMockForAbstractClass(); + $this->optionRepositoryMock = $this->createMock(ProductOptionRepositoryInterface::class); + $this->productLinkManagementMock = $this->createMock(ProductLinkManagementInterface::class); + $this->linkMock = $this->createMock(LinkInterface::class); + $this->metadataPoolMock = $this->createMock(MetadataPool::class); + $this->metadataMock = $this->createMock(EntityMetadataInterface::class); + $this->metadataPoolMock->expects($this->any()) + ->method('getMetadata') + ->willReturn($this->metadataMock); + + $this->saveHandler = $this->objectManager->getObject( + SaveHandler::class, + [ + 'optionRepository' => $this->optionRepositoryMock, + 'productLinkManagement' => $this->productLinkManagementMock, + 'metadataPool' => $this->metadataPoolMock, + ] + ); + } + + /** + * @return void + */ + public function testExecuteWithInvalidProductType() + { + $productType = 'simple'; + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([]); + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $entity = $this->saveHandler->execute($this->productMock); + $this->assertSame($this->productMock, $entity); + } + + /** + * @return void + */ + public function testExecuteWithoutExistingOption() + { + $productType = 'bundle'; + $productSku = 'product-sku'; + $optionId = null; + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([$this->optionMock]); + + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $this->productMock->expects($this->once()) + ->method('getSku') + ->willReturn($productSku); + $this->optionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->willReturn([]); + + $this->optionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($optionId); + + $this->productMock->expects($this->once()) + ->method('getCopyFromView') + ->willReturn(false); + + $this->optionMock->expects($this->never())->method('setOptionId'); + $this->optionRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->productMock, $this->optionMock) + ->willReturn($optionId); + + $this->saveHandler->execute($this->productMock); + } + + /** + * @return void + */ + public function testExecuteWithExistingOption() + { + $productType = 'bundle'; + $productSku = 'product-sku'; + $productLinkSku = 'product-link-sku'; + $linkField = 'entity_id'; + $parentId = 1; + $existingOptionId = 1; + $optionId = 2; + + /** @var OptionInterface|MockObject $existingOptionMock */ + $existingOptionMock = $this->getMockBuilder(OptionInterface::class) + ->setMethods(['getOptionId']) + ->getMockForAbstractClass(); + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([$this->optionMock]); + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $this->productMock->expects($this->exactly(3)) + ->method('getSku') + ->willReturn($productSku); + $this->optionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->willReturn([$existingOptionMock]); + + $existingOptionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($existingOptionId); + $this->optionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($optionId); + + $this->productMock->expects($this->once()) + ->method('getCopyFromView') + ->willReturn(false); + $this->metadataMock->expects($this->once()) + ->method('getLinkField') + ->willReturn($linkField); + $this->productMock->expects($this->once()) + ->method('getData') + ->with($linkField) + ->willReturn($parentId); + + $this->optionRepositoryMock->expects($this->once()) + ->method('get') + ->with($productSku, $existingOptionId) + ->willReturn($this->optionMock); + $this->optionMock->expects($this->once()) + ->method('setParentId') + ->with($parentId) + ->willReturnSelf(); + $this->optionMock->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$this->linkMock]); + $this->linkMock->expects($this->once()) + ->method('getSku') + ->willReturn($productLinkSku); + + $this->optionMock->expects($this->any()) + ->method('getId') + ->willReturn($existingOptionId); + $this->productLinkManagementMock->expects($this->once()) + ->method('removeChild') + ->with($productSku, $existingOptionId, $productLinkSku) + ->willReturn(true); + $this->optionRepositoryMock->expects($this->once()) + ->method('delete') + ->with($this->optionMock) + ->willReturn(true); + + $this->optionRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->productMock, $this->optionMock) + ->willReturn($optionId); + + $this->saveHandler->execute($this->productMock); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index d5a11e0310d35..2b4f81337da52 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -513,10 +513,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('getSelectionId') ->willReturn(314); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals([$product, $productType], $result); } @@ -737,10 +733,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('prepareForCart') ->willReturn([]); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals('We can\'t add this item to your shopping cart right now.', $result); } @@ -961,10 +953,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('prepareForCart') ->willReturn('string'); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals('string', $result); } @@ -1595,7 +1583,7 @@ public function testGetSkuWithoutType() ->disableOriginalConstructor() ->getMock(); $selectionItemMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->setMethods(['getSku', '__wakeup']) + ->setMethods(['getSku', 'getEntityId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); @@ -1623,9 +1611,12 @@ public function testGetSkuWithoutType() ->will($this->returnValue($serializeIds)); $selectionMock = $this->getSelectionsByIdsMock($selectionIds, $productMock, 5, 6); $selectionMock->expects(($this->any())) - ->method('getItems') - ->will($this->returnValue([$selectionItemMock])); - $selectionItemMock->expects($this->any()) + ->method('getItemByColumnValue') + ->will($this->returnValue($selectionItemMock)); + $selectionItemMock->expects($this->at(0)) + ->method('getEntityId') + ->will($this->returnValue(1)); + $selectionItemMock->expects($this->once()) ->method('getSku') ->will($this->returnValue($itemSku)); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php deleted file mode 100644 index cbe34639e8267..0000000000000 --- a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Selection/CollectionTest.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Bundle\Test\Unit\Model\ResourceModel\Selection; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Framework\Validator\UniversalFactory; -use Magento\Eav\Model\Entity\AbstractEntity; -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -use Magento\Framework\DB\Select; - -/** - * Class CollectionTest. - */ -class CollectionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $storeManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $store; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $universalFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $entity; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $adapter; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $select; - - /** - * @var \Magento\Bundle\Model\ResourceModel\Selection\Collection - */ - private $model; - - protected function setUp() - { - $objectManager = new ObjectManager($this); - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->store = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->universalFactory = $this->getMockBuilder(UniversalFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->entity = $this->getMockBuilder(AbstractEntity::class) - ->disableOriginalConstructor() - ->getMock(); - $this->adapter = $this->getMockBuilder(AdapterInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->select = $this->getMockBuilder(Select::class) - ->disableOriginalConstructor() - ->getMock(); - $factory = $this->getMockBuilder(ProductLimitationFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - $this->store->expects($this->any()) - ->method('getId') - ->willReturn(1); - $this->universalFactory->expects($this->any()) - ->method('create') - ->willReturn($this->entity); - $this->entity->expects($this->any()) - ->method('getConnection') - ->willReturn($this->adapter); - $this->entity->expects($this->any()) - ->method('getDefaultAttributes') - ->willReturn([]); - $this->adapter->expects($this->any()) - ->method('select') - ->willReturn($this->select); - - $this->model = $objectManager->getObject( - \Magento\Bundle\Model\ResourceModel\Selection\Collection::class, - [ - 'storeManager' => $this->storeManager, - 'universalFactory' => $this->universalFactory, - 'productLimitationFactory' => $factory - ] - ); - } - - public function testAddQuantityFilter() - { - $tableName = 'cataloginventory_stock_status'; - $this->entity->expects($this->once()) - ->method('getTable') - ->willReturn($tableName); - $this->select->expects($this->once()) - ->method('joinInner') - ->with( - ['stock' => $tableName], - 'selection.product_id = stock.product_id', - [] - )->willReturnSelf(); - $this->assertEquals($this->model, $this->model->addQuantityFilter()); - } -} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php index ecce34363819e..3e9aeaed5c5b4 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/AbstractItemsTest.php @@ -49,6 +49,9 @@ public function testGetChildrenEmptyItems($class, $method, $returnClass) $this->assertSame(null, $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenEmptyItemsDataProvider() { return [ @@ -97,6 +100,9 @@ public function testGetChildren($parentItem) $this->assertSame([2 => $this->orderItem], $this->model->getChildren($item)); } + /** + * @return array + */ public function getChildrenDataProvider() { return [ @@ -116,6 +122,9 @@ public function testIsShipmentSeparatelyWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isShipmentSeparately()); } + /** + * @return array + */ public function isShipmentSeparatelyWithoutItemDataProvider() { return [ @@ -146,6 +155,9 @@ public function testIsShipmentSeparatelyWithItem($productOptions, $result, $pare $this->assertSame($result, $this->model->isShipmentSeparately($this->orderItem)); } + /** + * @return array + */ public function isShipmentSeparatelyWithItemDataProvider() { return [ @@ -167,6 +179,9 @@ public function testIsChildCalculatedWithoutItem($productOptions, $result) $this->assertSame($result, $this->model->isChildCalculated()); } + /** + * @return array + */ public function isChildCalculatedWithoutItemDataProvider() { return [ @@ -197,6 +212,9 @@ public function testIsChildCalculatedWithItem($productOptions, $result, $parentI $this->assertSame($result, $this->model->isChildCalculated($this->orderItem)); } + /** + * @return array + */ public function isChildCalculatedWithItemDataProvider() { return [ @@ -217,6 +235,9 @@ public function testGetBundleOptions($productOptions, $result) $this->assertSame($result, $this->model->getBundleOptions()); } + /** + * @return array + */ public function getBundleOptionsDataProvider() { return [ @@ -277,6 +298,9 @@ public function testCanShowPriceInfo($parentItem, $productOptions, $result) $this->assertSame($result, $this->model->canShowPriceInfo($this->orderItem)); } + /** + * @return array + */ public function canShowPriceInfoDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php index f7f6b30daa300..423155661f1ef 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/CalculatorTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Test\Unit\Pricing\Adjustment; use Magento\Bundle\Model\ResourceModel\Selection\Collection; @@ -110,7 +108,8 @@ function ($type) { \Magento\Bundle\Pricing\Adjustment\SelectionPriceListProviderInterface::class )->getMock(); - $this->model = (new ObjectManager($this))->getObject(\Magento\Bundle\Pricing\Adjustment\Calculator::class, + $this->model = (new ObjectManager($this))->getObject( + \Magento\Bundle\Pricing\Adjustment\Calculator::class, [ 'calculator' => $this->baseCalculator, 'amountFactory' => $this->amountFactory, @@ -577,7 +576,8 @@ public function testGetOptionsAmount($searchMin, $useRegularPrice) $result = $calculatorMock->getOptionsAmount( $this->saleableItem, - $exclude, $searchMin, + $exclude, + $searchMin, $amount, $useRegularPrice ); @@ -585,6 +585,9 @@ public function testGetOptionsAmount($searchMin, $useRegularPrice) $this->assertEquals($expectedResult, $result, 'Incorrect result'); } + /** + * @return array + */ public function getOptionsAmountDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php index b6485d0e441e9..8be7de774bab8 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionPriceTest.php @@ -8,95 +8,42 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ class BundleOptionPriceTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Bundle\Pricing\Price\BundleOptionPrice */ - protected $bundleOptionPrice; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $baseCalculator; + private $bundleOptionPrice; /** * @var ObjectManagerHelper */ - protected $objectManagerHelper; + private $objectManagerHelper; /** * @var \Magento\Framework\Pricing\SaleableInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $saleableItemMock; + private $saleableItemMock; /** * @var \Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $bundleCalculatorMock; + private $bundleCalculatorMock; /** - * @var \Magento\Bundle\Pricing\Price\BundleSelectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Bundle\Pricing\Price\BundleOptions|\PHPUnit_Framework_MockObject_MockObject */ - protected $selectionFactoryMock; + private $bundleOptionsMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - protected $amountFactory; - - /** - * @var \Magento\Framework\Pricing\PriceInfo\Base|\PHPUnit_Framework_MockObject_MockObject - */ - protected $priceInfoMock; - protected function setUp() { - $this->priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class); + $this->bundleOptionsMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleOptions::class); $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class)->getMock(); - $this->saleableItemMock->expects($this->once()) - ->method('getPriceInfo') - ->will($this->returnValue($this->priceInfoMock)); + $this->bundleCalculatorMock = $this->createMock(\Magento\Bundle\Pricing\Adjustment\Calculator::class); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $priceCurrency->expects($this->any())->method('round')->will($this->returnArgument(0)); - - $this->saleableItemMock->expects($this->once()) - ->method('setQty') - ->will($this->returnSelf()); - - $this->saleableItemMock->expects($this->any()) - ->method('getStore') - ->will($this->returnValue($store)); - - $this->selectionFactoryMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Price\BundleSelectionFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->amountFactory = $this->createMock(\Magento\Framework\Pricing\Amount\AmountFactory::class); - $factoryCallback = $this->returnCallback( - function ($fullAmount, $adjustments) { - return $this->createAmountMock(['amount' => $fullAmount, 'adjustmentAmounts' => $adjustments]); - } - ); - $this->amountFactory->expects($this->any())->method('create')->will($factoryCallback); - $this->baseCalculator = $this->createMock(\Magento\Framework\Pricing\Adjustment\Calculator::class); - - $taxData = $this->getMockBuilder(\Magento\Tax\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->bundleCalculatorMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Adjustment\Calculator::class) - ->setConstructorArgs( - [$this->baseCalculator, $this->amountFactory, $this->selectionFactoryMock, $taxData, $priceCurrency] - ) - ->setMethods(['getOptionsAmount']) - ->getMock(); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->bundleOptionPrice = $this->objectManagerHelper->getObject( \Magento\Bundle\Pricing\Price\BundleOptionPrice::class, @@ -104,104 +51,47 @@ function ($fullAmount, $adjustments) { 'saleableItem' => $this->saleableItemMock, 'quantity' => 1., 'calculator' => $this->bundleCalculatorMock, - 'bundleSelectionFactory' => $this->selectionFactoryMock + 'bundleOptions' => $this->bundleOptionsMock ] ); } /** - * @dataProvider getOptionsDataProvider - */ - public function testGetOptions($selectionCollection) - { - $this->prepareOptionMocks($selectionCollection); - $this->assertSame($selectionCollection, $this->bundleOptionPrice->getOptions()); - $this->assertSame($selectionCollection, $this->bundleOptionPrice->getOptions()); - } - - /** - * @param array $selectionCollection + * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getOptions + * * @return void */ - protected function prepareOptionMocks($selectionCollection) + public function testGetOptions() { - $this->saleableItemMock->expects($this->atLeastOnce()) - ->method('getStoreId') - ->will($this->returnValue(1)); - - $priceTypeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class); - $priceTypeMock->expects($this->atLeastOnce()) - ->method('setStoreFilter') - ->with($this->equalTo(1), $this->equalTo($this->saleableItemMock)) - ->will($this->returnSelf()); - - $optionIds = ['41', '55']; - $priceTypeMock->expects($this->atLeastOnce()) - ->method('getOptionsIds') - ->with($this->equalTo($this->saleableItemMock)) - ->will($this->returnValue($optionIds)); - - $priceTypeMock->expects($this->atLeastOnce()) - ->method('getSelectionsCollection') - ->with($this->equalTo($optionIds), $this->equalTo($this->saleableItemMock)) - ->will($this->returnValue($selectionCollection)); - $collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); - $collection->expects($this->atLeastOnce()) - ->method('appendSelections') - ->with($this->equalTo($selectionCollection), $this->equalTo(true), $this->equalTo(false)) - ->will($this->returnValue($selectionCollection)); - - $priceTypeMock->expects($this->atLeastOnce()) - ->method('getOptionsCollection') - ->with($this->equalTo($this->saleableItemMock)) + $this->bundleOptionsMock->expects($this->any()) + ->method('getOptions') ->will($this->returnValue($collection)); - - $this->saleableItemMock->expects($this->atLeastOnce()) - ->method('getTypeInstance') - ->will($this->returnValue($priceTypeMock)); - } - - public function getOptionsDataProvider() - { - return [ - ['1', '2'] - ]; + $this->assertEquals($collection, $this->bundleOptionPrice->getOptions()); } /** - * @param float $selectionQty - * @param float|bool $selectionAmount - * @dataProvider selectionAmountDataProvider + * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getOptionSelectionAmount + * + * @return void */ - public function testGetOptionSelectionAmount($selectionQty, $selectionAmount) + public function testGetOptionSelectionAmount() { - $selection = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getSelectionQty', '__wakeup']); - $selection->expects($this->once()) - ->method('getSelectionQty') - ->will($this->returnValue($selectionQty)); - $priceMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleSelectionPrice::class); - $priceMock->expects($this->once()) - ->method('getAmount') - ->will($this->returnValue($selectionAmount)); - $this->selectionFactoryMock->expects($this->once()) - ->method('create') - ->with($this->equalTo($this->saleableItemMock), $this->equalTo($selection), $this->equalTo($selectionQty)) - ->will($this->returnValue($priceMock)); - $this->assertSame($selectionAmount, $this->bundleOptionPrice->getOptionSelectionAmount($selection)); + $selectionAmount = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $product = $this->createMock(\Magento\Catalog\Model\Product::class); + $selection = $this->createMock(\Magento\Bundle\Model\Selection::class); + $this->bundleOptionsMock->expects($this->any()) + ->method('getOptionSelectionAmount') + ->will($this->returnValue($selectionAmount)) + ->with($product, $selection, false); + $this->assertEquals($selectionAmount, $this->bundleOptionPrice->getOptionSelectionAmount($selection)); } /** - * @return array + * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getAmount + * + * @return void */ - public function selectionAmountDataProvider() - { - return [ - [1., 50.5], - [2.2, false] - ]; - } - public function testGetAmount() { $amountMock = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class); @@ -213,204 +103,14 @@ public function testGetAmount() } /** - * Create amount mock - * - * @param array $amountData - * @return \Magento\Framework\Pricing\Amount\Base|\PHPUnit_Framework_MockObject_MockObject - */ - protected function createAmountMock($amountData) - { - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Pricing\Amount\Base $amount */ - $amount = $this->createMock(\Magento\Framework\Pricing\Amount\Base::class); - $amount->expects($this->any())->method('getAdjustmentAmounts')->will( - $this->returnValue(isset($amountData['adjustmentAmounts']) ? $amountData['adjustmentAmounts'] : []) - ); - $amount->expects($this->any())->method('getValue')->will($this->returnValue($amountData['amount'])); - return $amount; - } - - /** - * Create option mock + * Test method \Magento\Bundle\Pricing\Price\BundleOptionPrice::getValue * - * @param array $optionData - * @return \Magento\Bundle\Model\Option|\PHPUnit_Framework_MockObject_MockObject - */ - protected function createOptionMock($optionData) - { - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Bundle\Model\Option $option */ - $option = $this->createPartialMock(\Magento\Bundle\Model\Option::class, ['isMultiSelection', '__wakeup']); - $option->expects($this->any())->method('isMultiSelection') - ->will($this->returnValue($optionData['isMultiSelection'])); - $selections = []; - foreach ($optionData['selections'] as $selectionData) { - $selections[] = $this->createSelectionMock($selectionData); - } - foreach ($optionData['data'] as $key => $value) { - $option->setData($key, $value); - } - $option->setData('selections', $selections); - return $option; - } - - /** - * Create selection product mock - * - * @param array $selectionData - * @return \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject - */ - protected function createSelectionMock($selectionData) - { - $selection = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['isSalable', 'getAmount', 'getQuantity', 'getProduct', '__wakeup']) - ->disableOriginalConstructor() - ->getMock(); - - // All items are saleable - $selection->expects($this->any())->method('isSalable')->will($this->returnValue(true)); - foreach ($selectionData['data'] as $key => $value) { - $selection->setData($key, $value); - } - $amountMock = $this->createAmountMock($selectionData['amount']); - $selection->expects($this->any())->method('getAmount')->will($this->returnValue($amountMock)); - $selection->expects($this->any())->method('getQuantity')->will($this->returnValue(1)); - - $innerProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['getSelectionCanChangeQty', '__wakeup']) - ->disableOriginalConstructor() - ->getMock(); - $innerProduct->expects($this->any())->method('getSelectionCanChangeQty')->will($this->returnValue(true)); - $selection->expects($this->any())->method('getProduct')->will($this->returnValue($innerProduct)); - - return $selection; - } - - /** - * @dataProvider getTestDataForCalculation - */ - public function testCalculation($optionList, $expected) - { - $storeId = 1; - $this->saleableItemMock->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId)); - $this->selectionFactoryMock->expects($this->any())->method('create')->will($this->returnArgument(1)); - - $this->baseCalculator->expects($this->atLeastOnce())->method('getAmount') - ->will($this->returnValue($this->createAmountMock(['amount' => 0.]))); - - $options = []; - foreach ($optionList as $optionData) { - $options[] = $this->createOptionMock($optionData); - } - /** @var \PHPUnit_Framework_MockObject_MockObject $optionsCollection */ - $optionsCollection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); - $optionsCollection->expects($this->atLeastOnce())->method('appendSelections')->will($this->returnSelf()); - $optionsCollection->expects($this->atLeastOnce())->method('getIterator') - ->will($this->returnValue(new \ArrayIterator($options))); - - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Type\AbstractType $typeMock */ - $typeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class); - $typeMock->expects($this->any())->method('setStoreFilter')->with($storeId, $this->saleableItemMock); - $typeMock->expects($this->any())->method('getOptionsCollection')->with($this->saleableItemMock) - ->will($this->returnValue($optionsCollection)); - $this->saleableItemMock->expects($this->any())->method('getTypeInstance')->will($this->returnValue($typeMock)); - - $this->assertEquals($expected['min'], $this->bundleOptionPrice->getValue()); - $this->assertEquals($expected['max'], $this->bundleOptionPrice->getMaxValue()); - } - - /** - * @return array + * @return void */ - public function getTestDataForCalculation() + public function testGetValue() { - return [ - 'first case' => [ - 'optionList' => [ - // first option with single choice of product - [ - 'isMultiSelection' => false, - 'data' => [ - 'title' => 'test option 1', - 'default_title' => 'test option 1', - 'type' => 'select', - 'option_id' => '1', - 'position' => '0', - 'required' => '1', - ], - 'selections' => [ - [ - 'data' => ['price' => 70.], - 'amount' => ['amount' => 70], - ], - [ - 'data' => ['price' => 80.], - 'amount' => ['amount' => 80] - ], - [ - 'data' => ['price' => 50.], - 'amount' => ['amount' => 50] - ], - ] - ], - // second not required option - [ - 'isMultiSelection' => false, - 'data' => [ - 'title' => 'test option 2', - 'default_title' => 'test option 2', - 'type' => 'select', - 'option_id' => '2', - 'position' => '1', - 'required' => '0', - ], - 'selections' => [ - [ - 'data' => ['value' => 20.], - 'amount' => ['amount' => 20], - ], - ] - ], - // third with multi-selection - [ - 'isMultiSelection' => true, - 'data' => [ - 'title' => 'test option 3', - 'default_title' => 'test option 3', - 'type' => 'select', - 'option_id' => '3', - 'position' => '2', - 'required' => '1', - ], - 'selections' => [ - [ - 'data' => ['price' => 40.], - 'amount' => ['amount' => 40], - ], - [ - 'data' => ['price' => 20.], - 'amount' => ['amount' => 20] - ], - [ - 'data' => ['price' => 60.], - 'amount' => ['amount' => 60] - ], - ] - ], - // fourth without selections - [ - 'isMultiSelection' => true, - 'data' => [ - 'title' => 'test option 3', - 'default_title' => 'test option 3', - 'type' => 'select', - 'option_id' => '4', - 'position' => '3', - 'required' => '1', - ], - 'selections' => [] - ], - ], - 'expected' => ['min' => 70, 'max' => 220], - ] - ]; + $value = 1; + $this->bundleOptionsMock->expects($this->any())->method('calculateOptions')->will($this->returnValue($value)); + $this->assertEquals($value, $this->bundleOptionPrice->getValue()); } } diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php new file mode 100644 index 0000000000000..c03955a1855a5 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionRegularPriceTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Unit\Pricing\Price; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class BundleOptionRegularPriceTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice + */ + private $bundleOptionRegularPrice; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \Magento\Framework\Pricing\SaleableInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $saleableItemMock; + + /** + * @var \Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $bundleCalculatorMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $bundleOptionsMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->bundleOptionsMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleOptions::class); + $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class); + $this->bundleCalculatorMock = $this->createMock(\Magento\Bundle\Pricing\Adjustment\Calculator::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->bundleOptionRegularPrice = $this->objectManagerHelper->getObject( + \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::class, + [ + 'saleableItem' => $this->saleableItemMock, + 'quantity' => 1., + 'calculator' => $this->bundleCalculatorMock, + 'bundleOptions' => $this->bundleOptionsMock + ] + ); + } + + /** + * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getOptions + * + * @return void + */ + public function testGetOptions() + { + $collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); + $this->bundleOptionsMock->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($collection)); + $this->assertEquals($collection, $this->bundleOptionRegularPrice->getOptions()); + } + + /** + * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getOptionSelectionAmount + * + * @return void + */ + public function testGetOptionSelectionAmount() + { + $selectionAmount = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $product = $this->createMock(\Magento\Catalog\Model\Product::class); + $selection = $this->createMock(\Magento\Bundle\Model\Selection::class); + $this->bundleOptionsMock->expects($this->any()) + ->method('getOptionSelectionAmount') + ->will($this->returnValue($selectionAmount)) + ->with($product, $selection, true); + $this->assertEquals($selectionAmount, $this->bundleOptionRegularPrice->getOptionSelectionAmount($selection)); + } + + /** + * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getAmount + * + * @return void + */ + public function testGetAmount() + { + $amountMock = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $this->bundleCalculatorMock->expects($this->once()) + ->method('getOptionsAmount') + ->with($this->equalTo($this->saleableItemMock)) + ->will($this->returnValue($amountMock)); + $this->assertSame($amountMock, $this->bundleOptionRegularPrice->getAmount()); + } + + /** + * Test method \Magento\Bundle\Pricing\Price\BundleOptionRegularPrice::getValue + * + * @return void + */ + public function testGetValue() + { + $value = 1; + $this->bundleOptionsMock->expects($this->any())->method('calculateOptions')->will($this->returnValue($value)); + $this->assertEquals($value, $this->bundleOptionRegularPrice->getValue()); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php new file mode 100644 index 0000000000000..2d8a73164008b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleOptionsTest.php @@ -0,0 +1,396 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Unit\Pricing\Price; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BundleOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Bundle\Pricing\Price\BundleOptions + */ + private $bundleOptions; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $baseCalculator; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \Magento\Framework\Pricing\SaleableInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $saleableItemMock; + + /** + * @var \Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $bundleCalculatorMock; + + /** + * @var \Magento\Bundle\Pricing\Price\BundleSelectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectionFactoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $amountFactory; + + /** + * @var \Magento\Framework\Pricing\PriceInfo\Base|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceInfoMock; + + protected function setUp() + { + $this->priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class); + $this->saleableItemMock = $this->createMock(\Magento\Catalog\Model\Product::class); + $priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class)->getMock(); + $priceCurrency->expects($this->any())->method('round')->will($this->returnArgument(0)); + $this->selectionFactoryMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Price\BundleSelectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->amountFactory = $this->createMock(\Magento\Framework\Pricing\Amount\AmountFactory::class); + $factoryCallback = $this->returnCallback( + function ($fullAmount, $adjustments) { + return $this->createAmountMock(['amount' => $fullAmount, 'adjustmentAmounts' => $adjustments]); + } + ); + $this->amountFactory->expects($this->any())->method('create')->will($factoryCallback); + $this->baseCalculator = $this->createMock(\Magento\Framework\Pricing\Adjustment\Calculator::class); + + $taxData = $this->getMockBuilder(\Magento\Tax\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->bundleCalculatorMock = $this->getMockBuilder(\Magento\Bundle\Pricing\Adjustment\Calculator::class) + ->setConstructorArgs( + [$this->baseCalculator, $this->amountFactory, $this->selectionFactoryMock, $taxData, $priceCurrency] + ) + ->setMethods(['getOptionsAmount']) + ->getMock(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->bundleOptions = $this->objectManagerHelper->getObject( + \Magento\Bundle\Pricing\Price\BundleOptions::class, + [ + 'calculator' => $this->bundleCalculatorMock, + 'bundleSelectionFactory' => $this->selectionFactoryMock + ] + ); + } + + /** + * @dataProvider getOptionsDataProvider + */ + public function testGetOptions(string $selectionCollection) + { + $this->prepareOptionMocks($selectionCollection); + $this->bundleOptions->getOptions($this->saleableItemMock); + $this->assertSame($selectionCollection, $this->bundleOptions->getOptions($this->saleableItemMock)); + $this->assertSame($selectionCollection, $this->bundleOptions->getOptions($this->saleableItemMock)); + } + + /** + * @param array $selectionCollection + * @return void + */ + private function prepareOptionMocks($selectionCollection) + { + $this->saleableItemMock->expects($this->atLeastOnce()) + ->method('getStoreId') + ->will($this->returnValue(1)); + + $priceTypeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class); + $priceTypeMock->expects($this->atLeastOnce()) + ->method('setStoreFilter') + ->with($this->equalTo(1), $this->equalTo($this->saleableItemMock)) + ->will($this->returnSelf()); + + $optionIds = ['41', '55']; + $priceTypeMock->expects($this->atLeastOnce()) + ->method('getOptionsIds') + ->with($this->equalTo($this->saleableItemMock)) + ->will($this->returnValue($optionIds)); + + $priceTypeMock->expects($this->atLeastOnce()) + ->method('getSelectionsCollection') + ->with($this->equalTo($optionIds), $this->equalTo($this->saleableItemMock)) + ->will($this->returnValue($selectionCollection)); + + $collection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); + $collection->expects($this->atLeastOnce()) + ->method('appendSelections') + ->with($this->equalTo($selectionCollection), $this->equalTo(true), $this->equalTo(false)) + ->will($this->returnValue($selectionCollection)); + + $priceTypeMock->expects($this->atLeastOnce()) + ->method('getOptionsCollection') + ->with($this->equalTo($this->saleableItemMock)) + ->will($this->returnValue($collection)); + + $this->saleableItemMock->expects($this->atLeastOnce()) + ->method('getTypeInstance') + ->will($this->returnValue($priceTypeMock)); + } + + /** + * @return array + */ + public function getOptionsDataProvider() + { + return [ + ['1', '2'] + ]; + } + + /** + * @param float $selectionQty + * @param float|bool $selectionAmount + * @param bool $useRegularPrice + * @dataProvider selectionAmountDataProvider + */ + public function testGetOptionSelectionAmount($selectionQty, $selectionAmount, $useRegularPrice) + { + $selection = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getSelectionQty', '__wakeup']); + $selection->expects($this->once()) + ->method('getSelectionQty') + ->will($this->returnValue($selectionQty)); + $priceMock = $this->createMock(\Magento\Bundle\Pricing\Price\BundleSelectionPrice::class); + $priceMock->expects($this->once()) + ->method('getAmount') + ->will($this->returnValue($selectionAmount)); + $this->selectionFactoryMock->expects($this->once()) + ->method('create') + ->with($this->equalTo($this->saleableItemMock), $this->equalTo($selection), $this->equalTo($selectionQty)) + ->will($this->returnValue($priceMock)); + $this->assertSame( + $selectionAmount, + $this->bundleOptions->getOptionSelectionAmount($this->saleableItemMock, $selection, $useRegularPrice) + ); + } + + /** + * @return array + */ + public function selectionAmountDataProvider(): array + { + return [ + [1., 50.5, false], + [2.2, false, true] + ]; + } + + /** + * Create amount mock + * + * @param array $amountData + * @return \Magento\Framework\Pricing\Amount\Base|\PHPUnit_Framework_MockObject_MockObject + */ + private function createAmountMock($amountData) + { + /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Pricing\Amount\Base $amount */ + $amount = $this->createMock(\Magento\Framework\Pricing\Amount\Base::class); + $amount->expects($this->any())->method('getAdjustmentAmounts')->will( + $this->returnValue($amountData['adjustmentAmounts'] ?? []) + ); + $amount->expects($this->any())->method('getValue')->will($this->returnValue($amountData['amount'])); + return $amount; + } + + /** + * Create option mock + * + * @param array $optionData + * @return \Magento\Bundle\Model\Option|\PHPUnit_Framework_MockObject_MockObject + */ + private function createOptionMock($optionData) + { + /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Bundle\Model\Option $option */ + $option = $this->createPartialMock(\Magento\Bundle\Model\Option::class, ['isMultiSelection', '__wakeup']); + $option->expects($this->any())->method('isMultiSelection') + ->will($this->returnValue($optionData['isMultiSelection'])); + $selections = []; + foreach ($optionData['selections'] as $selectionData) { + $selections[] = $this->createSelectionMock($selectionData); + } + foreach ($optionData['data'] as $key => $value) { + $option->setData($key, $value); + } + $option->setData('selections', $selections); + return $option; + } + + /** + * Create selection product mock + * + * @param array $selectionData + * @return \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + */ + private function createSelectionMock($selectionData) + { + $selection = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->setMethods(['isSalable', 'getAmount', 'getQuantity', 'getProduct', '__wakeup']) + ->disableOriginalConstructor() + ->getMock(); + + // All items are saleable + $selection->expects($this->any())->method('isSalable')->will($this->returnValue(true)); + foreach ($selectionData['data'] as $key => $value) { + $selection->setData($key, $value); + } + $amountMock = $this->createAmountMock($selectionData['amount']); + $selection->expects($this->any())->method('getAmount')->will($this->returnValue($amountMock)); + $selection->expects($this->any())->method('getQuantity')->will($this->returnValue(1)); + + $innerProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->setMethods(['getSelectionCanChangeQty', '__wakeup']) + ->disableOriginalConstructor() + ->getMock(); + $innerProduct->expects($this->any())->method('getSelectionCanChangeQty')->will($this->returnValue(true)); + $selection->expects($this->any())->method('getProduct')->will($this->returnValue($innerProduct)); + + return $selection; + } + + /** + * @dataProvider getTestDataForCalculation + */ + public function testCalculation(array $optionList, array $expected) + { + $storeId = 1; + $this->saleableItemMock->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId)); + $this->selectionFactoryMock->expects($this->any())->method('create')->will($this->returnArgument(1)); + + $this->baseCalculator->expects($this->atLeastOnce())->method('getAmount') + ->will($this->returnValue($this->createAmountMock(['amount' => 0.]))); + + $options = []; + foreach ($optionList as $optionData) { + $options[] = $this->createOptionMock($optionData); + } + /** @var \PHPUnit_Framework_MockObject_MockObject $optionsCollection */ + $optionsCollection = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); + $optionsCollection->expects($this->atLeastOnce())->method('appendSelections')->will($this->returnSelf()); + $optionsCollection->expects($this->atLeastOnce())->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($options))); + + /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Type\AbstractType $typeMock */ + $typeMock = $this->createMock(\Magento\Bundle\Model\Product\Type::class); + $typeMock->expects($this->any())->method('setStoreFilter')->with($storeId, $this->saleableItemMock); + $typeMock->expects($this->any())->method('getOptionsCollection')->with($this->saleableItemMock) + ->will($this->returnValue($optionsCollection)); + $this->saleableItemMock->expects($this->any())->method('getTypeInstance')->will($this->returnValue($typeMock)); + + $this->assertEquals($expected['min'], $this->bundleOptions->calculateOptions($this->saleableItemMock)); + $this->assertEquals($expected['max'], $this->bundleOptions->calculateOptions($this->saleableItemMock, false)); + } + + /** + * @return array + */ + public function getTestDataForCalculation() + { + return [ + 'first case' => [ + 'optionList' => [ + // first option with single choice of product + [ + 'isMultiSelection' => false, + 'data' => [ + 'title' => 'test option 1', + 'default_title' => 'test option 1', + 'type' => 'select', + 'option_id' => '1', + 'position' => '0', + 'required' => '1', + ], + 'selections' => [ + [ + 'data' => ['price' => 70.], + 'amount' => ['amount' => 70], + ], + [ + 'data' => ['price' => 80.], + 'amount' => ['amount' => 80] + ], + [ + 'data' => ['price' => 50.], + 'amount' => ['amount' => 50] + ], + ] + ], + // second not required option + [ + 'isMultiSelection' => false, + 'data' => [ + 'title' => 'test option 2', + 'default_title' => 'test option 2', + 'type' => 'select', + 'option_id' => '2', + 'position' => '1', + 'required' => '0', + ], + 'selections' => [ + [ + 'data' => ['value' => 20.], + 'amount' => ['amount' => 20], + ], + ] + ], + // third with multi-selection + [ + 'isMultiSelection' => true, + 'data' => [ + 'title' => 'test option 3', + 'default_title' => 'test option 3', + 'type' => 'select', + 'option_id' => '3', + 'position' => '2', + 'required' => '1', + ], + 'selections' => [ + [ + 'data' => ['price' => 40.], + 'amount' => ['amount' => 40], + ], + [ + 'data' => ['price' => 20.], + 'amount' => ['amount' => 20] + ], + [ + 'data' => ['price' => 60.], + 'amount' => ['amount' => 60] + ], + ] + ], + // fourth without selections + [ + 'isMultiSelection' => true, + 'data' => [ + 'title' => 'test option 3', + 'default_title' => 'test option 3', + 'type' => 'select', + 'option_id' => '4', + 'position' => '3', + 'required' => '1', + ], + 'selections' => [] + ], + ], + 'expected' => ['min' => 70, 'max' => 220], + ] + ]; + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php index 64140ef920cbe..cbcfb1b85fc97 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/BundleSelectionPriceTest.php @@ -103,6 +103,9 @@ protected function setUp() $this->setupSelectionPrice(); } + /** + * @param bool $useRegularPrice + */ protected function setupSelectionPrice($useRegularPrice = false) { $this->selectionPrice = new \Magento\Bundle\Pricing\Price\BundleSelectionPrice( @@ -201,6 +204,7 @@ public function testGetValueTypeFixedWithSelectionPriceType($useRegularPrice) [ ['qty', null, 1], ['final_price', null, 100], + ['price', null, 100], ] ) ); @@ -338,6 +342,9 @@ public function testFixedPriceWithMultipleQty($useRegularPrice) $this->assertEquals($expectedPrice, $selectionPrice->getValue()); } + /** + * @return array + */ public function useRegularPriceDataProvider() { return [ diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/DiscountCalculatorTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/DiscountCalculatorTest.php index b4d5029dc6386..b467d5a1eaa3f 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/DiscountCalculatorTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/DiscountCalculatorTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Bundle\Test\Unit\Pricing\Price; /** @@ -44,7 +42,10 @@ class DiscountCalculatorTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); - $this->priceInfoMock = $this->createPartialMock(\Magento\Framework\Pricing\PriceInfo\Base::class, ['getPrice', 'getPrices']); + $this->priceInfoMock = $this->createPartialMock( + \Magento\Framework\Pricing\PriceInfo\Base::class, + ['getPrice', 'getPrices'] + ); $this->finalPriceMock = $this->createMock(\Magento\Catalog\Pricing\Price\FinalPrice::class); $this->priceMock = $this->getMockForAbstractClass( \Magento\Bundle\Pricing\Price\DiscountProviderInterface::class @@ -63,7 +64,7 @@ protected function getPriceMock($value) $price = clone $this->priceMock; $price->expects($this->exactly(3)) ->method('getDiscountPercent') - ->will($this->returnValue($value)); + ->willReturn($value); return $price; } @@ -74,24 +75,23 @@ public function testCalculateDiscountWithDefaultAmount() { $this->productMock->expects($this->exactly(2)) ->method('getPriceInfo') - ->will($this->returnValue($this->priceInfoMock)); + ->willReturn($this->priceInfoMock); $this->priceInfoMock->expects($this->once()) ->method('getPrice') ->with($this->equalTo(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)) - ->will($this->returnValue($this->finalPriceMock)); + ->willReturn($this->finalPriceMock); $this->finalPriceMock->expects($this->once()) ->method('getValue') - ->will($this->returnValue(100)); + ->willReturn(100); $this->priceInfoMock->expects($this->once()) ->method('getPrices') - ->will($this->returnValue( + ->willReturn( [ $this->getPriceMock(30), $this->getPriceMock(20), $this->getPriceMock(40), ] - ) - ); + ); $this->assertEquals(20, $this->calculator->calculateDiscount($this->productMock)); } @@ -102,16 +102,15 @@ public function testCalculateDiscountWithCustomAmount() { $this->productMock->expects($this->once()) ->method('getPriceInfo') - ->will($this->returnValue($this->priceInfoMock)); + ->willReturn($this->priceInfoMock); $this->priceInfoMock->expects($this->once()) ->method('getPrices') - ->will($this->returnValue( - [ - $this->getPriceMock(30), - $this->getPriceMock(20), - $this->getPriceMock(40), - ] - ) + ->willReturn( + [ + $this->getPriceMock(30), + $this->getPriceMock(20), + $this->getPriceMock(40), + ] ); $this->assertEquals(10, $this->calculator->calculateDiscount($this->productMock, 50)); } diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php index f38dfc5538cf3..3e60e057fe62b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Pricing\Price; use \Magento\Bundle\Pricing\Price\SpecialPrice; +use Magento\Store\Api\Data\WebsiteInterface; class SpecialPriceTest extends \PHPUnit\Framework\TestCase { @@ -77,12 +78,6 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva ->method('getSpecialPrice') ->will($this->returnValue($specialPrice)); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $this->saleable->expects($this->once()) - ->method('getStore') - ->will($this->returnValue($store)); $this->saleable->expects($this->once()) ->method('getSpecialFromDate') ->will($this->returnValue($specialFromDate)); @@ -92,7 +87,7 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva $this->localeDate->expects($this->once()) ->method('isScopeDateInInterval') - ->with($store, $specialFromDate, $specialToDate) + ->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate) ->will($this->returnValue($isScopeDateInInterval)); $this->priceCurrencyMock->expects($this->never()) diff --git a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php index 7b4d42568f686..1c3cf33cbf73b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php @@ -76,6 +76,9 @@ protected function setUp() ->getMock(); } + /** + * @return object + */ protected function getModel() { return $this->objectManager->getObject(BundleDataProvider::class, [ diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index c0624be8e7a97..150247729f125 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -14,6 +14,7 @@ use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; use Magento\Ui\Component\Form; +use Magento\Ui\Component\Form\Fieldset; use Magento\Ui\Component\Modal; /** @@ -69,13 +70,26 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) { $meta = $this->removeFixedTierPrice($meta); - $path = $this->arrayManager->findPath(static::CODE_BUNDLE_DATA, $meta, null, 'children'); + + $groupCode = static::CODE_BUNDLE_DATA; + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + if (empty($path)) { + $meta[$groupCode]['children'] = []; + $meta[$groupCode]['arguments']['data']['config'] = [ + 'componentType' => Fieldset::NAME, + 'label' => __('Bundle Items'), + 'collapsible' => true, + ]; + + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + } $meta = $this->arrayManager->merge( $path, @@ -220,7 +234,7 @@ private function removeFixedTierPrice(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -314,6 +328,7 @@ protected function getBundleOptions() 'template' => 'ui/dynamic-rows/templates/collapsible', 'additionalClasses' => 'admin__field-wide', 'dataScope' => 'data.bundle_options', + 'isDefaultFieldScope' => 'is_default', 'bundleSelectionsName' => 'product_bundle_container.bundle_selections' ], ], @@ -378,6 +393,9 @@ protected function getBundleOptions() 'selection_qty' => '', ], 'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'], + 'imports' => [ + 'inputType' => '${$.provider}:${$.dataScope}.type' + ], 'source' => 'product' ], ], @@ -594,10 +612,13 @@ protected function getBundleSelections() 'config' => [ 'componentType' => Container::NAME, 'isTemplate' => true, - 'component' => 'Magento_Bundle/js/components/bundle-record', + 'component' => 'Magento_Ui/js/dynamic-rows/record', 'is_collection' => true, 'imports' => [ - 'onTypeChanged' => '${ $.provider }:${ $.bundleOptionsDataScope }.type' + 'inputType' => '${$.parentName}:inputType' + ], + 'exports' => [ + 'isDefaultValue' => '${$.parentName}:isDefaultValue.${$.index}' ] ], ], @@ -691,11 +712,15 @@ protected function getBundleSelections() 'componentType' => Form\Field::NAME, 'formElement' => Form\Element\Checkbox::NAME, 'dataType' => Form\Element\DataType\Price::NAME, + 'component' => 'Magento_Bundle/js/components/bundle-user-defined-checkbox', 'label' => __('User Defined'), 'dataScope' => 'selection_can_change_qty', 'value' => '1', 'valueMap' => ['true' => '1', 'false' => '0'], 'sortOrder' => 110, + 'imports' => [ + 'inputType' => '${$.parentName}:inputType' + ] ], ], ], diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index 510206054fcf0..1fb1da9a0e2bb 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-bundle", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-tax": "100.2.*", @@ -22,10 +22,11 @@ }, "suggest": { "magento/module-webapi": "100.2.*", - "magento/module-bundle-sample-data": "Sample Data version:100.2.*" + "magento/module-bundle-sample-data": "Sample Data version:100.2.*", + "magento/module-sales-rule": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 287a6c8bfdbc0..6fe4935100720 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -50,7 +50,9 @@ <item name="custom_option_price" xsi:type="string">Magento\Catalog\Pricing\Price\CustomOptionPrice</item> <item name="base_price" xsi:type="string">Magento\Catalog\Pricing\Price\BasePrice</item> <item name="configured_price" xsi:type="string">Magento\Bundle\Pricing\Price\ConfiguredPrice</item> + <item name="configured_regular_price" xsi:type="string">Magento\Bundle\Pricing\Price\ConfiguredRegularPrice</item> <item name="bundle_option" xsi:type="string">Magento\Bundle\Pricing\Price\BundleOptionPrice</item> + <item name="bundle_option_regular_price" xsi:type="string">Magento\Bundle\Pricing\Price\BundleOptionRegularPrice</item> <item name="catalog_rule_price" xsi:type="string">Magento\CatalogRule\Pricing\Price\CatalogRulePrice</item> </argument> </arguments> @@ -75,6 +77,11 @@ <argument name="calculator" xsi:type="object">Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface</argument> </arguments> </type> + <type name="Magento\Catalog\Pricing\Price\ConfiguredPriceSelection"> + <arguments> + <argument name="calculator" xsi:type="object">Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface</argument> + </arguments> + </type> <type name="Magento\Catalog\Model\Product\Attribute\Backend\Price"> <plugin name="bundle" type="Magento\Bundle\Model\Plugin\PriceBackend" sortOrder="100" /> </type> @@ -116,6 +123,9 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote\Item"> + <plugin name="update_price_for_bundle_in_quote_item_option" type="Magento\Bundle\Model\Plugin\UpdatePriceInQuoteItemOptions"/> + </type> <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> <plugin name="append_bundle_data_to_order" type="Magento\Bundle\Model\Plugin\QuoteItem"/> </type> @@ -133,6 +143,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\Bundle\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice"> <arguments> <argument name="excludeAdjustments" xsi:type="array"> @@ -200,4 +217,11 @@ </argument> </arguments> </type> + <type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator"> + <arguments> + <argument name="productTypeChildrenValidationMap" xsi:type="array"> + <item name="bundle" xsi:type="boolean">false</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml index 224cd71538b7b..2f13fa8cf01bd 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml @@ -4,13 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes\Extend */ $elementHtml = $block->getParentElementHtml(); -$attributeCode = $block->getAttribute() - ->getAttributeCode(); +$attributeCode = $block->getAttribute()->getAttributeCode(); $switchAttributeCode = "{$attributeCode}_type"; $switchAttributeValue = $block->getProduct() @@ -21,7 +18,7 @@ $isElementReadonly = $block->getElement() ?> <?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) { ?> - <div class="<?= /* @escapeNotVerified */ $attributeCode ?> "><?= /* @escapeNotVerified */ $elementHtml ?></div> + <div class="<?= $block->escapeHtmlAttr($attributeCode) ?> "><?= /* @noEscape */ $elementHtml ?></div> <?php } ?> <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> @@ -29,9 +26,9 @@ $isElementReadonly = $block->getElement() <?php if (!$isElementReadonly && $block->getDisableChild()) { ?> <script> require(['prototype'], function () { - function <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change() { - var $attribute = $('<?= /* @escapeNotVerified */ $attributeCode ?>'); - if ($('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').value == '<?= /* @escapeNotVerified */ $block::DYNAMIC ?>') { + function <?= $block->escapeJs($switchAttributeCode) ?>_change() { + var $attribute = $('<?= /* @noEscape */ $attributeCode ?>'); + if ($('<?= $block->escapeJs($switchAttributeCode) ?>').value == '<?= $block->escapeJs($block::DYNAMIC) ?>') { if ($attribute) { $attribute.disabled = true; $attribute.value = ''; @@ -44,8 +41,8 @@ $isElementReadonly = $block->getElement() if ($attribute) { <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() && $block->getProduct()->isObjectNew()) { ?> - <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> - $attribute.value = <?= /* @escapeNotVerified */ $defaultProductPrice ?>; + <?php $defaultProductPrice = $block->escapeJs($block->getDefaultProductPrice() ?: "''") ?> + $attribute.value = <?= $block->escapeJs($defaultProductPrice) ?>; <?php } else { ?> $attribute.disabled = false; $attribute.addClassName('required-entry'); @@ -59,10 +56,10 @@ $isElementReadonly = $block->getElement() <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() && !$block->getProduct()->isObjectNew())) { ?> - $('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').observe('change', <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change); + $('<?= $block->escapeJs($switchAttributeCode) ?>').observe('change', <?= $block->escapeJs($switchAttributeCode) ?>_change); <?php } ?> Event.observe(window, 'load', function(){ - <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change(); + <?= $block->escapeJs($switchAttributeCode) ?>_change(); }); }); </script> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml index 87798a6ba622f..8c70deb5785a6 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml @@ -3,17 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ ?> <?php $options = $block->decorateArray($block->getOptions(true)); ?> -<?php if (count($options)): ?> +<?php if (count($options)) : ?> <fieldset id="catalog_product_composite_configure_fields_bundle" class="fieldset admin__fieldset composite-bundle<?= $block->getIsLastFieldset() ? ' last-fieldset' : '' ?>"> - <legend class="legend admin__legend"><span><?= /* @escapeNotVerified */ __('Bundle Items') ?></span></legend><br /> + <legend class="legend admin__legend"><span><?= $block->escapeHtml(__('Bundle Items')) ?></span></legend><br /> <?php foreach ($options as $option) : ?> <?php if ($option->getSelections()) : ?> <?= $block->getOptionHtml($option) ?> @@ -71,7 +68,7 @@ require([ } } }; - ProductConfigure.bundleControl = new BundleControl(<?= /* @escapeNotVerified */ $block->getJsonConfig() ?>); + ProductConfigure.bundleControl = new BundleControl(<?= /* @noEscape */ $block->getJsonConfig() ?>); }); </script> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml index 44ed02f2758d0..b043b58888378 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/checkbox.phtml @@ -3,60 +3,57 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Checkbox */ ?> <?php $_option = $block->getOption(); ?> <?php $_selections = $_option->getSelections(); ?> -<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> +<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -<div class="field admin__field options<?php if ($_option->getRequired()) echo ' required _required' ?>"> +<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> <label class="label admin__field-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control admin__field-control"> - <div class="nested <?php if ($_option->getDecoratedIsLast()):?> last<?php endif;?>"> + <div class="nested <?php if ($_option->getDecoratedIsLast()) :?> last<?php endif;?>"> - <?php if (count($_selections) == 1 && $_option->getRequired()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?php if (count($_selections) == 1 && $_option->getRequired()) : ?> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> <input type="hidden" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> - <?php else:?> - - <?php foreach ($_selections as $_selection): ?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> + <?php else :?> + <?php foreach ($_selections as $_selection) : ?> <div class="field choice admin__field admin__field-option"> <input - class="change-container-classname admin__control-checkbox checkbox bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> <?php if ($_option->getRequired()) echo 'validate-one-required-by-name' ?>" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + class="change-container-classname admin__control-checkbox checkbox bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> <?php if ($_option->getRequired()) { echo 'validate-one-required-by-name'; } ?>" + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" type="checkbox" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" - <?php if ($block->isSelected($_selection)):?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" + <?php if ($block->isSelected($_selection)) :?> <?= ' checked="checked"' ?> <?php endif;?> - <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck):?> + <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) :?> <?= ' disabled="disabled"' ?> <?php endif;?> - value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" onclick="ProductConfigure.bundleControl.changeSelection(this)" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" /> + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" /> <label class="admin__field-label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> - <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> + <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> </label> - <?php if ($_option->getRequired()): ?> - <?= /* @escapeNotVerified */ $block->setValidationContainer('bundle-option-' . $_option->getId() . '-' . $_selection->getSelectionId(), 'bundle-option-' . $_option->getId() . '-container') ?> + <?php if ($_option->getRequired()) : ?> + <?= /* @noEscape */ $block->setValidationContainer('bundle-option-' . $_option->getId() . '-' . $_selection->getSelectionId(), 'bundle-option-' . $_option->getId() . '-container') ?> <?php endif;?> </div> <?php endforeach; ?> - <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> + <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml index 8c13dd6479d4d..f4c4e3e51ae09 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/multi.phtml @@ -3,32 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Multi */ ?> <?php $_option = $block->getOption(); ?> <?php $_selections = $_option->getSelections(); ?> -<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> -<div class="field admin__field <?php if ($_option->getRequired()) echo ' required' ?><?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?>"> +<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> +<div class="field admin__field <?php if ($_option->getRequired()) { echo ' required'; } ?><?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> - <?php if (count($_selections) == 1 && $_option->getRequired()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> - <input type="hidden" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> - <?php else: ?> - <select multiple="multiple" size="5" id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" - class="admin__control-multiselect bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?><?php if ($_option->getRequired()) echo ' required-entry' ?> multiselect change-container-classname" + <?php if (count($_selections) == 1 && $_option->getRequired()) : ?> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <input type="hidden" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> + <?php else : ?> + <select multiple="multiple" size="5" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" + class="admin__control-multiselect bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?><?php if ($_option->getRequired()) { echo ' required-entry'; } ?> multiselect change-container-classname" onchange="ProductConfigure.bundleControl.changeSelection(this)"> - <?php if(!$_option->getRequired()): ?> - <option value=""><?= /* @escapeNotVerified */ __('None') ?></option> + <?php if (!$_option->getRequired()) : ?> + <option value=""><?= $block->escapeHtml(__('None')) ?></option> <?php endif; ?> - <?php foreach ($_selections as $_selection): ?> - <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"<?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>"><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection, false) ?></option> + <?php foreach ($_selections as $_selection) : ?> + <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> + <?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>"> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection, false) ?></option> <?php endforeach; ?> </select> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml index f0912979a9248..7878376d3418a 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/radio.phtml @@ -3,69 +3,69 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Radio */ ?> <?php $_option = $block->getOption(); ?> <?php $_selections = $_option->getSelections(); ?> <?php $_default = $_option->getDefaultSelection(); ?> -<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> +<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> -<div class="field admin__field options<?php if ($_option->getRequired()) echo ' required' ?>"> +<div class="field admin__field options<?php if ($_option->getRequired()) { echo ' required'; } ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> - <div class="nested<?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?>"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <div class="nested<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?>"> + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> <input type="hidden" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> - <?php else:?> - <?php if (!$_option->getRequired()): ?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> + <?php else :?> + <?php if (!$_option->getRequired()) : ?> <div class="field choice admin__field admin__field-option"> <input type="radio" class="radio admin__control-radio" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]"<?= ($_default && $_default->isSalable()) ? '' : ' checked="checked" ' ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]"<?= ($_default && $_default->isSalable()) ? '' : ' checked="checked" ' ?> value="" onclick="ProductConfigure.bundleControl.changeSelection(this)" /> <label class="admin__field-label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"><span><?= /* @escapeNotVerified */ __('None') ?></span></label> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"><span><?= $block->escapeHtml(__('None')) ?></span></label> </div> <?php endif; ?> - <?php foreach ($_selections as $_selection): ?> + <?php foreach ($_selections as $_selection) : ?> <div class="field choice admin__field admin__field-option"> <input type="radio" class="radio admin__control-radio <?= $_option->getRequired() ? ' validate-one-required-by-name' : '' ?> change-container-classname" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> - value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" onclick="ProductConfigure.bundleControl.changeSelection(this)" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" - qtyId="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" /> + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" + qtyId="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" /> <label class="admin__field-label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"><span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span></label> - <?php if ($_option->getRequired()): ?> - <?= /* @escapeNotVerified */ $block->setValidationContainer('bundle-option-'.$_option->getId().'-'.$_selection->getSelectionId(), 'bundle-option-'.$_option->getId().'-container') ?> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> + <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> + </label> + <?php if ($_option->getRequired()) : ?> + <?= /* @noEscape */ $block->setValidationContainer('bundle-option-'.$_option->getId().'-'.$_selection->getSelectionId(), 'bundle-option-'.$_option->getId().'-container') ?> <?php endif; ?> </div> <?php endforeach; ?> - <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> + <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> <?php endif; ?> <div class="field admin__field qty"> <label class="label admin__field-label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><span><?= /* @escapeNotVerified */ __('Quantity:') ?></span></label> - <div class="control admin__field-control"><input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" - class="input-text admin__control-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"><span><?= $block->escapeHtml(__('Quantity:')) ?></span></label> + <div class="control admin__field-control"><input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="text" - name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= /* @escapeNotVerified */ $_defaultQty ?>" /> + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> </div> </div> </div> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml index 32766f62163ed..0e202c87fd875 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/type/select.phtml @@ -3,36 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Select */ ?> <?php $_option = $block->getOption(); ?> <?php $_selections = $_option->getSelections(); ?> <?php $_default = $_option->getDefaultSelection(); ?> -<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> +<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> -<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()):?> last<?php endif; ?><?php if ($_option->getRequired()) echo ' required _required' ?>"> +<div class="field admin__field option<?php if ($_option->getDecoratedIsLast()) :?> last<?php endif; ?><?php if ($_option->getRequired()) { echo ' required _required'; } ?>"> <label class="label admin__field-label"><span><?= $block->escapeHtml($_option->getTitle()) ?></span></label> <div class="control admin__field-control"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> - <input type="hidden" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>" - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selections[0]) ?>" /> - <?php else:?> - <select id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?><?php if ($_option->getRequired()) echo ' required-entry' ?> select admin__control-select change-container-classname" + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> + <input type="hidden" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>" + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selections[0])) ?>" /> + <?php else :?> + <select id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?><?php if ($_option->getRequired()) { echo ' required-entry'; } ?> select admin__control-select change-container-classname" onchange="ProductConfigure.bundleControl.changeSelection(this)"> - <option value=""><?= /* @escapeNotVerified */ __('Choose a selection...') ?></option> - <?php foreach ($_selections as $_selection): ?> + <option value=""><?= $block->escapeHtml(__('Choose a selection...')) ?></option> + <?php foreach ($_selections as $_selection) : ?> <option - value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"<?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) echo ' disabled="disabled"' ?> - price="<?= /* @escapeNotVerified */ $block->getSelectionPrice($_selection) ?>" - qtyId="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection, false) ?></option> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"<?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?><?php if (!$_selection->isSaleable() && !$_skipSaleableCheck) { echo ' disabled="disabled"'; } ?> + price="<?= $block->escapeHtmlAttr($block->getSelectionPrice($_selection)) ?>" + qtyId="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection, false) ?></option> <?php endforeach; ?> </select> <?php endif; ?> @@ -40,12 +38,12 @@ <div class="nested"> <div class="field admin__field qty"> <label class="label admin__field-label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"><span><?= /* @escapeNotVerified */ __('Quantity:') ?></span></label> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"><span><?= $block->escapeHtml(__('Quantity:')) ?></span></label> <div class="control admin__field-control"> - <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" - class="input-text admin__control-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" type="text" - name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= /* @escapeNotVerified */ $_defaultQty ?>" /> + <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text admin__control-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="text" + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_defaultQty) ?>" /> </div> </div> </div> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml index 5b27412dd885b..cb7cc9522f6c6 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle */ ?> <script> @@ -19,14 +17,20 @@ if(typeof Bundle=='undefined') { <div id="bundle_product_container" class="entry-edit form-inline"> <fieldset class="fieldset"> <div class="field field-ship-bundle-items"> - <label for="shipment_type" class="label"><?= /* @escapeNotVerified */ __('Ship Bundle Items') ?></label> + <label for="shipment_type" class="label"><?= $block->escapeHtml(__('Ship Bundle Items')) ?></label> <div class="control"> - <select <?php if ($block->isReadonly()): ?>disabled="disabled" <?php endif;?> + <select <?php if ($block->isReadonly()) : ?>disabled="disabled" <?php endif;?> id="shipment_type" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[shipment_type]" + name="<?= $block->escapeHtmlAttr($block->getFieldSuffix()) ?>[shipment_type]" class="select"> - <option value="1"><?= /* @escapeNotVerified */ __('Separately') ?></option> - <option value="0"<?php if ($block->getProduct()->getShipmentType() == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Together') ?></option> + <option value="1"> + <?= $block->escapeHtml(__('Separately')) ?> + </option> + <option value="0" + <?php if ($block->getProduct()->getShipmentType() == 0) : ?> selected="selected" + <?php endif; ?>> + <?= $block->escapeHtml(__('Together')) ?> + </option> </select> </div> </div> @@ -48,7 +52,7 @@ require(["prototype", "mage/adminhtml/form"], function(){ // re-bind form elements onchange varienWindowOnload(true); - <?php if ($block->isReadonly()):?> + <?php if ($block->isReadonly()) :?> $('product_bundle_container').select('input', 'select', 'textarea', 'button').each(function(input){ input.disabled = true; if (input.tagName.toLowerCase() == 'button') { diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml index 62f61a725097e..9e38b96b04307 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml @@ -3,16 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option */ ?> <script id="bundle-option-template" type="text/x-magento-template"> - <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>" class="option-box"> - <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-wrapper"> + <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>" class="option-box"> + <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> <div class="fieldset-wrapper-title"> - <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-content"> + <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <span><%- data.default_title %></span> </strong> <div class="actions"> @@ -20,55 +18,55 @@ </div> <div data-role="draggable-handle" class="draggable-handle"></div> </div> - <div class="fieldset-wrapper-content in collapse" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>-content"> + <div class="fieldset-wrapper-content in collapse" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <fieldset class="fieldset"> <fieldset class="fieldset-alt"> <div class="field field-option-title required"> - <label class="label" for="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title"> - <?= /* @escapeNotVerified */ __('Option Title') ?> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title"> + <?= $block->escapeHtml(__('Option Title')) ?> </label> <div class="control"> - <?php if ($block->isDefaultStore()): ?> + <?php if ($block->isDefaultStore()) : ?> <input class="input-text required-entry" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][title]" - id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title" value="<%- data.title %>" data-original-value="<%- data.title %>" /> - <?php else: ?> + <?php else : ?> <input class="input-text required-entry" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][default_title]" - id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_default_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][default_title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_default_title" value="<%- data.default_title %>" data-original-value="<%- data.default_title %>" /> <?php endif; ?> <input type="hidden" - id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_id_<%- data.index %>" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][option_id]" + id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id_<%- data.index %>" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][option_id]" value="<%- data.option_id %>" /> <input type="hidden" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][delete]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][delete]" value="" data-state="deleted" /> </div> </div> - <?php if (!$block->isDefaultStore()): ?> + <?php if (!$block->isDefaultStore()) : ?> <div class="field field-option-store-view required"> - <label class="label" for="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title_store"> - <?= /* @escapeNotVerified */ __('Store View Title') ?> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store"> + <?= $block->escapeHtml(__('Store View Title')) ?> </label> <div class="control"> <input class="input-text required-entry" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][title]" - id="id_<?= /* @escapeNotVerified */ $block->getFieldName() ?>_<%- data.index %>_title_store" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store" value="<%- data.title %>" /> </div> </div> <?php endif; ?> <div class="field field-option-input-type required"> - <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() . '_<%- data.index %>_type' ?>"> - <?= /* @escapeNotVerified */ __('Input Type') ?> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . '_<%- data.index %>_type') ?>"> + <?= $block->escapeHtml(__('Input Type')) ?> </label> <div class="control"> <?= $block->getTypeSelectHtml() ?> @@ -81,19 +79,19 @@ checked="checked" id="field-option-req" /> <label for="field-option-req"> - <?= /* @escapeNotVerified */ __('Required') ?> + <?= $block->escapeHtml(__('Required')) ?> </label> <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> </div> </div> <div class="field field-option-position no-display"> <label class="label" for="field-option-position"> - <?= /* @escapeNotVerified */ __('Position') ?> + <?= $block->escapeHtml(__('Position')) ?> </label> <div class="control"> <input class="input-text validate-zero-or-greater" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][position]" value="<%- data.position %>" id="field-option-position" /> </div> @@ -101,13 +99,13 @@ </fieldset> <div class="no-products-message"> - <?= /* @escapeNotVerified */ __('There are no products in this option.') ?> + <?= $block->escapeHtml(__('There are no products in this option.')) ?> </div> <?= $block->getAddSelectionButtonHtml() ?> </fieldset> </div> </div> - <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_search_<%- data.index %>" class="selection-search"></div> + <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" class="selection-search"></div> </div> </script> @@ -141,7 +139,7 @@ function changeInputType(oldObject, oType) { Bundle.Option = Class.create(); Bundle.Option.prototype = { - idLabel : '<?= /* @escapeNotVerified */ $block->getFieldId() ?>', + idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', templateText : '', itemsCount : 0, initialize : function(template) { @@ -150,7 +148,7 @@ Bundle.Option.prototype = { add : function(data) { if (!data) { - data = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode(['default_title' => __('New Option')]) ?>; + data = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode(['default_title' => __('New Option')]) ?>; } else { data.title = data.title.replace(/</g, "<"); data.title = data.title.replace(/"/g, """); @@ -280,17 +278,17 @@ Bundle.Option.prototype = { var optionIndex = 0; bOption = new Bundle.Option(optionTemplate); <?php - foreach ($block->getOptions() as $_option) { - /** @var $_option \Magento\Bundle\Model\Option */ - /* @escapeNotVerified */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; - if ($_option->getSelections()) { - foreach ($_option->getSelections() as $_selection) { - /** @var $_selection \Magento\Catalog\Model\Product */ - $_selection->setName($block->escapeHtml($_selection->getName())); - /* @escapeNotVerified */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; - } +foreach ($block->getOptions() as $_option) { + /** @var $_option \Magento\Bundle\Model\Option */ + /* @noEscape */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; + if ($_option->getSelections()) { + foreach ($_option->getSelections() as $_selection) { + /** @var $_selection \Magento\Catalog\Model\Product */ + $_selection->setName($block->escapeHtml($_selection->getName())); + /* @noEscape */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; } } +} ?> function togglePriceType() { bOption['priceType' + ($('price_type').value == '1' ? 'Fixed' : 'Dynamic')](); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml index 91c245afe5717..7b51e6da57809 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option\Selection */ ?> <script id="bundle-option-selection-box-template" type="text/x-magento-template"> @@ -13,16 +11,16 @@ <thead> <tr class="headings"> <th class="col-draggable"></th> - <th class="col-default"><?= /* @escapeNotVerified */ __('Default') ?></th> - <th class="col-name"><?= /* @escapeNotVerified */ __('Name') ?></th> - <th class="col-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <?php if ($block->getCanReadPrice() !== false): ?> - <th class="col-price price-type-box"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col-price price-type-box"><?= /* @escapeNotVerified */ __('Price Type') ?></th> + <th class="col-default"><?= $block->escapeHtml(__('Default')) ?></th> + <th class="col-name"><?= $block->escapeHtml(__('Name')) ?></th> + <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <?php if ($block->getCanReadPrice() !== false) : ?> + <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> - <th class="col-qty"><?= /* @escapeNotVerified */ __('Default Quantity') ?></th> - <th class="col-uqty qty-box"><?= /* @escapeNotVerified */ __('User Defined') ?></th> - <th class="col-order type-order" style="display:none"><?= /* @escapeNotVerified */ __('Position') ?></th> + <th class="col-qty"><?= $block->escapeHtml(__('Default Quantity')) ?></th> + <th class="col-uqty qty-box"><?= $block->escapeHtml(__('User Defined')) ?></th> + <th class="col-order type-order" style="display:none"><?= $block->escapeHtml(__('Position')) ?></th> <th class="col-actions"></th> </tr> </thead> @@ -33,31 +31,31 @@ <script id="bundle-option-selection-row-template" type="text/x-magento-template"> <td class="col-draggable"> <span data-role="draggable-handle" class="draggable-handle"></span> - <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_id<%- data.index %>" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" + <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id<%- data.index %>" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" value="<%- data.selection_id %>"/> - <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" + <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" value="<%- data.option_id %>"/> <input type="hidden" class="product" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" value="<%- data.product_id %>"/> - <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" + <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" value="" class="delete"/> </td> <td class="col-default"> <input onclick="bSelection.checkGroup(event)" type="<%- data.option_type %>" class="default" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" value="1" <%- data.checked %> /> </td> <td class="col-name"><%- data.name %></td> <td class="col-sku"><%- data.sku %></td> -<?php if ($block->getCanReadPrice() !== false): ?> +<?php if ($block->getCanReadPrice() !== false) : ?> <td class="col-price price-type-box"> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_value" + <input id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="<%- data.selection_price_value %>" - <?php if ($block->getCanEditPrice() === false): ?> + <?php if ($block->getCanEditPrice() === false) : ?> disabled="disabled" <?php endif; ?>/> </td> @@ -65,19 +63,19 @@ <?= $block->getPriceTypeSelectHtml() ?> <div><?= $block->getCheckboxScopeHtml() ?></div> </td> -<?php else: ?> - <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_value" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> - <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_type" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> - <?php if ($block->isUsedWebsitePrice()): ?> - <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.index %>_price_scope" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> +<?php else : ?> + <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> + <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_type" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> + <?php if ($block->isUsedWebsitePrice()) : ?> + <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_scope" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> <?php endif; ?> <?php endif; ?> <td class="col-qty"> <input class="input-text required-entry validate-greater-zero-based-on-option validate-zero-or-greater" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" value="<%- data.selection_qty %>" /> </td> <td class="col-uqty qty-box"> @@ -86,7 +84,7 @@ </td> <td class="col-order type-order" style="display:none"> <input class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.parentIndex %>][<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][position]" value="<%- data.position %>" /> </td> <td class="col-actions"> @@ -106,7 +104,7 @@ var bundleTemplateBox = jQuery('#bundle-option-selection-box-template').html(), Bundle.Selection = Class.create(); Bundle.Selection.prototype = { - idLabel : '<?= /* @escapeNotVerified */ $block->getFieldId() ?>', + idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', scopePrice : <?= (int)$block->isUsedWebsitePrice() ?>, templateBox : '', templateRow : '', @@ -115,7 +113,7 @@ Bundle.Selection.prototype = { gridSelection: new Hash(), gridRemoval: new Hash(), gridSelectedProductSkus: [], - selectionSearchUrl: '<?= /* @escapeNotVerified */ $block->getSelectionSearchUrl() ?>', + selectionSearchUrl: '<?= $block->escapeUrl($block->getSelectionSearchUrl()) ?>', initialize : function() { this.templateBox = '<div class="tier form-list" id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml index 3aba02fadffbb..c480d9b126da6 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @@ -21,19 +19,19 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td> </td> <td> </td> <td> </td> @@ -43,165 +41,164 @@ <td> </td> <td class="last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyInvoiced()*1 ?></td> + <th><?= $block->escapeHtml(__('Invoiced')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyRefunded()*1 ?></td> + <th><?= $block->escapeHtml(__('Refunded')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyCanceled()*1 ?></td> + <th><?= $block->escapeHtml(__('Canceled')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)): ?> + <?php elseif ($block->isShipmentSeparately($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <?php if ($block->canParentReturnToStock($_item)) : ?> <td class="col-return-to-stock"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?php if ($block->canReturnItemToStock($_item)) : ?> <input type="checkbox" class="admin__control-checkbox" - name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][back_to_stock]" - value="1"<?php if ($_item->getBackToStock()):?> checked="checked"<?php endif;?> /> + name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][back_to_stock]" + value="1"<?php if ($_item->getBackToStock()) :?> checked="checked"<?php endif;?> /> <label class="admin__field-label"></label> <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <?php endif; ?> <td class="col-refund col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" - name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][qty]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> - <?php else: ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> + name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][qty]" + value="<?= (float)$_item->getQty() * 1 ?>" /> + <?php else : ?> + <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else: ?> + <?php else : ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml index a9e71a79f8977..0d54e1528dfe9 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @@ -21,19 +19,19 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td> </td> <td> </td> <td> </td> @@ -41,88 +39,87 @@ <td> </td> <td class="last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()): ?> + <?php if ($block->getOrderOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions() as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index ff26d67bd8378..b3c4b0b1d999c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @@ -21,19 +19,19 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td> </td> <td> </td> <td> </td> @@ -42,152 +40,152 @@ <td> </td> <td class="last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><span><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></span></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><span><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></span></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyInvoiced()*1 ?></td> + <th><?= $block->escapeHtml(__('Invoiced')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyRefunded()*1 ?></td> + <th><?= $block->escapeHtml(__('Refunded')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyCanceled()*1 ?></td> + <th><?= $block->escapeHtml(__('Canceled')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)): ?> + <?php elseif ($block->isShipmentSeparately($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyOrdered()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getOrderItem()->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" - name="invoice[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> + name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" + value="<?= (float)$_item->getQty() * 1 ?>" /> <?php else : ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> + <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); -}); -</script> + }); + </script> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else: ?> + <?php else : ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml index 5f344409b6a4c..e29bb5dbc9479 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @@ -21,19 +19,19 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td> </td> <td> </td> <td> </td> @@ -41,89 +39,88 @@ <td> </td> <td class="last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> - <?php else: ?> + <?php else : ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()): ?> + <?php if ($block->getOrderOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions() as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['protoype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml index bb0857a80d689..356a510888d8e 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @@ -16,24 +14,24 @@ <?php $_item = $block->getItem() ?> <?php $items = array_merge([$_item], $_item->getChildrenItems()); ?> -<?php $_count = count ($items) ?> +<?php $_count = count($items) ?> <?php $_index = 0 ?> <?php $_prevOptionId = '' ?> -<?php if($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getParentItem()): ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_item->getParentItem()) : ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td> </td> <td> </td> <td> </td> @@ -44,161 +42,160 @@ <td> </td> <td class="last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index==$_count && !$_showlastRow)?' class="border"':'' ?>> - <?php if (!$_item->getParentItem()): ?> + <?php if (!$_item->getParentItem()) : ?> <td class="col-product"> - <div class="product-title" id="order_item_<?= /* @escapeNotVerified */ $_item->getId() ?>_title"> + <div class="product-title" id="order_item_<?= $block->escapeHtmlAttr($_item->getId()) ?>_title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-status"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getStatus() ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= $block->escapeHtml($_item->getStatus()) ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-price-original"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('original_price') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('original_price') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyInvoiced()): ?> + <?php if ((float) $_item->getQtyInvoiced()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyInvoiced()*1 ?></td> + <th><?= $block->escapeHtml(__('Invoiced')) ?></th> + <td><?= (float)$_item->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> + <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyRefunded()): ?> + <?php if ((float) $_item->getQtyRefunded()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></td> + <th><?= $block->escapeHtml(__('Refunded')) ?></th> + <td><?= (float)$_item->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyCanceled()): ?> + <?php if ((float) $_item->getQtyCanceled()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></td> + <th><?= $block->escapeHtml(__('Canceled')) ?></th> + <td><?= (float)$_item->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)): ?> + <?php elseif ($block->isShipmentSeparately($_item)) : ?> <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')) ?></th> + <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyShipped()): ?> + <?php if ((float) $_item->getQtyShipped()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></td> + <th><?= $block->escapeHtml(__('Shipped')) ?></th> + <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-tax-percent"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayTaxPercent($_item) ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= $block->escapeHtml($block->displayTaxPercent($_item)) ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if($_showlastRow): ?> - <tr<?php if (!$block->canDisplayGiftmessage()) echo ' class="border"' ?>> +<?php if ($_showlastRow) : ?> + <tr<?php if (!$block->canDisplayGiftmessage()) { echo ' class="border"'; } ?>> <td class="col-product"> - <?php if ($block->getOrderOptions()): ?> + <?php if ($block->getOrderOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?>:</dt> + <?php foreach ($block->getOrderOptions() as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?>:</dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else: ?> + <?php else : ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml index 2ede8277bcfc9..2e52ed906626b 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> @@ -17,85 +15,84 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td class="col-product"> </td> <td class="col-qty last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr class="<?= (++$_index == $_count && !$_showlastRow) ? 'border' : '' ?>"> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-ordered-qty"> - <?php if ($block->isShipmentSeparately($_item)): ?> + <?php if ($block->isShipmentSeparately($_item)) : ?> <?= $block->getColumnHtml($_item, 'qty') ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> <td class="col-qty last"> - <?php if ($block->isShipmentSeparately($_item)): ?> + <?php if ($block->isShipmentSeparately($_item)) : ?> <input type="text" class="input-text admin__control-text" - name="shipment[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> - <?php else: ?> + name="shipment[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" + value="<?= (float)$_item->getQty() * 1 ?>" /> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else: ?> + <?php else : ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml index 71eabd45cbb57..521669700e10a 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> @@ -17,74 +15,73 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item): ?> +<?php foreach ($items as $_item) : ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getParentItem()): ?> + <?php if ($_item->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr> - <td class="col-product"><div class="option-label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> <td class="col-qty last"> </td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getParentItem()): ?> + <?php if (!$_item->getParentItem()) : ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> </div> </td> - <?php else: ?> + <?php else : ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-qty last"> - <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> - <?php if (isset($shipItems[$_item->getId()])): ?> - <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty()*1 ?> - <?php elseif ($_item->getIsVirtual()): ?> - <?= /* @escapeNotVerified */ __('N/A') ?> - <?php else: ?> + <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> + <?php if (isset($shipItems[$_item->getId()])) : ?> + <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> + <?php elseif ($_item->getIsVirtual()) : ?> + <?= $block->escapeHtml(__('N/A')) ?> + <?php else : ?> 0 <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> - <dt><?= /* @escapeNotVerified */ $option['label'] ?></dt> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']): ?> - <?= /* @escapeNotVerified */ $option['value'] ?> - <?php else: ?> - <?= $block->truncateString($option['value'], 55, '', $_remainder) ?> - <?php if ($_remainder):?> - ... <span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_remainder ?></span> + <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?= $block->escapeHtml($option['value']) ?> + <?php else : ?> + <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> + <?php if ($_remainder) :?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> <script> -require(['prototype'], function(){ - - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - -}); -</script> + require(['prototype'], function(){ + <?php $escapedId = $block->escapeJs($_id) ?> + $('<?= /* @noEscape */ $escapedId ?>').hide(); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); + $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + }); + </script> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js index b608cff85b067..09331d37bb3b6 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js @@ -14,7 +14,10 @@ define([ clearing: false, parentContainer: '', parentSelections: '', - changer: '' + changer: '', + exports: { + value: '${$.parentName}:isDefaultValue' + } }, /** @@ -58,10 +61,6 @@ define([ this.prefer = typeMap[type]; this.elementTmpl(this.templates[typeMap[type]]); - - if (this.prefer === 'radio' && this.checked()) { - this.clearValues(); - } }, /** diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js index 428361f459544..a6fc84765cc65 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js @@ -14,7 +14,57 @@ define([ label: '', columnsHeader: false, columnsHeaderAfterRender: true, - addButton: false + addButton: false, + isDefaultFieldScope: 'is_default', + defaultRecords: { + use: [], + moreThanOne: false, + state: {} + }, + listens: { + inputType: 'onInputTypeChange', + isDefaultValue: 'onIsDefaultValue' + } + }, + + /** + * Handler for type select. + * + * @param {String} inputType - changed. + */ + onInputTypeChange: function (inputType) { + if (this.defaultRecords.moreThanOne && (inputType === 'radio' || inputType === 'select')) { + _.each(this.defaultRecords.use, function (index, counter) { + this.source.set( + this.dataScope + '.bundle_selections.' + index + '.' + this.isDefaultFieldScope, + counter ? '0' : '1' + ); + }.bind(this)); + } + }, + + /** + * Handler for is_default field. + * + * @param {Object} data - changed data. + */ + onIsDefaultValue: function (data) { + var cb, + use = 0; + + this.defaultRecords.use = []; + + cb = function (elem, key) { + + if (~~elem) { + this.defaultRecords.use.push(key); + use++; + } + + this.defaultRecords.moreThanOne = use > 1; + }.bind(this); + + _.each(data, cb); }, /** @@ -29,7 +79,6 @@ define([ recordIndex; this.parsePagesData(data); - this.templates.record.bundleOptionsDataScope = this.dataScope; if (newData.length) { if (this.insertData().length) { diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js new file mode 100644 index 0000000000000..a7ceded02d0c3 --- /dev/null +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + listens: { + inputType: 'onInputTypeChange' + } + }, + + /** + * Handler for "inputType" property + * + * @param {String} data + */ + onInputTypeChange: function (data) { + data === 'checkbox' || data === 'multi' ? + this.clear() + .visible(false) : + this.visible(true); + } + }); +}); diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml index 5955d0337bb6e..819a73b262ca8 100644 --- a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php @@ -22,16 +19,16 @@ $regularPriceModel = $block->getPriceType('regular_price'); $maximalRegularPrice = $regularPriceModel->getMaximalPrice(); $minimalRegularPrice = $regularPriceModel->getMinimalPrice(); ?> -<?php if ($block->getSaleableItem()->getPriceView()): ?> +<?php if ($block->getSaleableItem()->getPriceView()) : ?> <p class="minimal-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ + <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ 'display_label' => __('As low as'), 'price_id' => $block->getPriceId('from-'), 'include_container' => true ]); ?> - <?php if ($minimalPrice < $minimalRegularPrice): ?> + <?php if ($minimalPrice < $minimalRegularPrice) : ?> <span class="old-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ + <?= /* @noEscape */ $block->renderAmount($minimalRegularPrice, [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, @@ -40,18 +37,18 @@ $minimalRegularPrice = $regularPriceModel->getMinimalPrice(); </span> <?php endif ?> </p> -<?php else: ?> - <?php if ($block->showRangePrice()): ?> +<?php else : ?> + <?php if ($block->showRangePrice()) : ?> <p class="price-from"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ + <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ 'display_label' => __('From'), 'price_id' => $block->getPriceId('from-'), 'price_type' => 'minPrice', 'include_container' => true ]); ?> - <?php if ($minimalPrice < $minimalRegularPrice): ?> + <?php if ($minimalPrice < $minimalRegularPrice) : ?> <span class="old-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ + <?= /* @noEscape */ $block->renderAmount($minimalRegularPrice, [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, @@ -61,15 +58,15 @@ $minimalRegularPrice = $regularPriceModel->getMinimalPrice(); <?php endif ?> </p> <p class="price-to"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($maximalPrice, [ + <?= /* @noEscape */ $block->renderAmount($maximalPrice, [ 'display_label' => __('To'), 'price_id' => $block->getPriceId('to-'), 'price_type' => 'maxPrice', 'include_container' => true ]); ?> - <?php if ($maximalPrice < $maximalRegularPrice): ?> + <?php if ($maximalPrice < $maximalRegularPrice) : ?> <span class="old-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($maximalRegularPrice, [ + <?= /* @noEscape */ $block->renderAmount($maximalRegularPrice, [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, @@ -78,14 +75,14 @@ $minimalRegularPrice = $regularPriceModel->getMinimalPrice(); </span> <?php endif ?> </p> - <?php else: ?> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalPrice, [ + <?php else : ?> + <?= /* @noEscape */ $block->renderAmount($minimalPrice, [ 'price_id' => $block->getPriceId('product-price-'), 'include_container' => true ]); ?> - <?php if ($minimalPrice < $minimalRegularPrice): ?> + <?php if ($minimalPrice < $minimalRegularPrice) : ?> <span class="old-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($minimalRegularPrice, [ + <?= /* @noEscape */ $block->renderAmount($minimalRegularPrice, [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml index 53d24dd7c2c07..1ae4ced759f75 100644 --- a/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml +++ b/app/code/Magento/Bundle/view/base/templates/product/price/selection/amount.phtml @@ -4,10 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?php /** @var \Magento\Framework\Pricing\Render\Amount $block */ ?> -<?= /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool) $block->getIncludeContainer()) ?> +<?= /* @noEscape */ $block->formatCurrency($block->getDisplayValue(), (bool) $block->getIncludeContainer()); ?> diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml index 5f152c4bbefbc..f5f67588a1c49 100644 --- a/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml +++ b/app/code/Magento/Bundle/view/base/templates/product/price/tier_prices.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php @@ -16,10 +13,10 @@ $tierPriceModel = $block->getPrice(); $tierPrices = $tierPriceModel->getTierPriceList(); ?> <?php if (count($tierPrices)) : ?> - <ul class="<?= /* @escapeNotVerified */ ($block->hasListClass() ? $block->getListClass() : 'prices-tier items') ?>"> + <ul class="<?= $block->escapeHtmlAttr(($block->hasListClass() ? $block->getListClass() : 'prices-tier items')) ?>"> <?php foreach ($tierPrices as $index => $price) : ?> <li class="item"> - <?php /* @escapeNotVerified */ echo __( + <?= /* @noEscape */ __( 'Buy %1 with %2 discount each', $price['price_qty'], '<strong class="benefit">' . round($price['percentage_value']) . '%</strong>' diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index e56cc6f32d804..49ee253ad1e88 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -27,7 +27,8 @@ define([ '<% } %>', controlContainer: 'dd', // should be eliminated priceFormat: {}, - isFixedPrice: false + isFixedPrice: false, + optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]' }; $.widget('mage.priceBundle', { @@ -91,6 +92,8 @@ define([ if (changes) { priceBox.trigger('updatePrice', changes); } + + this._displayTierPriceBlock(bundleOption); this.updateProductSummary(); }, @@ -207,6 +210,35 @@ define([ return this; }, + /** + * Show or hide option tier prices block + * + * @param {Object} optionElement + * @private + */ + _displayTierPriceBlock: function (optionElement) { + var optionType = optionElement.prop('type'), + optionId, + optionValue, + optionTierPricesElements; + + if (optionType === 'select-one') { + optionId = utils.findOptionId(optionElement[0]); + optionValue = optionElement.val() || null; + optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId)); + + _.each(optionTierPricesElements, function (tierPriceElement) { + var selectionId = $(tierPriceElement).data('selection-id') + ''; + + if (selectionId === optionValue) { + $(tierPriceElement).show(); + } else { + $(tierPriceElement).hide(); + } + }); + } + }, + /** * Handler to update productSummary box */ @@ -374,8 +406,17 @@ define([ function applyTierPrice(oneItemPrice, qty, optionConfig) { var tiers = optionConfig.tierPrice, magicKey = _.keys(oneItemPrice)[0], + tiersFirstKey = _.keys(optionConfig)[0], lowest = false; + if (!tiers) {//tiers is undefined when options has only one option + tiers = optionConfig[tiersFirstKey].tierPrice; + } + + tiers.sort(function (a, b) {//sorting based on "price_qty" + return a['price_qty'] - b['price_qty']; + }); + _.each(tiers, function (tier, index) { if (tier['price_qty'] > qty) { return; diff --git a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml index 5b8c050e5af54..d12f2e8f6a952 100644 --- a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml +++ b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml @@ -29,10 +29,22 @@ <container name="product.info.bundle.options.top" as="product_info_bundle_options_top"> <block class="Magento\Catalog\Block\Product\View" name="bundle.back.button" as="backButton" before="-" template="Magento_Bundle::catalog/product/view/backbutton.phtml"/> </container> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi" name="product.info.bundle.options.multi" as="multi"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> </block> </referenceBlock> <referenceBlock name="product.info.form.options"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml index 31a39c1cd162c..ba58544c26af5 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/backbutton.phtml @@ -6,5 +6,5 @@ ?> <button type="button" class="action back customization"> - <span><?= /* @escapeNotVerified */ __('Go back to product details') ?></span> + <span><?= $block->escapeHtml(__('Go back to product details')) ?></span> </button> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml index d7aea4237b100..480ffea5bc8b3 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/customize.phtml @@ -3,18 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_product = $block->getProduct() ?> -<?php if ($_product->isSaleable() && $block->hasOptions()):?> +<?php if ($_product->isSaleable() && $block->hasOptions()) :?> <div class="bundle-actions"> <button id="bundle-slide" class="action primary customize" type="button"> - <span><?= /* @escapeNotVerified */ __('Customize and Add to Cart') ?></span> + <span><?= $block->escapeHtml(__('Customize and Add to Cart')) ?></span> </button> </div> <?php endif;?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml index 2dbea0fd21395..5fe55be1db68f 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/options/notice.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<p class="required"><?= /* @escapeNotVerified */ __('* Required Fields') ?></p> +<p class="required"><?=$block->escapeHtml(__('* Required Fields')) ?></p> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml index bc4337fa7ae24..9bf179e622f17 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/summary.phtml @@ -3,40 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_product = $block->getProduct(); ?> -<?php if ($_product->isSaleable() && $block->hasOptions()): ?> +<?php if ($_product->isSaleable() && $block->hasOptions()) : ?> <div id="bundleSummary" class="block-bundle-summary" data-mage-init='{"sticky":{"container": ".product-add-form"}}'> <div class="title"> - <strong><?= /* @escapeNotVerified */ __('Your Customization') ?></strong> + <strong><?= $block->escapeHtml(__('Your Customization')) ?></strong> </div> <div class="content"> <div class="bundle-info"> <?= $block->getImage($_product, 'bundled_product_customization_page')->toHtml() ?> <div class="product-details"> <strong class="product name"><?= $block->escapeHtml($_product->getName()) ?></strong> - <?php if ($_product->getIsSalable()): ?> - <p class="available stock" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('In stock') ?></span> + <?php if ($_product->getIsSalable()) : ?> + <p class="available stock" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('In stock')) ?></span> </p> - <?php else: ?> - <p class="unavailable stock" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> + <?php else : ?> + <p class="unavailable stock" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('Out of stock')) ?></span> </p> <?php endif; ?> <?= $block->getChildHtml('', true) ?> </div> </div> <div class="bundle-summary"> - <strong class="subtitle"><?= /* @escapeNotVerified */ __('Summary') ?></strong> + <strong class="subtitle"><?= $block->escapeHtml(__('Summary')) ?></strong> <div id="bundle-summary" data-container="product-summary"> <ul data-mage-init='{"productSummary": []}' class="bundle items"></ul> <script data-template="bundle-summary" type="text/x-magento-template"> @@ -46,7 +43,7 @@ </li> </script> <script data-template="bundle-option" type="text/x-magento-template"> - <div><?= /* @escapeNotVerified */ __('%1 x %2', '<%- data._quantity_ %>', '<%- data._label_ %>') ?></div> + <div><?= /* @noEscape */ __('%1 x %2', '<%- data._quantity_ %>', '<%- data._label_ %>') ?></div> </script> </div> </div> @@ -61,7 +58,7 @@ "slideBackSelector": ".action.customization.back", "bundleProductSelector": "#bundleProduct", "bundleOptionsContainer": ".product-add-form" - <?php if ($block->isStartCustomization()): ?> + <?php if ($block->isStartCustomization()) : ?> ,"autostart": true <?php endif;?> } diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml index ce9ef89a82bd1..ee29fc61d0145 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle.phtml @@ -4,19 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle */ ?> <?php $_product = $block->getProduct() ?> -<?php if ($block->displayProductStockStatus()): ?> - <?php if ($_product->isAvailable()): ?> - <p class="stock available" title="<?= /* @escapeNotVerified */ __('Availability:') ?>"> - <span><?= /* @escapeNotVerified */ __('In stock') ?></span> +<?php if ($block->displayProductStockStatus()) : ?> + <?php if ($_product->isAvailable()) : ?> + <p class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability:')) ?>"> + <span><?= $block->escapeHtml(__('In stock')) ?></span> </p> - <?php else: ?> - <p class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability:') ?>"> - <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> + <?php else : ?> + <p class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability:')) ?>"> + <span><?= $block->escapeHtml(__('Out of stock')) ?></span> </p> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index bda649eb603e6..1d990000ac3ef 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox */ ?> @@ -17,31 +14,34 @@ </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" - class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> - <?php else:?> - <?php foreach($_selections as $_selection): ?> + class="bundle-option-<?=$block->escapeHtmlAttr($_option->getId()) ?> product bundle option" + name="bundle_option[<?=$block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?=$block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> + <?php else :?> + <?php foreach ($_selections as $_selection) : ?> <div class="field choice"> - <input class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> checkbox product bundle option change-container-classname" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + <input class="bundle-option-<?=$block->escapeHtmlAttr($_option->getId()) ?> checkbox product bundle option change-container-classname" + id="bundle-option-<?=$block->escapeHtmlAttr($_option->getId()) ?>-<?=$block->escapeHtmlAttr($_selection->getSelectionId()) ?>" type="checkbox" - <?php if ($_option->getRequired()) /* @escapeNotVerified */ echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $_option->getId() . ']"]:checked\'}"'?> - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" - data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][<?= /* @escapeNotVerified */ $_selection->getId() ?>]" - <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?> - <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?> - value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"/> + <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; }?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" + data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" + <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> <label class="label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> - <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> + <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> - <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> + <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml index 718d43070a5fd..d6f9fdb74ef62 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/multi.phtml @@ -3,39 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi */ ?> <?php $_option = $block->getOption() ?> <?php $_selections = $_option->getSelections() ?> <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> - <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> + <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> <input type="hidden" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> - <?php else: ?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> + <?php else : ?> <select multiple="multiple" size="5" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" - data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>][]" - class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> multiselect product bundle option change-container-classname" - <?php if ($_option->getRequired()) echo 'data-validate={required:true}' ?>> - <?php if(!$_option->getRequired()): ?> - <option value=""><?= /* @escapeNotVerified */ __('None') ?></option> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" + data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][]" + class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> multiselect product bundle option change-container-classname" + <?php if ($_option->getRequired()) { echo 'data-validate={required:true}'; } ?>> + <?php if (!$_option->getRequired()) : ?> + <option value=""><?= $block->escapeHtml(__('None')) ?></option> <?php endif; ?> - <?php foreach ($_selections as $_selection): ?> - <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" - <?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?> - <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?>> - <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection, false) ?> + <?php foreach ($_selections as $_selection) : ?> + <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?>> + <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection, false) ?> </option> <?php endforeach; ?> </select> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 7ea89e8609818..538e341894f90 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio */ ?> <?php $_option = $block->getOption(); ?> @@ -19,8 +16,9 @@ </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" name="bundle_option[<?= (int)$_option->getId() ?>]" @@ -28,52 +26,54 @@ id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" checked="checked" /> - <?php else:?> - <?php if (!$_option->getRequired()): ?> + <?php else :?> + <?php if (!$_option->getRequired()) : ?> <div class="field choice"> <input type="radio" class="radio product bundle option" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> value=""/> - <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> - <span><?= /* @escapeNotVerified */ __('None') ?></span> + <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> + <span><?= $block->escapeHtml(__('None')) ?></span> </label> </div> <?php endif; ?> - <?php foreach ($_selections as $_selection): ?> + <?php foreach ($_selections as $_selection) : ?> <div class="field choice"> <input type="radio" class="radio product bundle option change-container-classname" - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" - <?php if ($_option->getRequired()) echo 'data-validate="{\'validate-one-required-by-name\':true}"'?> - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - <?php if ($block->isSelected($_selection)) echo ' checked="checked"' ?> - <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?> - value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"/> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':true}"'; }?> + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> <label class="label" - for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> - <span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span> + for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> + <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> - <div id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></div> + <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> <?php endif; ?> <div class="field qty qty-holder"> - <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"> - <span><?= /* @escapeNotVerified */ __('Quantity') ?></span> + <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> + <span><?= $block->escapeHtml(__('Quantity')) ?></span> </label> <div class="control"> - <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" - class="input-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" + <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="number" - name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_defaultQty ?>"/> + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> </div> </div> </div> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml index 977daa2b2a446..654d4d885801c 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select */ ?> @@ -14,45 +11,55 @@ <?php $_default = $_option->getDefaultSelection(); ?> <?php list($_defaultQty, $_canChangeQty) = $block->getDefaultValues(); ?> <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> - <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>"> + <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> - <?php if ($block->showSingle()): ?> - <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?php if ($block->showSingle()) : ?> + <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" - class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_selections[0]->getSelectionId() ?>"/> - <?php else:?> - <select id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>" - name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option bundle-option-select change-container-classname" - <?php if ($_option->getRequired()) echo 'data-validate = {required:true}' ?>> - <option value=""><?= /* @escapeNotVerified */ __('Choose a selection...') ?></option> - <?php foreach ($_selections as $_selection): ?> - <option value="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" - <?php if ($block->isSelected($_selection)) echo ' selected="selected"' ?> - <?php if (!$_selection->isSaleable()) echo ' disabled="disabled"' ?>> - <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection, false) ?> + class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> + <?php else :?> + <select id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" + name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option bundle-option-select change-container-classname" + <?php if ($_option->getRequired()) { echo 'data-validate = {required:true}'; } ?>> + <option value=""><?= $block->escapeHtml(__('Choose a selection...')) ?></option> + <?php foreach ($_selections as $_selection) : ?> + <option value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <?php if ($block->isSelected($_selection)) { echo ' selected="selected"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?>> + <?= /* @noEscape */ $block->getSelectionTitlePrice($_selection, false) ?> </option> <?php endforeach; ?> </select> + <div id="option-tier-prices-<?= $block->escapeHtml($_option->getId()) ?>" class="option-tier-prices"> + <?php foreach ($_selections as $_selection) : ?> + <div data-role="selection-tier-prices" + data-selection-id="<?= $block->escapeHtml($_selection->getSelectionId()) ?>" + class="selection-tier-prices"> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + </div> + <?php endforeach; ?> + </div> <?php endif; ?> <div class="nested"> <div class="field qty qty-holder"> - <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input"> - <span><?= /* @escapeNotVerified */ __('Quantity') ?></span> + <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> + <span><?= $block->escapeHtml(__('Quantity')) ?></span> </label> <div class="control"> - <input <?php if (!$_canChangeQty) echo ' disabled="disabled"' ?> - id="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-qty-input" - class="input-text qty<?php if (!$_canChangeQty) echo ' qty-disabled' ?>" + <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" type="number" - name="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="bundle_option_qty[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - value="<?= /* @escapeNotVerified */ $_defaultQty ?>"/> + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> </div> </div> </div> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml index 157e2d959720b..ec425ca7711ae 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/options.phtml @@ -3,24 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block Magento\Bundle\Block\Catalog\Product\View\Type\Bundle */ ?> <?php $product = $block->getProduct(); -$helper = $this->helper('Magento\Catalog\Helper\Output'); +$helper = $this->helper(Magento\Catalog\Helper\Output::class); $stripSelection = $product->getConfigureMode() ? true : false; $options = $block->decorateArray($block->getOptions($stripSelection)); ?> -<?php if ($product->isSaleable()):?> - <?php if (count($options)): ?> +<?php if ($product->isSaleable()) :?> + <?php if (count($options)) : ?> <script type="text/x-magento-init"> { "#product_addtocart_form": { "priceBundle": { - "optionConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, + "optionConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, "controlContainer": ".field.option" } } @@ -28,17 +26,20 @@ $options = $block->decorateArray($block->getOptions($stripSelection)); </script> <fieldset class="fieldset fieldset-bundle-options"> <legend id="customizeTitle" class="legend title"> - <span><?= /* @escapeNotVerified */ __('Customize %1', $helper->productAttribute($product, $product->getName(), 'name')) ?></span> + <span><?= $block->escapeHtml(__('Customize %1', $helper->productAttribute($product, $product->getName(), 'name'))) ?></span> </legend><br /> <?= $block->getChildHtml('product_info_bundle_options_top') ?> - <?php foreach ($options as $option): ?> - <?php if (!$option->getSelections()): ?> - <?php continue; ?> - <?php endif; ?> - <?= $block->getOptionHtml($option) ?> + <?php foreach ($options as $option) : ?> + <?php + if (!$option->getSelections()) { + continue; + } else { + echo $block->getOptionHtml($option); + } + ?> <?php endforeach; ?> </fieldset> - <?php else: ?> - <p class="empty"><?= /* @escapeNotVerified */ __('No options of this product are available.') ?></p> + <?php else : ?> + <p class="empty"><?= $block->escapeHtml(__('No options of this product are available.')) ?></p> <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml index a87c2167e66d4..6230b7e8580fa 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/creditmemo/default.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> <?php $parentItem = $block->getItem() ?> @@ -13,16 +11,15 @@ <?php $items = $block->getChildren($parentItem) ?> -<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - +<?php foreach ($items as $_item) : ?> <?php // As part of invoice item renderer logic, the order is set on each invoice item. // In the case of a bundle product, this template takes over rendering its children, @@ -30,40 +27,40 @@ $_item->setOrder($_order); ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="bundle-option-label"> <td colspan="3"> - <strong><?= /* @escapeNotVerified */ $attributes['option_label'] ?></strong> + <strong><?= $block->escapeHtml($attributes['option_label']) ?></strong> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <tr class="bundle-item bundle-parent"> <td class="item-info"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> </td> - <?php else: ?> + <?php else : ?> <tr class="bundle-item bundle-option-value"> <td class="item-info"> <p><?= $block->getValueHtml($_item) ?></p> </td> <?php endif; ?> <td class="item-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty() * 1 ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> <td class="item-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->getItemPrice($_item) ?> + <?php else : ?>   <?php endif; ?> </td> @@ -71,14 +68,14 @@ <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr> <td colspan="3" class="item-extra"> - <?php if ($block->getItemOptions()): ?> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> - <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> + <dd><?= $block->escapeHtml($option['value']) ?></dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml index ee79ca3b0a7a5..25702ef480cbb 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/invoice/default.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> @@ -14,16 +12,15 @@ <?php $_index = 0 ?> <?php $_order = $block->getItem()->getOrder(); ?> -<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - +<?php foreach ($items as $_item) : ?> <?php // As part of invoice item renderer logic, the order is set on each invoice item. // In the case of a bundle product, this template takes over rendering its children, @@ -31,40 +28,40 @@ $_item->setOrder($_order); ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="bundle-option-label"> <td colspan="3"> - <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> + <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> <tr class="bundle-item bundle-parent"> <td class="item-info"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> </td> - <?php else: ?> + <?php else : ?> <tr class="bundle-item bundle-option-value"> <td class="item-info"> <p><?= $block->getValueHtml($_item) ?></p> </td> <?php endif; ?> <td class="item-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty() * 1 ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> <td class="item-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> - <?php else: ?> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= /* @noEscape */ $block->getItemPrice($_item) ?> + <?php else : ?>   <?php endif; ?> </td> @@ -72,14 +69,14 @@ <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr> <td colspan="3" class="item-extra"> - <?php if ($block->getItemOptions()): ?> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> - <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> + <dd><?= $block->escapeHtml($option['value']) ?></dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml index c9c3103c448e4..b50b455b59a08 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/order/default.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> <?php $_item = $block->getItem() ?> @@ -14,41 +12,40 @@ <?php $parentItem = $block->getItem() ?> <?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> -<?php if ($block->getItemOptions() || $_item->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $_item) && $_item->getGiftMessageId()): ?> +<?php if ($block->getItemOptions() || $_item->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $_item) && $_item->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - - <?php if ($_item->getParentItem()): ?> +<?php foreach ($items as $_item) : ?> + <?php if ($_item->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="bundle-option-label"> <td colspan="3"> - <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> + <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <?php if (!$_item->getParentItem()): ?> + <?php if (!$_item->getParentItem()) : ?> <tr class="bundle-item bundle-parent"> <td class="item-info"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> </td> <td class="item-qty"> - <?= /* @escapeNotVerified */ $_item->getQtyOrdered() * 1 ?> + <?= (float)$_item->getQtyOrdered() * 1 ?> </td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> + <?= /* @noEscape */ $block->getItemPrice($_item) ?> </td> </tr> - <?php else: ?> + <?php else : ?> <tr class="bundle-item bundle-option-value"> <td class="item-info" colspan="3"> <p><?= $block->getValueHtml($_item) ?></p> @@ -58,25 +55,25 @@ <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr> <td colspan="3" class="item-extra"> - <?php if ($block->getItemOptions()): ?> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> - <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> + <dd><?= $block->escapeHtml($option['value']) ?></dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_item->getGiftMessageId())): ?> + <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper(Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_item->getGiftMessageId())) : ?> <table class="message-gift"> <tr> <td> - <h3><?= /* @escapeNotVerified */ __('Gift Message') ?></h3> - <strong><?= /* @escapeNotVerified */ __('From:') ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('To:') ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('Message:') ?></strong> + <h3><?= $block->escapeHtml(__('Gift Message')) ?></h3> + <strong><?= $block->escapeHtml(__('From:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> + <br /><strong><?= $block->escapeHtml(__('To:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> + <br /><strong><?= $block->escapeHtml(__('Message:')) ?></strong> <br /><?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> diff --git a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml index 61582087d1fcc..e63660802929e 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/email/order/items/shipment/default.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> @@ -14,49 +12,48 @@ <?php $items = array_merge([$parentItem->getOrderItem()], $parentItem->getOrderItem()->getChildrenItems()) ?> <?php $shipItems = $block->getChildren($parentItem) ?> -<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> -<?php else: ?> +<?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - - <?php if ($_item->getParentItem()): ?> +<?php foreach ($items as $_item) : ?> + <?php if ($_item->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="bundle-option-label"> <td colspan="2"> - <strong><em><?= /* @escapeNotVerified */ $attributes['option_label'] ?></em></strong> + <strong><em><?= $block->escapeHtml($attributes['option_label']) ?></em></strong> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <?php if (!$_item->getParentItem()): ?> + <?php if (!$_item->getParentItem()) : ?> <tr class="bundle-item bundle-parent"> <td class="item-info"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> </td> - <?php else: ?> + <?php else : ?> <tr class="bundle-item bundle-option-value"> <td class="item-info"> <p><?= $block->getValueHtml($_item) ?></p> </td> <?php endif; ?> <td class="item-qty"> - <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> - <?php if (isset($shipItems[$_item->getId()])): ?> - <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty() * 1 ?> - <?php elseif ($_item->getIsVirtual()): ?> - <?= /* @escapeNotVerified */ __('N/A') ?> - <?php else: ?> + <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> + <?php if (isset($shipItems[$_item->getId()])) : ?> + <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> + <?php elseif ($_item->getIsVirtual()) : ?> + <?= $block->escapeHtml(__('N/A')) ?> + <?php else : ?> 0 <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> @@ -64,14 +61,14 @@ <?php endforeach; ?> -<?php if ($_showlastRow): ?> +<?php if ($_showlastRow) : ?> <tr> <td colspan="2" class="item-extra"> - <?php if ($block->getItemOptions()): ?> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> - <dd><?= /* @escapeNotVerified */ $option['value'] ?></dd> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> + <dd><?= $block->escapeHtml($option['value']) ?></dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml b/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml index bad5acc209b5f..4ca5983081a6f 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/js/components.phtml @@ -3,8 +3,5 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?= $block->getChildHtml() ?> +<?= $block->getChildHtml(); diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml index b4c2c00e5ac43..87efa31a06a91 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/creditmemo/items/renderer.phtml @@ -3,105 +3,104 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> <?php $parentItem = $block->getItem() ?> <?php $items = $block->getChildren($parentItem) ?> <?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> -<?php $_count = count($items) ?> <?php $_index = 0 ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php foreach ($items as $_item) : ?> + <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> - <?php else: ?> + <?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="options-label"> - <td class="col label" colspan="7"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col label" colspan="7"><div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getOrderItem()->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> +<tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" + class="<?php if ($_item->getOrderItem()->getParentItem()) : ?>item-options-container<?php else : ?>item-parent<?php endif; ?>" + <?php if ($_item->getParentItem()) : ?> + data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>"<?php endif; ?>> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> </td> - <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> + <?php else : ?> + <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> + <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getItemPriceHtml($_item) ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> - <?php else: ?> + <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Quantity')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getItemRowTotalHtml($_item) ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $block->getOrder()->formatPrice(-$_item->getDiscountAmount()) ?> - <?php else: ?> + <td class="col discount" data-th="<?= $block->escapeHtmlAttr(__('Discount Amount')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= $block->escapeHtml($block->getOrder()->formatPrice(-$_item->getDiscountAmount()), ['span']) ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col rowtotal" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <td class="col rowtotal" data-th="<?= $block->escapeHtmlAttr(__('Row Total')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getItemRowTotalAfterDiscountHtml($_item) ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> <tr> <td class="col options" colspan="7"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($_options = $block->getItemOptions()) : ?> <dl class="item-options"> <?php foreach ($_options as $_option) : ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> + <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> + <?= /* @noEscape */ $_formatedOptionValue['value'] ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> </dl> </div> <?php endif; ?> </dd> - <?php else: ?> + <?php else : ?> <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> <?php endif; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml index f3866d7b1b682..590ca301c29ff 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/invoice/items/renderer.phtml @@ -3,90 +3,89 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> <?php $parentItem = $block->getItem() ?> <?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> <?php $items = $block->getChildren($parentItem) ?> -<?php $_count = count($items) ?> <?php $_index = 0 ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php foreach ($items as $_item) : ?> + <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> - <?php else: ?> + <?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> - <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_item->getOrderItem()->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="options-label"> - <td class="col label" colspan="5"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td class="col label" colspan="5"><div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getOrderItem()->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getOrderItem()->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getOrderItem()->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> + <tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" + class="<?php if ($_item->getOrderItem()->getParentItem()) : ?>item-options-container<?php else : ?>item-parent<?php endif; ?>" + <?php if ($_item->getOrderItem()->getParentItem()) : ?> + data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>"<?php endif; ?>> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> </td> - <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> + <?php else : ?> + <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> + <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getItemPriceHtml($_item) ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Invoiced')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> - <?php else: ?> + <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Qty Invoiced')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> + <?= (float)$_item->getQty() * 1 ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> + <?php if ($block->canShowPriceInfo($_item)) : ?> <?= $block->getItemRowTotalHtml($_item) ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> <tr> <td class="col options" colspan="5"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($_options = $block->getItemOptions()) : ?> <dl class="item-options"> <?php foreach ($_options as $_option) : ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> + <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> + <?= /* @noEscape */ $_formatedOptionValue['value'] ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> </dl> </div> <?php endif; ?> </dd> - <?php else: ?> + <?php else : ?> <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> <?php endif; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml index 84cbd54d3bdcc..0a946a2522ed9 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml @@ -3,134 +3,149 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ +$parentItem = $block->getItem(); +$items = array_merge([$parentItem], $parentItem->getChildrenItems()); +$index = 0; +$prevOptionId = ''; ?> -<?php $parentItem = $block->getItem() ?> -<?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> -<?php $_count = count($items) ?> -<?php $_index = 0 ?> - -<?php $_prevOptionId = '' ?> - -<?php foreach ($items as $_item): ?> - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> - <?php $_showlastRow = true ?> - <?php else: ?> - <?php $_showlastRow = false ?> +<?php foreach ($items as $item) : ?> + <?php if ($block->getItemOptions() + || $parentItem->getDescription() + || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) + && $parentItem->getGiftMessageId()) : ?> + <?php $showLastRow = true; ?> + <?php else : ?> + <?php $showLastRow = false; ?> <?php endif; ?> - <?php if ($_item->getParentItem()): ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($item->getParentItem()) : ?> + <?php $attributes = $block->getSelectionAttributes($item) ?> + <?php if ($prevOptionId != $attributes['option_id']) : ?> <tr class="options-label"> - <td class="col label" colspan="5"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></td> + <td class="col label" colspan="5"><?= $block->escapeHtml($attributes['option_label']); ?></td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> +<tr id="order-item-row-<?= /* @noEscape */ $item->getId() ?>" + class="<?php if ($item->getParentItem()) : ?> + item-options-container + <?php else : ?> + item-parent + <?php endif; ?>" + <?php if ($item->getParentItem()) : ?> + data-th="<?= $block->escapeHtmlAttr($attributes['option_label']); ?>" + <?php endif; ?>> + <?php if (!$item->getParentItem()) : ?> + <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')); ?>"> + <strong class="product name product-item-name"><?= $block->escapeHtml($item->getName()); ?></strong> + </td> + <?php else : ?> + <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')); ?>"> + <?= $block->getValueHtml($item); ?> </td> - <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($_item->getSku()) ?></td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemPriceHtml() ?> - <?php else: ?> + <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')); ?>"> + <?= /* @noEscape */ $block->prepareSku($item->getSku()); ?> + </td> + <td class="col price" data-th="<?= $block->escapeHtmlAttr(__('Price')); ?>"> + <?php if (!$item->getParentItem()) : ?> + <?= /* @noEscape */ $block->getItemPriceHtml(); ?> + <?php else : ?>   <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> - <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Quantity')); ?>"> + <?php if (($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())) : ?> <ul class="items-qty"> <?php endif; ?> - <?php if (($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated())): ?> - <?php if ($_item->getQtyOrdered() > 0): ?> + <?php if (($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated())) : ?> + <?php if ($item->getQtyOrdered() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Ordered') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Ordered')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyOrdered() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> + <?php if ($item->getQtyShipped() > 0 && !$block->isShipmentSeparately()) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyCanceled() > 0): ?> + <?php if ($item->getQtyCanceled() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Canceled') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Canceled')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyCanceled() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyRefunded() > 0): ?> + <?php if ($item->getQtyRefunded() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Refunded') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Refunded')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyRefunded() * 1; ?></span> </li> <?php endif; ?> - <?php elseif ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately()): ?> + <?php elseif ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately()) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> - <?php else: ?> -   + <?php else : ?> + <span class="content"><?= /* @noEscape */ $parentItem->getQtyOrdered() * 1; ?></span> <?php endif; ?> - <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + <?php if (($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())) :?> </ul> <?php endif; ?> </td> - <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemRowTotalHtml() ?> - <?php else: ?> + <td class="col subtotal" data-th="<?= $block->escapeHtmlAttr(__('Subtotal')) ?>"> + <?php if (!$item->getParentItem()) : ?> + <?= /* @noEscape */ $block->getItemRowTotalHtml(); ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($showLastRow && (($options = $block->getItemOptions()) || $block->escapeHtml($item->getDescription()))) : ?> <tr> <td class="col options" colspan="5"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($options = $block->getItemOptions()) : ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <?php foreach ($options as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> + <?php if (!$block->getPrintStatus()) : ?> + <?php $formattedOptionValue = $block->getFormatedOptionValue($option) ?> + <dd<?php if (isset($formattedOptionValue['full_view'])) : ?> + class="tooltip wrapper" + <?php endif; ?>> + <?= /* @noEscape */ $formattedOptionValue['value'] ?> + <?php if (isset($formattedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dt><?= $block->escapeHtml($option['label']); ?></dt> + <dd><?= /* @noEscape */ $formattedOptionValue['full_view']; ?></dd> </dl> </div> <?php endif; ?> </dd> - <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else : ?> + <dd><?= $block->escapeHtml((isset($option['print_value']) ? + $option['print_value'] : + $option['value'])); ?> + </dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> + <?= $block->escapeHtml($item->getDescription()); ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml index bd99afc59a8c0..ae6e12b80d54e 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/shipment/items/renderer.phtml @@ -3,83 +3,82 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ ?> <?php $parentItem = $block->getItem() ?> <?php $items = array_merge([$parentItem->getOrderItem()], $parentItem->getOrderItem()->getChildrenItems()) ?> <?php $shipItems = $block->getChildren($parentItem) ?> -<?php $_count = count($items) ?> <?php $_index = 0 ?> <?php $_prevOptionId = '' ?> -<?php foreach ($items as $_item): ?> - - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> +<?php foreach ($items as $_item) : ?> + <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper(Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()) : ?> <?php $_showlastRow = true ?> - <?php else: ?> + <?php else : ?> <?php $_showlastRow = false ?> <?php endif; ?> - <?php if ($_item->getParentItem()): ?> + <?php if ($_item->getParentItem()) : ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($_prevOptionId != $attributes['option_id']) : ?> <tr class="options-label"> - <td colspan="3" class="col label"><div class="option label"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></div></td> + <td colspan="3" class="col label"><div class="option label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> - <tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> + <tr id="order-item-row-<?= $block->escapeHtmlAttr($_item->getId()) ?>" + class="<?php if ($_item->getParentItem()) : ?>item-options-container<?php else : ?>item-parent<?php endif; ?>" + <?php if ($_item->getParentItem()) : ?> + data-th="<?= $block->escapeHtmlAttr($attributes['option_label']) ?>"<?php endif; ?>> + <?php if (!$_item->getParentItem()) : ?> + <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> </td> - <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> + <?php else : ?> + <td class="col value" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"> - <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> - <?php if (isset($shipItems[$_item->getId()])): ?> - <?= /* @escapeNotVerified */ $shipItems[$_item->getId()]->getQty()*1 ?> - <?php elseif ($_item->getIsVirtual()): ?> - <?= /* @escapeNotVerified */ __('N/A') ?> - <?php else: ?> + <td class="col sku" data-th="<?= $block->escapeHtmlAttr(__('SKU')) ?>"><?= $block->escapeHtml($_item->getSku()) ?></td> + <td class="col qty" data-th="<?= $block->escapeHtmlAttr(__('Qty Shipped')) ?>"> + <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> + <?php if (isset($shipItems[$_item->getId()])) : ?> + <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> + <?php elseif ($_item->getIsVirtual()) : ?> + <?= $block->escapeHtml(__('N/A')) ?> + <?php else : ?> 0 <?php endif; ?> - <?php else: ?> + <?php else : ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))) : ?> <tr> <td class="col options" colspan="3"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($_options = $block->getItemOptions()) : ?> <dl class="item-options"> <?php foreach ($_options as $_option) : ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> + <?php if (!$block->getPrintStatus()) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <dd<?php if (isset($_formatedOptionValue['full_view'])) : ?> class="tooltip wrapper"<?php endif; ?>> + <?= /* @noEscape */ $_formatedOptionValue['value'] ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dd><?= /* @noEscape */ $_formatedOptionValue['full_view'] ?></dd> </dl> </div> <?php endif; ?> </dd> - <?php else: ?> + <?php else : ?> <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> <?php endif; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js index d8d4cb1e99b7f..1e7fe6b6673d6 100644 --- a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js +++ b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js @@ -56,8 +56,9 @@ define([ // Clear Summary box this.element.html(''); - - $.each(this.cache.currentElement.selected, $.proxy(this._renderOption, this)); + this.cache.currentElement.positions.forEach(function (optionId) { + this._renderOption(optionId, this.cache.currentElement.selected[optionId]); + }, this); this.element .parents(this.options.bundleSummaryContainer) .toggleClass('empty', !this.cache.currentElementCount); // Zero elements equal '.empty' container diff --git a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php index 9b8518a41ffff..15e4dafc7cb1b 100644 --- a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php @@ -9,9 +9,10 @@ use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel; use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection; -use Magento\ImportExport\Controller\Adminhtml\Import; use Magento\ImportExport\Model\Import as ImportModel; use \Magento\Catalog\Model\Product\Type\AbstractType; +use \Magento\Framework\App\ObjectManager; +use \Magento\Store\Model\StoreManagerInterface; /** * Class RowCustomizer @@ -105,6 +106,35 @@ class RowCustomizer implements RowCustomizerInterface AbstractType::SHIPMENT_SEPARATELY => 'separately', ]; + /** + * @var \Magento\Bundle\Model\ResourceModel\Option\Collection[] + */ + private $optionCollections = []; + + /** + * @var array + */ + private $storeIdToCode = []; + + /** + * @var string + */ + private $optionCollectionCacheKey = '_cache_instance_options_collection'; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + * @throws \RuntimeException + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + /** * Retrieve list of bundle specific columns * @return array @@ -207,15 +237,13 @@ protected function populateBundleData($collection) */ protected function getFormattedBundleOptionValues($product) { - /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ - $optionsCollection = $product->getTypeInstance() - ->getOptionsCollection($product) - ->setOrder('position', Collection::SORT_ORDER_ASC); - + $optionCollections = $this->getProductOptionCollections($product); $bundleData = ''; - foreach ($optionsCollection as $option) { + $optionTitles = $this->getBundleOptionTitles($product); + foreach ($optionCollections->getItems() as $option) { + $optionValues = $this->getFormattedOptionValues($option, $optionTitles); $bundleData .= $this->getFormattedBundleSelections( - $this->getFormattedOptionValues($option), + $optionValues, $product->getTypeInstance() ->getSelectionsCollection([$option->getId()], $product) ->setOrder('position', Collection::SORT_ORDER_ASC) @@ -266,16 +294,23 @@ function ($value, $key) { * Retrieve option value of bundle product * * @param \Magento\Bundle\Model\Option $option + * @param string[] $optionTitles * @return string */ - protected function getFormattedOptionValues($option) + protected function getFormattedOptionValues($option, $optionTitles = []) { - return 'name' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getTitle() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getRequired(); + $names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map( + function ($title, $storeName) { + return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title; + }, + $optionTitles[$option->getOptionId()], + array_keys($optionTitles[$option->getOptionId()]) + )); + return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getRequired(); } /** @@ -286,7 +321,7 @@ protected function getFormattedOptionValues($option) */ protected function getTypeValue($type) { - return isset($this->typeMapping[$type]) ? $this->typeMapping[$type] : self::VALUE_DYNAMIC; + return $this->typeMapping[$type] ?? self::VALUE_DYNAMIC; } /** @@ -297,7 +332,7 @@ protected function getTypeValue($type) */ protected function getPriceViewValue($type) { - return isset($this->priceViewMapping[$type]) ? $this->priceViewMapping[$type] : self::VALUE_PRICE_RANGE; + return $this->priceViewMapping[$type] ?? self::VALUE_PRICE_RANGE; } /** @@ -308,7 +343,7 @@ protected function getPriceViewValue($type) */ protected function getPriceTypeValue($type) { - return isset($this->priceTypeMapping[$type]) ? $this->priceTypeMapping[$type] : null; + return $this->priceTypeMapping[$type] ?? null; } /** @@ -319,7 +354,7 @@ protected function getPriceTypeValue($type) */ private function getShipmentTypeValue($type) { - return isset($this->shipmentTypeMapping[$type]) ? $this->shipmentTypeMapping[$type] : null; + return $this->shipmentTypeMapping[$type] ?? null; } /** @@ -380,4 +415,82 @@ private function parseAdditionalAttributes($additionalAttributes) } return $preparedAttributes; } + + /** + * Get product options titles. + * + * Values for all store views (default) should be specified with 'name' key. + * If user want to specify value or change existing for non default store views it should be specified with + * 'name_' prefix and needed store view suffix. + * + * For example: + * - 'name=All store views name' for all store views + * - 'name_specific_store=Specific store name' for store view with 'specific_store' store code + * + * @param \Magento\Catalog\Model\Product $product + * @return array + */ + private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product): array + { + $optionCollections = $this->getProductOptionCollections($product); + $optionsTitles = []; + /** @var \Magento\Bundle\Model\Option $option */ + foreach ($optionCollections->getItems() as $option) { + $optionsTitles[$option->getId()]['name'] = $option->getTitle(); + } + $storeIds = $product->getStoreIds(); + if (count($storeIds) > 1) { + foreach ($storeIds as $storeId) { + $optionCollections = $this->getProductOptionCollections($product, $storeId); + /** @var \Magento\Bundle\Model\Option $option */ + foreach ($optionCollections->getItems() as $option) { + $optionTitle = $option->getTitle(); + if ($optionsTitles[$option->getId()]['name'] != $optionTitle) { + $optionsTitles[$option->getId()]['name_' . $this->getStoreCodeById($storeId)] = $optionTitle; + } + } + } + } + return $optionsTitles; + } + + /** + * Get product options collection by provided product model. + * + * Set given store id to the product if it was defined (default store id will be set if was not). + * + * @param \Magento\Catalog\Model\Product $product $product + * @param int $storeId + * @return \Magento\Bundle\Model\ResourceModel\Option\Collection + */ + private function getProductOptionCollections( + \Magento\Catalog\Model\Product $product, + $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID + ): \Magento\Bundle\Model\ResourceModel\Option\Collection { + $productSku = $product->getSku(); + if (!isset($this->optionCollections[$productSku][$storeId])) { + $product->unsetData($this->optionCollectionCacheKey); + $product->setStoreId($storeId); + $this->optionCollections[$productSku][$storeId] = $product->getTypeInstance() + ->getOptionsCollection($product) + ->setOrder('position', Collection::SORT_ORDER_ASC); + } + return $this->optionCollections[$productSku][$storeId]; + } + + /** + * Retrieve store code by it's ID. + * + * Collect store id in $storeIdToCode[] private variable if it was not initialized earlier. + * + * @param int $storeId + * @return string + */ + private function getStoreCodeById($storeId): string + { + if (!isset($this->storeIdToCode[$storeId])) { + $this->storeIdToCode[$storeId] = $this->storeManager->getStore($storeId)->getCode(); + } + return $this->storeIdToCode[$storeId]; + } } diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index 96b7c7b1430b0..0acaf1aecd2ad 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -8,15 +8,18 @@ */ namespace Magento\BundleImportExport\Model\Import\Product\Type; -use \Magento\Framework\App\ObjectManager; -use \Magento\Bundle\Model\Product\Price as BundlePrice; -use \Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Framework\App\ObjectManager; +use Magento\Bundle\Model\Product\Price as BundlePrice; +use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\CatalogImportExport\Model\Import\Product; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Bundle\Model\ResourceModel\Bundle as BundleResourceModel; /** * Class Bundle * @package Magento\BundleImportExport\Model\Import\Product\Type * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType { @@ -136,6 +139,21 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst */ private $relationsDataSaver; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var array + */ + private $storeCodeToId = []; + + /** + * @var BundleResourceModel + */ + private $bundleResourceModel; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac @@ -143,6 +161,9 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst * @param array $params * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool * @param Bundle\RelationsDataSaver|null $relationsDataSaver + * @param StoreManagerInterface $storeManager + * @param BundleResourceModel $bundleResourceModel + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac, @@ -150,12 +171,17 @@ public function __construct( \Magento\Framework\App\ResourceConnection $resource, array $params, \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - Bundle\RelationsDataSaver $relationsDataSaver = null + Bundle\RelationsDataSaver $relationsDataSaver = null, + StoreManagerInterface $storeManager = null, + BundleResourceModel $bundleResourceModel = null ) { parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool); - $this->relationsDataSaver = $relationsDataSaver ?: ObjectManager::getInstance()->get(Bundle\RelationsDataSaver::class); + $this->storeManager = $storeManager + ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + $this->bundleResourceModel = $bundleResourceModel + ?: ObjectManager::getInstance()->get(BundleResourceModel::class); } /** @@ -261,20 +287,28 @@ protected function populateOptionTemplate($option, $entityId, $index = null) * @param array $option * @param int $optionId * @param int $storeId - * - * @return array|bool + * @return array */ protected function populateOptionValueTemplate($option, $optionId, $storeId = 0) { - if (!isset($option['name']) || !isset($option['parent_id']) || !$optionId) { - return false; + $optionValues = []; + if (isset($option['name']) && isset($option['parent_id']) && $optionId) { + $pattern = '/^name[_]?(.*)/'; + $keys = array_keys($option); + $optionNames = preg_grep($pattern, $keys); + foreach ($optionNames as $optionName) { + preg_match($pattern, $optionName, $storeCodes); + $storeCode = array_pop($storeCodes); + $storeId = $storeCode ? $this->getStoreIdByCode($storeCode) : $storeId; + $optionValues[] = [ + 'option_id' => $optionId, + 'parent_product_id' => $option['parent_id'], + 'store_id' => $storeId, + 'title' => $option[$optionName], + ]; + } } - return [ - 'option_id' => $optionId, - 'parent_product_id' => $option['parent_id'], - 'store_id' => $storeId, - 'title' => $option['name'], - ]; + return $optionValues; } /** @@ -284,7 +318,7 @@ protected function populateOptionValueTemplate($option, $optionId, $storeId = 0) * @param int $optionId * @param int $parentId * @param int $index - * @return array + * @return array|bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -364,13 +398,15 @@ public function saveData() if ($this->_type != $productData['type_id']) { continue; } - $this->parseSelections($rowData, $productData[$this->getProductEntityLinkField()]); + $productId = $productData[$this->getProductEntityLinkField()]; + $this->parseSelections($rowData, $productId); } if (!empty($this->_cachedOptions)) { $this->retrieveProducsByCachedSkus(); $this->populateExistingOptions(); $this->insertOptions(); $this->insertSelections(); + $this->insertProductRelations(); $this->clear(); } } @@ -564,21 +600,24 @@ protected function insertOptions() */ protected function populateInsertOptionValues($optionIds) { - $insertValues = []; + $optionValues = []; foreach ($this->_cachedOptions as $entityId => $options) { foreach ($options as $key => $option) { foreach ($optionIds as $optionId => $assoc) { if ($assoc['position'] == $this->_cachedOptions[$entityId][$key]['index'] && $assoc['parent_id'] == $entityId) { $option['parent_id'] = $entityId; - $insertValues[] = $this->populateOptionValueTemplate($option, $optionId); + $optionValues = array_merge( + $optionValues, + $this->populateOptionValueTemplate($option, $optionId) + ); $this->_cachedOptions[$entityId][$key]['option_id'] = $optionId; break; } } } } - return $insertValues; + return $optionValues; } /** @@ -615,6 +654,29 @@ protected function insertSelections() return $this; } + /** + * Insert product relations. + * + * @return void + */ + private function insertProductRelations() + { + foreach ($this->_cachedOptions as $productId => $options) { + $childIds = []; + foreach ($options as $option) { + foreach ($option['selections'] as $selection) { + if (isset($this->_cachedSkuToProducts[$selection['sku']])) { + $childIds[] = $this->_cachedSkuToProducts[$selection['sku']]; + } + } + } + if (!empty($childIds)) { + $childIds = array_unique($childIds); + $this->bundleResourceModel->saveProductRelations($productId, $childIds); + } + } + } + /** * Initialize attributes parameters for all attributes' sets. * @@ -695,4 +757,21 @@ protected function clear() $this->_cachedSkuToProducts = []; return $this; } + + /** + * Get store id by store code. + * + * @param string $storeCode + * @return int + */ + private function getStoreIdByCode(string $storeCode): int + { + if (!isset($this->storeIdToCode[$storeCode])) { + /** @var $store \Magento\Store\Model\Store */ + foreach ($this->storeManager->getStores() as $store) { + $this->storeCodeToId[$store->getCode()] = $store->getId(); + } + } + return $this->storeCodeToId[$storeCode]; + } } diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE.txt b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/BundleImportExport/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 <insert your license name here>" 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/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/BundleImportExport/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/BundleImportExport/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 <insert your license name here>" 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/app/code/Magento/BundleImportExport/Test/Mftf/README.md b/app/code/Magento/BundleImportExport/Test/Mftf/README.md new file mode 100644 index 0000000000000..e4c6855132d05 --- /dev/null +++ b/app/code/Magento/BundleImportExport/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Bundle Import Export Functional Tests + +The Functional Test Module for **Magento Bundle Import Export** module. diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php index e76e9e1ba565f..027d30e0da39d 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -52,14 +52,24 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase */ protected $selection; + /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeResolver; + /** * Set up */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getScope']) + ->getMockForAbstractClass(); $this->rowCustomizerMock = $this->objectManagerHelper->getObject( - \Magento\BundleImportExport\Model\Export\RowCustomizer::class + \Magento\BundleImportExport\Model\Export\RowCustomizer::class, + [ + 'scopeResolver' => $this->scopeResolver + ] ); $this->productResourceCollection = $this->createPartialMock( \Magento\Catalog\Model\ResourceModel\Product\Collection::class, @@ -72,6 +82,8 @@ protected function setUp() 'getPriceType', 'getShipmentType', 'getSkuType', + 'getSku', + 'getStoreIds', 'getPriceView', 'getWeightType', 'getTypeInstance', @@ -79,6 +91,7 @@ protected function setUp() 'getSelectionsCollection' ] ); + $this->product->expects($this->any())->method('getStoreIds')->willReturn([1]); $this->product->expects($this->any())->method('getEntityId')->willReturn(1); $this->product->expects($this->any())->method('getPriceType')->willReturn(1); $this->product->expects($this->any())->method('getShipmentType')->willReturn(1); @@ -88,19 +101,20 @@ protected function setUp() $this->product->expects($this->any())->method('getTypeInstance')->willReturnSelf(); $this->optionsCollection = $this->createPartialMock( \Magento\Bundle\Model\ResourceModel\Option\Collection::class, - ['setOrder', 'getIterator'] + ['setOrder', 'getItems'] ); $this->product->expects($this->any())->method('getOptionsCollection')->willReturn($this->optionsCollection); $this->optionsCollection->expects($this->any())->method('setOrder')->willReturnSelf(); $this->option = $this->createPartialMock( \Magento\Bundle\Model\Option::class, - ['getId', 'getTitle', 'getType', 'getRequired'] + ['getId', 'getOptionId', 'getTitle', 'getType', 'getRequired'] ); $this->option->expects($this->any())->method('getId')->willReturn(1); + $this->option->expects($this->any())->method('getOptionId')->willReturn(1); $this->option->expects($this->any())->method('getTitle')->willReturn('title'); $this->option->expects($this->any())->method('getType')->willReturn(1); $this->option->expects($this->any())->method('getRequired')->willReturn(1); - $this->optionsCollection->expects($this->any())->method('getIterator')->will( + $this->optionsCollection->expects($this->any())->method('getItems')->will( $this->returnValue(new \ArrayIterator([$this->option])) ); $this->selection = $this->createPartialMock( @@ -122,6 +136,7 @@ protected function setUp() $this->product->expects($this->any())->method('getSelectionsCollection')->willReturn( $this->selectionsCollection ); + $this->product->expects($this->any())->method('getSku')->willReturn(1); $this->productResourceCollection->expects($this->any())->method('addAttributeToFilter')->willReturnSelf(); $this->productResourceCollection->expects($this->any())->method('getIterator')->will( $this->returnValue(new \ArrayIterator([$this->product])) @@ -133,6 +148,9 @@ protected function setUp() */ public function testPrepareData() { + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope') + ->willReturn($scope); $result = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); $this->assertNotNull($result); } @@ -160,6 +178,9 @@ public function testAddHeaderColumns() */ public function testAddData() { + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope') + ->willReturn($scope); $preparedData = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); $attributes = 'attribute=1,sku_type=1,attribute2="Text",price_type=1,price_view=1,weight_type=1,' . 'values=values,shipment_type=1,attribute3=One,Two,Three'; diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php index 8e1243b5eb3af..59a704d5305e4 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php @@ -58,6 +58,9 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm */ protected $setCollection; + /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeResolver; + /** * * @return void @@ -170,14 +173,18 @@ protected function setUp() 0 => $this->entityModel, 1 => 'bundle' ]; - + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getScope']) + ->getMockForAbstractClass(); $this->bundle = $this->objectManagerHelper->getObject( \Magento\BundleImportExport\Model\Import\Product\Type\Bundle::class, [ 'attrSetColFac' => $this->attrSetColFac, 'prodAttrColFac' => $this->prodAttrColFac, 'resource' => $this->resource, - 'params' => $this->params + 'params' => $this->params, + 'scopeResolver' => $this->scopeResolver ] ); @@ -214,7 +221,8 @@ public function testSaveData($skus, $bunch, $allowImport) $this->entityModel->expects($this->any())->method('isRowAllowedToImport')->will($this->returnValue( $allowImport )); - + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope')->willReturn($scope); $this->connection->expects($this->any())->method('fetchAssoc')->with($this->select)->will($this->returnValue([ '1' => [ 'option_id' => '1', @@ -234,7 +242,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'fixed', 'shipment_type' => '1', 'default_qty' => '1', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '1', 'option_id' => '1'] ] @@ -256,7 +264,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'percent', 'shipment_type' => 0, 'default_qty' => '2', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '6', 'option_id' => '6'] ] @@ -316,7 +324,7 @@ public function saveDataProvider() . 'price_type=fixed,' . 'shipment_type=separately,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=1,' . 'option_id=1 | name=Bundle2,' . 'type=dropdown,' @@ -325,7 +333,7 @@ public function saveDataProvider() . 'price=10,' . 'price_type=fixed,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=2,' . 'option_id=2' ], diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index f5ad6143dc501..5f1384ee2e34b 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -2,16 +2,17 @@ "name": "magento/module-bundle-import-export", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-import-export": "100.2.*", "magento/module-catalog-import-export": "100.2.*", "magento/module-bundle": "100.2.*", + "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index 8acf170d43cfb..8e0c00b587460 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -7,6 +7,9 @@ use Magento\Framework\Cache\InvalidateLogger; +/** + * Purge cache action. + */ class PurgeCache { const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern'; @@ -26,6 +29,18 @@ class PurgeCache */ private $logger; + /** + * Batch size of the purge request. + * + * Based on default Varnish 4 http_req_hdr_len size minus a 512 bytes margin for method, + * header name, line feeds etc. + * + * @see https://varnish-cache.org/docs/4.1/reference/varnishd.html + * + * @var int + */ + private $requestSize = 7680; + /** * Constructor * @@ -44,18 +59,68 @@ public function __construct( } /** - * Send curl purge request - * to invalidate cache by tags pattern + * Send curl purge request to invalidate cache by tags pattern. * * @param string $tagsPattern * @return bool Return true if successful; otherwise return false */ public function sendPurgeRequest($tagsPattern) { + $successful = true; $socketAdapter = $this->socketAdapterFactory->create(); $servers = $this->cacheServer->getUris(); - $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $tagsPattern]; $socketAdapter->setOptions(['timeout' => 10]); + + $formattedTagsChunks = $this->splitTags($tagsPattern); + foreach ($formattedTagsChunks as $formattedTagsChunk) { + if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) { + $successful = false; + } + } + + return $successful; + } + + /** + * Split tags by batches + * + * @param string $tagsPattern + * @return \Generator + */ + private function splitTags(string $tagsPattern) : \Generator + { + $tagsBatchSize = 0; + $formattedTagsChunk = []; + $formattedTags = explode('|', $tagsPattern); + foreach ($formattedTags as $formattedTag) { + if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { + yield implode('|', $formattedTagsChunk); + $formattedTagsChunk = []; + $tagsBatchSize = 0; + } + + $tagsBatchSize += strlen($formattedTag); + $formattedTagsChunk[] = $formattedTag; + } + if (!empty($formattedTagsChunk)) { + yield implode('|', $formattedTagsChunk); + } + } + + /** + * Send curl purge request to servers to invalidate cache by tags pattern. + * + * @param \Zend\Http\Client\Adapter\Socket $socketAdapter + * @param \Zend\Uri\Uri[] $servers + * @param string $formattedTagsChunk + * @return bool Return true if successful; otherwise return false + */ + private function sendPurgeRequestToServers( + \Zend\Http\Client\Adapter\Socket $socketAdapter, + array $servers, + string $formattedTagsChunk + ): bool { + $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; foreach ($servers as $server) { $headers['Host'] = $server->getHost(); try { @@ -69,12 +134,13 @@ public function sendPurgeRequest($tagsPattern) $socketAdapter->read(); $socketAdapter->close(); } catch (\Exception $e) { - $this->logger->critical($e->getMessage(), compact('server', 'tagsPattern')); + $this->logger->critical($e->getMessage(), compact('server', 'formattedTagsChunk')); + return false; } } + $this->logger->execute(compact('servers', 'formattedTagsChunk')); - $this->logger->execute(compact('servers', 'tagsPattern')); return true; } } diff --git a/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE.txt b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CacheInvalidate/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 <insert your license name here>" 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/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CacheInvalidate/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CacheInvalidate/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 <insert your license name here>" 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/app/code/Magento/CacheInvalidate/Test/Mftf/README.md b/app/code/Magento/CacheInvalidate/Test/Mftf/README.md new file mode 100644 index 0000000000000..403a6f15d089d --- /dev/null +++ b/app/code/Magento/CacheInvalidate/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Cache Invalidate Functional Tests + +The Functional Test Module for **Magento Cache Invalidate** module. diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php index 013ec3a467104..c66e27ea41025 100644 --- a/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php +++ b/app/code/Magento/CacheInvalidate/Test/Unit/Model/PurgeCacheTest.php @@ -84,6 +84,9 @@ public function testSendPurgeRequest($hosts) $this->assertTrue($this->model->sendPurgeRequest('tags')); } + /** + * @return array + */ public function sendPurgeRequestDataProvider() { return [ diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json index 4585dbf85c30d..3259857b87504 100644 --- a/app/code/Magento/CacheInvalidate/composer.json +++ b/app/code/Magento/CacheInvalidate/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-cache-invalidate", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-page-cache": "100.2.*", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php b/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php index c0a091ca2d0d9..027c9a9085b47 100644 --- a/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php +++ b/app/code/Magento/Captcha/Block/Captcha/DefaultCaptcha.php @@ -13,7 +13,7 @@ class DefaultCaptcha extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'default.phtml'; + protected $_template = 'Magento_Captcha::default.phtml'; /** * @var string diff --git a/app/code/Magento/Captcha/Controller/Refresh/Index.php b/app/code/Magento/Captcha/Controller/Refresh/Index.php index e89a80646ed8e..3f831606570ca 100644 --- a/app/code/Magento/Captcha/Controller/Refresh/Index.php +++ b/app/code/Magento/Captcha/Controller/Refresh/Index.php @@ -40,10 +40,15 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $formId = $this->_request->getPost('formId'); if (null === $formId) { $params = []; @@ -51,7 +56,7 @@ public function execute() if ($content) { $params = $this->serializer->unserialize($content); } - $formId = isset($params['formId']) ? $params['formId'] : null; + $formId = $params['formId'] ?? null; } $captchaModel = $this->captchaHelper->getCaptcha($formId); $captchaModel->generate(); diff --git a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php index e496c36f4de75..cc1eb42b9a9c7 100644 --- a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php +++ b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php @@ -3,12 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Captcha\Model\Customer\Plugin; use Magento\Captcha\Helper\Data as CaptchaHelper; -use Magento\Framework\Session\SessionManagerInterface; +use Magento\Customer\Controller\Ajax\Login; +use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Session\SessionManagerInterface; +/** + * The plugin for ajax login controller. + */ class AjaxLogin { /** @@ -60,14 +66,14 @@ public function __construct( } /** - * @param \Magento\Customer\Controller\Ajax\Login $subject + * Validates captcha during request execution. + * + * @param Login $subject * @param \Closure $proceed * @return $this - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function aroundExecute( - \Magento\Customer\Controller\Ajax\Login $subject, + Login $subject, \Closure $proceed ) { $captchaFormIdField = 'captcha_form_id'; @@ -81,27 +87,42 @@ public function aroundExecute( if ($content) { $loginParams = $this->serializer->unserialize($content); } - $username = isset($loginParams['username']) ? $loginParams['username'] : null; - $captchaString = isset($loginParams[$captchaInputName]) ? $loginParams[$captchaInputName] : null; - $loginFormId = isset($loginParams[$captchaFormIdField]) ? $loginParams[$captchaFormIdField] : null; + $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) { - $captchaModel = $this->helper->getCaptcha($formId); - if ($captchaModel->isRequired($username) && !in_array($loginFormId, $this->formIds)) { - $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData(['errors' => true, 'message' => __('Provided form does not exist')]); - } + if ($formId === $loginFormId) { + $captchaModel = $this->helper->getCaptcha($formId); - if ($formId == $loginFormId) { - $captchaModel->logAttempt($username); - if (!$captchaModel->isCorrect($captchaString)) { - $this->sessionManager->setUsername($username); - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData(['errors' => true, 'message' => __('Incorrect CAPTCHA')]); + if ($captchaModel->isRequired($username)) { + if (!$captchaModel->isCorrect($captchaString)) { + $this->sessionManager->setUsername($username); + $captchaModel->logAttempt($username); + return $this->returnJsonError(__('Incorrect CAPTCHA'), true); + } } + + $captchaModel->logAttempt($username); } } return $proceed(); } + + /** + * Gets Json response. + * + * @param \Magento\Framework\Phrase $phrase + * @param bool $isCaptchaRequired + * @return Json + */ + private function returnJsonError(\Magento\Framework\Phrase $phrase, bool $isCaptchaRequired = false): Json + { + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData(['errors' => true, 'message' => $phrase, 'captcha' => $isCaptchaRequired]); + } } diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php index e5c72ba1ae82e..81df7d1919b35 100644 --- a/app/code/Magento/Captcha/Model/DefaultModel.php +++ b/app/code/Magento/Captcha/Model/DefaultModel.php @@ -3,11 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Captcha\Model; +use Magento\Captcha\Helper\Data; +use Magento\Framework\Math\Random; + /** * Implementation of \Zend\Captcha\Image * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * * @api * @since 100.0.2 */ @@ -29,7 +36,7 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model const DEFAULT_WORD_LENGTH_TO = 5; /** - * @var \Magento\Captcha\Helper\Data + * @var Data * @since 100.2.0 */ protected $captchaData; @@ -76,24 +83,32 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model */ protected $session; + /** + * @var Random + */ + private $randomMath; + /** * @param \Magento\Framework\Session\SessionManagerInterface $session * @param \Magento\Captcha\Helper\Data $captchaData * @param ResourceModel\LogFactory $resLogFactory * @param string $formId + * @param Random $randomMath * @throws \Zend\Captcha\Exception\ExtensionNotLoadedException */ public function __construct( \Magento\Framework\Session\SessionManagerInterface $session, \Magento\Captcha\Helper\Data $captchaData, \Magento\Captcha\Model\ResourceModel\LogFactory $resLogFactory, - $formId + $formId, + Random $randomMath = null ) { parent::__construct(); $this->session = $session; $this->captchaData = $captchaData; $this->resLogFactory = $resLogFactory; $this->formId = $formId; + $this->randomMath = $randomMath ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Random::class); } /** @@ -125,8 +140,8 @@ public function getBlockName() */ public function isRequired($login = null) { - if ($this->isUserAuth() - && !$this->isShownToLoggedInUser() + if (($this->isUserAuth() + && !$this->isShownToLoggedInUser()) || !$this->isEnabled() || !in_array( $this->formId, @@ -375,23 +390,9 @@ public function setShowCaptchaInSession($value = true) */ protected function generateWord() { - $word = ''; - $symbols = $this->getSymbols(); + $symbols = (string)$this->captchaData->getConfig('symbols'); $wordLen = $this->getWordLen(); - for ($i = 0; $i < $wordLen; $i++) { - $word .= $symbols[array_rand($symbols)]; - } - return $word; - } - - /** - * Get symbols array to use for word generation - * - * @return array - */ - private function getSymbols() - { - return str_split((string)$this->captchaData->getConfig('symbols')); + return $this->randomMath->getRandomString($wordLen, $symbols); } /** @@ -431,12 +432,14 @@ public function getWordLen() */ private function isShowAlways() { - if ((string)$this->captchaData->getConfig('mode') == \Magento\Captcha\Helper\Data::MODE_ALWAYS) { + $captchaMode = (string)$this->captchaData->getConfig('mode'); + + if ($captchaMode === Data::MODE_ALWAYS) { return true; } - if ((string)$this->captchaData->getConfig('mode') == \Magento\Captcha\Helper\Data::MODE_AFTER_FAIL - && $this->getAllowedAttemptsForSameLogin() == 0 + if ($captchaMode === Data::MODE_AFTER_FAIL + && $this->getAllowedAttemptsForSameLogin() === 0 ) { return true; } diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php index 9b97225e60de9..39579616fa928 100644 --- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php +++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php @@ -18,6 +18,6 @@ public function resolve(\Magento\Framework\App\RequestInterface $request, $formI { $captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); - return isset($captchaParams[$formId]) ? $captchaParams[$formId] : ''; + return $captchaParams[$formId] ?? ''; } } diff --git a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php deleted file mode 100644 index 40c215ec218a1..0000000000000 --- a/app/code/Magento/Captcha/Observer/CheckGuestCheckoutObserver.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Captcha\Observer; - -use Magento\Framework\Event\ObserverInterface; - -class CheckGuestCheckoutObserver implements ObserverInterface -{ - /** - * @var \Magento\Captcha\Helper\Data - */ - protected $_helper; - - /** - * @var \Magento\Framework\App\ActionFlag - */ - protected $_actionFlag; - - /** - * @var CaptchaStringResolver - */ - protected $captchaStringResolver; - - /** - * @var \Magento\Checkout\Model\Type\Onepage - */ - protected $_typeOnepage; - - /** - * @var \Magento\Framework\Json\Helper\Data - */ - protected $jsonHelper; - - /** - * @param \Magento\Captcha\Helper\Data $helper - * @param \Magento\Framework\App\ActionFlag $actionFlag - * @param CaptchaStringResolver $captchaStringResolver - * @param \Magento\Checkout\Model\Type\Onepage $typeOnepage - * @param \Magento\Framework\Json\Helper\Data $jsonHelper - */ - public function __construct( - \Magento\Captcha\Helper\Data $helper, - \Magento\Framework\App\ActionFlag $actionFlag, - CaptchaStringResolver $captchaStringResolver, - \Magento\Checkout\Model\Type\Onepage $typeOnepage, - \Magento\Framework\Json\Helper\Data $jsonHelper - ) { - $this->_helper = $helper; - $this->_actionFlag = $actionFlag; - $this->captchaStringResolver = $captchaStringResolver; - $this->_typeOnepage = $typeOnepage; - $this->jsonHelper = $jsonHelper; - } - - /** - * Check Captcha On Checkout as Guest Page - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - $formId = 'guest_checkout'; - $captchaModel = $this->_helper->getCaptcha($formId); - $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod(); - if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_GUEST) { - if ($captchaModel->isRequired()) { - $controller = $observer->getControllerAction(); - if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId)) - ) { - $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); - $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')]; - $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); - } - } - } - - return $this; - } -} diff --git a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php b/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php deleted file mode 100644 index 3bf2ac38debee..0000000000000 --- a/app/code/Magento/Captcha/Observer/CheckRegisterCheckoutObserver.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Captcha\Observer; - -use Magento\Framework\Event\ObserverInterface; - -class CheckRegisterCheckoutObserver implements ObserverInterface -{ - /** - * @var \Magento\Captcha\Helper\Data - */ - protected $_helper; - - /** - * @var \Magento\Framework\App\ActionFlag - */ - protected $_actionFlag; - - /** - * @var CaptchaStringResolver - */ - protected $captchaStringResolver; - - /** - * @var \Magento\Checkout\Model\Type\Onepage - */ - protected $_typeOnepage; - - /** - * @var \Magento\Framework\Json\Helper\Data - */ - protected $jsonHelper; - - /** - * @param \Magento\Captcha\Helper\Data $helper - * @param \Magento\Framework\App\ActionFlag $actionFlag - * @param CaptchaStringResolver $captchaStringResolver - * @param \Magento\Checkout\Model\Type\Onepage $typeOnepage - * @param \Magento\Framework\Json\Helper\Data $jsonHelper - */ - public function __construct( - \Magento\Captcha\Helper\Data $helper, - \Magento\Framework\App\ActionFlag $actionFlag, - CaptchaStringResolver $captchaStringResolver, - \Magento\Checkout\Model\Type\Onepage $typeOnepage, - \Magento\Framework\Json\Helper\Data $jsonHelper - ) { - $this->_helper = $helper; - $this->_actionFlag = $actionFlag; - $this->captchaStringResolver = $captchaStringResolver; - $this->_typeOnepage = $typeOnepage; - $this->jsonHelper = $jsonHelper; - } - - /** - * Check Captcha On Checkout Register Page - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - $formId = 'register_during_checkout'; - $captchaModel = $this->_helper->getCaptcha($formId); - $checkoutMethod = $this->_typeOnepage->getQuote()->getCheckoutMethod(); - if ($checkoutMethod == \Magento\Checkout\Model\Type\Onepage::METHOD_REGISTER) { - if ($captchaModel->isRequired()) { - $controller = $observer->getControllerAction(); - if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId)) - ) { - $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); - $result = ['error' => 1, 'message' => __('Incorrect CAPTCHA')]; - $controller->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); - } - } - } - - return $this; - } -} diff --git a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php index 9d3cd8d367093..a3921b6cb17aa 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php @@ -111,19 +111,21 @@ public function execute(\Magento\Framework\Event\Observer $observer) ) )) { $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->addError($message); + if ($customerId) { + $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->addError($message); + } + $this->messageManager->addError(__('Incorrect CAPTCHA')); + $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->redirect->redirect($controller->getResponse(), '*/*/edit'); } - $this->messageManager->addError(__('Incorrect CAPTCHA')); - $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); - $this->redirect->redirect($controller->getResponse(), '*/*/edit'); } } diff --git a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php index 402fc028c5ad0..2de93dcf6b59b 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php @@ -69,18 +69,17 @@ public function execute(\Magento\Framework\Event\Observer $observer) $controller = $observer->getControllerAction(); $email = (string)$observer->getControllerAction()->getRequest()->getParam('email'); $params = $observer->getControllerAction()->getRequest()->getParams(); - if (!empty($email) && !empty($params)) { - if ($captchaModel->isRequired()) { - if (!$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->addError(__('Incorrect CAPTCHA')); - $controller->getResponse()->setRedirect( - $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true]) - ); - } - } + 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->addError(__('Incorrect CAPTCHA')); + $controller->getResponse()->setRedirect( + $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true]) + ); } return $this; diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php index 8cc907d7bd12b..924514cd48c5d 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserLoginBackendObserver.php @@ -52,11 +52,11 @@ public function execute(\Magento\Framework\Event\Observer $observer) $formId = 'backend_login'; $captchaModel = $this->_helper->getCaptcha($formId); $login = $observer->getEvent()->getUsername(); - if ($captchaModel->isRequired($login)) { - if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($this->_request, $formId))) { - $captchaModel->logAttempt($login); - throw new PluginAuthenticationException(__('Incorrect CAPTCHA.')); - } + if ($captchaModel->isRequired($login) + && !$captchaModel->isCorrect($this->captchaStringResolver->resolve($this->_request, $formId)) + ) { + $captchaModel->logAttempt($login); + throw new PluginAuthenticationException(__('Incorrect CAPTCHA.')); } $captchaModel->logAttempt($login); diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml new file mode 100644 index 0000000000000..9640da9bd2f67 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CaptchaFormsDisplayingActionGroup"> + <click selector="{{AdminCaptchaFormsDisplayingSection.store}}" stepKey="clickToGoStores"/> + <waitForPageLoad stepKey="waitForStoresLoaded"/> + <click selector="{{AdminCaptchaFormsDisplayingSection.config}}" stepKey="clickToGoConfiguration"/> + <waitForPageLoad stepKey="waitForConfigurationsLoaded"/> + <scrollTo selector="{{AdminCaptchaFormsDisplayingSection.customer}}" x="0" y="-100" stepKey="scrollToCustomers"/> + <click selector="{{AdminCaptchaFormsDisplayingSection.customer}}" stepKey="clickToCustomers"/> + <waitForPageLoad stepKey="waitForCustomerConfigurationsLoaded"/> + <click selector="{{AdminCaptchaFormsDisplayingSection.customerConfig}}" stepKey="clickToGoCustomerConfiguration"/> + <scrollTo selector="{{AdminCaptchaFormsDisplayingSection.captcha}}" stepKey="scrollToCaptcha"/> + <conditionalClick selector="{{AdminCaptchaFormsDisplayingSection.captcha}}" dependentSelector="{{AdminCaptchaFormsDisplayingSection.dependent}}" visible="false" stepKey="clickToOpenCaptcha"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml new file mode 100644 index 0000000000000..45b9e59a5e767 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CaptchaData" type="captcha"> + <data key="createUser">Create user</data> + <data key="login">Login</data> + <data key="passwd">Forgot password</data> + <data key="contactUs">Contact Us</data> + <data key="changePasswd">Change password</data> + <data key="register">Register during Checkout</data> + <data key="checkoutAsGuest">Check Out as Guest</data> + </entity> +</entities> diff --git a/app/code/Magento/Captcha/Test/Mftf/LICENSE.txt b/app/code/Magento/Captcha/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Captcha/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 <insert your license name here>" 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/app/code/Magento/Captcha/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Captcha/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Captcha/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 <insert your license name here>" 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/app/code/Magento/Captcha/Test/Mftf/README.md b/app/code/Magento/Captcha/Test/Mftf/README.md new file mode 100644 index 0000000000000..48be768712f2f --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Captcha Functional Tests + +The Functional Test Module for **Magento Captcha** module. diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml new file mode 100644 index 0000000000000..6def5f9ccdac9 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/AdminCaptchaFormsDisplayingSection.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCaptchaFormsDisplayingSection"> + <element name="store" type="button" selector="#menu-magento-backend-stores"/> + <element name="config" type="button" selector="li[data-ui-id=menu-magento-config-system-config] span"/> + <element name="customer" type="button" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='Customers']"/> + <element name="customerConfig" type="text" selector="//span[text()='Customer Configuration']"/> + <element name="captcha" type="button" selector="#customer_captcha-head"/> + <element name="dependent" type="button" selector="#customer_captcha-head.open"/> + <element name="forms" type="multiselect" selector="#customer_captcha_forms"/> + <element name="createUser" type="multiselect" selector="select#customer_captcha_forms option[value='user_create']"/> + <element name="forgotpassword" type="multiselect" selector="select#customer_captcha_forms option[value='user_forgotpassword']"/> + <element name="userLogin" type="multiselect" selector="select#customer_captcha_forms option[value='user_login']"/> + <element name="guestCheckout" type="multiselect" selector="select#customer_captcha_forms option[value='guest_checkout']"/> + <element name="register" type="multiselect" selector="select#customer_captcha_forms option[value='register_during_checkout']"/> + <element name="userEdit" type="multiselect" selector="select#customer_captcha_forms option[value='user_edit']"/> + <element name="contactUs" type="multiselect" selector="select#customer_captcha_forms option[value='contact_us']"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml new file mode 100644 index 0000000000000..6e330390b372b --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminCaptchaFormsDisplayingTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCaptchaFormsDisplayingTest"> + <annotations> + <features value="Captcha"/> + <stories value="MAGETWO-86274 - [github] CAPTCHA doesn't show when check out as guest"/> + <title value="Captcha forms displaying"/> + <description value="Captcha forms displaying"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94364"/> + <group value="captcha"/> + </annotations> + + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Go to Captcha--> + <actionGroup ref="CaptchaFormsDisplayingActionGroup" stepKey="captchaFormsDisplayingActionGroup"/> + <waitForPageLoad time="30" stepKey="waitForPageLoaded"/> + <!--Verify fields removed--> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.forms}}" stepKey="formItems"/> + <assertNotContains stepKey="checkoutAsGuest"> + <expectedResult type="string">{{CaptchaData.checkoutAsGuest}}</expectedResult> + <actualResult type="variable">$formItems</actualResult> + </assertNotContains> + <assertNotContains stepKey="register"> + <expectedResult type="string">{{CaptchaData.register}}</expectedResult> + <actualResult type="variable">$formItems</actualResult> + </assertNotContains> + <!--Verify fields existence--> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.createUser}}" stepKey="createUser"/> + <assertEquals stepKey="createUserFieldIsPresent"> + <expectedResult type="string">{{CaptchaData.createUser}}</expectedResult> + <actualResult type="variable">$createUser</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.userLogin}}" stepKey="login"/> + <assertEquals stepKey="loginFieldIsPresent"> + <expectedResult type="string">{{CaptchaData.login}}</expectedResult> + <actualResult type="variable">login</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.forgotpassword}}" stepKey="forgotpassword"/> + <assertEquals stepKey="passwordFieldIsPresent"> + <expectedResult type="string">{{CaptchaData.passwd}}</expectedResult> + <actualResult type="variable">$forgotpassword</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.contactUs}}" stepKey="contactUs"/> + <assertEquals stepKey="contactUsFieldIsPresent"> + <expectedResult type="string">{{CaptchaData.contactUs}}</expectedResult> + <actualResult type="variable">$contactUs</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminCaptchaFormsDisplayingSection.userEdit}}" stepKey="userEdit"/> + <assertEquals stepKey="userEditFieldIsPresent"> + <expectedResult type="string">{{CaptchaData.changePasswd}}</expectedResult> + <actualResult type="variable">$userEdit</actualResult> + </assertEquals> + + <!--Roll back configuration--> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminCaptchaFormsDisplayingSection.captcha}}" stepKey="clickToCloseCaptcha"/> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php b/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php index 99ac2e2d8fccc..ee97c11a58315 100644 --- a/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php @@ -95,6 +95,7 @@ public function testExecute($formId, $callsNumber) $blockMethods = ['setFormId', 'setIsAjax', 'toHtml']; $blockMock = $this->createPartialMock(\Magento\Captcha\Block\Captcha::class, $blockMethods); + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); $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))); diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php index be9574fff2cfa..315bc9afb5431 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php @@ -3,22 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Captcha\Test\Unit\Model\Customer\Plugin; class AjaxLoginTest extends \PHPUnit\Framework\TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Checkout\Model\Session */ protected $sessionManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Captcha\Helper\Data */ protected $captchaHelperMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Controller\Result\JsonFactory */ protected $jsonFactoryMock; @@ -38,12 +39,12 @@ class AjaxLoginTest extends \PHPUnit\Framework\TestCase protected $requestMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Customer\Controller\Ajax\Login */ protected $loginControllerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Serialize\Serializer\Json */ protected $serializerMock; @@ -72,8 +73,12 @@ protected function setUp() $this->loginControllerMock->expects($this->any())->method('getRequest') ->will($this->returnValue($this->requestMock)); - $this->captchaHelperMock->expects($this->once())->method('getCaptcha') - ->with('user_login')->will($this->returnValue($this->captchaMock)); + + $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); @@ -103,11 +108,18 @@ public function testAroundExecute() $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)); + $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)); } @@ -128,18 +140,21 @@ public function testAroundExecuteIncorrectCaptcha() $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->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->returnValue('response')); + $this->resultJsonMock + ->expects($this->once()) + ->method('setData') + ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA'), 'captcha' => true]) + ->will($this->returnSelf()); $closure = function () { }; - $this->assertEquals('response', $this->model->aroundExecute($this->loginControllerMock, $closure)); + $this->assertEquals($this->resultJsonMock, $this->model->aroundExecute($this->loginControllerMock, $closure)); } /** @@ -151,7 +166,7 @@ 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') + $this->serializerMock->expects($this->once())->method('unserialize') ->will($this->returnValue($requestContent)); $this->captchaMock->expects($this->once())->method('isRequired')->with($username) @@ -168,16 +183,39 @@ public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent /** * @return array */ - public function aroundExecuteCaptchaIsNotRequired() + 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' => ['captcha_string' => 'string'], + '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/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index 429c13e802a87..1d316b35561e8 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Captcha\Test\Unit\Model; +use Magento\Framework\Math\Random; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -24,7 +26,7 @@ class DefaultTest extends \PHPUnit\Framework\TestCase 'enable' => '1', 'font' => 'linlibertine', 'mode' => 'after_fail', - 'forms' => 'user_forgotpassword,user_create,guest_checkout,register_during_checkout', + 'forms' => 'user_forgotpassword,user_create', 'failed_attempts_login' => '3', 'failed_attempts_ip' => '1000', 'timeout' => '7', @@ -35,8 +37,6 @@ class DefaultTest extends \PHPUnit\Framework\TestCase 'always_for' => [ 'user_create', 'user_forgotpassword', - 'guest_checkout', - 'register_during_checkout', 'contact_us', ], ]; @@ -354,13 +354,52 @@ public function testIsShownToLoggedInUser($expectedResult, $formId) $this->assertEquals($expectedResult, $captcha->isShownToLoggedInUser()); } + /** + * @return array + */ public function isShownToLoggedInUserDataProvider() { return [ [true, 'contact_us'], [false, 'user_create'], [false, 'user_forgotpassword'], - [false, 'guest_checkout'] + ]; + } + + /** + * @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/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php new file mode 100644 index 0000000000000..2bd8ac6f16986 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CaptchaStringResolverTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Observer; + +use Magento\Captcha\Helper\Data as CaptchaDataHelper; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class CaptchaStringResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManagerHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + + /** + * @var HttpRequest|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + protected function setUp() + { + $this->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/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php index 26fd8fd928c56..ee5001d146793 100644 --- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php @@ -160,4 +160,53 @@ public function testExecute() $this->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller])); } + + /** + * @return void + */ + public function testExecuteWithCustomerIdNull() + { + $customerId = null; + $captchaValue = 'some-value'; + + $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); + + $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)); + + $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->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller])); + } } diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php new file mode 100644 index 0000000000000..6966bd4cce7f9 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Observer; + +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Captcha\Observer\CheckUserLoginBackendObserver; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\Message\ManagerInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Class CheckUserLoginBackendObserverTest + */ +class CheckUserLoginBackendObserverTest extends TestCase +{ + /** + * @var CheckUserLoginBackendObserver + */ + private $observer; + + /** + * @var ManagerInterface|MockObject + */ + private $messageManagerMock; + + /** + * @var CaptchaStringResolver|MockObject + */ + private $captchaStringResolverMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Data|MockObject + */ + private $helperMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->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) + { + $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() + { + $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/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index ebc6238919ce3..0d7fb25a460db 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-captcha", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-customer": "101.0.*", "magento/module-checkout": "100.2.*", @@ -13,7 +13,7 @@ "zendframework/zend-session": "^2.7.3" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Captcha/etc/config.xml b/app/code/Magento/Captcha/etc/config.xml index 71c474de90ff4..dd748dd05ccda 100644 --- a/app/code/Magento/Captcha/etc/config.xml +++ b/app/code/Magento/Captcha/etc/config.xml @@ -53,8 +53,6 @@ <always_for> <user_create>1</user_create> <user_forgotpassword>1</user_forgotpassword> - <guest_checkout>1</guest_checkout> - <register_during_checkout>1</register_during_checkout> <contact_us>1</contact_us> </always_for> </captcha> @@ -77,12 +75,6 @@ <user_forgotpassword> <label>Forgot password</label> </user_forgotpassword> - <guest_checkout> - <label>Check Out as Guest</label> - </guest_checkout> - <register_during_checkout> - <label>Register during Checkout</label> - </register_during_checkout> <contact_us> <label>Contact Us</label> </contact_us> diff --git a/app/code/Magento/Captcha/etc/di.xml b/app/code/Magento/Captcha/etc/di.xml index 955896eb12744..83c4e8aa1e2c1 100644 --- a/app/code/Magento/Captcha/etc/di.xml +++ b/app/code/Magento/Captcha/etc/di.xml @@ -27,13 +27,12 @@ </arguments> </type> <type name="Magento\Customer\Controller\Ajax\Login"> - <plugin name="configurable_product" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> + <plugin name="captcha_validation" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> </type> <type name="Magento\Captcha\Model\Customer\Plugin\AjaxLogin"> <arguments> <argument name="formIds" xsi:type="array"> <item name="user_login" xsi:type="string">user_login</item> - <item name="guest_checkout" xsi:type="string">guest_checkout</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Captcha/etc/events.xml b/app/code/Magento/Captcha/etc/events.xml index e3ddd19de2d12..970c0d077260c 100644 --- a/app/code/Magento/Captcha/etc/events.xml +++ b/app/code/Magento/Captcha/etc/events.xml @@ -18,10 +18,6 @@ <event name="admin_user_authenticate_before"> <observer name="captcha" instance="Magento\Captcha\Observer\CheckUserLoginBackendObserver" /> </event> - <event name="controller_action_predispatch_checkout_onepage_saveBilling"> - <observer name="captcha_guest" instance="Magento\Captcha\Observer\CheckGuestCheckoutObserver" /> - <observer name="captcha_register" instance="Magento\Captcha\Observer\CheckRegisterCheckoutObserver" /> - </event> <event name="customer_customer_authenticated"> <observer name="captcha_reset_attempt" instance="Magento\Captcha\Observer\ResetAttemptForFrontendObserver" /> </event> diff --git a/app/code/Magento/Captcha/etc/frontend/di.xml b/app/code/Magento/Captcha/etc/frontend/di.xml index 225e62c8e8203..0c4ab0cda0735 100644 --- a/app/code/Magento/Captcha/etc/frontend/di.xml +++ b/app/code/Magento/Captcha/etc/frontend/di.xml @@ -17,7 +17,6 @@ <arguments> <argument name="formIds" xsi:type="array"> <item name="user_login" xsi:type="string">user_login</item> - <item name="guest_checkout" xsi:type="string">guest_checkout</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Captcha/i18n/en_US.csv b/app/code/Magento/Captcha/i18n/en_US.csv index 2de4ab5345c88..480107df8adfe 100644 --- a/app/code/Magento/Captcha/i18n/en_US.csv +++ b/app/code/Magento/Captcha/i18n/en_US.csv @@ -4,10 +4,10 @@ Always,Always "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 from the image","Please enter the letters from the image" +"Please enter the letters and numbers from the image","Please enter the letters and numbers from the image" "<strong>Attention</strong>: Captcha is case sensitive.","<strong>Attention</strong>: Captcha is case sensitive." "Reload captcha","Reload captcha" -"Please type the letters below","Please type the letters below" +"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" @@ -20,11 +20,7 @@ Forms,Forms "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.<br />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.<br />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.<br />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.<br />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/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml index b8dcd6c654c8e..88e0d5edc2a7d 100644 --- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml +++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ /** @var \Magento\Captcha\Model\DefaultModel $captcha */ @@ -13,7 +11,7 @@ $captcha = $block->getCaptchaModel(); ?> <div class="admin__field _required"> <label for="captcha" class="admin__field-label"> - <span><?= $block->escapeHtml(__('Please enter the letters from the image')) ?></span> + <span><?= $block->escapeHtml(__('Please enter the letters and numbers from the image')) ?></span> </label> <div class="admin__field-control"> <input diff --git a/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml index 4ed56fd56cc3a..7180372f004e5 100644 --- a/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Captcha/view/frontend/layout/checkout_index_index.xml @@ -36,29 +36,7 @@ <item name="captcha" xsi:type="array"> <item name="component" xsi:type="string">Magento_Captcha/js/view/checkout/loginCaptcha</item> <item name="displayArea" xsi:type="string">additional-login-form-fields</item> - <item name="formId" xsi:type="string">guest_checkout</item> - <item name="configSource" xsi:type="string">checkoutConfig</item> - </item> - </item> - </item> - </item> - </item> - </item> - </item> - </item> - </item> - <item name="billing-step" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="payment" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="customer-email" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="additional-login-form-fields" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="captcha" xsi:type="array"> - <item name="component" xsi:type="string">Magento_Captcha/js/view/checkout/loginCaptcha</item> - <item name="displayArea" xsi:type="string">additional-login-form-fields</item> - <item name="formId" xsi:type="string">guest_checkout</item> + <item name="formId" xsi:type="string">user_login</item> <item name="configSource" xsi:type="string">checkoutConfig</item> </item> </item> diff --git a/app/code/Magento/Captcha/view/frontend/requirejs-config.js b/app/code/Magento/Captcha/view/frontend/requirejs-config.js index 3b322711f8b1f..42c80632d3e92 100644 --- a/app/code/Magento/Captcha/view/frontend/requirejs-config.js +++ b/app/code/Magento/Captcha/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - captcha: 'Magento_Captcha/captcha' + captcha: 'Magento_Captcha/js/captcha', + 'Magento_Captcha/captcha': 'Magento_Captcha/js/captcha' } } }; diff --git a/app/code/Magento/Captcha/view/frontend/templates/default.phtml b/app/code/Magento/Captcha/view/frontend/templates/default.phtml index 9851b1cd8bd7d..ead8c590eee94 100644 --- a/app/code/Magento/Captcha/view/frontend/templates/default.phtml +++ b/app/code/Magento/Captcha/view/frontend/templates/default.phtml @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ /** @var \Magento\Captcha\Model\DefaultModel $captcha */ $captcha = $block->getCaptchaModel(); ?> <div class="field captcha required" role="<?= $block->escapeHtmlAttr($block->getFormId()) ?>"> - <label for="captcha_<?= $block->escapeHtmlAttr($block->getFormId()) ?>" class="label"><span><?= $block->escapeHtml(__('Please type the letters below')) ?></span></label> + <label for="captcha_<?= $block->escapeHtmlAttr($block->getFormId()) ?>" class="label"><span><?= $block->escapeHtml(__('Please type the letters and numbers below')) ?></span></label> <div class="control captcha"> - <input name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) ?>[<?= $block->escapeHtmlAttr($block->getFormId()) ?>]" type="text" class="input-text required-entry" data-validate="{required:true}" id="captcha_<?= $block->escapeHtmlAttr($block->getFormId()) ?>" /> + <input name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) ?>[<?= $block->escapeHtmlAttr($block->getFormId()) ?>]" type="text" class="input-text required-entry" data-validate="{required:true}" id="captcha_<?= $block->escapeHtmlAttr($block->getFormId()) ?>" autocomplete="off"/> <div class="nested"> <div class="field captcha no-label" data-captcha="<?= $block->escapeHtmlAttr($block->getFormId()) ?>" @@ -23,7 +21,7 @@ $captcha = $block->getCaptchaModel(); "imageLoader": "<?= $block->escapeUrl($block->getViewFileUrl('images/loader-2.gif')) ?>", "type": "<?= $block->escapeHtmlAttr($block->getFormId()) ?>"}}'> <div class="control captcha-image"> - <img alt="<?= $block->escapeHtmlAttr(__('Please type the letters below')) ?>" class="captcha-img" height="<?= /* @noEscape */ (float) $block->getImgHeight() ?>" src="<?= $block->escapeUrl($captcha->getImgSrc()) ?>"/> + <img alt="<?= $block->escapeHtmlAttr(__('Please type the letters and numbers below')) ?>" class="captcha-img" height="<?= /* @noEscape */ (float) $block->getImgHeight() ?>" src="<?= $block->escapeUrl($captcha->getImgSrc()) ?>"/> <button type="button" class="action reload captcha-reload" title="<?= $block->escapeHtmlAttr(__('Reload captcha')) ?>"><span><?= $block->escapeHtml(__('Reload captcha')) ?></span></button> </div> </div> diff --git a/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml b/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Captcha/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Captcha/view/frontend/web/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js similarity index 100% rename from app/code/Magento/Captcha/view/frontend/web/captcha.js rename to app/code/Magento/Captcha/view/frontend/web/js/captcha.js diff --git a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js index 3a235df73a916..52968e507e6bf 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js @@ -17,7 +17,7 @@ define([ imageSource: ko.observable(captchaData.imageSrc), visibility: ko.observable(false), captchaValue: ko.observable(null), - isRequired: captchaData.isRequired, + isRequired: ko.observable(captchaData.isRequired), isCaseSensitive: captchaData.isCaseSensitive, imageHeight: captchaData.imageHeight, refreshUrl: captchaData.refreshUrl, @@ -41,7 +41,7 @@ define([ * @return {Boolean} */ getIsVisible: function () { - return this.visibility; + return this.visibility(); }, /** @@ -55,14 +55,14 @@ define([ * @return {Boolean} */ getIsRequired: function () { - return this.isRequired; + return this.isRequired(); }, /** * @param {Boolean} flag */ setIsRequired: function (flag) { - this.isRequired = flag; + this.isRequired(flag); }, /** diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js index f80b2ab163ffd..f78b848312702 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js @@ -89,6 +89,13 @@ define([ return this.currentCaptcha !== null ? this.currentCaptcha.getIsRequired() : false; }, + /** + * @param {Boolean} flag + */ + setIsRequired: function (flag) { + this.currentCaptcha.setIsRequired(flag); + }, + /** * @return {Boolean} */ diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js index 7709febea60a3..a8efd0865bbb8 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js @@ -4,34 +4,44 @@ */ define([ - 'Magento_Captcha/js/view/checkout/defaultCaptcha', - 'Magento_Captcha/js/model/captchaList', - 'Magento_Customer/js/action/login' -], -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 //eslint-disable-line eqeqeq - ) { + 'underscore', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Customer/js/action/login' + ], + 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, response) { + if (!loginData['captcha_form_id'] || loginData['captcha_form_id'] !== self.formId) { + return; + } + + if (_.isUndefined(response) || !response.errors) { + return; + } + + // check if captcha should be required after login attempt + if (!self.isRequired() && response.captcha && self.isRequired() !== response.captcha) { + self.setIsRequired(response.captcha); + } + self.refresh(); - } - }); + }); + } } - } + }); }); -}); diff --git a/app/code/Magento/Captcha/view/frontend/web/onepage.js b/app/code/Magento/Captcha/view/frontend/web/onepage.js deleted file mode 100644 index 7f5f11d20572b..0000000000000 --- a/app/code/Magento/Captcha/view/frontend/web/onepage.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * @deprecated since version 2.2.0 - */ -define(['jquery'], function ($) { - 'use strict'; - - $(document).on('login', function () { - var type; - - $('[data-captcha="guest_checkout"], [data-captcha="register_during_checkout"]').hide(); - $('[role="guest_checkout"], [role="register_during_checkout"]').hide(); - type = $('#login\\:guest').is(':checked') ? 'guest_checkout' : 'register_during_checkout'; - $('[role="' + type + '"], [data-captcha="' + type + '"]').show(); - }).on('billingSave', function () { - $('.captcha-reload:visible').trigger('click'); - }); -}); diff --git a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html index 6767a121d849d..8923c81bf4bb3 100644 --- a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html +++ b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html @@ -4,17 +4,20 @@ * See COPYING.txt for license details. */ --> +<!-- ko if: (getIsVisible())--> +<input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> +<!-- /ko --> <!-- ko if: (isRequired() && getIsVisible())--> <div class="field captcha required" data-bind="blockLoader: getIsLoading()"> - <label data-bind="attr: {for: 'captcha_' + formId}" class="label"><span data-bind="i18n: 'Please type the letters below'"></span></label> + <label data-bind="attr: {for: 'captcha_' + formId}" class="label"><span data-bind="i18n: 'Please type the letters and numbers below'"></span></label> <div class="control captcha"> - <input name="captcha_string" type="text" class="input-text required-entry" data-bind="value: captchaValue(), attr: {id: 'captcha_' + formId, 'data-scope': dataScope}" /> - <input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> + <input name="captcha_string" type="text" class="input-text required-entry" data-bind="value: captchaValue(), attr: {'data-scope': dataScope}" autocomplete="off"/> <div class="nested"> <div class="field captcha no-label"> <div class="control captcha-image"> <img data-bind="attr: { - alt: $t('Please type the letters below'), + alt: $t('Please type the letters and numbers below'), + title: $t('Please type the letters and numbers below'), height: imageHeight(), src: getImageSource(), }" diff --git a/app/code/Magento/CardinalCommerce/LICENSE.txt b/app/code/Magento/CardinalCommerce/LICENSE.txt new file mode 100644 index 0000000000000..49dc8ec82eb0e --- /dev/null +++ b/app/code/Magento/CardinalCommerce/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 <insert your license name here>" 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/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt b/app/code/Magento/CardinalCommerce/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/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 <insert your license name here>" 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/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php new file mode 100644 index 0000000000000..f22bdf3909740 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Adminhtml/Source/Environment.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Adminhtml\Source; + +/** + * CardinalCommerce Environment Dropdown source + */ +class Environment implements \Magento\Framework\Data\OptionSourceInterface +{ + const ENVIRONMENT_PRODUCTION = 'production'; + const ENVIRONMENT_SANDBOX = 'sandbox'; + + /** + * Possible environment types + * + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => self::ENVIRONMENT_SANDBOX, + 'label' => 'Sandbox', + ], + [ + 'value' => self::ENVIRONMENT_PRODUCTION, + 'label' => 'Production' + ] + ]; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php new file mode 100644 index 0000000000000..a6794eae90084 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Checkout/ConfigProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Checkout; + +use Magento\CardinalCommerce\Model\Config; +use Magento\CardinalCommerce\Model\Request\TokenBuilder; +use Magento\Checkout\Model\ConfigProviderInterface; + +/** + * Configuration provider. + */ +class ConfigProvider implements ConfigProviderInterface +{ + /** + * @var TokenBuilder + */ + private $requestJwtBuilder; + + /** + * @var Config + */ + private $config; + + /** + * @param TokenBuilder $requestJwtBuilder + * @param Config $config + */ + public function __construct( + TokenBuilder $requestJwtBuilder, + Config $config + ) { + $this->requestJwtBuilder = $requestJwtBuilder; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['cardinal'] = [ + 'environment' => $this->config->getEnvironment(), + 'requestJWT' => $this->requestJwtBuilder->build() + ]; + + return $config; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Config.php b/app/code/Magento/CardinalCommerce/Model/Config.php new file mode 100644 index 0000000000000..1cf38d6f4cf8d --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Config.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * CardinalCommerce integration configuration. + * + * Class is a proxy service for retrieving configuration settings. + */ +class Config +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Returns CardinalCommerce API Key used for authentication. + * + * A shared secret value between the merchant and Cardinal. This value should never be exposed to the public. + * + * @param int|null $storeId + * @return string + */ + public function getApiKey($storeId = null): string + { + $apiKey = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/api_key', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $apiKey; + } + + /** + * Returns CardinalCommerce API Identifier. + * + * GUID used to identify the specific API Key. + * + * @param int|null $storeId + * @return string + */ + public function getApiIdentifier($storeId = null): string + { + $apiIdentifier = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/api_identifier', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $apiIdentifier; + } + + /** + * Returns CardinalCommerce Org Unit Id. + * + * GUID to identify the merchant organization within Cardinal systems. + * + * @param int|null $storeId + * @return string + */ + public function getOrgUnitId($storeId = null): string + { + $orgUnitId = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/org_unit_id', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $orgUnitId; + } + + /** + * Returns CardinalCommerce environment. + * + * Sandbox or production. + * + * @param int|null $storeId + * @return string + */ + public function getEnvironment($storeId = null): string + { + $environment = $this->scopeConfig->getValue( + 'three_d_secure/cardinal/environment', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $environment; + } + + /** + * If is "true" extra information about interaction with CardinalCommerce API are written to payment.log file + * + * @param int|null $storeId + * @return bool + */ + public function isDebugModeEnabled($storeId = null): bool + { + $debugModeEnabled = $this->scopeConfig->isSetFlag( + 'three_d_secure/cardinal/debug', + ScopeInterface::SCOPE_STORE, + $storeId + ); + return $debugModeEnabled; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php new file mode 100644 index 0000000000000..cfc6efe6150be --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model; + +use Magento\Framework\Serialize\Serializer\Json; + +/** + * JSON Web Token management. + */ +class JwtManagement +{ + /** + * The signing algorithm. Cardinal supported algorithm is 'HS256' + */ + const SIGN_ALGORITHM = 'HS256'; + + /** + * @var Json + */ + private $json; + + /** + * @param Json $json + */ + public function __construct( + Json $json + ) { + $this->json = $json; + } + + /** + * Converts JWT string into array. + * + * @param string $jwt The JWT + * @param string $key The secret key + * + * @return array + * @throws \InvalidArgumentException + */ + public function decode(string $jwt, string $key): array + { + if (empty($jwt)) { + throw new \InvalidArgumentException('JWT is empty'); + } + + $parts = explode('.', $jwt); + if (count($parts) != 3) { + throw new \InvalidArgumentException('Wrong number of segments in JWT'); + } + + list($headB64, $payloadB64, $signatureB64) = $parts; + + $headerJson = $this->urlSafeB64Decode($headB64); + $header = $this->json->unserialize($headerJson); + + $payloadJson = $this->urlSafeB64Decode($payloadB64); + $payload = $this->json->unserialize($payloadJson); + + $signature = $this->urlSafeB64Decode($signatureB64); + if ($signature !== $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg'])) { + throw new \InvalidArgumentException('JWT signature verification failed'); + } + + return $payload; + } + + /** + * Converts and signs array into a JWT string. + * + * @param array $payload + * @param string $key + * + * @return string + * @throws \InvalidArgumentException + */ + public function encode(array $payload, string $key): string + { + $header = ['typ' => 'JWT', 'alg' => self::SIGN_ALGORITHM]; + + $headerJson = $this->json->serialize($header); + $segments[] = $this->urlSafeB64Encode($headerJson); + + $payloadJson = $this->json->serialize($payload); + $segments[] = $this->urlSafeB64Encode($payloadJson); + + $signature = $this->sign(implode('.', $segments), $key, $header['alg']); + $segments[] = $this->urlSafeB64Encode($signature); + + return implode('.', $segments); + } + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign. + * @param string $key The secret key. + * @param string $algorithm The signing algorithm. + * + * @return string + * @throws \InvalidArgumentException + */ + private function sign(string $msg, string $key, string $algorithm): string + { + if ($algorithm !== self::SIGN_ALGORITHM) { + throw new \InvalidArgumentException('Algorithm ' . $algorithm . ' is not supported'); + } + + return hash_hmac('sha256', $msg, $key, true); + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string + */ + private function urlSafeB64Decode(string $input): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_decode( + str_pad(strtr($input, '-_', '+/'), strlen($input) % 4, '=', STR_PAD_RIGHT) + ); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string + */ + private function urlSafeB64Encode(string $input): string + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php new file mode 100644 index 0000000000000..e045d00dc55fe --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Request/TokenBuilder.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Request; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\CardinalCommerce\Model\Config; +use Magento\Checkout\Model\Session; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Cardinal request token builder. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class TokenBuilder +{ + /** + * @var JwtManagement + */ + private $jwtManagement; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @var Config + */ + private $config; + + /** + * @var IdentityGeneratorInterface + */ + private $identityGenerator; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param JwtManagement $jwtManagement + * @param Session $checkoutSession + * @param Config $config + * @param IdentityGeneratorInterface $identityGenerator + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + JwtManagement $jwtManagement, + Session $checkoutSession, + Config $config, + IdentityGeneratorInterface $identityGenerator, + DateTimeFactory $dateTimeFactory + ) { + $this->jwtManagement = $jwtManagement; + $this->checkoutSession = $checkoutSession; + $this->config = $config; + $this->identityGenerator = $identityGenerator; + $this->dateTimeFactory = $dateTimeFactory; + } + + /** + * Builds request JWT. + * + * @return string + */ + public function build() + { + $quote = $this->checkoutSession->getQuote(); + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $orderDetails = [ + 'OrderDetails' => [ + 'OrderNumber' => $quote->getId(), + 'Amount' => $quote->getBaseGrandTotal() * 100, + 'CurrencyCode' => $quote->getBaseCurrencyCode() + ] + ]; + + $token = [ + 'jti' => $this->identityGenerator->generateId(), + 'iss' => $this->config->getApiIdentifier(), + 'iat' => $currentDate->getTimestamp(), + 'OrgUnitId' => $this->config->getOrgUnitId(), + 'Payload' => $orderDetails, + 'ObjectifyPayload' => true + ]; + + $jwt = $this->jwtManagement->encode($token, $this->config->getApiKey()); + + return $jwt; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php new file mode 100644 index 0000000000000..6b1a67a02a18d --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtParser.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Response; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\CardinalCommerce\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Psr\Log\LoggerInterface; +use Magento\Payment\Model\Method\Logger as PaymentLogger; + +/** + * Parses content of CardinalCommerce response JWT. + */ +class JwtParser implements JwtParserInterface +{ + /** + * @var JwtManagement + */ + private $jwtManagement; + + /** + * @var Config + */ + private $config; + + /** + * @var JwtPayloadValidatorInterface + */ + private $tokenValidator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var PaymentLogger + */ + private $paymentLogger; + + /** + * @param JwtManagement $jwtManagement + * @param Config $config + * @param JwtPayloadValidatorInterface $tokenValidator + * @param PaymentLogger $paymentLogger + * @param LoggerInterface $logger + */ + public function __construct( + JwtManagement $jwtManagement, + Config $config, + JwtPayloadValidatorInterface $tokenValidator, + PaymentLogger $paymentLogger, + LoggerInterface $logger + ) { + $this->jwtManagement = $jwtManagement; + $this->config = $config; + $this->tokenValidator = $tokenValidator; + $this->paymentLogger = $paymentLogger; + $this->logger = $logger; + } + + /** + * Returns response JWT payload. + * + * @param string $jwt + * @return array + * @throws LocalizedException + */ + public function execute(string $jwt): array + { + $jwtPayload = ''; + try { + $this->debug(['Cardinal Response JWT:' => $jwt]); + $jwtPayload = $this->jwtManagement->decode($jwt, $this->config->getApiKey()); + $this->debug(['Cardinal Response JWT payload:' => $jwtPayload]); + if (!$this->tokenValidator->validate($jwtPayload)) { + $this->throwException(); + } + } catch (\InvalidArgumentException $e) { + $this->logger->critical($e, ['CardinalCommerce3DSecure']); + $this->throwException(); + } + + return $jwtPayload; + } + + /** + * Log JWT data. + * + * @param array $data + * @return void + */ + private function debug(array $data) + { + if ($this->config->isDebugModeEnabled()) { + $this->paymentLogger->debug($data, ['iss'], true); + } + } + + /** + * Throw general localized exception. + * + * @return void + * @throws LocalizedException + */ + private function throwException() + { + throw new LocalizedException( + __( + 'Authentication Failed. Your card issuer cannot authenticate this card. ' . + 'Please select another card or form of payment to complete your purchase.' + ) + ); + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtParserInterface.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtParserInterface.php new file mode 100644 index 0000000000000..c6f9a5f60d10d --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtParserInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CardinalCommerce\Model\Response; + +/** + * Parses content of CardinalCommerce response JWT. + */ +interface JwtParserInterface +{ + /** + * Returns response JWT content. + * + * @param string $jwt + * @return array + */ + public function execute(string $jwt): array; +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php new file mode 100644 index 0000000000000..9720b90cad915 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidator.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Model\Response; + +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Validates payload of CardinalCommerce response JWT. + */ +class JwtPayloadValidator implements JwtPayloadValidatorInterface +{ + /** + * Resulting state of the transaction. + * + * SUCCESS - The transaction resulted in success for the payment type used. For example, + * with a CCA transaction this would indicate the user has successfully completed authentication. + * + * NOACTION - The transaction was successful but requires in no additional action. For example, + * with a CCA transaction this would indicate that the user is not currently enrolled in 3D Secure, + * but the API calls were successful. + * + * FAILURE - The transaction resulted in an error. For example, with a CCA transaction this would indicate + * that the user failed authentication or an error was encountered while processing the transaction. + * + * ERROR - A service level error was encountered. These are generally reserved for connectivity + * or API authentication issues. For example if your JWT was incorrectly signed, or Cardinal + * services are currently unreachable. + * + * @var array + */ + private $allowedActionCode = ['SUCCESS', 'NOACTION']; + + /** + * 3DS status of transaction from ECI Flag value. Liability shift applies. + * + * 05 - Successful 3D Authentication (Visa, AMEX, JCB) + * 02 - Successful 3D Authentication (MasterCard) + * 06 - Attempted Processing or User Not Enrolled (Visa, AMEX, JCB) + * 01 - Attempted Processing or User Not Enrolled (MasterCard) + * 07 - 3DS authentication is either failed or could not be attempted; + * possible reasons being both card and Issuing Bank are not secured by 3DS, + * technical errors, or improper configuration. (Visa, AMEX, JCB) + * 00 - 3DS authentication is either failed or could not be attempted; + * possible reasons being both card and Issuing Bank are not secured by 3DS, + * technical errors, or improper configuration. (MasterCard) + * + * @var array + */ + private $allowedECIFlag = ['05', '02', '06', '01']; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + DateTimeFactory $dateTimeFactory + ) { + $this->dateTimeFactory = $dateTimeFactory; + } + /** + * @inheritdoc + */ + public function validate(array $jwtPayload): bool + { + $transactionState = $jwtPayload['Payload']['ActionCode'] ?? ''; + $errorNumber = $jwtPayload['Payload']['ErrorNumber'] ?? -1; + $eciFlag = $jwtPayload['Payload']['Payment']['ExtendedData']['ECIFlag'] ?? ''; + $expTimestamp = $jwtPayload['exp'] ?? 0; + + return $this->isValidErrorNumber((int)$errorNumber) + && $this->isValidTransactionState($transactionState) + && $this->isValidEciFlag($eciFlag) + && $this->isNotExpired((int)$expTimestamp); + } + + /** + * Checks application error number. + * + * A non-zero value represents the error encountered while attempting the process the message request. + * + * @param int $errorNumber + * @return bool + */ + private function isValidErrorNumber(int $errorNumber) + { + return $errorNumber === 0; + } + + /** + * Checks if value of transaction state identifier is in allowed list. + * + * @param string $transactionState + * @return bool + */ + private function isValidTransactionState(string $transactionState) + { + return in_array($transactionState, $this->allowedActionCode); + } + + /** + * Checks if value of ECI Flag identifier is in allowed list. + * + * @param string $eciFlag + * @return bool + */ + private function isValidEciFlag(string $eciFlag) + { + return in_array($eciFlag, $this->allowedECIFlag); + } + + /** + * Checks if token is not expired. + * + * @param int $expTimestamp + * @return bool + */ + private function isNotExpired(int $expTimestamp) + { + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() < $expTimestamp; + } +} diff --git a/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php new file mode 100644 index 0000000000000..774c0daee6ca2 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Model/Response/JwtPayloadValidatorInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CardinalCommerce\Model\Response; + +/** + * Validates payload of CardinalCommerce response JWT. + */ +interface JwtPayloadValidatorInterface +{ + /** + * Validates token payload. + * + * @param array $jwtPayload + * @return bool + */ + public function validate(array $jwtPayload); +} diff --git a/app/code/Magento/CardinalCommerce/Plugin/ExcludeFilesFromMinification.php b/app/code/Magento/CardinalCommerce/Plugin/ExcludeFilesFromMinification.php new file mode 100644 index 0000000000000..a8f660c17e657 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Plugin/ExcludeFilesFromMinification.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CardinalCommerce\Plugin; + +use Magento\Framework\View\Asset\Minification; + +/** + * Plugin for Magento\Framework\View\Asset\Minification. + */ +class ExcludeFilesFromMinification +{ + /** + * Add songbird.js to exclude from minification + * + * @param Minification $subject + * @param array $result + * @param $contentType + * + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetExcludes(Minification $subject, array $result, $contentType) + { + if ($contentType == 'js') { + $result[] = '/v1/songbird'; + } + return $result; + } +} diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md new file mode 100644 index 0000000000000..aa68470a496bc --- /dev/null +++ b/app/code/Magento/CardinalCommerce/README.md @@ -0,0 +1 @@ +The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php new file mode 100644 index 0000000000000..18d9794d78edd --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/JwtManagementTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Test\Unit\Model; + +use Magento\CardinalCommerce\Model\JwtManagement; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Tests JWT encode and decode. + */ +class JwtManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * API key + */ + const API_KEY = 'API key'; + + /** + * @var JwtManagement + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = new JwtManagement(new Json()); + } + + /** + * Tests JWT encode. + */ + public function testEncode() + { + $jwt = $this->model->encode($this->getTokenPayload(), self::API_KEY); + + $this->assertEquals( + $this->getValidHS256Jwt(), + $jwt, + 'Generated JWT isn\'t equal to expected' + ); + } + + /** + * Tests JWT decode. + */ + public function testDecode() + { + $tokenPayload = $this->model->decode($this->getValidHS256Jwt(), self::API_KEY); + + $this->assertEquals( + $this->getTokenPayload(), + $tokenPayload, + 'JWT payload isn\'t equal to expected' + ); + } + + /** + * Tests JWT decode. + * + * @param string $jwt + * @param string $errorMessage + * @dataProvider decodeWithExceptionDataProvider + */ + public function testDecodeWithException(string $jwt, string $errorMessage) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($errorMessage); + + $this->model->decode($jwt, self::API_KEY); + } + + /** + * @return array + */ + public function decodeWithExceptionDataProvider(): array + { + return [ + [ + 'jwt' => '', + 'errorMessage' => 'JWT is empty', + ], + [ + 'jwt' => 'dddd.dddd', + 'errorMessage' => 'Wrong number of segments in JWT', + ], + [ + 'jwt' => 'dddd.dddd.dddd', + 'errorMessage' => 'Unable to unserialize value. Error: Syntax error', + ], + [ + 'jwt' => $this->getHS512Jwt(), + 'errorMessage' => 'Algorithm HS512 is not supported', + ], + [ + 'jwt' => $this->getJwtWithInvalidSignature(), + 'errorMessage' => 'JWT signature verification failed', + ], + ]; + } + + /** + * Returns valid JWT, signed using HS256. + * + * @return string + */ + private function getValidHS256Jwt(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . + 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . + 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . + 'CI6dHJ1ZX0.emv9N75JIvyk_gQHMNJYQ2UzmbM5ISBQs1Y222zO1jk'; + } + + /** + * Returns JWT, signed using not supported HS512. + * + * @return string + */ + private function getHS512Jwt(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1V' . + 'uaXRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGV' . + 'yTnVtYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF' . + '5bG9hZCI6dHJ1ZX0.4fwdXfIgUe7bAiHP2bZsxSG-s-wJOyaCmCe9MOQhs-m6LLjRGarguA_0SqZA2EeUaytMO4R' . + 'G84ZEOfbYfS8c1A'; + } + + /** + * Returns JWT with invalid signature. + * + * @return string + */ + private function getJwtWithInvalidSignature(): string + { + return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNWE1OWJmYi1hYzA2LTRjNWYtYmU1Yy0zNTFiNjR' . + 'hZTYwOGUiLCJpc3MiOiI1NjU2MGEzNThiOTQ2ZTBjODQ1MjM2NWRzIiwiaWF0IjoiMTQ0ODk5Nzg2NSIsIk9yZ1Vua' . + 'XRJZCI6IjU2NTYwN2MxOGI5NDZlMDU4NDYzZHM4ciIsIlBheWxvYWQiOnsiT3JkZXJEZXRhaWxzIjp7Ik9yZGVyTnV' . + 'tYmVyIjoiMTI1IiwiQW1vdW50IjoiMTUwMCIsIkN1cnJlbmN5Q29kZSI6IlVTRCJ9fSwiT2JqZWN0aWZ5UGF5bG9hZ' . + 'CI6dHJ1ZX0.InvalidSign'; + } + + /** + * Returns token decoded payload. + * + * @return array + */ + private function getTokenPayload(): array + { + return [ + 'jti' => 'a5a59bfb-ac06-4c5f-be5c-351b64ae608e', + 'iss' => '56560a358b946e0c8452365ds', + 'iat' => '1448997865', + 'OrgUnitId' => '565607c18b946e058463ds8r', + 'Payload' => [ + 'OrderDetails' => [ + 'OrderNumber' => '125', + 'Amount' => '1500', + 'CurrencyCode' => 'USD' + ] + ], + 'ObjectifyPayload' => true + ]; + } +} diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php new file mode 100644 index 0000000000000..cbaae9f777a61 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtPayloadValidatorTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CardinalCommerce\Test\Unit\Model\Response; + +use Magento\CardinalCommerce\Model\Response\JwtPayloadValidator; +use Magento\Framework\Intl\DateTimeFactory; + +/** + * Class JwtPayloadValidatorTest + */ +class JwtPayloadValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var JwtPayloadValidator + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = new JwtPayloadValidator(new DateTimeFactory()); + } + + /** + * Tests successful cases. + * + * @param array $token + * @dataProvider validateSuccessDataProvider + */ + public function testValidateSuccess(array $token) + { + $this->assertTrue( + $this->model->validate($token) + ); + } + + /** + * @case 1. All data are correct, the transaction was successful (Visa, AMEX) + * @case 2. All data are correct, the transaction was successful but requires in no additional action (Visa, AMEX) + * @case 3. All data are correct, the transaction was successful (MasterCard) + * @case 4. All data are correct, the transaction was successful but requires in no additional action (MasterCard) + * + * @return array + */ + public function validateSuccessDataProvider() + { + $expTimestamp = $this->getValidExpTimestamp(); + + return [ + 1 => $this->createToken('05', '0', 'SUCCESS', $expTimestamp), + 2 => $this->createToken('06', '0', 'NOACTION', $expTimestamp), + 3 => $this->createToken('02', '0', 'SUCCESS', $expTimestamp), + 4 => $this->createToken('01', '0', 'NOACTION', $expTimestamp), + ]; + } + + /** + * Case when 3DS authentication is either failed or could not be attempted. + * + * @param array $token + * @dataProvider validationEciFailsDataProvider + */ + public function testValidationEciFails(array $token) + { + $this->assertFalse( + $this->model->validate($token), + 'Negative ECIFlag value validation fails' + ); + } + + /** + * ECIFlag value when 3DS authentication is either failed or could not be attempted. + * + * @case 1. Visa, AMEX, JCB + * @case 2. MasterCard + * @return array + */ + public function validationEciFailsDataProvider(): array + { + $expTimestamp = $this->getValidExpTimestamp(); + return [ + 1 => $this->createToken('07', '0', 'NOACTION', $expTimestamp), + 2 => $this->createToken('00', '0', 'NOACTION', $expTimestamp), + ]; + } + + /** + * Case when resulting state of the transaction is negative. + * + * @param array $token + * @dataProvider validationActionCodeFailsDataProvider + */ + public function testValidationActionCodeFails(array $token) + { + $this->assertFalse( + $this->model->validate($token), + 'Negative ActionCode value validation fails' + ); + } + + /** + * ECIFlag value when 3DS authentication is either failed or could not be attempted. + * + * @return array + */ + public function validationActionCodeFailsDataProvider(): array + { + $expTimestamp = $this->getValidExpTimestamp(); + return [ + $this->createToken('05', '0', 'FAILURE', $expTimestamp), + $this->createToken('05', '0', 'ERROR', $expTimestamp), + ]; + } + + /** + * Case when ErrorNumber not equal 0. + */ + public function testValidationErrorNumberFails() + { + $notAllowedErrorNumber = '10'; + $expTimestamp = $this->getValidExpTimestamp(); + $token = $this->createToken('05', $notAllowedErrorNumber, 'SUCCESS', $expTimestamp); + $this->assertFalse( + $this->model->validate($token), + 'Negative ErrorNumber value validation fails' + ); + } + + /** + * Case when ErrorNumber not equal 0. + */ + public function testValidationExpirationFails() + { + $expTimestamp = $this->getOutdatedExpTimestamp(); + $token = $this->createToken('05', '0', 'SUCCESS', $expTimestamp); + $this->assertFalse( + $this->model->validate($token), + 'Expiration date validation fails' + ); + } + + /** + * Creates a token. + * + * @param string $eciFlag + * @param string $errorNumber + * @param string $actionCode + * @param int $expTimestamp + * + * @return array + */ + private function createToken(string $eciFlag, string $errorNumber, string $actionCode, int $expTimestamp): array + { + return [ + [ + 'Payload' => [ + 'Payment' => [ + 'ExtendedData' => [ + 'ECIFlag' => $eciFlag, + ], + ], + 'ActionCode' => $actionCode, + 'ErrorNumber' => $errorNumber + ], + 'exp' => $expTimestamp + ] + ]; + } + + /** + * Returns valid expiration timestamp. + * + * @return int + */ + private function getValidExpTimestamp() + { + $dateTimeFactory = new DateTimeFactory(); + $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() + 3600; + } + + /** + * Returns outdated expiration timestamp. + * + * @return int + */ + private function getOutdatedExpTimestamp() + { + $dateTimeFactory = new DateTimeFactory(); + $currentDate = $dateTimeFactory->create('now', new \DateTimeZone('UTC')); + + return $currentDate->getTimestamp() - 3600; + } +} diff --git a/app/code/Magento/CardinalCommerce/composer.json b/app/code/Magento/CardinalCommerce/composer.json new file mode 100644 index 0000000000000..758108dbb4320 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-cardinal-commerce", + "description": "Provides a possibility to enable 3-D Secure 2.0 support for payment methods.", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.0.13||~7.1.0||~7.2.0", + "magento/framework": "101.0.*", + "magento/module-checkout": "100.2.*", + "magento/module-payment": "100.2.*", + "magento/module-store": "100.2.*" + }, + "type": "magento2-module", + "version": "100.2.1", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CardinalCommerce\\": "" + } + } +} diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..532fcdd0f598f --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="three_d_secure" translate="label" type="text" sortOrder="410" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>3D Secure</label> + <tab>sales</tab> + <resource>Magento_Sales::three_d_secure</resource> + <group id="cardinal" type="text" sortOrder="13" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="config" translate="label comment" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>CardinalCommerce</label> + <comment><![CDATA[Please visit <a href="https://www.cardinalcommerce.com/" target="_blank">www.cardinalcommerce.com</a> to get the CardinalCommerce credentials and find out more details about PSD2 SCA requirements. For support contact <a href="mailto:support@cardinalcommerce.com">support@cardinalcommerce.com</a>.]]></comment> + <field id="environment" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Environment</label> + <source_model>Magento\CardinalCommerce\Model\Adminhtml\Source\Environment</source_model> + <config_path>three_d_secure/cardinal/environment</config_path> + </field> + <field id="org_unit_id" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Org Unit Id</label> + <config_path>three_d_secure/cardinal/org_unit_id</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="api_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Key</label> + <config_path>three_d_secure/cardinal/api_key</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="api_identifier" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Identifier</label> + <config_path>three_d_secure/cardinal/api_identifier</config_path> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> + <field id="debug" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Debug</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>three_d_secure/cardinal/debug</config_path> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/config.xml b/app/code/Magento/CardinalCommerce/etc/config.xml new file mode 100644 index 0000000000000..60b111a59cbc9 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/config.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <three_d_secure> + <cardinal> + <environment>production</environment> + <api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <org_unit_id backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <api_identifier backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <debug>0</debug> + </cardinal> + </three_d_secure> + </default> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/di.xml b/app/code/Magento/CardinalCommerce/etc/di.xml new file mode 100644 index 0000000000000..ad895e992a9bb --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\CardinalCommerce\Model\Response\JwtPayloadValidatorInterface" type="Magento\CardinalCommerce\Model\Response\JwtPayloadValidator" /> + <preference for="Magento\CardinalCommerce\Model\Response\JwtParserInterface" type="Magento\CardinalCommerce\Model\Response\JwtParser" /> + <type name="Magento\Framework\View\Asset\Minification"> + <plugin name="excludeSongbirdJsFromMin" type="Magento\CardinalCommerce\Plugin\ExcludeFilesFromMinification" /> + </type> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/frontend/di.xml b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml new file mode 100644 index 0000000000000..e3913291aa683 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/frontend/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="cardinal_config_provider" xsi:type="object"> + Magento\CardinalCommerce\Model\Checkout\ConfigProvider + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CardinalCommerce/etc/module.xml b/app/code/Magento/CardinalCommerce/etc/module.xml new file mode 100644 index 0000000000000..5dcd06409151a --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_CardinalCommerce" setup_version="1.0.0"> + <sequence> + <module name="Magento_Checkout"/> + <module name="Magento_Payment"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CardinalCommerce/registration.php b/app/code/Magento/CardinalCommerce/registration.php new file mode 100644 index 0000000000000..26fb168fb0ae2 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CardinalCommerce', __DIR__); diff --git a/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..0c5e3964d04e7 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/requirejs-config.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + shim: { + cardinaljs: { + exports: 'Cardinal' + }, + cardinaljsSandbox: { + exports: 'Cardinal' + } + }, + paths: { + cardinaljsSandbox: 'https://includestest.ccdc02.com/cardinalcruise/v1/songbird', + cardinaljs: 'https://songbird.cardinalcommerce.com/edge/v1/songbird' + } +}; + diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js new file mode 100644 index 0000000000000..2ddb450d2f81c --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-client.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiClass', + 'Magento_CardinalCommerce/js/cardinal-factory', + 'Magento_Checkout/js/model/quote', + 'mage/translate' +], function ($, Class, cardinalFactory, quote, $t) { + 'use strict'; + + return { + /** + * Starts Cardinal Consumer Authentication + * + * @param {Object} cardData + * @return {jQuery.Deferred} + */ + startAuthentication: function (cardData) { + var deferred = $.Deferred(); + + if (this.cardinalClient) { + this._startAuthentication(deferred, cardData); + } else { + cardinalFactory(this.getEnvironment()) + .done(function (client) { + this.cardinalClient = client; + this._startAuthentication(deferred, cardData); + }.bind(this)); + } + + return deferred.promise(); + }, + + /** + * Cardinal Consumer Authentication + * + * @param {jQuery.Deferred} deferred + * @param {Object} cardData + */ + _startAuthentication: function (deferred, cardData) { + //this.cardinalClient.configure({ logging: { level: 'verbose' } }); + this.cardinalClient.on('payments.validated', function (data, jwt) { + if (data.ErrorNumber !== 0) { + deferred.reject(data.ErrorDescription); + } else if ($.inArray(data.ActionCode, ['FAILURE', 'ERROR']) !== -1) { + deferred.reject($t('Authentication Failed. Please try again with another form of payment.')); + } else { + deferred.resolve(jwt); + } + this.cardinalClient.off('payments.validated'); + }.bind(this)); + + this.cardinalClient.on('payments.setupComplete', function () { + this.cardinalClient.start('cca', this.getRequestOrderObject(cardData)); + this.cardinalClient.off('payments.setupComplete'); + }.bind(this)); + + this.cardinalClient.setup('init', { + jwt: this.getRequestJWT() + }); + }, + + /** + * Returns request order object. + * + * The request order object is structured object that is used to pass data + * to Cardinal that describes an order you'd like to process. + * + * If you pass a request object in both the JWT and the browser, + * Cardinal will merge the objects together where the browser overwrites + * the JWT object as it is considered the most recently captured data. + * + * @param {Object} cardData + * @returns {Object} + */ + getRequestOrderObject: function (cardData) { + var totalAmount = quote.totals()['base_grand_total'], + currencyCode = quote.totals()['base_currency_code'], + billingAddress = quote.billingAddress(), + requestObject; + + requestObject = { + OrderDetails: { + Amount: totalAmount * 100, + CurrencyCode: currencyCode + }, + Consumer: { + Account: { + AccountNumber: cardData.accountNumber, + ExpirationMonth: cardData.expMonth, + ExpirationYear: cardData.expYear, + CardCode: cardData.cardCode + }, + BillingAddress: { + FirstName: billingAddress.firstname, + LastName: billingAddress.lastname, + Address1: billingAddress.street[0], + Address2: billingAddress.street[1], + City: billingAddress.city, + State: billingAddress.region, + PostalCode: billingAddress.postcode, + CountryCode: billingAddress.countryId, + Phone1: billingAddress.telephone + } + } + }; + + return requestObject; + }, + + /** + * Returns request JWT + * @returns {String} + */ + getRequestJWT: function () { + return window.checkoutConfig.cardinal.requestJWT; + }, + + /** + * Returns type of environment + * @returns {String} + */ + getEnvironment: function () { + return window.checkoutConfig.cardinal.environment; + } + }; +}); diff --git a/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js new file mode 100644 index 0000000000000..1da92ba2ff787 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/view/frontend/web/js/cardinal-factory.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (environment) { + var deferred = $.Deferred(), + dependency = 'cardinaljs'; + + if (environment === 'sandbox') { + dependency = 'cardinaljsSandbox'; + } + + require( + [dependency], + function (Cardinal) { + deferred.resolve(Cardinal); + }, + deferred.reject + ); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php index b65cdafbe26f4..cf5e49c954b43 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php @@ -43,7 +43,7 @@ public function setParentId($parentId); /** * Get category name * - * @return string + * @return string|null */ public function getName(); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php index 331679874629b..bfeab3f71ebc1 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php @@ -7,6 +7,11 @@ use Magento\Framework\Data\Tree\Node; use Magento\Store\Model\Store; +use Magento\Framework\Registry; +use Magento\Catalog\Model\ResourceModel\Category\Tree; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Backend\Block\Template\Context; +use Magento\Catalog\Model\Category; /** * Class AbstractCategory @@ -16,17 +21,17 @@ class AbstractCategory extends \Magento\Backend\Block\Template /** * Core registry * - * @var \Magento\Framework\Registry + * @var Registry */ protected $_coreRegistry = null; /** - * @var \Magento\Catalog\Model\ResourceModel\Category\Tree + * @var Tree */ protected $_categoryTree; /** - * @var \Magento\Catalog\Model\CategoryFactory + * @var CategoryFactory */ protected $_categoryFactory; @@ -36,17 +41,17 @@ class AbstractCategory extends \Magento\Backend\Block\Template protected $_withProductCount; /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree - * @param \Magento\Framework\Registry $registry - * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory + * @param Context $context + * @param Tree $categoryTree + * @param Registry $registry + * @param CategoryFactory $categoryFactory * @param array $data */ public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree, - \Magento\Framework\Registry $registry, - \Magento\Catalog\Model\CategoryFactory $categoryFactory, + Context $context, + Tree $categoryTree, + Registry $registry, + CategoryFactory $categoryFactory, array $data = [] ) { $this->_categoryTree = $categoryTree; @@ -67,36 +72,47 @@ public function getCategory() } /** + * Get category id + * * @return int|string|null */ public function getCategoryId() { if ($this->getCategory()) { - return $this->getCategory()->getId(); + return $this->getCategory() + ->getId(); } - return \Magento\Catalog\Model\Category::TREE_ROOT_ID; + return Category::TREE_ROOT_ID; } /** + * Get category name + * * @return string */ public function getCategoryName() { - return $this->getCategory()->getName(); + return $this->getCategory() + ->getName(); } /** + * Get category path + * * @return mixed */ public function getCategoryPath() { if ($this->getCategory()) { - return $this->getCategory()->getPath(); + return $this->getCategory() + ->getPath(); } - return \Magento\Catalog\Model\Category::TREE_ROOT_ID; + return Category::TREE_ROOT_ID; } /** + * Check store root category + * * @return bool */ public function hasStoreRootCategory() @@ -109,15 +125,20 @@ public function hasStoreRootCategory() } /** + * Get store from request + * * @return Store */ public function getStore() { - $storeId = (int)$this->getRequest()->getParam('store'); + $storeId = (int)$this->getRequest() + ->getParam('store'); return $this->_storeManager->getStore($storeId); } /** + * Get root category for tree + * * @param mixed|null $parentNodeCategory * @param int $recursionLevel * @return Node|array|null @@ -130,13 +151,14 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } $root = $this->_coreRegistry->registry('root'); if ($root === null) { - $storeId = (int)$this->getRequest()->getParam('store'); + $storeId = (int)$this->getRequest() + ->getParam('store'); if ($storeId) { $store = $this->_storeManager->getStore($storeId); $rootId = $store->getRootCategoryId(); } else { - $rootId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; + $rootId = Category::TREE_ROOT_ID; } $tree = $this->_categoryTree->load(null, $recursionLevel); @@ -149,10 +171,11 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) $root = $tree->getNodeById($rootId); - if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($root) { $root->setIsVisible(true); - } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - $root->setName(__('Root')); + if ($root->getId() == Category::TREE_ROOT_ID) { + $root->setName(__('Root')); + } } $this->_coreRegistry->register('root', $root); @@ -162,22 +185,28 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } /** + * Get Default Store Id + * * @return int */ protected function _getDefaultStoreId() { - return \Magento\Store\Model\Store::DEFAULT_STORE_ID; + return Store::DEFAULT_STORE_ID; } /** + * Get category collection + * * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getCategoryCollection() { - $storeId = $this->getRequest()->getParam('store', $this->_getDefaultStoreId()); + $storeId = $this->getRequest() + ->getParam('store', $this->_getDefaultStoreId()); $collection = $this->getData('category_collection'); if ($collection === null) { - $collection = $this->_categoryFactory->create()->getCollection(); + $collection = $this->_categoryFactory->create() + ->getCollection(); $collection->addAttributeToSelect( 'name' @@ -212,11 +241,11 @@ public function getRootByIds($ids) if (null === $root) { $ids = $this->_categoryTree->getExistingCategoryIdsBySpecifiedIds($ids); $tree = $this->_categoryTree->loadByIds($ids); - $rootId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; + $rootId = Category::TREE_ROOT_ID; $root = $tree->getNodeById($rootId); - if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($root && $rootId != Category::TREE_ROOT_ID) { $root->setIsVisible(true); - } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + } elseif ($root && $root->getId() == Category::TREE_ROOT_ID) { $root->setName(__('Root')); } @@ -227,6 +256,8 @@ public function getRootByIds($ids) } /** + * Get category node for tree + * * @param mixed $parentNodeCategory * @param int $recursionLevel * @return Node @@ -237,9 +268,9 @@ public function getNode($parentNodeCategory, $recursionLevel = 2) $node = $this->_categoryTree->loadNode($nodeId); $node->loadChildren($recursionLevel); - if ($node && $nodeId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($node && $nodeId != Category::TREE_ROOT_ID) { $node->setIsVisible(true); - } elseif ($node && $node->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + } elseif ($node && $node->getId() == Category::TREE_ROOT_ID) { $node->setName(__('Root')); } @@ -249,17 +280,26 @@ public function getNode($parentNodeCategory, $recursionLevel = 2) } /** + * Get category save url + * * @param array $args * @return string */ public function getSaveUrl(array $args = []) { - $params = ['_current' => false, '_query' => false, 'store' => $this->getStore()->getId()]; + $params = [ + '_current' => false, + '_query' => false, + 'store' => $this->getStore() + ->getId() + ]; $params = array_merge($params, $args); return $this->getUrl('catalog/*/save', $params); } /** + * Get category edit url + * * @return string */ public function getEditUrl() @@ -279,7 +319,7 @@ public function getRootIds() { $ids = $this->getData('root_ids'); if ($ids === null) { - $ids = [\Magento\Catalog\Model\Category::TREE_ROOT_ID]; + $ids = [Category::TREE_ROOT_ID]; foreach ($this->_storeManager->getGroups() as $store) { $ids[] = $store->getRootCategoryId(); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/AssignProducts.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/AssignProducts.php index 69618f04eb2af..c718563d7576e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/AssignProducts.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/AssignProducts.php @@ -13,7 +13,7 @@ class AssignProducts extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'catalog/category/edit/assign_products.phtml'; + protected $_template = 'Magento_Catalog::catalog/category/edit/assign_products.phtml'; /** * @var \Magento\Catalog\Block\Adminhtml\Category\Tab\Product diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php index 20411a4c4d767..2eef1188e3910 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php @@ -27,7 +27,8 @@ public function getButtonData() return [ 'id' => 'delete', 'label' => __('Delete'), - 'on_click' => "categoryDelete('" . $this->getDeleteUrl() . "')", + 'on_click' => "deleteConfirm('" .__('Are you sure you want to delete this category?') ."', '" + . $this->getDeleteUrl() . "', {data: {}})", 'class' => 'delete', 'sort_order' => 10 ]; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php index df5894bf4cbd1..20bd1b379beef 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php @@ -14,6 +14,9 @@ use Magento\Backend\Block\Widget\Grid; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Extended; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\ObjectManager; class Product extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -29,22 +32,38 @@ class Product extends \Magento\Backend\Block\Widget\Grid\Extended */ protected $_productFactory; + /** + * @var Status + */ + private $status; + + /** + * @var Visibility + */ + private $visibility; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Framework\Registry $coreRegistry * @param array $data + * @param Visibility|null $visibility + * @param Status|null $status */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Framework\Registry $coreRegistry, - array $data = [] + array $data = [], + Visibility $visibility = null, + Status $status = null ) { $this->_productFactory = $productFactory; $this->_coreRegistry = $coreRegistry; + $this->visibility = $visibility ?: ObjectManager::getInstance()->get(Visibility::class); + $this->status = $status ?: ObjectManager::getInstance()->get(Status::class); parent::__construct($context, $backendHelper, $data); } @@ -102,6 +121,10 @@ protected function _prepareCollection() 'name' )->addAttributeToSelect( 'sku' + )->addAttributeToSelect( + 'visibility' + )->addAttributeToSelect( + 'status' )->addAttributeToSelect( 'price' )->joinField( @@ -159,6 +182,28 @@ protected function _prepareColumns() ); $this->addColumn('name', ['header' => __('Name'), 'index' => 'name']); $this->addColumn('sku', ['header' => __('SKU'), 'index' => 'sku']); + $this->addColumn( + 'visibility', + [ + 'header' => __('Visibility'), + 'index' => 'visibility', + 'type' => 'options', + 'options' => $this->visibility->getOptionArray(), + 'header_css_class' => 'col-visibility', + 'column_css_class' => 'col-visibility' + ] + ); + + $this->addColumn( + 'status', + [ + 'header' => __('Status'), + 'index' => 'status', + 'type' => 'options', + 'options' => $this->status->getOptionArray() + ] + ); + $this->addColumn( 'price', [ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index 6f8a45c6ac7ed..69d56a060e839 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Categories tree block */ @@ -29,7 +27,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory /** * @var string */ - protected $_template = 'catalog/category/tree.phtml'; + protected $_template = 'Magento_Catalog::catalog/category/tree.phtml'; /** * @var \Magento\Backend\Model\Auth\Session @@ -73,7 +71,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -82,14 +80,15 @@ protected function _construct() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { $addUrl = $this->getUrl("*/*/add", ['_current' => false, 'id' => null, '_query' => false]); if ($this->getStore()->getId() == Store::DEFAULT_STORE_ID) { $this->addChild( - 'add_sub_button', \Magento\Backend\Block\Widget\Button::class, + 'add_sub_button', + \Magento\Backend\Block\Widget\Button::class, [ 'label' => __('Add Subcategory'), 'onclick' => "addNew('" . $addUrl . "', false)", @@ -101,7 +100,8 @@ protected function _prepareLayout() if ($this->canAddRootCategory()) { $this->addChild( - 'add_root_button', \Magento\Backend\Block\Widget\Button::class, + 'add_root_button', + \Magento\Backend\Block\Widget\Button::class, [ 'label' => __('Add Root Category'), 'onclick' => "addNew('" . $addUrl . "', true)", @@ -182,6 +182,8 @@ public function getSuggestedCategoriesJson($namePart) } /** + * Get add root button html + * * @return string */ public function getAddRootButtonHtml() @@ -190,6 +192,8 @@ public function getAddRootButtonHtml() } /** + * Get add sub button html + * * @return string */ public function getAddSubButtonHtml() @@ -198,6 +202,8 @@ public function getAddSubButtonHtml() } /** + * Get expand button html + * * @return string */ public function getExpandButtonHtml() @@ -206,6 +212,8 @@ public function getExpandButtonHtml() } /** + * Get collapse button html + * * @return string */ public function getCollapseButtonHtml() @@ -214,6 +222,8 @@ public function getCollapseButtonHtml() } /** + * Get store switcher + * * @return string */ public function getStoreSwitcherHtml() @@ -222,19 +232,23 @@ public function getStoreSwitcherHtml() } /** + * Get loader tree url + * * @param bool|null $expanded * @return string */ public function getLoadTreeUrl($expanded = null) { $params = ['_current' => true, 'id' => null, 'store' => null]; - if (is_null($expanded) && $this->_backendSession->getIsTreeWasExpanded() || $expanded == true) { + if ($expanded === null && $this->_backendSession->getIsTreeWasExpanded() || $expanded == true) { $params['expand_all'] = true; } return $this->getUrl('*/*/categoriesJson', $params); } /** + * Get nodes url + * * @return string */ public function getNodesUrl() @@ -243,6 +257,8 @@ public function getNodesUrl() } /** + * Get switcher tree url + * * @return string */ public function getSwitchTreeUrl() @@ -254,6 +270,8 @@ public function getSwitchTreeUrl() } /** + * Get is was expanded + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -263,7 +281,10 @@ public function getIsWasExpanded() } /** + * Get move url + * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getMoveUrl() { @@ -271,6 +292,8 @@ public function getMoveUrl() } /** + * Get tree + * * @param mixed|null $parenNodeCategory * @return array */ @@ -282,6 +305,8 @@ public function getTree($parenNodeCategory = null) } /** + * Get tree json + * * @param mixed|null $parenNodeCategory * @return string */ @@ -316,8 +341,8 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName) $categories ) . ';' . - ($this->canAddSubCategory() ? '$("add_subcategory_button").show();' : '$("add_subcategory_button").hide();') . - '});</script>'; + ($this->canAddSubCategory() ? '$("add_subcategory_button").show();' : '$("add_subcategory_button").hide();') + . '});</script>'; } /** @@ -325,7 +350,7 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName) * * @param Node|array $node * @param int $level - * @return string + * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -367,7 +392,7 @@ protected function _getNodeJson($node, $level = 0) } } - if ($isParent || $node->getLevel() < 2) { + if ($isParent || $node->getLevel() < 1) { $item['expanded'] = true; } @@ -390,6 +415,8 @@ public function buildNodeName($node) } /** + * Is category movable + * * @param Node|array $node * @return bool */ @@ -403,6 +430,8 @@ protected function _isCategoryMoveable($node) } /** + * Is parent selected category + * * @param Node|array $node * @return bool */ @@ -422,6 +451,7 @@ protected function _isParentSelectedCategory($node) * Check if page loaded by outside link to category edit * * @return boolean + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function isClearEdit() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php index 5e98313f95f0f..9c83d4aea61c7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php @@ -24,7 +24,7 @@ class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Tree * * @var string */ - protected $_template = 'catalog/category/widget/tree.phtml'; + protected $_template = 'Magento_Catalog::catalog/category/widget/tree.phtml'; /** * @return void @@ -144,7 +144,7 @@ function (node, e) { * * @param \Magento\Framework\Data\Tree\Node|array $node * @param int $level - * @return string + * @return array */ protected function _getNodeJson($node, $level = 0) { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php index 0026e52e039ef..cd6c5021f0cc9 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php @@ -32,10 +32,9 @@ protected function _getElementHtml(AbstractElement $element) $from = $element->setValue(isset($values[0]) ? $values[0] : null)->getElementHtml(); $to = $element->setValue(isset($values[1]) ? $values[1] : null)->getElementHtml(); - return __( - '<label class="label"><span>from</span></label>' - ) . $from . __( - '<label class="label"><span>to</span></label>' - ) . $to; + return '<label class="label"><span>' . __('from') . '</span></label>' + . $from . + '<label class="label"><span>' . __('to') . '</span></label>' + . $to; } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php index 10214fc1d16fd..ad6df27b89334 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php @@ -21,7 +21,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme /** * Retrieve data object related with form * - * @return \Magento\Catalog\Model\Product || \Magento\Catalog\Model\Category + * @return \Magento\Catalog\Model\Product|\Magento\Catalog\Model\Category */ public function getDataObject() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 6ab039aa27849..695ea6a7288e3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -91,7 +91,23 @@ protected function _construct() if (!$entityAttribute || !$entityAttribute->getIsUserDefined()) { $this->buttonList->remove('delete'); } else { - $this->buttonList->update('delete', 'label', __('Delete Attribute')); + $this->buttonList->update( + 'delete', + 'onclick', + sprintf( + "deleteConfirm('%s','%s', %s)", + __('Are you sure you want to do this?'), + $this->getDeleteUrl(), + json_encode( + [ + 'action' => '', + 'data' => [ + 'form_key' => $this->getFormKey() + ] + ] + ) + ) + ); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php index db0b0dc1d00dd..dd09e40ac5b35 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Product attribute add/edit form main tab * diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php index ab5026b1e69b9..66e04ef03f771 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php @@ -101,8 +101,7 @@ protected function _prepareColumns() 'type' => 'options', 'options' => ['1' => __('Yes'), '0' => __('No')], 'align' => 'center' - ], - 'is_user_defined' + ] ); $this->_eventManager->dispatch('product_attribute_grid_build', ['grid' => $this]); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php index c6e48c02805a6..2cd27f2785af2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php @@ -22,7 +22,7 @@ class Main extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/main.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main.phtml'; /** * Core registry @@ -140,7 +140,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'catalog/*/delete', ['id' => $setId] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); @@ -233,7 +233,7 @@ public function getGroupTreeJson() /* @var $node \Magento\Eav\Model\Entity\Attribute\Group */ foreach ($groups as $node) { $item = []; - $item['text'] = $node->getAttributeGroupName(); + $item['text'] = $this->escapeHtml($node->getAttributeGroupName()); $item['id'] = $node->getAttributeGroupId(); $item['cls'] = 'folder'; $item['allowDrop'] = true; @@ -280,7 +280,7 @@ public function getAttributeTreeJson() foreach ($attributes as $child) { $attr = [ - 'text' => $child->getAttributeCode(), + 'text' => $this->escapeHtml($child->getAttributeCode()), 'id' => $child->getAttributeId(), 'cls' => 'leaf', 'allowDrop' => false, diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php index ee92fd7c19b80..6f6ad4f909815 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php @@ -81,12 +81,10 @@ protected function _prepareForm() */ protected function _getSetId() { - return intval( - $this->getRequest()->getParam('id') - ) > 0 ? intval( - $this->getRequest()->getParam('id') - ) : $this->_typeFactory->create()->load( - $this->_coreRegistry->registry('entityType') - )->getDefaultAttributeSetId(); + return (int)$this->getRequest()->getParam('id') > 0 + ? (int)$this->getRequest()->getParam('id') + : $this->_typeFactory->create()->load( + $this->_coreRegistry->registry('entityType') + )->getDefaultAttributeSetId(); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php index f5e3f94418687..cb0a739b56e4e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php @@ -14,5 +14,5 @@ class Attribute extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/main/tree/attribute.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main/tree/attribute.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php index cf8de44c3d9df..93c2dcc76263c 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php @@ -14,5 +14,5 @@ class Group extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/main/tree/group.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main/tree/group.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php index 329afa968307c..f69e58985bfc5 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php @@ -18,7 +18,7 @@ class Add extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/toolbar/add.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/add.phtml'; /** * @return AbstractBlock diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php index f707c5c340b68..e29ab26065dc3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php @@ -20,7 +20,7 @@ class Main extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/toolbar/main.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/main.phtml'; /** * @return $this diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php index 9270c1ac38ba3..98280d8d31237 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Composite/Configure.php @@ -22,7 +22,7 @@ class Configure extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'catalog/product/composite/configure.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/composite/configure.phtml'; /** * Core registry diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php index c18106fe567d0..95ccd2ed3e726 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php @@ -3,21 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Catalog\Block\Adminhtml\Product; /** * Customer edit block * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ -namespace Magento\Catalog\Block\Adminhtml\Product; - class Edit extends \Magento\Backend\Block\Widget { + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @var string */ - protected $_template = 'catalog/product/edit.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit.phtml'; /** * Core registry @@ -47,6 +52,7 @@ class Edit extends \Magento\Backend\Block\Widget * @param \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory * @param \Magento\Framework\Registry $registry * @param \Magento\Catalog\Helper\Product $productHelper + * @param \Magento\Framework\Escaper $escaper * @param array $data */ public function __construct( @@ -55,16 +61,20 @@ public function __construct( \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory, \Magento\Framework\Registry $registry, \Magento\Catalog\Helper\Product $productHelper, + \Magento\Framework\Escaper $escaper, array $data = [] ) { $this->_productHelper = $productHelper; $this->_attributeSetFactory = $attributeSetFactory; $this->_coreRegistry = $registry; $this->jsonEncoder = $jsonEncoder; + $this->escaper = $escaper; parent::__construct($context, $data); } /** + * Edit Product constructor + * * @return void */ protected function _construct() @@ -144,6 +154,8 @@ protected function _prepareLayout() } /** + * Retrieve back button html + * * @return string */ public function getBackButtonHtml() @@ -152,6 +164,8 @@ public function getBackButtonHtml() } /** + * Retrieve cancel button html + * * @return string */ public function getCancelButtonHtml() @@ -160,6 +174,8 @@ public function getCancelButtonHtml() } /** + * Retrieve save button html + * * @return string */ public function getSaveButtonHtml() @@ -168,6 +184,8 @@ public function getSaveButtonHtml() } /** + * Retrieve save and edit button html + * * @return string */ public function getSaveAndEditButtonHtml() @@ -176,6 +194,8 @@ public function getSaveAndEditButtonHtml() } /** + * Retrieve delete button html + * * @return string */ public function getDeleteButtonHtml() @@ -194,6 +214,8 @@ public function getSaveSplitButtonHtml() } /** + * Retrieve validation url + * * @return string */ public function getValidationUrl() @@ -202,6 +224,8 @@ public function getValidationUrl() } /** + * Retrieve save url + * * @return string */ public function getSaveUrl() @@ -210,6 +234,8 @@ public function getSaveUrl() } /** + * Retrieve save and continue url + * * @return string */ public function getSaveAndContinueUrl() @@ -221,6 +247,8 @@ public function getSaveAndContinueUrl() } /** + * Retrieve product id + * * @return mixed */ public function getProductId() @@ -229,6 +257,8 @@ public function getProductId() } /** + * Retrieve product set id + * * @return mixed */ public function getProductSetId() @@ -241,6 +271,8 @@ public function getProductSetId() } /** + * Retrieve duplicate url + * * @return string */ public function getDuplicateUrl() @@ -249,6 +281,8 @@ public function getDuplicateUrl() } /** + * Retrieve product header + * * @deprecated 101.1.0 * @return string */ @@ -263,6 +297,8 @@ public function getHeader() } /** + * Get product attribute set name + * * @return string */ public function getAttributeSetName() @@ -275,11 +311,14 @@ public function getAttributeSetName() } /** + * Retrieve id of selected tab + * * @return string */ public function getSelectedTabId() { - return addslashes(htmlspecialchars($this->getRequest()->getParam('tab'))); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return addslashes($this->escaper->escapeHtml($this->getRequest()->getParam('tab'))); } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php index 9c7604d27eae0..b5bd224fe42a7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Adminhtml catalog product edit action attributes update tab block * @@ -16,8 +14,11 @@ use Magento\Framework\Data\Form\Element\AbstractElement; /** + * Attributes tab block + * * @api * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements @@ -33,6 +34,9 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements */ protected $_attributeAction; + /** @var array */ + private $excludeFields; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -40,6 +44,7 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction * @param array $data + * @param array $excludeFields */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -47,14 +52,19 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction, - array $data = [] + array $data = [], + array $excludeFields = [] ) { $this->_attributeAction = $attributeAction; $this->_productFactory = $productFactory; + $this->excludeFields = $excludeFields; + parent::__construct($context, $registry, $formFactory, $data); } /** + * Construct block + * * @return void */ protected function _construct() @@ -64,20 +74,14 @@ protected function _construct() } /** + * Prepares form + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _prepareForm() { - $this->setFormExcludedFieldList( - [ - 'category_ids', - 'gallery', - 'image', - 'media_gallery', - 'quantity_and_stock_status', - 'tier_price', - ] - ); + $this->setFormExcludedFieldList($this->getExcludedFields()); $this->_eventManager->dispatch( 'adminhtml_catalog_product_form_prepare_excluded_field_list', ['object' => $this] @@ -136,6 +140,7 @@ protected function _getAdditionalElementHtml($element) $dataAttribute = "data-disable='{$elementId}'"; $dataCheckboxName = "toggle_" . "{$elementId}"; $checkboxLabel = __('Change'); + // @codingStandardsIgnoreStart $html = <<<HTML <span class="attribute-change-checkbox"> <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml onclick="toogleFieldEditMode(this, '{$elementId}')" $dataAttribute /> @@ -150,11 +155,14 @@ protected function _getAdditionalElementHtml($element) weightHandle.hideWeightSwitcher(); });</script> HTML; + // @codingStandardsIgnoreEnd } return $html; } /** + * Returns tab label + * * @return \Magento\Framework\Phrase */ public function getTabLabel() @@ -163,6 +171,8 @@ public function getTabLabel() } /** + * Return Tab title + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -171,6 +181,8 @@ public function getTabTitle() } /** + * Can show tab in tabs + * * @return bool */ public function canShowTab() @@ -179,10 +191,22 @@ public function canShowTab() } /** + * Tab not hidden + * * @return bool */ public function isHidden() { return false; } + + /** + * Returns excluded fields + * + * @return array + */ + private function getExcludedFields(): array + { + return $this->excludeFields; + } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index 750bf6f8a0216..964872b6e51bd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php @@ -70,11 +70,11 @@ public function getFieldSuffix() * Retrieve current store id * * @return int + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreId() { - $storeId = $this->getRequest()->getParam('store'); - return intval($storeId); + return (int)$this->getRequest()->getParam('store'); } /** @@ -99,6 +99,8 @@ public function getTabLabel() } /** + * Return Tab title. + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -107,7 +109,7 @@ public function getTabTitle() } /** - * @return bool + * @inheritdoc */ public function canShowTab() { @@ -115,7 +117,7 @@ public function canShowTab() } /** - * @return bool + * @inheritdoc */ public function isHidden() { @@ -123,6 +125,8 @@ public function isHidden() } /** + * Get availability status. + * * @param string $fieldName * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php index 3467c7aac289b..d95ee7f8f2cf9 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; +/** + * Admin AttributeSet block + */ class AttributeSet extends \Magento\Backend\Block\Widget\Form { /** @@ -42,12 +45,14 @@ public function __construct( public function getSelectorOptions() { return [ - 'source' => $this->getUrl('catalog/product/suggestAttributeSets'), + 'source' => $this->escapeUrl($this->getUrl('catalog/product/suggestAttributeSets')), 'className' => 'category-select', 'showRecent' => true, 'storageKey' => 'product-template-key', 'minLength' => 0, - 'currentlySelected' => $this->_coreRegistry->registry('product')->getAttributeSetId() + 'currentlySelected' => $this->escapeHtml( + $this->_coreRegistry->registry('product')->getAttributeSetId() + ) ]; } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php index 4a3d7c4e14e30..0177a90fcb5a7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Alerts.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Product alerts tab * @@ -18,19 +16,26 @@ class Alerts extends \Magento\Backend\Block\Widget\Tab /** * @var string */ - protected $_template = 'catalog/product/tab/alert.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/tab/alert.phtml'; /** * @return $this */ protected function _prepareLayout() { - $accordion = $this->getLayout()->createBlock( - \Magento\Backend\Block\Widget\Accordion::class)->setId('productAlerts'); - /* @var $accordion \Magento\Backend\Block\Widget\Accordion */ + /** @var \Magento\Backend\Block\Widget\Accordion $accordion */ + $accordion = $this->getLayout() + ->createBlock(\Magento\Backend\Block\Widget\Accordion::class) + ->setId('productAlerts'); - $alertPriceAllow = $this->_scopeConfig->getValue('catalog/productalert/allow_price', \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - $alertStockAllow = $this->_scopeConfig->getValue('catalog/productalert/allow_stock', \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + $alertPriceAllow = $this->_scopeConfig->getValue( + 'catalog/productalert/allow_price', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $alertStockAllow = $this->_scopeConfig->getValue( + 'catalog/productalert/allow_stock', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); if ($alertPriceAllow) { $accordion->addItem( @@ -77,8 +82,15 @@ public function getAccordionHtml() */ public function canShowTab() { - $alertPriceAllow = $this->_scopeConfig->getValue('catalog/productalert/allow_price', \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - $alertStockAllow = $this->_scopeConfig->getValue('catalog/productalert/allow_stock', \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + $alertPriceAllow = $this->_scopeConfig->getValue( + 'catalog/productalert/allow_price', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $alertStockAllow = $this->_scopeConfig->getValue( + 'catalog/productalert/allow_stock', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + return ($alertPriceAllow || $alertStockAllow) && parent::canShowTab(); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php index d5f66231f1d82..78b519c4b0c3d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes; +/** + * Admin product attribute search block + */ class Search extends \Magento\Backend\Block\Widget { /** @@ -62,13 +65,15 @@ protected function _construct() } /** + * Get selector options + * * @return array */ public function getSelectorOptions() { $templateId = $this->_coreRegistry->registry('product')->getAttributeSetId(); return [ - 'source' => $this->getUrl('catalog/product/suggestAttributes'), + 'source' => $this->escapeUrl($this->getUrl('catalog/product/suggestAttributes')), 'minLength' => 0, 'ajaxOptions' => ['data' => ['template_id' => $templateId]], 'template' => '[data-template-for="product-attribute-search-' . $this->getGroupId() . '"]', @@ -81,7 +86,8 @@ public function getSelectorOptions() * * @param string $labelPart * @param int $templateId - * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection + * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSuggestedAttributes($labelPart, $templateId = null) { @@ -95,7 +101,9 @@ public function getSuggestedAttributes($labelPart, $templateId = null) ['like' => $escapedLabelPart] ); - $collection->setExcludeSetFilter($templateId ?: $this->getRequest()->getParam('template_id'))->setPageSize(20); + $paramTemplateId = $this->getRequest()->getParam('template_id'); + $paramTemplateId = is_int($paramTemplateId) ? $paramTemplateId : null; + $collection->setExcludeSetFilter($templateId ?: $paramTemplateId)->setPageSize(20); $result = []; foreach ($collection->getItems() as $attribute) { @@ -110,6 +118,8 @@ public function getSuggestedAttributes($labelPart, $templateId = null) } /** + * Get add attribute url + * * @return string */ public function getAddAttributeUrl() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php index e52c1d3aa4985..20e12889cae0d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Inventory.php @@ -15,7 +15,7 @@ class Inventory extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'catalog/product/tab/inventory.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/tab/inventory.phtml'; /** * @var \Magento\Framework\Module\Manager diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php index a7a919e53f56e..2c79f5a6fa718 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options.php @@ -16,7 +16,7 @@ class Options extends Widget /** * @var string */ - protected $_template = 'catalog/product/edit/options.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options.phtml'; /** * @return Widget diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php index 64856a5c69dc7..c0160c076aeec 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Customers defined options */ @@ -38,7 +36,7 @@ class Option extends Widget /** * @var string */ - protected $_template = 'catalog/product/edit/options/option.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options/option.phtml'; /** * Core registry @@ -313,9 +311,9 @@ public function getOptionValues() $value['checkboxScopeTitle'] = $this->getCheckboxScopeHtml( $option->getOptionId(), 'title', - is_null($option->getStoreTitle()) + $option->getStoreTitle() === null ); - $value['scopeTitleDisabled'] = is_null($option->getStoreTitle()) ? 'disabled' : null; + $value['scopeTitleDisabled'] = $option->getStoreTitle() === null ? 'disabled' : null; } if ($option->getGroupByType() == ProductCustomOptionInterface::OPTION_GROUP_SELECT) { @@ -341,22 +339,22 @@ public function getOptionValues() $value['optionValues'][$i]['checkboxScopeTitle'] = $this->getCheckboxScopeHtml( $_value->getOptionId(), 'title', - is_null($_value->getStoreTitle()), + $_value->getStoreTitle() === null, $_value->getOptionTypeId() ); - $value['optionValues'][$i]['scopeTitleDisabled'] = is_null( - $_value->getStoreTitle() + $value['optionValues'][$i]['scopeTitleDisabled'] = ( + $_value->getStoreTitle() === null ) ? 'disabled' : null; if ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) { $value['optionValues'][$i]['checkboxScopePrice'] = $this->getCheckboxScopeHtml( $_value->getOptionId(), 'price', - is_null($_value->getstorePrice()), + $_value->getstorePrice() === null, $_value->getOptionTypeId(), ['$(this).up(1).previous()'] ); - $value['optionValues'][$i]['scopePriceDisabled'] = is_null( - $_value->getStorePrice() + $value['optionValues'][$i]['scopePriceDisabled'] = ( + $_value->getStorePrice() === null ) ? 'disabled' : null; } } @@ -379,9 +377,9 @@ public function getOptionValues() $value['checkboxScopePrice'] = $this->getCheckboxScopeHtml( $option->getOptionId(), 'price', - is_null($option->getStorePrice()) + $option->getStorePrice() === null ); - $value['scopePriceDisabled'] = is_null($option->getStorePrice()) ? 'disabled' : null; + $value['scopePriceDisabled'] = $option->getStorePrice() === null ? 'disabled' : null; } } $values[] = new \Magento\Framework\DataObject($value); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php index babfc1b072bd2..a0bbc4ad033de 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Date.php @@ -16,5 +16,5 @@ class Date extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ /** * @var string */ - protected $_template = 'catalog/product/edit/options/type/date.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/date.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php index 322aa02f97731..d3d5f08fa9eae 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/File.php @@ -16,5 +16,5 @@ class File extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ /** * @var string */ - protected $_template = 'catalog/product/edit/options/type/file.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/file.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php index 24de84958ef4a..f6ab5134ae6bd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Select.php @@ -16,7 +16,7 @@ class Select extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\T /** * @var string */ - protected $_template = 'catalog/product/edit/options/type/select.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/select.phtml'; /** * Class constructor diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php index 7241128fac3b4..e6f78dc3ed169 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Type/Text.php @@ -16,5 +16,5 @@ class Text extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Typ /** * @var string */ - protected $_template = 'catalog/product/edit/options/type/text.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/options/type/text.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php index a80ddd8c122a1..7cb1c2c9e4263 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php @@ -13,7 +13,7 @@ class Tier extends Group\AbstractGroup /** * @var string */ - protected $_template = 'catalog/product/edit/price/tier.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/price/tier.phtml'; /** * Retrieve list of initial customer groups diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php index 6a3347b44512f..6189a97dbe761 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Websites.php @@ -21,7 +21,7 @@ class Websites extends \Magento\Backend\Block\Store\Switcher /** * @var string */ - protected $_template = 'catalog/product/edit/websites.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/edit/websites.phtml'; /** * Core registry diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php index d95065cdcd108..1a5b67fe12482 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; use Magento\Catalog\Model\ResourceModel\Category\Collection; @@ -125,9 +123,8 @@ public function getAfterElementHtml() $selectorOptions = $this->_jsonEncoder->encode($this->_getSelectorOptions()); $newCategoryCaption = __('New Category'); - $button = $this->_layout->createBlock( - \Magento\Backend\Block\Widget\Button::class - )->setData( + $button = $this->_layout->createBlock(\Magento\Backend\Block\Widget\Button::class) + ->setData( [ 'id' => 'add_category_button', 'label' => $newCategoryCaption, diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php index 49a75990ec0d2..ad3da81368166 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Catalog product gallery attribute * @@ -13,6 +11,8 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Registry; use Magento\Catalog\Model\Product; use Magento\Eav\Model\Entity\Attribute; @@ -68,6 +68,11 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock */ protected $registry; + /** + * @var DataPersistorInterface + */ + private $dataPersistor; + /** * @param \Magento\Framework\View\Element\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -80,11 +85,13 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, Registry $registry, \Magento\Framework\Data\Form $form, - $data = [] + $data = [], + DataPersistorInterface $dataPersistor = null ) { $this->storeManager = $storeManager; $this->registry = $registry; $this->form = $form; + $this->dataPersistor = $dataPersistor ?: ObjectManager::getInstance()->get(DataPersistorInterface::class); parent::__construct($context, $data); } @@ -104,7 +111,24 @@ public function getElementHtml() */ public function getImages() { - return $this->registry->registry('current_product')->getData('media_gallery') ?: null; + $images = $this->getDataObject()->getData('media_gallery') ?: null; + if ($images === null) { + $images = ((array)$this->dataPersistor->get('catalog_product'))['product']['media_gallery'] ?? null; + } + + return $images; + } + + /** + * Get value for given type. + * + * @param string $type + * @return string|null + */ + public function getImageValue(string $type) + { + $product = (array)$this->dataPersistor->get('catalog_product'); + return $product['product'][$type] ?? null; } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 5188bf365e5e9..42eb5341721e6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -17,13 +17,21 @@ use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Block\DataProviders\UploadConfig as ImageUploadConfigDataProvider; +use Magento\MediaStorage\Helper\File\Storage\Database; class Content extends \Magento\Backend\Block\Widget { + /** + * @var ImageUploadConfigDataProvider + */ + private $imageUploadConfigDataProvider; + /** * @var string */ - protected $_template = 'catalog/product/helper/gallery.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/helper/gallery.phtml'; /** * @var \Magento\Catalog\Model\Product\Media\Config @@ -40,21 +48,34 @@ class Content extends \Magento\Backend\Block\Widget */ private $imageHelper; + /** + * @var Database + */ + private $fileStorageDatabase; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param array $data + * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider + * @param Database $fileStorageDatabase */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, - array $data = [] + array $data = [], + ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null, + Database $fileStorageDatabase = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; parent::__construct($context, $data); + $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider + ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); + $this->fileStorageDatabase = $fileStorageDatabase + ?: ObjectManager::getInstance()->get(Database::class); } /** @@ -62,7 +83,11 @@ public function __construct( */ protected function _prepareLayout() { - $this->addChild('uploader', \Magento\Backend\Block\Media\Uploader::class); + $this->addChild( + 'uploader', + \Magento\Backend\Block\Media\Uploader::class, + ['image_upload_config_data' => $this->imageUploadConfigDataProvider] + ); $this->getUploader()->getConfig()->setUrl( $this->_urlBuilder->addSessionParam()->getUrl('catalog/product_gallery/upload') @@ -138,6 +163,13 @@ public function getImagesJson() $images = $this->sortImagesByPosition($value['images']); foreach ($images as &$image) { $image['url'] = $this->_mediaConfig->getMediaUrl($image['file']); + if ($this->fileStorageDatabase->checkDbUsage() && + !$mediaDir->isFile($this->_mediaConfig->getMediaPath($image['file'])) + ) { + $this->fileStorageDatabase->saveFileToFilesystem( + $this->_mediaConfig->getMediaPath($image['file']) + ); + } try { $fileHandler = $mediaDir->stat($this->_mediaConfig->getMediaPath($image['file'])); $image['size'] = $fileHandler['size']; @@ -193,9 +225,11 @@ public function getImageTypes() $imageTypes = []; foreach ($this->getMediaAttributes() as $attribute) { /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ + $value = $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()) + ?: $this->getElement()->getImageValue($attribute->getAttributeCode()); $imageTypes[$attribute->getAttributeCode()] = [ 'code' => $attribute->getAttributeCode(), - 'value' => $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()), + 'value' => $value, 'label' => $attribute->getFrontend()->getLabel(), 'scope' => __($this->getElement()->getScopeLabel($attribute)), 'name' => $this->getElement()->getAttributeFieldName($attribute), diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php index 19c1574d6e9a5..b8967f1f30e55 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Widget/Chooser/Container.php @@ -18,5 +18,5 @@ class Container extends Template /** * @var string */ - protected $_template = 'catalog/product/widget/chooser/container.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/widget/chooser/container.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php index dbeff93683bc0..a9ec80c5f0232 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php @@ -13,7 +13,7 @@ class Link extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rss/grid/link.phtml'; + protected $_template = 'Magento_Catalog::rss/grid/link.phtml'; /** * @var \Magento\Framework\App\Rss\UrlBuilderInterface @@ -69,7 +69,7 @@ public function isRssAllowed() } /** - * @return string + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Catalog/Block/Breadcrumbs.php b/app/code/Magento/Catalog/Block/Breadcrumbs.php index 8e5604d75bba2..674c99001b01a 100644 --- a/app/code/Magento/Catalog/Block/Breadcrumbs.php +++ b/app/code/Magento/Catalog/Block/Breadcrumbs.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Catalog breadcrumbs */ @@ -43,7 +41,11 @@ public function __construct(Context $context, Data $catalogData, array $data = [ */ public function getTitleSeparator($store = null) { - $separator = (string)$this->_scopeConfig->getValue('catalog/seo/title_separator', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + $separator = (string)$this->_scopeConfig->getValue( + 'catalog/seo/title_separator', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); return ' ' . $separator . ' '; } diff --git a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php index 03932151358ab..99399110505b7 100644 --- a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php +++ b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php @@ -71,7 +71,7 @@ public function afterGetCacheKey(PriceBox $subject, $result) '-', [ $result, - $this->priceCurrency->getCurrencySymbol(), + $this->priceCurrency->getCurrency()->getCode(), $this->dateTime->scopeDate($this->scopeResolver->getScope()->getId())->format('Ymd'), $this->scopeResolver->getScope()->getId(), $this->customerSession->getCustomerGroupId(), diff --git a/app/code/Magento/Catalog/Block/Category/Rss/Link.php b/app/code/Magento/Catalog/Block/Category/Rss/Link.php index 0599d5f4b989c..e40b81200574c 100644 --- a/app/code/Magento/Catalog/Block/Category/Rss/Link.php +++ b/app/code/Magento/Catalog/Block/Category/Rss/Link.php @@ -62,7 +62,7 @@ public function getLabel() } /** - * @return string + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Catalog/Block/Navigation.php b/app/code/Magento/Catalog/Block/Navigation.php index 52a142fc1a6b6..2ff70a4cea8a7 100644 --- a/app/code/Magento/Catalog/Block/Navigation.php +++ b/app/code/Magento/Catalog/Block/Navigation.php @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Block; use Magento\Catalog\Model\Category; use Magento\Customer\Model\Context; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\View\Element\Template; /** * Catalog navigation @@ -19,7 +19,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Navigation extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface +class Navigation extends Template implements IdentityInterface { /** * @var Category diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php index d4af775ad20da..58800802cc60d 100644 --- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php +++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php @@ -195,7 +195,7 @@ public function getAddToCompareUrl() * Gets minimal sales quantity * * @param \Magento\Catalog\Model\Product $product - * @return int|null + * @return float|null */ public function getMinimalQty($product) { diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index 05699f0cce5ab..f6a3ef8712192 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -214,6 +214,23 @@ public function getProductAttributeValue($product, $attribute) return (string)$value == '' ? __('No') : $value; } + /** + * Check if any of the products has a value set for the attribute. + * + * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute + * @return bool + */ + public function hasAttributeValueForProducts(\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute) : bool + { + foreach ($this->getItems() as $item) { + if ($item->hasData($attribute->getAttributeCode())) { + return true; + } + } + + return false; + } + /** * Retrieve Print URL * diff --git a/app/code/Magento/Catalog/Block/Product/Gallery.php b/app/code/Magento/Catalog/Block/Product/Gallery.php index e7c7b81ec29c9..e25a042d8ff4b 100644 --- a/app/code/Magento/Catalog/Block/Product/Gallery.php +++ b/app/code/Magento/Catalog/Block/Product/Gallery.php @@ -16,6 +16,8 @@ use Magento\Framework\Data\Collection; /** + * Product gallery block + * * @api * @since 100.0.2 */ @@ -43,6 +45,8 @@ public function __construct( } /** + * Prepare layout + * * @return $this */ protected function _prepareLayout() @@ -52,6 +56,8 @@ protected function _prepareLayout() } /** + * Get product + * * @return Product */ public function getProduct() @@ -60,6 +66,8 @@ public function getProduct() } /** + * Get gallery collection + * * @return Collection */ public function getGalleryCollection() @@ -68,13 +76,16 @@ public function getGalleryCollection() } /** + * Get current image + * * @return Image|null + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getCurrentImage() { $imageId = $this->getRequest()->getParam('image'); $image = null; - if ($imageId) { + if (is_int($imageId)) { $image = $this->getGalleryCollection()->getItemById($imageId); } @@ -85,6 +96,8 @@ public function getCurrentImage() } /** + * Get image url + * * @return string */ public function getImageUrl() @@ -93,6 +106,8 @@ public function getImageUrl() } /** + * Get image file + * * @return mixed */ public function getImageFile() @@ -115,7 +130,7 @@ public function getImageWidth() if ($size[0] > 600) { return 600; } else { - return $size[0]; + return (int) $size[0]; } } } @@ -124,6 +139,8 @@ public function getImageWidth() } /** + * Get previous image + * * @return Image|false */ public function getPreviousImage() @@ -143,6 +160,8 @@ public function getPreviousImage() } /** + * Get next image + * * @return Image|false */ public function getNextImage() @@ -166,6 +185,8 @@ public function getNextImage() } /** + * Get previous image url + * * @return false|string */ public function getPreviousImageUrl() @@ -178,6 +199,8 @@ public function getPreviousImageUrl() } /** + * Get next image url + * * @return false|string */ public function getNextImageUrl() diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php index a1fcdf43f6eb0..3ce97bd53f8d7 100644 --- a/app/code/Magento/Catalog/Block/Product/Image.php +++ b/app/code/Magento/Catalog/Block/Product/Image.php @@ -20,16 +20,19 @@ class Image extends \Magento\Framework\View\Element\Template { /** + * @deprecated Property isn't used * @var \Magento\Catalog\Helper\Image */ protected $imageHelper; /** + * @deprecated Property isn't used * @var \Magento\Catalog\Model\Product */ protected $product; /** + * @deprecated Property isn't used * @var array */ protected $attributes = []; diff --git a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php index d47218db37142..f1149f15c41d3 100644 --- a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php +++ b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Block\Product; use Magento\Catalog\Helper\ImageFactory as HelperFactory; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException; class ImageBuilder { @@ -20,7 +22,7 @@ class ImageBuilder protected $helperFactory; /** - * @var \Magento\Catalog\Model\Product + * @var Product */ protected $product; @@ -49,10 +51,10 @@ public function __construct( /** * Set product * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return $this */ - public function setProduct(\Magento\Catalog\Model\Product $product) + public function setProduct(Product $product) { $this->product = $product; return $this; @@ -78,9 +80,7 @@ public function setImageId($imageId) */ public function setAttributes(array $attributes) { - if ($attributes) { - $this->attributes = $attributes; - } + $this->attributes = $attributes; return $this; } @@ -129,7 +129,11 @@ public function create() ? 'Magento_Catalog::product/image.phtml' : 'Magento_Catalog::product/image_with_borders.phtml'; - $imagesize = $helper->getResizedImageInfo(); + try { + $imagesize = $helper->getResizedImageInfo(); + } catch (NotLoadInfoImageException $exception) { + $imagesize = [$helper->getWidth(), $helper->getHeight()]; + } $data = [ 'data' => [ @@ -140,8 +144,9 @@ public function create() 'label' => $helper->getLabel(), 'ratio' => $this->getRatio($helper), 'custom_attributes' => $this->getCustomAttributes(), - 'resized_image_width' => !empty($imagesize[0]) ? $imagesize[0] : $helper->getWidth(), - 'resized_image_height' => !empty($imagesize[1]) ? $imagesize[1] : $helper->getHeight(), + 'resized_image_width' => $imagesize[0], + 'resized_image_height' => $imagesize[1], + 'product_id' => $this->product->getId() ], ]; diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 7289aa85ea016..bdc99a3bc9524 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -9,11 +9,20 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Block\Product\ProductList\Toolbar; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Layer; +use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Config\Element; +use Magento\Framework\Data\Helper\PostHelper; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Pricing\Render; +use Magento\Framework\Url\Helper\Data; /** * Product list @@ -40,17 +49,17 @@ class ListProduct extends AbstractProduct implements IdentityInterface /** * Catalog layer * - * @var \Magento\Catalog\Model\Layer + * @var Layer */ protected $_catalogLayer; /** - * @var \Magento\Framework\Data\Helper\PostHelper + * @var PostHelper */ protected $_postDataHelper; /** - * @var \Magento\Framework\Url\Helper\Data + * @var Data */ protected $urlHelper; @@ -61,18 +70,18 @@ class ListProduct extends AbstractProduct implements IdentityInterface /** * @param Context $context - * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper - * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver + * @param PostHelper $postDataHelper + * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository - * @param \Magento\Framework\Url\Helper\Data $urlHelper + * @param Data $urlHelper * @param array $data */ public function __construct( - \Magento\Catalog\Block\Product\Context $context, - \Magento\Framework\Data\Helper\PostHelper $postDataHelper, - \Magento\Catalog\Model\Layer\Resolver $layerResolver, + Context $context, + PostHelper $postDataHelper, + Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository, - \Magento\Framework\Url\Helper\Data $urlHelper, + Data $urlHelper, array $data = [] ) { $this->_catalogLayer = $layerResolver->get(); @@ -113,7 +122,7 @@ protected function _getProductCollection() /** * Get catalog layer model * - * @return \Magento\Catalog\Model\Layer + * @return Layer */ public function getLayer() { @@ -137,7 +146,35 @@ public function getLoadedProductCollection() */ public function getMode() { - return $this->getChildBlock('toolbar')->getCurrentMode(); + if ($this->getChildBlock('toolbar')) { + return $this->getChildBlock('toolbar')->getCurrentMode(); + } + + return $this->getDefaultListingMode(); + } + + /** + * Get listing mode for products if toolbar is removed from layout. + * Use the general configuration for product list mode from config path catalog/frontend/list_mode as default value + * or mode data from block declaration from layout. + * + * @return string + */ + private function getDefaultListingMode() + { + // default Toolbar when the toolbar layout is not used + $defaultToolbar = $this->getToolbarBlock(); + $availableModes = $defaultToolbar->getModes(); + + // layout config mode + $mode = $this->getData('mode'); + + if (!$mode || !isset($availableModes[$mode])) { + // default config mode + $mode = $defaultToolbar->getCurrentMode(); + } + + return $mode; } /** @@ -148,28 +185,60 @@ public function getMode() protected function _beforeToHtml() { $collection = $this->_getProductCollection(); - $this->configureToolbar($this->getToolbarBlock(), $collection); + + $this->addToolbarBlock($collection); + $collection->load(); return parent::_beforeToHtml(); } /** - * Retrieve Toolbar block + * Add toolbar block from product listing layout + * + * @param Collection $collection + */ + private function addToolbarBlock(Collection $collection) + { + $toolbarLayout = $this->getToolbarFromLayout(); + + if ($toolbarLayout) { + $this->configureToolbar($toolbarLayout, $collection); + } + } + + /** + * Retrieve Toolbar block from layout or a default Toolbar * * @return Toolbar */ public function getToolbarBlock() + { + $block = $this->getToolbarFromLayout(); + + if (!$block) { + $block = $this->getLayout()->createBlock($this->_defaultToolbarBlock, uniqid(microtime())); + } + + return $block; + } + + /** + * Get toolbar block from layout + * + * @return bool|Toolbar + */ + private function getToolbarFromLayout() { $blockName = $this->getToolbarBlockName(); + + $toolbarLayout = false; + if ($blockName) { - $block = $this->getLayout()->getBlock($blockName); - if ($block) { - return $block; - } + $toolbarLayout = $this->getLayout()->getBlock($blockName); } - $block = $this->getLayout()->createBlock($this->_defaultToolbarBlock, uniqid(microtime())); - return $block; + + return $toolbarLayout; } /** @@ -203,7 +272,7 @@ public function setCollection($collection) } /** - * @param array|string|integer|\Magento\Framework\App\Config\Element $code + * @param array|string|integer| Element $code * @return $this */ public function addAttribute($code) @@ -223,7 +292,7 @@ public function getPriceBlockTemplate() /** * Retrieve Catalog Config object * - * @return \Magento\Catalog\Model\Config + * @return Config */ protected function _getConfig() { @@ -233,8 +302,8 @@ protected function _getConfig() /** * Prepare Sort By fields from Category Data * - * @param \Magento\Catalog\Model\Category $category - * @return \Magento\Catalog\Block\Product\ListProduct + * @param Category $category + * @return $this */ public function prepareSortableFieldsByCategory($category) { @@ -265,51 +334,59 @@ public function prepareSortableFieldsByCategory($category) public function getIdentities() { $identities = []; - foreach ($this->_getProductCollection() as $item) { - $identities = array_merge($identities, $item->getIdentities()); - } + $category = $this->getLayer()->getCurrentCategory(); if ($category) { $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId(); } + + //Check if category page shows only static block (No products) + if ($category->getData('display_mode') == Category::DM_PAGE) { + return $identities; + } + + foreach ($this->_getProductCollection() as $item) { + $identities = array_merge($identities, $item->getIdentities()); + } + return $identities; } /** * Get post parameters * - * @param \Magento\Catalog\Model\Product $product - * @return string + * @param Product $product + * @return array */ - public function getAddToCartPostParams(\Magento\Catalog\Model\Product $product) + public function getAddToCartPostParams(Product $product) { - $url = $this->getAddToCartUrl($product); + $url = $this->getAddToCartUrl($product, ['_escape' => false]); return [ 'action' => $url, 'data' => [ - 'product' => $product->getEntityId(), - \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlHelper->getEncodedUrl($url), + 'product' => (int) $product->getEntityId(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlHelper->getEncodedUrl($url), ] ]; } /** - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string */ - public function getProductPrice(\Magento\Catalog\Model\Product $product) + public function getProductPrice(Product $product) { $priceRender = $this->getPriceRender(); $price = ''; if ($priceRender) { $price = $priceRender->render( - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + FinalPrice::PRICE_CODE, $product, [ 'include_container' => true, 'display_minimal_price' => true, - 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + 'zone' => Render::ZONE_ITEM_LIST, 'list_category_page' => true ] ); @@ -322,7 +399,7 @@ public function getProductPrice(\Magento\Catalog\Model\Product $product) * Specifies that price rendering should be done for the list of products * i.e. rendering happens in the scope of product list, but not single product * - * @return \Magento\Framework\Pricing\Render + * @return Render */ protected function getPriceRender() { @@ -348,7 +425,7 @@ protected function getPriceRender() private function initializeProductCollection() { $layer = $this->getLayer(); - /* @var $layer \Magento\Catalog\Model\Layer */ + /* @var $layer Layer */ if ($this->getShowRootCategory()) { $this->setCategoryId($this->_storeManager->getStore()->getRootCategoryId()); } @@ -386,9 +463,8 @@ private function initializeProductCollection() if ($origCategory) { $layer->setCurrentCategory($origCategory); } - - $toolbar = $this->getToolbarBlock(); - $this->configureToolbar($toolbar, $collection); + + $this->addToolbarBlock($collection); $this->_eventManager->dispatch( 'catalog_block_product_list_collection', diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php index 0c547f81c85d6..043704a9f2bca 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php @@ -25,7 +25,7 @@ class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getCrossSellProductCollection()->addAttributeToSelect( diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 95d9b1ae61208..f7fe2000de23e 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Block\Product\ProductList; +use Magento\Catalog\Block\Product\AbstractProduct; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\DataObject\IdentityInterface; /** * Catalog product related items block @@ -18,7 +17,7 @@ * @SuppressWarnings(PHPMD.LongVariable) * @since 100.0.2 */ -class Related extends \Magento\Catalog\Block\Product\AbstractProduct implements \Magento\Framework\DataObject\IdentityInterface +class Related extends AbstractProduct implements IdentityInterface { /** * @var Collection @@ -82,7 +81,7 @@ public function __construct( */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getRelatedProductCollection()->addAttributeToSelect( @@ -121,7 +120,7 @@ public function getItems() * getIdentities() depends on _itemCollection populated, but it can be empty if the block is hidden * @see https://github.com/magento/magento2/issues/5897 */ - if (is_null($this->_itemCollection)) { + if ($this->_itemCollection === null) { $this->_prepareData(); } return $this->_itemCollection; diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php index f461b52515253..30811150af143 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php @@ -7,6 +7,10 @@ use Magento\Catalog\Helper\Product\ProductList; use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Http\Context; +use Magento\Framework\Data\Form\FormKey; /** * Product list toolbar @@ -77,13 +81,14 @@ class Toolbar extends \Magento\Framework\View\Element\Template /** * @var bool $_paramsMemorizeAllowed + * @deprecated */ protected $_paramsMemorizeAllowed = true; /** * @var string */ - protected $_template = 'product/list/toolbar.phtml'; + protected $_template = 'Magento_Catalog::product/list/toolbar.phtml'; /** * Catalog config @@ -96,6 +101,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template * Catalog session * * @var \Magento\Catalog\Model\Session + * @deprecated */ protected $_catalogSession; @@ -104,6 +110,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_toolbarModel; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * @var ProductList */ @@ -119,6 +130,16 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_postDataHelper; + /** + * @var Context + */ + private $httpContext; + + /** + * @var FormKey + */ + private $formKey; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Model\Session $catalogSession @@ -128,6 +149,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template * @param ProductList $productListHelper * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper * @param array $data + * @param ToolbarMemorizer|null $toolbarMemorizer + * @param Context|null $httpContext + * @param FormKey|null $formKey + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -137,7 +163,10 @@ public function __construct( \Magento\Framework\Url\EncoderInterface $urlEncoder, ProductList $productListHelper, \Magento\Framework\Data\Helper\PostHelper $postDataHelper, - array $data = [] + array $data = [], + ToolbarMemorizer $toolbarMemorizer = null, + Context $httpContext = null, + FormKey $formKey = null ) { $this->_catalogSession = $catalogSession; $this->_catalogConfig = $catalogConfig; @@ -145,6 +174,15 @@ public function __construct( $this->urlEncoder = $urlEncoder; $this->_productListHelper = $productListHelper; $this->_postDataHelper = $postDataHelper; + $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get( + ToolbarMemorizer::class + ); + $this->httpContext = $httpContext ?: ObjectManager::getInstance()->get( + Context::class + ); + $this->formKey = $formKey ?: ObjectManager::getInstance()->get( + FormKey::class + ); parent::__construct($context, $data); } @@ -152,6 +190,7 @@ public function __construct( * Disable list state params memorizing * * @return $this + * @deprecated */ public function disableParamsMemorizing() { @@ -165,6 +204,7 @@ public function disableParamsMemorizing() * @param string $param parameter name * @param mixed $value parameter value * @return $this + * @deprecated */ protected function _memorizeParam($param, $value) { @@ -192,7 +232,14 @@ public function setCollection($collection) $this->_collection->setPageSize($limit); } if ($this->getCurrentOrder()) { - $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection()); + if ($this->getCurrentOrder() == 'position') { + $this->_collection->addAttributeToSort( + $this->getCurrentOrder(), + $this->getCurrentDirection() + ); + } else { + $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection()); + } } return $this; } @@ -237,13 +284,13 @@ public function getCurrentOrder() $defaultOrder = $keys[0]; } - $order = $this->_toolbarModel->getOrder(); + $order = $this->toolbarMemorizer->getOrder(); if (!$order || !isset($orders[$order])) { $order = $defaultOrder; } - if ($order != $defaultOrder) { - $this->_memorizeParam('sort_order', $order); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::ORDER_PARAM_NAME, $order, $defaultOrder); } $this->setData('_current_grid_order', $order); @@ -263,13 +310,13 @@ public function getCurrentDirection() } $directions = ['asc', 'desc']; - $dir = strtolower($this->_toolbarModel->getDirection()); + $dir = strtolower($this->toolbarMemorizer->getDirection()); if (!$dir || !in_array($dir, $directions)) { $dir = $this->_direction; } - if ($dir != $this->_direction) { - $this->_memorizeParam('sort_direction', $dir); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::DIRECTION_PARAM_NAME, $dir, $this->_direction); } $this->setData('_current_grid_direction', $dir); @@ -385,6 +432,8 @@ public function getPagerUrl($params = []) } /** + * Get pager encoded url. + * * @param array $params * @return string */ @@ -405,11 +454,15 @@ public function getCurrentMode() return $mode; } $defaultMode = $this->_productListHelper->getDefaultViewMode($this->getModes()); - $mode = $this->_toolbarModel->getMode(); + $mode = $this->toolbarMemorizer->getMode(); if (!$mode || !isset($this->_availableMode[$mode])) { $mode = $defaultMode; } + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::MODE_PARAM_NAME, $mode, $defaultMode); + } + $this->setData('_current_grid_mode', $mode); return $mode; } @@ -561,13 +614,13 @@ public function getLimit() $defaultLimit = $keys[0]; } - $limit = $this->_toolbarModel->getLimit(); + $limit = $this->toolbarMemorizer->getLimit(); if (!$limit || !isset($limits[$limit])) { $limit = $defaultLimit; } - if ($limit != $defaultLimit) { - $this->_memorizeParam('limit_page', $limit); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::LIMIT_PARAM_NAME, $limit, $defaultLimit); } $this->setData('_current_limit', $limit); @@ -575,6 +628,8 @@ public function getLimit() } /** + * Check if limit is current used in toolbar. + * * @param int $limit * @return bool */ @@ -584,6 +639,8 @@ public function isLimitCurrent($limit) } /** + * Pager number of items from which products started on current page. + * * @return int */ public function getFirstNum() @@ -593,6 +650,8 @@ public function getFirstNum() } /** + * Pager number of items products finished on current page. + * * @return int */ public function getLastNum() @@ -602,6 +661,8 @@ public function getLastNum() } /** + * Total number of products in current category. + * * @return int */ public function getTotalNum() @@ -610,6 +671,8 @@ public function getTotalNum() } /** + * Check if current page is the first. + * * @return bool */ public function isFirstPage() @@ -618,6 +681,8 @@ public function isFirstPage() } /** + * Return last page number. + * * @return int */ public function getLastPageNum() @@ -682,9 +747,11 @@ public function getWidgetOptionsJson(array $customOptions = []) 'limit' => ToolbarModel::LIMIT_PARAM_NAME, 'modeDefault' => $defaultMode, 'directionDefault' => $this->_direction ?: ProductList::DEFAULT_SORT_DIRECTION, - 'orderDefault' => $this->_productListHelper->getDefaultSortField(), + 'orderDefault' => $this->getOrderField(), 'limitDefault' => $this->_productListHelper->getDefaultLimitPerPageValue($defaultMode), 'url' => $this->getPagerUrl(), + 'formKey' => $this->formKey->getFormKey(), + 'post' => $this->toolbarMemorizer->isMemorizingAllowed() ? true : false, ]; $options = array_replace_recursive($options, $customOptions); return json_encode(['productListToolbarForm' => $options]); diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index f97d1a788dafb..b1b7755adeb10 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Block\Product\ProductList; +use Magento\Catalog\Block\Product\AbstractProduct; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\DataObject\IdentityInterface; /** * Catalog product upsell items block @@ -18,7 +17,7 @@ * @SuppressWarnings(PHPMD.LongVariable) * @since 100.0.2 */ -class Upsell extends \Magento\Catalog\Block\Product\AbstractProduct implements \Magento\Framework\DataObject\IdentityInterface +class Upsell extends AbstractProduct implements IdentityInterface { /** * @var int @@ -97,7 +96,7 @@ public function __construct( */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getUpSellProductCollection()->setPositionOrder()->addStoreFilter(); if ($this->moduleManager->isEnabled('Magento_Checkout')) { @@ -140,7 +139,7 @@ public function getItemCollection() * getIdentities() depends on _itemCollection populated, but it can be empty if the block is hidden * @see https://github.com/magento/magento2/issues/5897 */ - if (is_null($this->_itemCollection)) { + if ($this->_itemCollection === null) { $this->_prepareData(); } return $this->_itemCollection; @@ -151,7 +150,7 @@ public function getItemCollection() */ public function getItems() { - if (is_null($this->_items)) { + if ($this->_items === null) { $this->_items = $this->getItemCollection()->getItems(); } return $this->_items; @@ -171,8 +170,8 @@ public function getRowCount() */ public function setColumnCount($columns) { - if (intval($columns) > 0) { - $this->_columnCount = intval($columns); + if ((int)$columns > 0) { + $this->_columnCount = (int)$columns; } return $this; } @@ -214,8 +213,8 @@ public function getIterableItem() */ public function setItemLimit($type, $limit) { - if (intval($limit) > 0) { - $this->_itemLimits[$type] = intval($limit); + if ((int)$limit > 0) { + $this->_itemLimits[$type] = (int)$limit; } return $this; } diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index b3b5dea9602f6..2a364bcadf6ef 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -120,51 +120,6 @@ public function getWishlistOptions() return ['productType' => $this->getProduct()->getTypeId()]; } - /** - * Add meta information from product to head block - * - * @return \Magento\Catalog\Block\Product\View - */ - protected function _prepareLayout() - { - $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class); - $product = $this->getProduct(); - if (!$product) { - return parent::_prepareLayout(); - } - - $title = $product->getMetaTitle(); - if ($title) { - $this->pageConfig->getTitle()->set($title); - } - $keyword = $product->getMetaKeyword(); - $currentCategory = $this->_coreRegistry->registry('current_category'); - if ($keyword) { - $this->pageConfig->setKeywords($keyword); - } elseif ($currentCategory) { - $this->pageConfig->setKeywords($product->getName()); - } - $description = $product->getMetaDescription(); - if ($description) { - $this->pageConfig->setDescription($description); - } else { - $this->pageConfig->setDescription($this->string->substr($product->getDescription(), 0, 255)); - } - if ($this->_productHelper->canUseCanonicalTag()) { - $this->pageConfig->addRemotePageAsset( - $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]), - 'canonical', - ['attributes' => ['rel' => 'canonical']] - ); - } - - $pageMainTitle = $this->getLayout()->getBlock('page.main.title'); - if ($pageMainTitle) { - $pageMainTitle->setPageTitle($product->getName()); - } - return parent::_prepareLayout(); - } - /** * Retrieve current product model * @@ -214,8 +169,7 @@ public function getAddToCartUrl($product, $additional = []) } /** - * Get JSON encoded configuration array which can be used for JS dynamic - * price calculation depending on product options + * Get JSON encoded configuration which can be used for JS dynamic price calculation depending on product options * * @return string */ @@ -233,24 +187,25 @@ public function getJsonConfig() } $tierPrices = []; - $tierPricesList = $product->getPriceInfo()->getPrice('tier_price')->getTierPriceList(); + $priceInfo = $product->getPriceInfo(); + $tierPricesList = $priceInfo->getPrice('tier_price')->getTierPriceList(); foreach ($tierPricesList as $tierPrice) { - $tierPrices[] = $tierPrice['price']->getValue(); + $tierPrices[] = $tierPrice['price']->getValue() * 1; } $config = [ - 'productId' => $product->getId(), + 'productId' => (int)$product->getId(), 'priceFormat' => $this->_localeFormat->getPriceFormat(), 'prices' => [ 'oldPrice' => [ - 'amount' => $product->getPriceInfo()->getPrice('regular_price')->getAmount()->getValue(), + 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1, 'adjustments' => [] ], 'basePrice' => [ - 'amount' => $product->getPriceInfo()->getPrice('final_price')->getAmount()->getBaseAmount(), + 'amount' => $priceInfo->getPrice('final_price')->getAmount()->getBaseAmount() * 1, 'adjustments' => [] ], 'finalPrice' => [ - 'amount' => $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue(), + 'amount' => $priceInfo->getPrice('final_price')->getAmount()->getValue() * 1, 'adjustments' => [] ] ], @@ -299,6 +254,7 @@ public function hasRequiredOptions() * instantly. * * @return bool + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function isStartCustomization() { @@ -307,6 +263,7 @@ public function isStartCustomization() /** * Get default qty - either as preconfigured, or as 1. + * * Also restricts it by minimal qty. * * @param null|\Magento\Catalog\Model\Product $product @@ -368,10 +325,7 @@ public function getQuantityValidators() public function getIdentities() { $identities = $this->getProduct()->getIdentities(); - $category = $this->_coreRegistry->registry('current_category'); - if ($category) { - $identities[] = Category::CACHE_TAG . '_' . $category->getId(); - } + return $identities; } diff --git a/app/code/Magento/Catalog/Block/Product/View/Additional.php b/app/code/Magento/Catalog/Block/Product/View/Additional.php index 37c82c2bc84c8..66527985e41fd 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Additional.php +++ b/app/code/Magento/Catalog/Block/Product/View/Additional.php @@ -25,7 +25,7 @@ class Additional extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'product/view/additional.phtml'; + protected $_template = 'Magento_Catalog::product/view/additional.phtml'; /** * @return array diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index fbdda684343b5..69c8b78b017d2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -16,6 +16,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Attributes attributes block + * * @api * @since 100.0.2 */ @@ -56,6 +58,8 @@ public function __construct( } /** + * Returns a Product. + * * @return Product */ public function getProduct() @@ -67,6 +71,8 @@ public function getProduct() } /** + * Additional data. + * * $excludeAttr is optional array of attribute codes to * exclude them from additional data array * @@ -83,17 +89,15 @@ public function getAdditionalData(array $excludeAttr = []) if ($attribute->getIsVisibleOnFront() && !in_array($attribute->getAttributeCode(), $excludeAttr)) { $value = $attribute->getFrontend()->getValue($product); - if (!$product->hasData($attribute->getAttributeCode())) { - $value = __('N/A'); - } elseif ((string)$value == '') { - $value = __('No'); + if ($value instanceof Phrase) { + $value = (string)$value; } elseif ($attribute->getFrontendInput() == 'price' && is_string($value)) { $value = $this->priceCurrency->convertAndFormat($value); } - if ($value instanceof Phrase || (is_string($value) && strlen($value))) { + if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Block/Product/View/Gallery.php b/app/code/Magento/Catalog/Block/Product/View/Gallery.php index 44dd3b9f97cbf..17828b9d375d3 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Gallery.php +++ b/app/code/Magento/Catalog/Block/Product/View/Gallery.php @@ -116,7 +116,7 @@ public function getGalleryImagesJson() 'thumb' => $image->getData('small_image_url'), 'img' => $image->getData('medium_image_url'), 'full' => $image->getData('large_image_url'), - 'caption' => $image->getLabel(), + 'caption' => ($image->getLabel() ?: $this->getProduct()->getName()), 'position' => $image->getPosition(), 'isMain' => $this->isMainImage($image), 'type' => str_replace('external-', '', $image->getMediaType()), @@ -175,7 +175,7 @@ public function getImageAttribute($imageId, $attributeName, $default = null) { $attributes = $this->getConfigView()->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); - return isset($attributes[$attributeName]) ? $attributes[$attributeName] : $default; + return $attributes[$attributeName] ?? $default; } /** diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php new file mode 100644 index 0000000000000..7790785133ddf --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Block\Product\Context; +use Magento\Framework\Stdlib\ArrayUtils; + +class GalleryOptions extends AbstractView implements ArgumentInterface +{ + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Gallery + */ + private $gallery; + + /** + * @param Context $context + * @param ArrayUtils $arrayUtils + * @param Json $jsonSerializer + * @param Gallery $gallery + * @param array $data + */ + public function __construct( + Context $context, + ArrayUtils $arrayUtils, + Json $jsonSerializer, + Gallery $gallery, + array $data = [] + ) { + $this->gallery = $gallery; + $this->jsonSerializer = $jsonSerializer; + parent::__construct($context, $arrayUtils, $data); + } + + /** + * Retrieve gallery options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getOptionsJson() + { + $optionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/nav"))) { + $optionItems['nav'] = $this->getVar("gallery/nav") ? 'true' : 'false'; + } else { + $optionItems['nav'] = $this->escapeHtml($this->getVar("gallery/nav")); + } + + $optionItems['loop'] = $this->getVar("gallery/loop"); + $optionItems['keyboard'] = $this->getVar("gallery/keyboard"); + $optionItems['arrows'] = $this->getVar("gallery/arrows"); + $optionItems['allowfullscreen'] = $this->getVar("gallery/allowfullscreen"); + $optionItems['showCaption'] = $this->getVar("gallery/caption"); + $optionItems['width'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + $optionItems['thumbwidth'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + + if ($this->gallery->getImageAttribute('product_page_image_small', 'height') || + $this->gallery->getImageAttribute('product_page_image_small', 'width')) { + $optionItems['thumbheight'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + } + + if ($this->gallery->getImageAttribute('product_page_image_medium', 'height') || + $this->gallery->getImageAttribute('product_page_image_medium', 'width')) { + $optionItems['height'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + } + + if ($this->getVar("gallery/transition/duration")) { + $optionItems['transitionduration'] = + (int)$this->escapeHtml($this->getVar("gallery/transition/duration")); + } + + $optionItems['transition'] = $this->escapeHtml($this->getVar("gallery/transition/effect")); + $optionItems['navarrows'] = $this->getVar("gallery/navarrows"); + $optionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/navtype")); + $optionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/navdir")); + + if ($this->getVar("gallery/thumbmargin")) { + $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); + } + + return $this->jsonSerializer->serialize($optionItems); + } + + /** + * Retrieve gallery fullscreen options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getFSOptionsJson() + { + $fsOptionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/fullscreen/nav"))) { + $fsOptionItems['nav'] = $this->getVar("gallery/fullscreen/nav") ? 'true' : 'false'; + } else { + $fsOptionItems['nav'] = $this->escapeHtml($this->getVar("gallery/fullscreen/nav")); + } + + $fsOptionItems['loop'] = $this->getVar("gallery/fullscreen/loop"); + $fsOptionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navdir")); + $fsOptionItems['navarrows'] = $this->getVar("gallery/fullscreen/navarrows"); + $fsOptionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navtype")); + $fsOptionItems['arrows'] = $this->getVar("gallery/fullscreen/arrows"); + $fsOptionItems['showCaption'] = $this->getVar("gallery/fullscreen/caption"); + + if ($this->getVar("gallery/fullscreen/transition/duration")) { + $fsOptionItems['transitionduration'] = (int)$this->escapeHtml( + $this->getVar("gallery/fullscreen/transition/duration") + ); + } + + $fsOptionItems['transition'] = $this->escapeHtml($this->getVar("gallery/fullscreen/transition/effect")); + + if ($this->getVar("gallery/fullscreen/keyboard")) { + $fsOptionItems['keyboard'] = $this->getVar("gallery/fullscreen/keyboard"); + } + + if ($this->getVar("gallery/fullscreen/thumbmargin")) { + $fsOptionItems['thumbmargin'] = + (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); + } + + return $this->jsonSerializer->serialize($fsOptionItems); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php index 0720c018f6a9b..c457b20cd0904 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options.php @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Product options block - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Product\View; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option\Value; /** + * Product options block + * + * @author Magento Core Team <core@magentocommerce.com> * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -121,6 +120,8 @@ public function setProduct(Product $product = null) } /** + * Get group of option. + * * @param string $type * @return string */ @@ -142,6 +143,8 @@ public function getOptions() } /** + * Check if block has options. + * * @return bool */ public function hasOptions() @@ -160,7 +163,10 @@ public function hasOptions() */ protected function _getPriceConfiguration($option) { - $optionPrice = $this->pricingHelper->currency($option->getPrice(true), false, false); + $optionPrice = $option->getPrice(true); + if ($option->getPriceType() !== Value::TYPE_PERCENT) { + $optionPrice = $this->pricingHelper->currency($optionPrice, false, false); + } $data = [ 'prices' => [ 'oldPrice' => [ @@ -195,7 +201,7 @@ protected function _getPriceConfiguration($option) ], ], 'type' => $option->getPriceType(), - 'name' => $option->getTitle() + 'name' => $option->getTitle(), ]; return $data; } @@ -231,7 +237,7 @@ public function getJsonConfig() //pass the return array encapsulated in an object for the other modules to be able to alter it eg: weee $this->_eventManager->dispatch('catalog_product_option_price_configuration_after', ['configObj' => $configObj]); - $config=$configObj->getConfig(); + $config = $configObj->getConfig(); return $this->_jsonEncoder->encode($config); } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 66bf5eafb156e..181211a0fc4a2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -105,9 +105,11 @@ public function getOption() } /** + * Retrieve formatted price + * * @return string */ - public function getFormatedPrice() + public function getFormattedPrice() { if ($option = $this->getOption()) { return $this->_formatPrice( @@ -120,6 +122,17 @@ public function getFormatedPrice() return ''; } + /** + * @return string + * + * @deprecated + * @see getFormattedPrice() + */ + public function getFormatedPrice() + { + return $this->getFormattedPrice(); + } + /** * Return formated price * @@ -178,7 +191,7 @@ public function getPrice($price, $includingTax = null) * Returns price converted to current currency rate * * @param float $price - * @return float + * @return float|string */ public function getCurrencyPrice($price) { diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php index d546ef483132b..52de7939d7eb2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php @@ -3,8 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Block\Product\View\Options\Type; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Block\Product\View\Options\Type\Select\CheckableFactory; +use Magento\Catalog\Block\Product\View\Options\Type\Select\MultipleFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Catalog\Helper\Data as CatalogHelper; + /** * Product options text type block * @@ -13,169 +22,64 @@ */ class Select extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions { + /** + * @var CheckableFactory + */ + private $checkableFactory; + + /** + * @var MultipleFactory + */ + private $multipleFactory; + + /** + * Select constructor. + * @param Context $context + * @param Data $pricingHelper + * @param CatalogHelper $catalogData + * @param array $data + * @param CheckableFactory|null $checkableFactory + * @param MultipleFactory|null $multipleFactory + */ + public function __construct( + Context $context, + Data $pricingHelper, + CatalogHelper $catalogData, + array $data = [], + CheckableFactory $checkableFactory = null, + MultipleFactory $multipleFactory = null + ) { + parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->checkableFactory = $checkableFactory ?: ObjectManager::getInstance()->get(CheckableFactory::class); + $this->multipleFactory = $multipleFactory ?: ObjectManager::getInstance()->get(MultipleFactory::class); + } + /** * Return html for control element * * @return string - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getValuesHtml() { - $_option = $this->getOption(); - $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $_option->getId()); - $store = $this->getProduct()->getStore(); - - $this->setSkipJsReloadPrice(1); - // Remove inline prototype onclick and onchange events + $option = $this->getOption(); + $optionType = $option->getType(); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || + $optionType === ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE ) { - $require = $_option->getIsRequire() ? ' required' : ''; - $extraParams = ''; - $select = $this->getLayout()->createBlock( - \Magento\Framework\View\Element\Html\Select::class - )->setData( - [ - 'id' => 'select_' . $_option->getId(), - 'class' => $require . ' product-custom-option admin__control-select' - ] - ); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { - $select->setName('options[' . $_option->getid() . ']')->addOption('', __('-- Please Select --')); - } else { - $select->setName('options[' . $_option->getid() . '][]'); - $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); - } - foreach ($_option->getValues() as $_value) { - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ], - false - ); - $select->addOption( - $_value->getOptionTypeId(), - $_value->getTitle() . ' ' . strip_tags($priceStr) . '', - ['price' => $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false)] - ); - } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { - $extraParams = ' multiple="multiple"'; - } - if (!$this->getSkipJsReloadPrice()) { - $extraParams .= ' onchange="opConfig.reloadPrice()"'; - } - $extraParams .= ' data-selector="' . $select->getName() . '"'; - $select->setExtraParams($extraParams); - - if ($configValue) { - $select->setValue($configValue); - } - - return $select->getHtml(); + $optionBlock = $this->multipleFactory->create(); } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_RADIO || + $optionType === ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX ) { - $selectHtml = '<div class="options-list nested" id="options-' . $_option->getId() . '-list">'; - $require = $_option->getIsRequire() ? ' required' : ''; - $arraySign = ''; - switch ($_option->getType()) { - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: - $type = 'radio'; - $class = 'radio admin__control-radio'; - if (!$_option->getIsRequire()) { - $selectHtml .= '<div class="field choice admin__field admin__field-option">' . - '<input type="radio" id="options_' . - $_option->getId() . - '" class="' . - $class . - ' product-custom-option" name="options[' . - $_option->getId() . - ']"' . - ' data-selector="options[' . $_option->getId() . ']"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' value="" checked="checked" /><label class="label admin__field-label" for="options_' . - $_option->getId() . - '"><span>' . - __('None') . '</span></label></div>'; - } - break; - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: - $type = 'checkbox'; - $class = 'checkbox admin__control-checkbox'; - $arraySign = '[]'; - break; - } - $count = 1; - foreach ($_option->getValues() as $_value) { - $count++; - - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ] - ); - - $htmlValue = $_value->getOptionTypeId(); - if ($arraySign) { - $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; - } else { - $checked = $configValue == $htmlValue ? 'checked' : ''; - } - - $dataSelector = 'options[' . $_option->getId() . ']'; - if ($arraySign) { - $dataSelector .= '[' . $htmlValue . ']'; - } - - $selectHtml .= '<div class="field choice admin__field admin__field-option' . - $require . - '">' . - '<input type="' . - $type . - '" class="' . - $class . - ' ' . - $require . - ' product-custom-option"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' name="options[' . - $_option->getId() . - ']' . - $arraySign . - '" id="options_' . - $_option->getId() . - '_' . - $count . - '" value="' . - $htmlValue . - '" ' . - $checked . - ' data-selector="' . $dataSelector . '"' . - ' price="' . - $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . - '" />' . - '<label class="label admin__field-label" for="options_' . - $_option->getId() . - '_' . - $count . - '"><span>' . - $_value->getTitle() . - '</span> ' . - $priceStr . - '</label>'; - $selectHtml .= '</div>'; - } - $selectHtml .= '</div>'; - - return $selectHtml; + $optionBlock = $this->checkableFactory->create(); } + + return $optionBlock + ->setOption($option) + ->setProduct($this->getProduct()) + ->setSkipJsReloadPrice(1) + ->_toHtml(); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php new file mode 100644 index 0000000000000..2b000b1e5105d --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; + +/** + * Represent necessary logic for checkbox and radio button option type + */ +class Checkable extends AbstractOptions +{ + protected $_template = 'Magento_Catalog::product/composite/fieldset/options/view/checkable.phtml'; + + /** + * @param $value + * @return string + */ + public function formatPrice(ProductCustomOptionValuesInterface $value) : string + { + + return parent::_formatPrice( + [ + 'is_percent' => $value->getPriceType() === 'percent', + 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') + ] + ); + } + + /** + * @param $value + * @return float + */ + public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) : float + { + return $this->pricingHelper->currencyByStore( + $value->getPrice(true), + $this->getProduct()->getStore(), + false + ); + } + + /** + * @param $option + * @return string|array|null + */ + public function getPreconfiguredValue($option) + { + return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php new file mode 100644 index 0000000000000..20164b0622a6d --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\View\Element\Html\Select; + +/** + * Class represents necessary logic for dropdown and multiselect option types + */ +class Multiple extends AbstractOptions +{ + /** + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { + $option = $this->getOption(); + $optionType = $option->getType(); + $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $require = $option->getIsRequire() ? ' required' : ''; + $extraParams = ''; + /** @var Select $select */ + $select = $this->getLayout()->createBlock( + Select::class + )->setData( + [ + 'id' => 'select_' . $option->getId(), + 'class' => $require . ' product-custom-option admin__control-select' + ] + ); + + $select = $this->insertSelectOption($select, $option); + $select = $this->processSelectOption($select, $option); + + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { + $extraParams = ' multiple="multiple"'; + } + + if (!$this->getSkipJsReloadPrice()) { + $extraParams .= ' onchange="opConfig.reloadPrice()"'; + } + + $extraParams .= ' data-selector="' . $select->getName() . '"'; + $select->setExtraParams($extraParams); + + if ($configValue) { + $select->setValue($configValue); + } + + return $select->getHtml(); + } + + /** + * @param Select $select + * @param Option $option + * @return Select + */ + private function insertSelectOption(Select $select, Option $option) : Select + { + $require = $option->getIsRequire() ? ' required' : ''; + + if ($option->getType() === ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { + $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); + } else { + $select->setName('options[' . $option->getId() . '][]'); + $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); + } + + return $select; + } + + /** + * @param Select $select + * @param Option $option + * @return Select + */ + private function processSelectOption(Select $select, Option $option) : Select + { + $store = $this->getProduct()->getStore(); + + foreach ($option->getValues() as $_value) { + $isPercentPriceType = $_value->getPriceType() === 'percent'; + $priceStr = $this->_formatPrice( + [ + 'is_percent' => $isPercentPriceType, + 'pricing_value' => $_value->getPrice($isPercentPriceType) + ], + false + ); + + $select->addOption( + $_value->getOptionTypeId(), + $_value->getTitle() . ' ' . strip_tags($priceStr) . '', + [ + 'price' => $this->pricingHelper->currencyByStore( + $_value->getPrice(true), + $store, + false + ) + ] + ); + } + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Price.php b/app/code/Magento/Catalog/Block/Product/View/Price.php index c38625247b533..37598dfb1a8da 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Price.php +++ b/app/code/Magento/Catalog/Block/Product/View/Price.php @@ -9,6 +9,8 @@ */ namespace Magento\Catalog\Block\Product\View; +use Magento\Catalog\Model\Product; + class Price extends \Magento\Framework\View\Element\Template { /** @@ -37,7 +39,8 @@ public function __construct( */ public function getPrice() { + /** @var Product $product */ $product = $this->_coreRegistry->registry('product'); - return $product->getFormatedPrice(); + return $product->getFormattedPrice(); } } diff --git a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php index 704271b58f483..e880e0aba35cc 100644 --- a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php +++ b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php @@ -139,7 +139,7 @@ public function getCacheKeyInfo() [ $this->getDisplayType(), $this->getProductsPerPage(), - intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), + (int)$this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->serializer->serialize($this->getRequest()->getParams()) ] ); diff --git a/app/code/Magento/Catalog/Block/Rss/Product/Special.php b/app/code/Magento/Catalog/Block/Rss/Product/Special.php index c61bee4417cbc..a9107f14cc5e4 100644 --- a/app/code/Magento/Catalog/Block/Rss/Product/Special.php +++ b/app/code/Magento/Catalog/Block/Rss/Product/Special.php @@ -107,7 +107,7 @@ protected function _construct() } /** - * @return string + * @return array */ public function getRssData() { diff --git a/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php b/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php index 49f82562f33db..e44f9a89609bf 100644 --- a/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php +++ b/app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php @@ -5,44 +5,101 @@ */ namespace Magento\Catalog\Console\Command; -class ImagesResizeCommand extends \Symfony\Component\Console\Command\Command +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Image\CacheFactory; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage; +use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\Console\Cli; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\View\ConfigInterface as ViewConfig; +use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection; +use Magento\Catalog\Model\Product\Image; +use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory; +use Symfony\Component\Console\Helper\ProgressBar; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImagesResizeCommand extends Command { /** - * @var \Magento\Framework\App\State + * @var State */ protected $appState; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory + * @deprecated + * @var CollectionFactory */ protected $productCollectionFactory; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @deprecated + * @var ProductRepositoryInterface */ protected $productRepository; /** - * @var \Magento\Catalog\Model\Product\Image\CacheFactory + * @deprecated + * @var CacheFactory */ protected $imageCacheFactory; /** - * @param \Magento\Framework\App\State $appState - * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository - * @param \Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory + * @var ProductImage + */ + private $productImage; + + /** + * @var ViewConfig + */ + private $viewConfig; + + /** + * @var ThemeCollection + */ + private $themeCollection; + + /** + * @var ProductImageFactory + */ + private $productImageFactory; + + /** + * @param State $appState + * @param CollectionFactory $productCollectionFactory + * @param ProductRepositoryInterface $productRepository + * @param CacheFactory $imageCacheFactory + * @param ProductImage $productImage + * @param ViewConfig $viewConfig + * @param ThemeCollection $themeCollection + * @param ProductImageFactory $productImageFactory */ public function __construct( - \Magento\Framework\App\State $appState, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, - \Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory + State $appState, + CollectionFactory $productCollectionFactory, + ProductRepositoryInterface $productRepository, + CacheFactory $imageCacheFactory, + ProductImage $productImage = null, + ViewConfig $viewConfig = null, + ThemeCollection $themeCollection = null, + ProductImageFactory $productImageFactory = null ) { $this->appState = $appState; $this->productCollectionFactory = $productCollectionFactory; $this->productRepository = $productRepository; $this->imageCacheFactory = $imageCacheFactory; + $this->productImage = $productImage ?: ObjectManager::getInstance()->get(ProductImage::class); + $this->viewConfig = $viewConfig ?: ObjectManager::getInstance()->get(ViewConfig::class); + $this->themeCollection = $themeCollection ?: ObjectManager::getInstance()->get(ThemeCollection::class); + $this->productImageFactory = $productImageFactory + ?: ObjectManager::getInstance()->get(ProductImageFactory::class); parent::__construct(); } @@ -58,43 +115,124 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute( - \Symfony\Component\Console\Input\InputInterface $input, - \Symfony\Component\Console\Output\OutputInterface $output - ) { - $this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_GLOBAL); - - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ - $productCollection = $this->productCollectionFactory->create(); - $productIds = $productCollection->getAllIds(); - if (!count($productIds)) { - $output->writeln("<info>No product images to resize</info>"); - // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; - } + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->appState->setAreaCode(Area::AREA_GLOBAL); try { - foreach ($productIds as $productId) { - try { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->getById($productId); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - continue; - } + $count = $this->productImage->getCountAllProductImages(); + if (!$count) { + $output->writeln("<info>No product images to resize</info>"); + return Cli::RETURN_SUCCESS; + } + + $productImages = $this->productImage->getAllProductImages(); - /** @var \Magento\Catalog\Model\Product\Image\Cache $imageCache */ - $imageCache = $this->imageCacheFactory->create(); - $imageCache->generate($product); + $themes = $this->themeCollection->loadRegisteredThemes(); + $viewImages = $this->getViewImages($themes->getItems()); - $output->write("."); + $progress = new ProgressBar($output, $count); + $progress->setFormat( + "%current%/%max% [%bar%] %percent:3s%% %elapsed% %memory:6s% \t| <info>%message%</info>" + ); + + if ($output->getVerbosity() !== OutputInterface::VERBOSITY_NORMAL) { + $progress->setOverwrite(false); + } + + foreach ($productImages as $image) { + $originalImageName = $image['filepath']; + + foreach ($viewImages as $viewImage) { + $image = $this->makeImage($originalImageName, $viewImage); + $image->resize(); + $image->saveFile(); + } + $progress->setMessage($originalImageName); + $progress->advance(); } } catch (\Exception $e) { $output->writeln("<error>{$e->getMessage()}</error>"); // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + return Cli::RETURN_FAILURE; } $output->write("\n"); - $output->writeln("<info>Product images resized successfully</info>"); + $output->writeln("<info>Product images resized successfully.</info>"); + + return 0; + } + + /** + * Make image + * @param string $originalImagePath + * @param array $imageParams + * @return Image + */ + private function makeImage(string $originalImagePath, array $imageParams): Image + { + $image = $this->productImageFactory->create(); + + if (isset($imageParams['height'])) { + $image->setHeight($imageParams['height']); + } + if (isset($imageParams['width'])) { + $image->setWidth($imageParams['width']); + } + if (isset($imageParams['aspect_ratio'])) { + $image->setKeepAspectRatio($imageParams['aspect_ratio']); + } + if (isset($imageParams['frame'])) { + $image->setKeepFrame($imageParams['frame']); + } + if (isset($imageParams['transparency'])) { + $image->setKeepTransparency($imageParams['transparency']); + } + if (isset($imageParams['constrain'])) { + $image->setConstrainOnly($imageParams['constrain']); + } + if (isset($imageParams['background'])) { + $image->setBackgroundColor($imageParams['background']); + } + + $image->setDestinationSubdir($imageParams['type']); + $image->setBaseFile($originalImagePath); + + return $image; + } + + /** + * Get view images data from themes + * @param array $themes + * @return array + */ + private function getViewImages(array $themes): array + { + $viewImages = []; + foreach ($themes as $theme) { + $config = $this->viewConfig->getViewConfig([ + 'area' => Area::AREA_FRONTEND, + 'themeModel' => $theme, + ]); + $images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE); + foreach ($images as $imageId => $imageData) { + $uniqIndex = $this->getUniqueImageIndex($imageData); + $imageData['id'] = $imageId; + $viewImages[$uniqIndex] = $imageData; + } + } + return $viewImages; + } + + /** + * Get unique image index + * @param array $imageData + * @return string + */ + private function getUniqueImageIndex(array $imageData): string + { + ksort($imageData); + unset($imageData['type']); + return md5(json_encode($imageData)); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php index 13c4353e65204..c2b705223e9ac 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php @@ -3,12 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Store\Model\Store; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Stdlib\DateTime\Filter\Date; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Backend\App\Action; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Registry; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\Controller\Result\Json; +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\DataObject; + /** * Catalog category controller */ -abstract class Category extends \Magento\Backend\App\Action +abstract class Category extends Action { /** * Authorization level of a basic admin session @@ -18,41 +33,40 @@ abstract class Category extends \Magento\Backend\App\Action const ADMIN_RESOURCE = 'Magento_Catalog::categories'; /** - * @var \Magento\Framework\Stdlib\DateTime\Filter\Date + * @var Date */ protected $dateFilter; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Stdlib\DateTime\Filter\Date|null $dateFilter + * @param Context $context + * @param Date|null $dateFilter */ - public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter = null - ) { + public function __construct(Context $context, Date $dateFilter = null) + { $this->dateFilter = $dateFilter; parent::__construct($context); } /** * Initialize requested category and put it into registry. + * * Root category can be returned, if inappropriate store/category is specified * * @param bool $getRootInstead - * @return \Magento\Catalog\Model\Category|false + * @return CategoryModel|false */ - protected function _initCategory($getRootInstead = false) + protected function _initCategory(bool $getRootInstead = false) { $categoryId = $this->resolveCategoryId(); - $storeId = (int)$this->getRequest()->getParam('store'); - $category = $this->_objectManager->create(\Magento\Catalog\Model\Category::class); + $storeId = $this->resolveStoreId(); + $category = $this->_objectManager->create(CategoryModel::class); $category->setStoreId($storeId); if ($categoryId) { $category->load($categoryId); if ($storeId) { $rootId = $this->_objectManager->get( - \Magento\Store\Model\StoreManagerInterface::class + StoreManagerInterface::class )->getStore( $storeId )->getRootCategoryId(); @@ -67,9 +81,13 @@ protected function _initCategory($getRootInstead = false) } } - $this->_objectManager->get(\Magento\Framework\Registry::class)->register('category', $category); - $this->_objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); - $this->_objectManager->get(\Magento\Cms\Model\Wysiwyg\Config::class) + /** @var \Magento\Framework\Registry $registry */ + $registry = $this->_objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('category'); + $registry->unregister('current_category'); + $registry->register('category', $category); + $registry->register('current_category', $category); + $this->_objectManager->get(Config::class) ->setStoreId($this->getRequest()->getParam('store')); return $category; } @@ -79,31 +97,47 @@ protected function _initCategory($getRootInstead = false) * * @return int */ - private function resolveCategoryId() + private function resolveCategoryId(): int { $categoryId = (int)$this->getRequest()->getParam('id', false); return $categoryId ?: (int)$this->getRequest()->getParam('entity_id', false); } + /** + * Resolve store id + * + * Tries to take store id from store HTTP parameter + * + * @see Store + * + * @return int + */ + private function resolveStoreId(): int + { + $storeId = (int)$this->getRequest()->getParam('store', false); + + return $storeId ?: (int)$this->getRequest()->getParam('store_id', Store::DEFAULT_STORE_ID); + } + /** * Build response for ajax request * - * @param \Magento\Catalog\Model\Category $category - * @param \Magento\Backend\Model\View\Result\Page $resultPage + * @param CategoryModel $category + * @param Page $resultPage * - * @return \Magento\Framework\Controller\Result\Json + * @return Json * * @deprecated 101.0.0 */ - protected function ajaxRequestResponse($category, $resultPage) + protected function ajaxRequestResponse(CategoryModel $category, Page $resultPage): Json { // prepare breadcrumbs of selected category, if any $breadcrumbsPath = $category->getPath(); if (empty($breadcrumbsPath)) { // but if no category, and it is deleted - prepare breadcrumbs from path, saved in session $breadcrumbsPath = $this->_objectManager->get( - \Magento\Backend\Model\Auth\Session::class + Session::class )->getDeletedPath( true ); @@ -119,33 +153,36 @@ protected function ajaxRequestResponse($category, $resultPage) } } - $eventResponse = new \Magento\Framework\DataObject([ - 'content' => $resultPage->getLayout()->getUiComponent('category_form')->getFormHtml() - . $resultPage->getLayout()->getBlock('category.tree') - ->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'), - 'messages' => $resultPage->getLayout()->getMessagesBlock()->getGroupedHtml(), - 'toolbar' => $resultPage->getLayout()->getBlock('page.actions.toolbar')->toHtml() - ]); + $eventResponse = new DataObject( + [ + 'content' => $resultPage->getLayout()->getUiComponent('category_form')->getFormHtml() + . $resultPage->getLayout()->getBlock('category.tree') + ->getBreadcrumbsJavascript($breadcrumbsPath, 'editingCategoryBreadcrumbs'), + 'messages' => $resultPage->getLayout()->getMessagesBlock()->getGroupedHtml(), + 'toolbar' => $resultPage->getLayout()->getBlock('page.actions.toolbar')->toHtml() + ] + ); $this->_eventManager->dispatch( 'category_prepare_ajax_response', ['response' => $eventResponse, 'controller' => $this] ); - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->_objectManager->get(\Magento\Framework\Controller\Result\Json::class); + /** @var Json $resultJson */ + $resultJson = $this->_objectManager->get(Json::class); $resultJson->setHeader('Content-type', 'application/json', true); $resultJson->setData($eventResponse->getData()); + return $resultJson; } /** * Datetime data preprocessing * - * @param \Magento\Catalog\Model\Category $category + * @param CategoryModel $category * @param array $postData * * @return array */ - protected function dateTimePreprocessing($category, $postData) + protected function dateTimePreprocessing(CategoryModel $category, array $postData): array { $dateFieldFilters = []; $attributes = $category->getAttributes(); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php index b8865f2de8d1e..3969d8193817b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php @@ -37,9 +37,14 @@ public function __construct( * Get tree node (Ajax version) * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if ($this->getRequest()->getParam('expand_all')) { $this->_objectManager->get(\Magento\Backend\Model\Auth\Session::class)->setIsTreeWasExpanded(true); } else { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php index 8f570e35989cb..4f14fb58487c1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php @@ -29,9 +29,14 @@ public function __construct( * Delete category action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); @@ -44,12 +49,12 @@ public function execute() $this->_eventManager->dispatch('catalog_controller_category_delete', ['category' => $category]); $this->_auth->getAuthStorage()->setDeletedPath($category->getPath()); $this->categoryRepository->delete($category); - $this->messageManager->addSuccess(__('You deleted the category.')); + $this->messageManager->addSuccessMessage(__('You deleted the category.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return $resultRedirect->setPath('catalog/*/edit', ['_current' => true]); } catch (\Exception $e) { - $this->messageManager->addError(__('Something went wrong while trying to delete the category.')); + $this->messageManager->addErrorMessage(__('Something went wrong while trying to delete the category.')); return $resultRedirect->setPath('catalog/*/edit', ['_current' => true]); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php index 4cc0f2d89d179..e24b142411b83 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php @@ -54,14 +54,6 @@ public function execute() try { $result = $this->imageUploader->saveFileToTmpDir($imageId); - - $result['cookie'] = [ - 'name' => $this->_getSession()->getName(), - 'value' => $this->_getSession()->getSessionId(), - 'lifetime' => $this->_getSession()->getCookieLifetime(), - 'path' => $this->_getSession()->getCookiePath(), - 'domain' => $this->_getSession()->getCookieDomain(), - ]; } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php index df2c80eda141c..02ddb162aff3a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php @@ -26,7 +26,7 @@ class Move extends \Magento\Catalog\Controller\Adminhtml\Category /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory, + * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param \Psr\Log\LoggerInterface $logger */ public function __construct( @@ -45,16 +45,17 @@ public function __construct( * Move category action * * @return \Magento\Framework\Controller\Result\Raw + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { - /** - * New parent category identifier - */ + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + + /** New parent category identifier */ $parentNodeId = $this->getRequest()->getPost('pid', false); - /** - * Category id after which we have put our category - */ + /** Category id after which we have put our category */ $prevNodeId = $this->getRequest()->getPost('aid', false); /** @var $block \Magento\Framework\View\Element\Messages */ diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php index 962cd52d39338..9384397b67f93 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php @@ -38,7 +38,11 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData(['id' => $categoryId, 'path' => $category->getPath()]); + return $resultJson->setData([ + 'id' => $categoryId, + 'path' => $category->getPath(), + 'parentId' => $category->getParentId(), + ]); } } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index d1ec3be1a8895..3bd3e73e5ebfa 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -126,8 +126,7 @@ public function execute() return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]); } - $data['general'] = $this->getRequest()->getPostValue(); - $categoryPostData = $data['general']; + $categoryPostData = $this->getRequest()->getPostValue(); $isNewCategory = !isset($categoryPostData['entity_id']); $categoryPostData = $this->stringToBoolConverting($categoryPostData); @@ -139,6 +138,9 @@ public function execute() $parentId = isset($categoryPostData['parent']) ? $categoryPostData['parent'] : null; if ($categoryPostData) { $category->addData($categoryPostData); + if ($parentId) { + $category->setParentId($parentId); + } if ($isNewCategory) { $parentCategory = $this->getParentCategory($parentId, $storeId); $category->setPath($parentCategory->getPath()); @@ -168,29 +170,29 @@ public function execute() $products = json_decode($categoryPostData['category_products'], true); $category->setPostedProducts($products); } - $this->_eventManager->dispatch( - 'catalog_category_prepare_save', - ['category' => $category, 'request' => $this->getRequest()] - ); - /** - * Check "Use Default Value" checkboxes values - */ - if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) { - foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) { - if ($attributeValue) { - $category->setData($attributeCode, null); + try { + $this->_eventManager->dispatch( + 'catalog_category_prepare_save', + ['category' => $category, 'request' => $this->getRequest()] + ); + /** + * Check "Use Default Value" checkboxes values + */ + if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) { + foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) { + if ($attributeValue) { + $category->setData($attributeCode, null); + } } } - } - /** - * Proceed with $_POST['use_config'] - * set into category model for processing through validation - */ - $category->setData('use_post_data_config', $useConfig); + /** + * Proceed with $_POST['use_config'] + * set into category model for processing through validation + */ + $category->setData('use_post_data_config', $useConfig); - try { $categoryResource = $category->getResource(); if ($category->hasCustomDesignTo()) { $categoryResource->getAttribute('custom_design_from')->setMaxValue($category->getCustomDesignTo()); @@ -205,7 +207,7 @@ public function execute() __('Attribute "%1" is required.', $attribute) ); } else { - throw new \Exception($error); + throw new \RuntimeException($error); } } } @@ -214,10 +216,12 @@ public function execute() $category->save(); $this->messageManager->addSuccessMessage(__('You saved the category.')); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addExceptionMessage($e); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $this->_getSession()->setCategoryData($categoryPostData); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Something went wrong while saving the category.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -277,7 +281,7 @@ public function imagePreprocessing($data) continue; } - $data[$attributeCode] = false; + $data[$attributeCode] = ''; } return $data; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php index 6958b6671d054..ca7652ebb43b5 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Controller\Adminhtml\Product\Action; use Magento\Backend\App\Action; @@ -23,7 +21,7 @@ abstract class Attribute extends Action const ADMIN_RESOURCE = 'Magento_Catalog::update_attributes'; /** - * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute + * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute */ protected $attributeHelper; @@ -50,13 +48,13 @@ protected function _validateProducts() $productIds = $this->attributeHelper->getProductIds(); if (!is_array($productIds)) { $error = __('Please select products for attributes update.'); - } elseif (!$this->_objectManager->create( - \Magento\Catalog\Model\Product::class)->isProductsHasSku($productIds)) { + } elseif (!$this->_objectManager->create(\Magento\Catalog\Model\Product::class) + ->isProductsHasSku($productIds)) { $error = __('Please make sure to define SKU values for all processed products.'); } if ($error) { - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); } return !$error; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 82496446aef9f..492288308a95c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; use Magento\Backend\App\Action; +use Magento\Framework\Exception\NotFoundException; /** * Class Save @@ -81,12 +82,17 @@ public function __construct( * Update product attributes * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + if (!$this->_validateProducts()) { return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['_current' => true]); } @@ -192,7 +198,7 @@ public function execute() $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds())) ); @@ -205,9 +211,9 @@ public function execute() $this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while updating the product(s) attributes.') ); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php index bb18436be6102..a873f08d082d7 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php @@ -68,7 +68,7 @@ public function execute() $response->setError(true); $response->setMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while updating the product(s) attributes.') ); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php index cc5a658a9296d..f4b55f081afc4 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php @@ -6,13 +6,20 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $id = $this->getRequest()->getParam('attribute_id'); $resultRedirect = $this->resultRedirectFactory->create(); if ($id) { @@ -21,23 +28,23 @@ public function execute() // entity type check $model->load($id); if ($model->getEntityTypeId() != $this->_entityTypeId) { - $this->messageManager->addError(__('We can\'t delete the attribute.')); + $this->messageManager->addErrorMessage(__('We can\'t delete the attribute.')); return $resultRedirect->setPath('catalog/*/'); } try { $model->delete(); - $this->messageManager->addSuccess(__('You deleted the product attribute.')); + $this->messageManager->addSuccessMessage(__('You deleted the product attribute.')); return $resultRedirect->setPath('catalog/*/'); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return $resultRedirect->setPath( 'catalog/*/edit', ['attribute_id' => $this->getRequest()->getParam('attribute_id')] ); } } - $this->messageManager->addError(__('We can\'t find an attribute to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find an attribute to delete.')); return $resultRedirect->setPath('catalog/*/'); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php index 5a9f244a2bbe1..52c79e2a66c74 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php @@ -25,14 +25,14 @@ public function execute() $model->load($id); if (!$model->getId()) { - $this->messageManager->addError(__('This attribute no longer exists.')); + $this->messageManager->addErrorMessage(__('This attribute no longer exists.')); $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('catalog/*/'); } // entity type check if ($model->getEntityTypeId() != $this->_entityTypeId) { - $this->messageManager->addError(__('This attribute cannot be edited.')); + $this->messageManager->addErrorMessage(__('This attribute cannot be edited.')); $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('catalog/*/'); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index f2803c2399474..5b15024f21ce8 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -5,79 +5,105 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; +use Magento\Catalog\Helper\Product; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Eav\Model\Entity\Attribute\Set; +use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator; +use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\FormData; +use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\Result\Json; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filter\FilterManager; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutFactory; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Exception\NotFoundException; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +class Save extends Attribute { /** - * @var \Magento\Catalog\Model\Product\AttributeSet\BuildFactory + * @var BuildFactory */ protected $buildFactory; /** - * @var \Magento\Framework\Filter\FilterManager + * @var FilterManager */ protected $filterManager; /** - * @var \Magento\Catalog\Helper\Product + * @var Product */ protected $productHelper; /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory + * @var AttributeFactory */ protected $attributeFactory; /** - * @var \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory + * @var ValidatorFactory */ protected $validatorFactory; /** - * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory + * @var CollectionFactory */ protected $groupCollectionFactory; /** - * @var \Magento\Framework\View\LayoutFactory + * @var LayoutFactory */ private $layoutFactory; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Cache\FrontendInterface $attributeLabelCache - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Catalog\Model\Product\AttributeSet\BuildFactory $buildFactory - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory - * @param \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory $attributeFactory - * @param \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory $validatorFactory - * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory $groupCollectionFactory - * @param \Magento\Framework\Filter\FilterManager $filterManager - * @param \Magento\Catalog\Helper\Product $productHelper - * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @var FormData + */ + private $formDataSerializer; + + /** + * @param Context $context + * @param FrontendInterface $attributeLabelCache + * @param Registry $coreRegistry + * @param BuildFactory $buildFactory + * @param PageFactory $resultPageFactory + * @param AttributeFactory $attributeFactory + * @param ValidatorFactory $validatorFactory + * @param CollectionFactory $groupCollectionFactory + * @param FilterManager $filterManager + * @param Product $productHelper + * @param LayoutFactory $layoutFactory + * @param FormData|null $formDataSerializer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Cache\FrontendInterface $attributeLabelCache, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Catalog\Model\Product\AttributeSet\BuildFactory $buildFactory, - \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory $attributeFactory, - \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory $validatorFactory, - \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory $groupCollectionFactory, - \Magento\Framework\Filter\FilterManager $filterManager, - \Magento\Catalog\Helper\Product $productHelper, - \Magento\Framework\View\LayoutFactory $layoutFactory + Context $context, + FrontendInterface $attributeLabelCache, + Registry $coreRegistry, + PageFactory $resultPageFactory, + BuildFactory $buildFactory, + AttributeFactory $attributeFactory, + ValidatorFactory $validatorFactory, + CollectionFactory $groupCollectionFactory, + FilterManager $filterManager, + Product $productHelper, + LayoutFactory $layoutFactory, + FormData $formDataSerializer = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->buildFactory = $buildFactory; @@ -87,17 +113,41 @@ public function __construct( $this->validatorFactory = $validatorFactory; $this->groupCollectionFactory = $groupCollectionFactory; $this->layoutFactory = $layoutFactory; + $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); } /** - * @return \Magento\Backend\Model\View\Result\Redirect + * @inheritdoc + * @throws NotFoundException + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + try { + $optionData = $this->formDataSerializer->unserialize( + $this->getRequest()->getParam('serialized_options', '[]') + ); + } catch (\InvalidArgumentException $e) { + $message = __("The attribute couldn't be saved due to an error. Verify your information and try again. " + . "If the error persists, please try again later."); + $this->messageManager->addErrorMessage($message); + + return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); + } + $data = $this->getRequest()->getPostValue(); + $data = array_replace_recursive( + $data, + $optionData + ); + if ($data) { $setId = $this->getRequest()->getParam('set'); @@ -107,53 +157,49 @@ public function execute() $name = trim($name); try { - /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ + /** @var Set $attributeSet */ $attributeSet = $this->buildFactory->create() ->setEntityTypeId($this->_entityTypeId) ->setSkeletonId($setId) ->setName($name) ->getAttributeSet(); } catch (AlreadyExistsException $alreadyExists) { - $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $name)); + $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $name)); $this->_session->setAttributeData($data); + return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving the attribute.')); + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while saving the attribute.') + ); } } $attributeId = $this->getRequest()->getParam('attribute_id'); - $attributeCode = $this->getRequest()->getParam('attribute_code') - ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]); - if (strlen($attributeCode) > 0) { - $validatorAttrCode = new \Zend_Validate_Regex(['pattern' => '/^[a-z\x{600}-\x{6FF}][a-z\x{600}-\x{6FF}_0-9]{0,30}$/u']); - if (!$validatorAttrCode->isValid($attributeCode)) { - $this->messageManager->addError( - __( - 'Attribute code "%1" is invalid. Please use only letters (a-z), ' . - 'numbers (0-9) or underscore(_) in this field, first character should be a letter.', - $attributeCode - ) - ); - return $this->returnResult( - 'catalog/*/edit', - ['attribute_id' => $attributeId, '_current' => true], - ['error' => true] - ); - } + + /** @var ProductAttributeInterface $model */ + $model = $this->attributeFactory->create(); + if ($attributeId) { + $model->load($attributeId); } + $attributeCode = $model && $model->getId() + ? $model->getAttributeCode() + : $this->getRequest()->getParam('attribute_code'); + $attributeCode = $attributeCode ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]); $data['attribute_code'] = $attributeCode; //validate frontend_input if (isset($data['frontend_input'])) { - /** @var $inputType \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator */ + /** @var Validator $inputType */ $inputType = $this->validatorFactory->create(); if (!$inputType->isValid($data['frontend_input'])) { foreach ($inputType->getMessages() as $message) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } + return $this->returnResult( 'catalog/*/edit', ['attribute_id' => $attributeId, '_current' => true], @@ -162,25 +208,23 @@ public function execute() } } - /* @var $model \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $model = $this->attributeFactory->create(); - if ($attributeId) { - $model->load($attributeId); if (!$model->getId()) { - $this->messageManager->addError(__('This attribute no longer exists.')); + $this->messageManager->addErrorMessage(__('This attribute no longer exists.')); + return $this->returnResult('catalog/*/', [], ['error' => true]); } // entity type check if ($model->getEntityTypeId() != $this->_entityTypeId) { - $this->messageManager->addError(__('We can\'t update the attribute.')); + $this->messageManager->addErrorMessage(__('We can\'t update the attribute.')); $this->_session->setAttributeData($data); + return $this->returnResult('catalog/*/', [], ['error' => true]); } $data['attribute_code'] = $model->getAttributeCode(); $data['is_user_defined'] = $model->getIsUserDefined(); - $data['frontend_input'] = $model->getFrontendInput(); + $data['frontend_input'] = $data['frontend_input'] ?? $model->getFrontendInput(); } else { /** * @todo add to helper and specify all relations for properties @@ -191,14 +235,14 @@ public function execute() $data['backend_model'] = $this->productHelper->getAttributeBackendModelByInputType( $data['frontend_input'] ); + + if ($model->getIsUserDefined() === null) { + $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); + } } $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0]; - if (is_null($model->getIsUserDefined()) || $model->getIsUserDefined() != 0) { - $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); - } - $defaultValueField = $model->getDefaultValueByInput($data['frontend_input']); if ($defaultValueField) { $data['default_value'] = $this->getRequest()->getParam($defaultValueField); @@ -241,7 +285,7 @@ public function execute() try { $model->save(); - $this->messageManager->addSuccess(__('You saved the product attribute.')); + $this->messageManager->addSuccessMessage(__('You saved the product attribute.')); $this->_attributeLabelCache->clean(); $this->_session->setAttributeData(false); @@ -252,9 +296,10 @@ public function execute() '_current' => true, 'product_tab' => $this->getRequest()->getParam('product_tab'), ]; - if (!is_null($attributeSet)) { + if ($attributeSet !== null) { $requestParams['new_attribute_set_id'] = $attributeSet->getId(); } + return $this->returnResult('catalog/product/addAttribute', $requestParams, ['error' => false]); } elseif ($this->getRequest()->getParam('back', false)) { return $this->returnResult( @@ -263,10 +308,12 @@ public function execute() ['error' => false] ); } + return $this->returnResult('catalog/*/', [], ['error' => false]); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_session->setAttributeData($data); + return $this->returnResult( 'catalog/*/edit', ['attribute_id' => $attributeId, '_current' => true], @@ -274,14 +321,17 @@ public function execute() ); } } + return $this->returnResult('catalog/*/', [], ['error' => true]); } /** + * Provides an initialized Result object. + * * @param string $path * @param array $params * @param array $response - * @return \Magento\Framework\Controller\Result\Json|\Magento\Backend\Model\View\Result\Redirect + * @return Json|Redirect */ private function returnResult($path = '', array $params = [], array $response = []) { @@ -291,8 +341,10 @@ private function returnResult($path = '', array $params = [], array $response = $response['messages'] = [$layout->getMessagesBlock()->getGroupedHtml()]; $response['params'] = $params; + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($response); } + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setPath($path, $params); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index 6d3ceaae7f0b6..03d143fff036f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -6,8 +6,15 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\Serialize\Serializer\FormData; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +/** + * Product attribute validate controller. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute { const DEFAULT_MESSAGE_KEY = 'message'; @@ -27,6 +34,11 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute */ private $multipleAttributeList; + /** + * @var FormData + */ + private $formDataSerializer; + /** * Constructor * @@ -37,6 +49,7 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param array $multipleAttributeList + * @param FormData|null $formDataSerializer */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -45,16 +58,19 @@ public function __construct( \Magento\Framework\View\Result\PageFactory $resultPageFactory, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Framework\View\LayoutFactory $layoutFactory, - array $multipleAttributeList = [] + array $multipleAttributeList = [], + FormData $formDataSerializer = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; $this->multipleAttributeList = $multipleAttributeList; + $this->formDataSerializer = $formDataSerializer ?? ObjectManager::getInstance()->get(FormData::class); } /** - * @return \Magento\Framework\Controller\ResultInterface + * @inheritdoc + * * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -62,6 +78,16 @@ public function execute() { $response = new DataObject(); $response->setError(false); + try { + $optionsData = $this->formDataSerializer->unserialize( + $this->getRequest()->getParam('serialized_options', '[]') + ); + } catch (\InvalidArgumentException $e) { + $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " + . "If the error persists, please try again later."); + $this->setMessageToResponse($response, [$message]); + $response->setError(true); + } $attributeCode = $this->getRequest()->getParam('attribute_code'); $frontendLabel = $this->getRequest()->getParam('frontend_label'); @@ -74,7 +100,7 @@ public function execute() $attributeCode ); - if ($attribute->getId() && !$attributeId) { + if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { $message = strlen($this->getRequest()->getParam('attribute_code')) ? __('An attribute with this code already exists.') : __('An attribute with the same code (%1) already exists.', $attributeCode); @@ -91,7 +117,7 @@ public function execute() $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { $setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName); - $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $setName)); + $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $setName)); $layout = $this->layoutFactory->create(); $layout->initMessages(); @@ -101,15 +127,20 @@ public function execute() } $multipleOption = $this->getRequest()->getParam("frontend_input"); - $multipleOption = null == $multipleOption ? 'select' : $multipleOption; + $multipleOption = (null === $multipleOption) ? 'select' : $multipleOption; if (isset($this->multipleAttributeList[$multipleOption]) && !(null == ($multipleOption))) { - $options = $this->getRequest()->getParam($this->multipleAttributeList[$multipleOption]); + $options = $optionsData[$this->multipleAttributeList[$multipleOption]] ?? null; $this->checkUniqueOption( $response, $options ); $valueOptions = (isset($options['value']) && is_array($options['value'])) ? $options['value'] : []; + foreach (array_keys($valueOptions) as $key) { + if (!empty($options['delete'][$key])) { + unset($valueOptions[$key]); + } + } $this->checkEmptyOption($response, $valueOptions); } @@ -117,7 +148,8 @@ public function execute() } /** - * Throws Exception if not unique values into options + * Throws Exception if not unique values into options. + * * @param array $optionsValues * @param array $deletedOptions * @return bool @@ -126,7 +158,7 @@ private function isUniqueAdminValues(array $optionsValues, array $deletedOptions { $adminValues = []; foreach ($optionsValues as $optionKey => $values) { - if (!(isset($deletedOptions[$optionKey]) and $deletedOptions[$optionKey] === '1')) { + if (!(isset($deletedOptions[$optionKey]) && $deletedOptions[$optionKey] === '1')) { $adminValues[] = reset($values); } } @@ -151,6 +183,8 @@ private function setMessageToResponse($response, $messages) } /** + * Performs checking the uniqueness of the attribute options. + * * @param DataObject $response * @param array|null $options * @return $this diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 4fa61b2b372c2..b19f20af1a6fa 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php @@ -11,7 +11,13 @@ use Magento\Store\Model\StoreFactory; use Psr\Log\LoggerInterface as Logger; use Magento\Framework\Registry; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type as ProductTypes; +/** + * Build a product based on a request. + */ class Builder { /** @@ -39,6 +45,11 @@ class Builder */ protected $storeFactory; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * Constructor * @@ -47,13 +58,15 @@ class Builder * @param Registry $registry * @param WysiwygModel\Config $wysiwygConfig * @param StoreFactory|null $storeFactory + * @param ProductRepositoryInterface|null $productRepository */ public function __construct( ProductFactory $productFactory, Logger $logger, Registry $registry, WysiwygModel\Config $wysiwygConfig, - StoreFactory $storeFactory = null + StoreFactory $storeFactory = null, + ProductRepositoryInterface $productRepository = null ) { $this->productFactory = $productFactory; $this->logger = $logger; @@ -61,6 +74,8 @@ public function __construct( $this->wysiwygConfig = $wysiwygConfig; $this->storeFactory = $storeFactory ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Store\Model\StoreFactory::class); + $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ProductRepositoryInterface::class); } /** @@ -68,40 +83,70 @@ public function __construct( * * @param RequestInterface $request * @return \Magento\Catalog\Model\Product + * @throws \RuntimeException */ public function build(RequestInterface $request) { - $productId = (int)$request->getParam('id'); - /** @var $product \Magento\Catalog\Model\Product */ - $product = $this->productFactory->create(); - $product->setStoreId($request->getParam('store', 0)); - $store = $this->storeFactory->create(); - $store->load($request->getParam('store', 0)); - + $productId = (int) $request->getParam('id'); + $storeId = $request->getParam('store', 0); + $attributeSetId = (int) $request->getParam('set'); $typeId = $request->getParam('type'); - if (!$productId && $typeId) { - $product->setTypeId($typeId); - } - $product->setData('_edit_mode', true); if ($productId) { try { - $product->load($productId); + $product = $this->productRepository->getById($productId, true, $storeId); + if ($attributeSetId) { + $product->setAttributeSetId($attributeSetId); + } } catch (\Exception $e) { - $product->setTypeId(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE); + $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId); $this->logger->critical($e); } + } else { + $product = $this->createEmptyProduct($typeId, $attributeSetId, $storeId); } - $setId = (int)$request->getParam('set'); - if ($setId) { - $product->setAttributeSetId($setId); - } + $store = $this->storeFactory->create(); + $store->load($storeId); + $this->registry->unregister('product'); + $this->registry->unregister('current_product'); + $this->registry->unregister('current_store'); $this->registry->register('product', $product); $this->registry->register('current_product', $product); $this->registry->register('current_store', $store); - $this->wysiwygConfig->setStoreId($request->getParam('store')); + + $this->wysiwygConfig->setStoreId($storeId); + + return $product; + } + + /** + * Create a product with the given properties + * + * @param int $typeId + * @param int $attributeSetId + * @param int $storeId + * @return \Magento\Catalog\Model\Product + */ + private function createEmptyProduct($typeId, $attributeSetId, $storeId): Product + { + /** @var $product \Magento\Catalog\Model\Product */ + $product = $this->productFactory->create(); + $product->setData('_edit_mode', true); + + if ($typeId !== null) { + $product->setTypeId($typeId); + } + + if ($storeId !== null) { + $product->setStoreId($storeId); + } + + if ($attributeSetId) { + $product->setAttributeSetId($attributeSetId); + } + return $product; } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php index 7e8b03a66f603..63e52eead064c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php @@ -43,11 +43,11 @@ public function execute() $product = $this->productBuilder->build($this->getRequest()); try { $newProduct = $this->productCopier->copy($product); - $this->messageManager->addSuccess(__('You duplicated the product.')); + $this->messageManager->addSuccessMessage(__('You duplicated the product.')); $resultRedirect->setPath('catalog/*/edit', ['_current' => true, 'id' => $newProduct->getId()]); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('catalog/*/edit', ['_current' => true]); } return $resultRedirect; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php index 9f99ad0fe6aca..3a4d3af4b39b1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php @@ -52,12 +52,12 @@ public function execute() if (($productId && !$product->getEntityId())) { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); - $this->messageManager->addError(__('This product doesn\'t exist.')); + $this->messageManager->addErrorMessage(__('This product doesn\'t exist.')); return $resultRedirect->setPath('catalog/*/'); } elseif ($productId === 0) { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); - $this->messageManager->addError(__('Invalid product id. Should be numeric value greater than 0')); + $this->messageManager->addErrorMessage(__('Invalid product id. Should be numeric value greater than 0')); return $resultRedirect->setPath('catalog/*/'); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php index 4909e22775e55..8a5f375f2b706 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Group/Save.php @@ -29,12 +29,12 @@ public function execute() ); if ($model->itemExists()) { - $this->messageManager->addError(__('A group with the same name already exists.')); + $this->messageManager->addErrorMessage(__('A group with the same name already exists.')); } else { try { $model->save(); } catch (\Exception $e) { - $this->messageManager->addError(__('Something went wrong while saving this group.')); + $this->messageManager->addErrorMessage(__('Something went wrong while saving this group.')); } } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index beb6f2b13bcfe..0c018c40c7584 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -3,18 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeDefaultValueFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver; +use Magento\Catalog\Model\Product\LinkTypeProvider; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; +use Magento\Catalog\Model\Product\Authorization as ProductAuthorization; /** + * Product helper + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -81,7 +88,7 @@ class Helper private $dateTimeFilter; /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider + * @var LinkTypeProvider */ private $linkTypeProvider; @@ -90,6 +97,11 @@ class Helper */ private $attributeFilter; + /** + * @var ProductAuthorization + */ + private $productAuthorization; + /** * Constructor * @@ -99,11 +111,12 @@ class Helper * @param ProductLinks $productLinks * @param \Magento\Backend\Helper\Js $jsHelper * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory|null $customOptionFactory - * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory - * @param \Magento\Catalog\Api\ProductRepositoryInterface|null $productRepository - * @param \Magento\Catalog\Model\Product\LinkTypeProvider|null $linkTypeProvider + * @param CustomOptionFactory|null $customOptionFactory + * @param ProductLinkFactory|null $productLinkFactory + * @param ProductRepositoryInterface|null $productRepository + * @param LinkTypeProvider|null $linkTypeProvider * @param AttributeFilter|null $attributeFilter + * @param ProductAuthorization|null $productAuthorization * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -113,11 +126,12 @@ public function __construct( \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks, \Magento\Backend\Helper\Js $jsHelper, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory = null, - \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository = null, - \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider = null, - AttributeFilter $attributeFilter = null + CustomOptionFactory $customOptionFactory = null, + ProductLinkFactory $productLinkFactory = null, + ProductRepositoryInterface $productRepository = null, + LinkTypeProvider $linkTypeProvider = null, + AttributeFilter $attributeFilter = null, + ProductAuthorization $productAuthorization = null ) { $this->request = $request; $this->storeManager = $storeManager; @@ -125,16 +139,14 @@ public function __construct( $this->productLinks = $productLinks; $this->jsHelper = $jsHelper; $this->dateFilter = $dateFilter; - $this->customOptionFactory = $customOptionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); - $this->productLinkFactory = $productLinkFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); - $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->linkTypeProvider = $linkTypeProvider ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\LinkTypeProvider::class); - $this->attributeFilter = $attributeFilter ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(AttributeFilter::class); + + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $this->customOptionFactory = $customOptionFactory ?: $objectManager->get(CustomOptionFactory::class); + $this->productLinkFactory = $productLinkFactory ?: $objectManager->get(ProductLinkFactory::class); + $this->productRepository = $productRepository ?: $objectManager->get(ProductRepositoryInterface::class); + $this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class); + $this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class); + $this->productAuthorization = $productAuthorization ?? $objectManager->get(ProductAuthorization::class); } /** @@ -150,8 +162,7 @@ public function __construct( */ public function initializeFromData(\Magento\Catalog\Model\Product $product, array $productData) { - unset($productData['custom_attributes']); - unset($productData['extension_attributes']); + unset($productData['custom_attributes'], $productData['extension_attributes']); if ($productData) { $stockData = isset($productData['stock_data']) ? $productData['stock_data'] : []; @@ -159,6 +170,7 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra } $productData = $this->normalize($productData); + $productData = $this->convertSpecialFromDateStringToObject($productData); if (!empty($productData['is_downloadable'])) { $productData['product_has_weight'] = 0; @@ -199,28 +211,13 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra $productData['tier_price'] = isset($productData['tier_price']) ? $productData['tier_price'] : []; $useDefaults = (array)$this->request->getPost('use_default', []); - $productData = $this->attributeFilter->prepareProductAttributes($product, $productData, $useDefaults); - $product->addData($productData); if ($wasLockedMedia) { $product->lockAttribute('media'); } - /** - * Check "Use Default Value" checkboxes values - */ - foreach ($useDefaults as $attributeCode => $useDefaultState) { - if ($useDefaultState) { - $product->setData($attributeCode, null); - // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false - if ($product->hasData('use_config_' . $attributeCode)) { - $product->setData('use_config_' . $attributeCode, false); - } - } - } - $product = $this->setProductLinks($product); $product = $this->fillProductOptions($product, $productOptions); @@ -240,7 +237,10 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra public function initialize(\Magento\Catalog\Model\Product $product) { $productData = $this->request->getPost('product', []); - return $this->initializeFromData($product, $productData); + $product = $this->initializeFromData($product, $productData); + $this->productAuthorization->authorizeSavingOf($product); + + return $product; } /** @@ -379,6 +379,8 @@ private function overwriteValue($optionId, $option, $overwriteOptions) } /** + * Get link resolver instance + * * @return LinkResolver * @deprecated 101.0.0 */ @@ -391,6 +393,8 @@ private function getLinkResolver() } /** + * Get DateTimeFilter instance + * * @return \Magento\Framework\Stdlib\DateTime\Filter\DateTime * @deprecated 101.0.0 */ @@ -405,6 +409,7 @@ private function getDateTimeFilter() /** * Remove ids of non selected websites from $websiteIds array and return filtered data + * * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on * @@ -455,9 +460,12 @@ private function fillProductOptions(Product $product, array $productOptions) } if (isset($customOptionData['values'])) { - $customOptionData['values'] = array_filter($customOptionData['values'], function ($valueData) { - return empty($valueData['is_delete']); - }); + $customOptionData['values'] = array_filter( + $customOptionData['values'], + function ($valueData) { + return empty($valueData['is_delete']); + } + ); } $customOption = $this->customOptionFactory->create(['data' => $customOptionData]); @@ -467,4 +475,20 @@ private function fillProductOptions(Product $product, array $productOptions) return $product->setOptions($customOptions); } + + /** + * Convert string date presentation into object + * + * @param array $productData + * @return array + */ + private function convertSpecialFromDateStringToObject($productData) + { + if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') { + $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']); + $productData['special_from_date'] = new \DateTime($productData['special_from_date']); + } + + return $productData; + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index 237168282afae..abd8c2dd9f377 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -25,17 +25,74 @@ class AttributeFilter * @param array $useDefaults * @return array */ - public function prepareProductAttributes(Product $product, array $productData, array $useDefaults) + public function prepareProductAttributes(Product $product, array $productData, array $useDefaults): array { - foreach ($productData as $attribute => $value) { - $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === "1"; - if ($value === '' && $considerUseDefaultsAttribute) { - /** @var $product Product */ - if ((bool)$product->getData($attribute) === (bool)$value) { - unset($productData[$attribute]); - } + $attributeList = $product->getAttributes(); + foreach ($productData as $attributeCode => $attributeValue) { + if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue)) { + unset($productData[$attributeCode]); + } + + if (isset($useDefaults[$attributeCode]) && $useDefaults[$attributeCode] === '1') { + $productData = $this->prepareDefaultData($attributeList, $attributeCode, $productData); + $productData = $this->prepareConfigData($product, $attributeCode, $productData); + } + } + + return $productData; + } + + /** + * @param Product $product + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareConfigData(Product $product, $attributeCode, array $productData): array + { + // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false + if ($product->hasData('use_config_' . $attributeCode)) { + $productData['use_config_' . $attributeCode] = false; + } + + return $productData; + } + + /** + * @param array $attributeList + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareDefaultData(array $attributeList, $attributeCode, array $productData): array + { + if (isset($attributeList[$attributeCode])) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + $attribute = $attributeList[$attributeCode]; + $attributeType = $attribute->getBackendType(); + // For non-numberic types set the attributeValue to 'false' to trigger their removal from the db + if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { + $attribute->setIsRequired(false); + $productData[$attributeCode] = false; + } else { + $productData[$attributeCode] = null; } } + return $productData; } + + /** + * @param Product $product + * @param $useDefaults + * @param $attribute + * @param $value + * @return bool + */ + private function isAttributeShouldNotBeUpdated(Product $product, $useDefaults, $attribute, $value): bool + { + $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === '1'; + + return ($value === '' && $considerUseDefaultsAttribute && !$product->getData($attribute)); + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php index 2402fb213cda0..d04284b4b323c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php @@ -11,6 +11,7 @@ use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NotFoundException; class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product { @@ -54,9 +55,15 @@ public function __construct( /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\StateException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } $collection = $this->filter->getCollection($this->collectionFactory->create()); $productDeleted = 0; /** @var \Magento\Catalog\Model\Product $product */ @@ -64,9 +71,12 @@ public function execute() $this->productRepository->delete($product); $productDeleted++; } - $this->messageManager->addSuccess( - __('A total of %1 record(s) have been deleted.', $productDeleted) - ); + + if ($productDeleted) { + $this->messageManager->addSuccessMessage( + __('A total of %1 record(s) have been deleted.', $productDeleted) + ); + } return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setPath('catalog/*/index'); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php index ec5eb9de3fe35..02b0025a7922a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NotFoundException; use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -77,9 +78,14 @@ public function _validateMassStatus(array $productIds, $status) * Update product(s) status action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $productIds = $collection->getAllIds(); $storeId = (int) $this->getRequest()->getParam('store', 0); @@ -94,10 +100,12 @@ public function execute() $this->_validateMassStatus($productIds, $status); $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) ->updateAttributes($productIds, ['status' => $status], $storeId); - $this->messageManager->addSuccess(__('A total of %1 record(s) have been updated.', count($productIds))); + $this->messageManager->addSuccessMessage( + __('A total of %1 record(s) have been updated.', count($productIds)) + ); $this->_productPriceIndexerProcessor->reindexList($productIds); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->_getSession()->addException($e, __('Something went wrong while updating the product(s) status.')); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index d34f4bedd80e4..7a382d1cb31bc 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Backend\App\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Request\DataPersistorInterface; @@ -78,11 +79,12 @@ public function __construct( } /** - * Save product action + * Save product action. * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { @@ -101,12 +103,12 @@ public function execute() $this->productBuilder->build($this->getRequest()) ); $this->productTypeManager->processProduct($product); - if (isset($data['product'][$product->getIdFieldName()])) { throw new \Magento\Framework\Exception\LocalizedException(__('Unable to save product')); } $originalSku = $product->getSku(); + $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); $this->getCategoryLinkManagement()->assignProductToCategories( @@ -116,9 +118,9 @@ public function execute() $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); - - $this->copyToStores($data, $productId); - + $extendedData = $data; + $extendedData['can_save_custom_options'] = $canSaveCustomOptions; + $this->copyToStores($extendedData, $productId); $this->messageManager->addSuccessMessage(__('You saved the product.')); $this->getDataPersistor()->clear('catalog_product'); if ($product->getSku() != $originalSku) { @@ -140,17 +142,21 @@ public function execute() ); if ($redirectBack === 'duplicate') { + $product->unsetData('quantity_and_stock_status'); $newProduct = $this->productCopier->copy($product); + $this->checkUniqueAttributes($product); $this->messageManager->addSuccessMessage(__('You duplicated the product.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $this->messageManager->addExceptionMessage($e); + $data = isset($product) ? $this->persistMediaData($product, $data) : $data; $this->getDataPersistor()->set('catalog_product', $data); $redirectBack = $productId ? true : 'new'; } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $this->messageManager->addErrorMessage($e->getMessage()); + $data = isset($product) ? $this->persistMediaData($product, $data) : $data; $this->getDataPersistor()->set('catalog_product', $data); $redirectBack = $productId ? true : 'new'; } @@ -213,6 +219,9 @@ private function handleImageRemoveError($postData, $productId) /** * Do copying data to stores * + * If the 'copy_from' field is not specified in the input data, + * the store fallback mechanism will automatically take the admin store's default value. + * * @param array $data * @param int $productId * @return void @@ -224,15 +233,18 @@ protected function copyToStores($data, $productId) if (isset($data['product']['website_ids'][$websiteId]) && (bool)$data['product']['website_ids'][$websiteId]) { foreach ($group as $store) { - $copyFrom = (isset($store['copy_from'])) ? $store['copy_from'] : 0; - $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0; - if ($copyTo) { - $this->_objectManager->create(\Magento\Catalog\Model\Product::class) - ->setStoreId($copyFrom) - ->load($productId) - ->setStoreId($copyTo) - ->setCopyFromView(true) - ->save(); + if (isset($store['copy_from'])) { + $copyFrom = $store['copy_from']; + $copyTo = (isset($store['copy_to'])) ? $store['copy_to'] : 0; + if ($copyTo) { + $this->_objectManager->create(\Magento\Catalog\Model\Product::class) + ->setStoreId($copyFrom) + ->load($productId) + ->setStoreId($copyTo) + ->setCanSaveCustomOptions($data['can_save_custom_options']) + ->setCopyFromView(true) + ->save(); + } } } } @@ -279,4 +291,57 @@ protected function getDataPersistor() return $this->dataPersistor; } + + /** + * Persist media gallery on error, in order to show already saved images on next run. + * + * @param ProductInterface $product + * @param array $data + * @return array + */ + private function persistMediaData(ProductInterface $product, array $data) + { + $mediaGallery = $product->getData('media_gallery'); + if (!empty($mediaGallery['images'])) { + foreach ($mediaGallery['images'] as $key => $image) { + if (!isset($image['new_file'])) { + //Remove duplicates. + unset($mediaGallery['images'][$key]); + } + } + $data['product']['media_gallery'] = $mediaGallery; + $fields = [ + 'image', + 'small_image', + 'thumbnail', + 'swatch_image', + ]; + foreach ($fields as $field) { + $data['product'][$field] = $product->getData($field); + } + } + + return $data; + } + + /** + * Check unique attributes and add error to message manager. + * + * @param \Magento\Catalog\Model\Product $product + */ + private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) + { + $uniqueLabels = []; + foreach ($product->getAttributes() as $attribute) { + if ($attribute->getIsUnique() && $attribute->getIsUserDefined() + && !empty($product->getData($attribute->getAttributeCode())) + ) { + $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); + } + } + if ($uniqueLabels) { + $uniqueLabels = implode('", "', $uniqueLabels); + $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); + } + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php index b49a4dabe223c..09d35c4f72de6 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php @@ -6,6 +6,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set { /** @@ -29,19 +31,25 @@ public function __construct( /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { - $setId = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $setId = (int)$this->getRequest()->getParam('id'); $resultRedirect = $this->resultRedirectFactory->create(); try { $this->attributeSetRepository->deleteById($setId); - $this->messageManager->addSuccess(__('The attribute set has been removed.')); + $this->messageManager->addSuccessMessage(__('The attribute set has been removed.')); $resultRedirect->setPath('catalog/*/'); } catch (\Exception $e) { - $this->messageManager->addError(__('We can\'t delete this set right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete this set right now.')); $resultRedirect->setUrl($this->_redirect->getRedirectUrl($this->getUrl('*'))); } + return $resultRedirect; } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php index 00a836309e58e..c5dd9ce6d8e77 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php @@ -6,6 +6,11 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\App\ObjectManager; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set { /** @@ -17,22 +22,49 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set * @var \Magento\Framework\Controller\Result\JsonFactory */ protected $resultJsonFactory; - + + /* + * @var \Magento\Eav\Model\Entity\Attribute\SetFactory + */ + private $attributeSetFactory; + + /* + * @var \Magento\Framework\Filter\FilterManager + */ + private $filterManager; + + /* + * @var \Magento\Framework\Json\Helper\Data + */ + private $jsonHelper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory + * @param \Magento\Framework\Filter\FilterManager $filterManager + * @param \Magento\Framework\Json\Helper\Data $jsonHelper */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\View\LayoutFactory $layoutFactory, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory = null, + \Magento\Framework\Filter\FilterManager $filterManager = null, + \Magento\Framework\Json\Helper\Data $jsonHelper = null ) { parent::__construct($context, $coreRegistry); $this->layoutFactory = $layoutFactory; $this->resultJsonFactory = $resultJsonFactory; + $this->attributeSetFactory = $attributeSetFactory ?: ObjectManager::getInstance() + ->get(\Magento\Eav\Model\Entity\Attribute\SetFactory::class); + $this->filterManager = $filterManager ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Filter\FilterManager::class); + $this->jsonHelper = $jsonHelper ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Json\Helper\Data::class); } /** @@ -65,16 +97,12 @@ public function execute() $isNewSet = $this->getRequest()->getParam('gotoEdit', false) == '1'; /* @var $model \Magento\Eav\Model\Entity\Attribute\Set */ - $model = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) - ->setEntityTypeId($entityTypeId); - - /** @var $filterManager \Magento\Framework\Filter\FilterManager */ - $filterManager = $this->_objectManager->get(\Magento\Framework\Filter\FilterManager::class); + $model = $this->attributeSetFactory->create()->setEntityTypeId($entityTypeId); try { if ($isNewSet) { //filter html tags - $name = $filterManager->stripTags($this->getRequest()->getParam('attribute_set_name')); + $name = $this->filterManager->stripTags($this->getRequest()->getParam('attribute_set_name')); $model->setAttributeSetName(trim($name)); } else { if ($attributeSetId) { @@ -85,11 +113,10 @@ public function execute() __('This attribute set no longer exists.') ); } - $data = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class) - ->jsonDecode($this->getRequest()->getPost('data')); + $data = $this->jsonHelper->jsonDecode($this->getRequest()->getPost('data')); //filter html tags - $data['attribute_set_name'] = $filterManager->stripTags($data['attribute_set_name']); + $data['attribute_set_name'] = $this->filterManager->stripTags($data['attribute_set_name']); $model->organizeData($data); } @@ -100,15 +127,15 @@ public function execute() $model->initFromSkeleton($this->getRequest()->getParam('skeleton_set')); } $model->save(); - $this->messageManager->addSuccess(__('You saved the attribute set.')); + $this->messageManager->addSuccessMessage(__('You saved the attribute set.')); } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { $this->messageManager->addErrorMessage($e->getMessage()); $hasError = true; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $hasError = true; } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving the attribute set.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the attribute set.')); $hasError = true; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php index 63f46fd32e6f7..e131bfe38c546 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php @@ -137,7 +137,7 @@ public function execute() $response->setError(true); $response->setMessages([$e->getMessage()]); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $layout = $this->layoutFactory->create(); $layout->initMessages(); $response->setError(true); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php index 933b5eaafbb39..f92bba0885c81 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php @@ -1,11 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Product\Widget; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\App\ObjectManager; + +/** + * Chooser Product container Action. + */ class Chooser extends \Magento\Backend\App\Action { /** @@ -23,28 +28,41 @@ class Chooser extends \Magento\Backend\App\Action */ protected $layoutFactory; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Framework\Escaper|null $escaper */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Framework\Escaper $escaper = null ) { parent::__construct($context); $this->resultRawFactory = $resultRawFactory; $this->layoutFactory = $layoutFactory; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); } /** - * Chooser Source action + * Chooser Source action. * * @return \Magento\Framework\Controller\Result\Raw + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $uniqId = $this->getRequest()->getParam('uniq_id'); $massAction = $this->getRequest()->getParam('use_massaction', false); $productTypeId = $this->getRequest()->getParam('product_type_id', null); @@ -55,11 +73,11 @@ public function execute() '', [ 'data' => [ - 'id' => $uniqId, + 'id' => $this->escaper->escapeHtml($uniqId), 'use_massaction' => $massAction, 'product_type_id' => $productTypeId, - 'category_id' => $this->getRequest()->getParam('category_id'), - ] + 'category_id' => (int)$this->getRequest()->getParam('category_id'), + ], ] ); @@ -71,10 +89,10 @@ public function execute() '', [ 'data' => [ - 'id' => $uniqId . 'Tree', + 'id' => $this->escaper->escapeHtml($uniqId) . 'Tree', 'node_click_listener' => $productsGrid->getCategoryClickListenerJs(), 'with_empty_node' => true, - ] + ], ] ); @@ -86,6 +104,7 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); + return $resultRaw->setContents($html); } } diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index a72f764a5f1f6..1b9225915cff9 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -8,13 +8,17 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\Action; /** + * View a category on storefront. Needs to be accessible by POST because of the store switching. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class View extends \Magento\Framework\App\Action\Action +class View extends Action { /** * Core registry @@ -69,6 +73,11 @@ class View extends \Magento\Framework\App\Action\Action */ protected $categoryRepository; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * Constructor * @@ -82,6 +91,7 @@ class View extends \Magento\Framework\App\Action\Action * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository + * @param ToolbarMemorizer|null $toolbarMemorizer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +104,8 @@ public function __construct( PageFactory $resultPageFactory, \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, Resolver $layerResolver, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + ToolbarMemorizer $toolbarMemorizer = null ) { parent::__construct($context); $this->_storeManager = $storeManager; @@ -106,12 +117,13 @@ public function __construct( $this->resultForwardFactory = $resultForwardFactory; $this->layerResolver = $layerResolver; $this->categoryRepository = $categoryRepository; + $this->toolbarMemorizer = $toolbarMemorizer ?: $context->getObjectManager()->get(ToolbarMemorizer::class); } /** * Initialize requested category object * - * @return \Magento\Catalog\Model\Category + * @return \Magento\Catalog\Model\Category|bool */ protected function _initCategory() { @@ -130,6 +142,7 @@ protected function _initCategory() } $this->_catalogSession->setLastVisitedCategoryId($category->getId()); $this->_coreRegistry->register('current_category', $category); + $this->toolbarMemorizer->memorizeParams(); try { $this->_eventManager->dispatch( 'catalog_controller_category_init_after', @@ -152,7 +165,9 @@ protected function _initCategory() */ public function execute() { - if ($this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED)) { + if (!$this->_request->getParam('___from_store') + && $this->_request->getParam(self::PARAM_NAME_URL_ENCODED) + ) { return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); } $category = $this->_initCategory(); @@ -172,6 +187,10 @@ public function execute() if ($settings->getPageLayout()) { $page->getConfig()->setPageLayout($settings->getPageLayout()); } + //Apply custom handles + if ($settings->getPageLayoutHandles()) { + $page->addPageLayoutHandles($settings->getPageLayoutHandles()); + } $hasChildren = $category->hasChildren(); if ($category->getIsAnchor()) { @@ -193,7 +212,7 @@ public function execute() if ($layoutUpdates && is_array($layoutUpdates)) { foreach ($layoutUpdates as $layoutUpdate) { $page->addUpdate($layoutUpdate); - $page->addPageLayoutHandles(['layout_update' => md5($layoutUpdate)], null, false); + $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare.php b/app/code/Magento/Catalog/Controller/Product/Compare.php index 1ee146e5aaa70..0a65a1bd42c3d 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare.php @@ -139,4 +139,12 @@ public function setCustomerId($customerId) $this->_customerId = $customerId; return $this; } + + /** + * @inheritdoc + */ + public function execute() + { + return $this->resultRedirectFactory->create()->setPath('catalog/product_compare'); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php index 89eb6c9be929f..387dabcd34f87 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php @@ -6,19 +6,80 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\View\Result\PageFactory; +/** + * Add item to compare list action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Add extends \Magento\Catalog\Controller\Product\Compare { /** - * Add item to compare list + * @var AddToCompareAvailability + */ + private $compareAvailability; + + /** + * @param \Magento\Framework\App\Action\Context $context + * @param \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory + * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory $itemCollectionFactory + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Customer\Model\Visitor $customerVisitor + * @param \Magento\Catalog\Model\Product\Compare\ListCompare $catalogProductCompareList + * @param \Magento\Catalog\Model\Session $catalogSession + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Validator $formKeyValidator + * @param PageFactory $resultPageFactory + * @param ProductRepositoryInterface $productRepository + * @param AddToCompareAvailability|null $compareAvailability + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\App\Action\Context $context, + \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory, + \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory $itemCollectionFactory, + \Magento\Customer\Model\Session $customerSession, + \Magento\Customer\Model\Visitor $customerVisitor, + \Magento\Catalog\Model\Product\Compare\ListCompare $catalogProductCompareList, + \Magento\Catalog\Model\Session $catalogSession, + \Magento\Store\Model\StoreManagerInterface $storeManager, + Validator $formKeyValidator, + PageFactory $resultPageFactory, + ProductRepositoryInterface $productRepository, + AddToCompareAvailability $compareAvailability = null + ) { + parent::__construct( + $context, + $compareItemFactory, + $itemCollectionFactory, + $customerSession, + $customerVisitor, + $catalogProductCompareList, + $catalogSession, + $storeManager, + $formKeyValidator, + $resultPageFactory, + $productRepository + ); + + $this->compareAvailability = $compareAvailability + ?: $this->_objectManager->get(AddToCompareAvailability::class); + } + + /** + * Add item to compare list. * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { $resultRedirect = $this->resultRedirectFactory->create(); - if (!$this->_formKeyValidator->validate($this->getRequest())) { + if (!$this->isActionAllowed()) { return $resultRedirect->setRefererUrl(); } @@ -26,22 +87,39 @@ public function execute() if ($productId && ($this->_customerVisitor->getId() || $this->_customerSession->isLoggedIn())) { $storeId = $this->_storeManager->getStore()->getId(); try { + /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { $product = null; } - if ($product) { + if ($product && $this->compareAvailability->isAvailableForCompare($product)) { $this->_catalogProductCompareList->addProduct($product); $productName = $this->_objectManager->get( \Magento\Framework\Escaper::class )->escapeHtml($product->getName()); - $this->messageManager->addSuccess(__('You added product %1 to the comparison list.', $productName)); + $this->messageManager->addComplexSuccessMessage( + 'addCompareSuccessMessage', + [ + 'product_name' => $productName, + 'compare_list_url' => $this->_url->getUrl('catalog/product_compare'), + ] + ); + $this->_eventManager->dispatch('catalog_product_compare_add_product', ['product' => $product]); } $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); } + return $resultRedirect->setRefererOrBaseUrl(); } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php index 30470d13f002d..ebbf90e0701ae 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php @@ -17,29 +17,42 @@ class Clear extends \Magento\Catalog\Controller\Product\Compare */ public function execute() { - /** @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection $items */ - $items = $this->_itemCollectionFactory->create(); + if ($this->isActionAllowed()) { + /** @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection $items */ + $items = $this->_itemCollectionFactory->create(); - if ($this->_customerSession->isLoggedIn()) { - $items->setCustomerId($this->_customerSession->getCustomerId()); - } elseif ($this->_customerId) { - $items->setCustomerId($this->_customerId); - } else { - $items->setVisitorId($this->_customerVisitor->getId()); - } + if ($this->_customerSession->isLoggedIn()) { + $items->setCustomerId($this->_customerSession->getCustomerId()); + } elseif ($this->_customerId) { + $items->setCustomerId($this->_customerId); + } else { + $items->setVisitorId($this->_customerVisitor->getId()); + } - try { - $items->clear(); - $this->messageManager->addSuccess(__('You cleared the comparison list.')); - $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong clearing the comparison list.')); + try { + $items->clear(); + $this->messageManager->addSuccessMessage(__('You cleared the comparison list.')); + $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong clearing the comparison list.') + ); + } } /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setRefererOrBaseUrl(); } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index fadb94761a236..8bce2581f59d8 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -6,27 +6,33 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Remove item from compare list action. + */ class Remove extends \Magento\Catalog\Controller\Product\Compare { /** - * Remove item from compare list + * Remove item from compare list. * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { $productId = (int)$this->getRequest()->getParam('product'); - if ($productId) { + if ($this->isActionAllowed() && $productId) { $storeId = $this->_storeManager->getStore()->getId(); try { + /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { $product = null; } - if ($product) { + if ($product && (int)$product->getStatus() !== Status::STATUS_DISABLED) { /** @var $item \Magento\Catalog\Model\Product\Compare\Item */ $item = $this->_compareItemFactory->create(); if ($this->_customerSession->isLoggedIn()) { @@ -44,7 +50,7 @@ public function execute() $item->delete(); $productName = $this->_objectManager->get(\Magento\Framework\Escaper::class) ->escapeHtml($product->getName()); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('You removed product %1 from the comparison list.', $productName) ); $this->_eventManager->dispatch( @@ -58,7 +64,16 @@ public function execute() if (!$this->getRequest()->getParam('isAjax', false)) { $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setRefererOrBaseUrl(); } } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php index 4c577eb897589..4ec35b3382786 100644 --- a/app/code/Magento/Catalog/Controller/Product/View.php +++ b/app/code/Magento/Catalog/Controller/Product/View.php @@ -76,15 +76,21 @@ public function execute() $productId = (int) $this->getRequest()->getParam('id'); $specifyOptions = $this->getRequest()->getParam('options'); - if ($this->getRequest()->isPost() && $this->getRequest()->getParam(self::PARAM_NAME_URL_ENCODED)) { + if (!$this->_request->getParam('___from_store') + && $this->_request->isPost() + && $this->_request->getParam(self::PARAM_NAME_URL_ENCODED) + ) { $product = $this->_initProduct(); + if (!$product) { return $this->noProductRedirect(); } + if ($specifyOptions) { $notice = $product->getTypeInstance()->getSpecifyOptionMessage(); - $this->messageManager->addNotice($notice); + $this->messageManager->addNoticeMessage($notice); } + if ($this->getRequest()->isAjax()) { $this->getResponse()->representJson( $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode([ diff --git a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php index 6e7699abb4776..99e9898eab3c0 100644 --- a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php +++ b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php @@ -57,8 +57,7 @@ private function getLifeTimeByNamespace($namespace) ]; } - return isset($configuration['lifetime']) ? - (int) $configuration['lifetime'] : FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; + return (int)$configuration['lifetime'] ?? FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; } /** diff --git a/app/code/Magento/Catalog/CustomerData/CompareProducts.php b/app/code/Magento/Catalog/CustomerData/CompareProducts.php index 0e688042c615a..afbeab8c9070e 100644 --- a/app/code/Magento/Catalog/CustomerData/CompareProducts.php +++ b/app/code/Magento/Catalog/CustomerData/CompareProducts.php @@ -19,6 +19,11 @@ class CompareProducts implements SectionSourceInterface */ protected $productUrl; + /** + * @var \Magento\Catalog\Helper\Output + */ + private $outputHelper; + /** * @param \Magento\Catalog\Helper\Product\Compare $helper * @param \Magento\Catalog\Model\Product\Url $productUrl @@ -54,6 +59,7 @@ public function getSectionData() protected function getItems() { $items = []; + /** @var \Magento\Catalog\Model\Product $item */ foreach ($this->helper->getItemCollection() as $item) { $items[] = [ 'id' => $item->getId(), diff --git a/app/code/Magento/Catalog/Helper/Data.php b/app/code/Magento/Catalog/Helper/Data.php index 7d153399f8ec0..495973176be12 100644 --- a/app/code/Magento/Catalog/Helper/Data.php +++ b/app/code/Magento/Catalog/Helper/Data.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\ScopeInterface; use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -269,7 +270,8 @@ public function setStoreId($store) /** * Return current category path or get it from current category - * and creating array of categories|product paths for breadcrumbs + * + * Creating array of categories|product paths for breadcrumbs * * @return array */ @@ -378,6 +380,7 @@ public function getLastViewedUrl() /** * Split SKU of an item by dashes and spaces + * * Words will not be broken, unless this length is greater than $length * * @param string $sku @@ -406,14 +409,15 @@ public function getAttributeHiddenFields() /** * Retrieve Catalog Price Scope * - * @return int + * @return int|null */ public function getPriceScope() { - return $this->scopeConfig->getValue( + $priceScope = $this->scopeConfig->getValue( self::XML_PATH_PRICE_SCOPE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); + return isset($priceScope) ? (int)$priceScope : null; } /** @@ -449,7 +453,7 @@ public function isUrlDirectivesParsingAllowed() { return $this->scopeConfig->isSetFlag( self::CONFIG_PARSE_URL_DIRECTIVES, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->_storeId ); } @@ -466,6 +470,7 @@ public function getPageTemplateProcessor() /** * Whether to display items count for each filter option + * * @param int $storeId Store view ID * @return bool */ @@ -473,12 +478,14 @@ public function shouldDisplayProductCountOnLayer($storeId = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_DISPLAY_PRODUCT_COUNT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ); } /** + * Convert tax address array to address data object with country id and postcode + * * @param array $taxAddress * @return \Magento\Customer\Api\Data\AddressInterface|null */ diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 380fd0298c2d7..4e8e63c17199d 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -859,7 +859,7 @@ public function getFrame() */ protected function getAttribute($name) { - return isset($this->attributes[$name]) ? $this->attributes[$name] : null; + return $this->attributes[$name] ?? null; } /** diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php index facd5351f269a..ecd3fe6a32a83 100644 --- a/app/code/Magento/Catalog/Helper/Output.php +++ b/app/code/Magento/Catalog/Helper/Output.php @@ -105,7 +105,7 @@ public function addHandler($method, $handler) public function getHandlers($method) { $method = strtolower($method); - return isset($this->_handlers[$method]) ? $this->_handlers[$method] : []; + return $this->_handlers[$method] ?? []; } /** @@ -151,7 +151,7 @@ public function productAttribute($product, $attributeHtml, $attributeName) $attributeHtml = nl2br($attributeHtml); } } - if ($attribute->getIsHtmlAllowedOnFront() && $attribute->getIsWysiwygEnabled()) { + if ($attribute->getIsHtmlAllowedOnFront() || $attribute->getIsWysiwygEnabled()) { if ($this->_catalogData->isUrlDirectivesParsingAllowed()) { $attributeHtml = $this->_getTemplateProcessor()->filter($attributeHtml); } diff --git a/app/code/Magento/Catalog/Helper/Product/ProductList.php b/app/code/Magento/Catalog/Helper/Product/ProductList.php index 07d90158f6b4d..b9bd3bb04879a 100644 --- a/app/code/Magento/Catalog/Helper/Product/ProductList.php +++ b/app/code/Magento/Catalog/Helper/Product/ProductList.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Helper\Product; /** @@ -53,7 +51,11 @@ public function __construct( */ public function getAvailableViewMode() { - switch ($this->scopeConfig->getValue(self::XML_PATH_LIST_MODE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE)) { + $value = $this->scopeConfig->getValue( + self::XML_PATH_LIST_MODE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + switch ($value) { case 'grid': $availableMode = ['grid' => __('Grid')]; break; diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php index 46ac05168715b..c56456e109b0c 100644 --- a/app/code/Magento/Catalog/Helper/Product/View.php +++ b/app/code/Magento/Catalog/Helper/Product/View.php @@ -4,10 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Helper\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Result\Page as ResultPage; /** @@ -61,6 +60,11 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper */ protected $categoryUrlPathGenerator; + /** + * @var \Magento\Framework\Stdlib\StringUtils + */ + private $string; + /** * Constructor * @@ -72,6 +76,8 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator * @param array $messageGroups + * @param \Magento\Framework\Stdlib\StringUtils|null $string + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -81,7 +87,8 @@ public function __construct( \Magento\Framework\Registry $coreRegistry, \Magento\Framework\Message\ManagerInterface $messageManager, \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, - array $messageGroups = [] + array $messageGroups = [], + \Magento\Framework\Stdlib\StringUtils $string = null ) { $this->_catalogSession = $catalogSession; $this->_catalogDesign = $catalogDesign; @@ -90,9 +97,56 @@ public function __construct( $this->messageGroups = $messageGroups; $this->messageManager = $messageManager; $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; + $this->string = $string ?: ObjectManager::getInstance()->get(\Magento\Framework\Stdlib\StringUtils::class); parent::__construct($context); } + /** + * Add meta information from product to layout + * + * @param \Magento\Framework\View\Result\Page $resultPage + * @param \Magento\Catalog\Model\Product $product + * @return \Magento\Framework\View\Result\Page + */ + private function preparePageMetadata(ResultPage $resultPage, $product) + { + $pageConfig = $resultPage->getConfig(); + + $metaTitle = $product->getMetaTitle(); + $pageConfig->setMetaTitle($metaTitle); + $pageConfig->getTitle()->set($metaTitle ?: $product->getName()); + + $keyword = $product->getMetaKeyword(); + $currentCategory = $this->_coreRegistry->registry('current_category'); + if ($keyword) { + $pageConfig->setKeywords($keyword); + } elseif ($currentCategory) { + $pageConfig->setKeywords($product->getName()); + } + + $description = $product->getMetaDescription(); + if ($description) { + $pageConfig->setDescription($description); + } else { + $pageConfig->setDescription($this->string->substr(strip_tags($product->getDescription()), 0, 255)); + } + + if ($this->_catalogProduct->canUseCanonicalTag()) { + $pageConfig->addRemotePageAsset( + $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]), + 'canonical', + ['attributes' => ['rel' => 'canonical']] + ); + } + + $pageMainTitle = $resultPage->getLayout()->getBlock('page.main.title'); + if ($pageMainTitle) { + $pageMainTitle->setPageTitle($product->getName()); + } + + return $this; + } + /** * Init layout for viewing product page * @@ -122,18 +176,18 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu // Load default page handles and page configurations if ($params && $params->getBeforeHandles()) { foreach ($params->getBeforeHandles() as $handle) { - $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle); $resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], $handle, false); + $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle); } } - - $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]); + $resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], null, false); + $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]); if ($params && $params->getAfterHandles()) { foreach ($params->getAfterHandles() as $handle) { - $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle); $resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], $handle, false); + $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle); } } @@ -147,6 +201,9 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu } } } + if ($settings->getPageLayoutHandles()) { + $resultPage->addPageLayoutHandles($settings->getPageLayoutHandles()); + } $currentCategory = $this->_coreRegistry->registry('current_category'); $controllerClass = $this->_request->getFullActionName(); @@ -227,6 +284,7 @@ public function prepareAndRender(ResultPage $resultPage, $productId, $controller } $this->initProductLayout($resultPage, $product, $params); + $this->preparePageMetadata($resultPage, $product); return $this; } } diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php new file mode 100644 index 0000000000000..ca8b53ea0db00 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Framework\Api\Filter; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + +/** + * Based on Magento\Framework\Api\Filter builds condition + * that can be applied to Catalog\Model\ResourceModel\Product\Collection + * to filter products that has specific value for EAV attribute + */ +class EavAttributeCondition implements CustomConditionInterface +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resourceConnection; + + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + + /** + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Framework\App\ResourceConnection $resourceConnection + */ + public function __construct( + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\App\ResourceConnection $resourceConnection + ) { + $this->eavConfig = $eavConfig; + $this->resourceConnection = $resourceConnection; + } + + /** + * @param Filter $filter + * @return string + * @throws \DomainException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function build(Filter $filter): string + { + $attribute = $this->getAttributeByCode($filter->getField()); + $tableAlias = 'ca_' . $attribute->getAttributeCode(); + + $conditionType = $this->mapConditionType($filter->getConditionType()); + $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); + + // NOTE: store scope was ignored intentionally to perform search across all stores + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.' . $attribute->getIdFieldName(), + ['eq' => $attribute->getAttributeId()] + ) + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.value', + [$conditionType => $conditionValue] + ) + ); + + return $this->resourceConnection + ->getConnection() + ->prepareSqlCondition( + Collection::MAIN_TABLE_ALIAS . '.' . $attribute->getEntityIdField(), + [ + 'in' => $attributeSelect + ] + ); + } + + /** + * @param string $field + * @return Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode(string $field): Attribute + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @return mixed + */ + private function mapConditionType(string $conditionType): string + { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin' + ]; + + return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; + } + + /** + * Wraps value with '%' if condition type is 'like' or 'not like' + * + * @param string $conditionType + * @param string $conditionValue + * @return string + */ + private function mapConditionValue(string $conditionType, string $conditionValue): string + { + $conditionsMap = ['like', 'nlike']; + + if (in_array($conditionType, $conditionsMap)) { + $conditionValue = '%' . $conditionValue . '%'; + } + + return $conditionValue; + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php new file mode 100644 index 0000000000000..808878d1481a9 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/Factory.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + +/** + * Creates appropriate condition builder based on filter field + * - native attribute condition builder if filter field is native attribute in product + * - eav condition builder if filter field is eav attribute + */ +class Factory +{ + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Product + */ + private $productResource; + + /** + * @var CustomConditionInterface + */ + private $eavAttributeConditionBuilder; + + /** + * @var CustomConditionInterface + */ + private $nativeAttributeConditionBuilder; + + /** + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Catalog\Model\ResourceModel\Product $productResource + * @param CustomConditionInterface $eavAttributeConditionBuilder + * @param CustomConditionInterface $nativeAttributeConditionBuilder + */ + public function __construct( + \Magento\Eav\Model\Config $eavConfig, + \Magento\Catalog\Model\ResourceModel\Product $productResource, + CustomConditionInterface $eavAttributeConditionBuilder, + CustomConditionInterface $nativeAttributeConditionBuilder + ) { + $this->eavConfig = $eavConfig; + $this->productResource = $productResource; + $this->eavAttributeConditionBuilder = $eavAttributeConditionBuilder; + $this->nativeAttributeConditionBuilder = $nativeAttributeConditionBuilder; + } + + /** + * @param Filter $filter + * @return CustomConditionInterface + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function createByFilter(Filter $filter): CustomConditionInterface + { + $attribute = $this->getAttributeByCode($filter->getField()); + + if ($attribute->getBackendTable() === $this->productResource->getEntityTable()) { + return $this->nativeAttributeConditionBuilder; + } + + return $this->eavAttributeConditionBuilder; + } + + /** + * @param string $field + * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributeByCode(string $field): Attribute + { + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field); + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php new file mode 100644 index 0000000000000..3c2837fd2e9a1 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Framework\Api\Filter; +use Magento\Catalog\Model\ResourceModel\Product\Collection; + +/** + * Based on Magento\Framework\Api\Filter builds condition + * that can be applied to Catalog\Model\ResourceModel\Product\Collection + * to filter products that has specific value for their native attribute + */ +class NativeAttributeCondition implements CustomConditionInterface +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resourceConnection; + + /** + * @param \Magento\Framework\App\ResourceConnection $resourceConnection + */ + public function __construct( + \Magento\Framework\App\ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * @param Filter $filter + * @return string + * @throws \DomainException + */ + public function build(Filter $filter): string + { + $conditionType = $this->mapConditionType($filter->getConditionType(), $filter->getField()); + $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); + + return $this->resourceConnection + ->getConnection() + ->prepareSqlCondition( + Collection::MAIN_TABLE_ALIAS . '.' . $filter->getField(), + [ + $conditionType => $conditionValue + ] + ); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @param string $field + * @return mixed + */ + private function mapConditionType(string $conditionType, string $field): string + { + if (strtolower($field) === ProductInterface::SKU) { + $conditionsMap = [ + 'eq' => 'like', + 'neq' => 'nlike' + ]; + } else { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin' + ]; + } + + return $conditionsMap[$conditionType] ?? $conditionType; + } + + /** + * Wraps value with '%' if condition type is 'like' or 'not like' + * + * @param string $conditionType + * @param string $conditionValue + * @return string + */ + private function mapConditionValue(string $conditionType, string $conditionValue): string + { + $conditionsMap = ['like', 'nlike']; + + if (in_array($conditionType, $conditionsMap)) { + $conditionValue = '%' . $conditionValue . '%'; + } + + return $conditionValue; + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php new file mode 100644 index 0000000000000..5189da35ab52a --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/DefaultCondition.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Framework\Api\Filter; +use Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\Factory; + +/** + * Default condition builder for Catalog\Model\ResourceModel\Product\Collection + */ +class DefaultCondition implements CustomConditionInterface +{ + /** + * @var Factory + */ + private $conditionBuilderFactory; + + /** + * @param Factory $conditionBuilderFactory + */ + public function __construct( + Factory $conditionBuilderFactory + ) { + $this->conditionBuilderFactory = $conditionBuilderFactory; + } + + /** + * @param Filter $filter + * @return string + */ + public function build(Filter $filter): string + { + $filterBuilder = $this->conditionBuilderFactory->createByFilter($filter); + + return $filterBuilder->build($filter); + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php new file mode 100644 index 0000000000000..b11696ed0d129 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\Api\Filter; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\NoSuchEntityException as CategoryDoesNotExistException; + +/** + * Based on Magento\Framework\Api\Filter builds condition + * that can be applied to Catalog\Model\ResourceModel\Product\Collection + * to filter products by specific categories + */ +class ProductCategoryCondition implements CustomConditionInterface +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resourceConnection; + + /** + * @var \Magento\Catalog\Model\CategoryRepository + */ + private $categoryRepository; + + /** + * Level that has store root categories + * @var int + */ + private $rootCategoryLevel = 1; + + /** + * @param \Magento\Framework\App\ResourceConnection $resourceConnection + */ + public function __construct( + \Magento\Framework\App\ResourceConnection $resourceConnection, + \Magento\Catalog\Model\CategoryRepository $categoryRepository + ) { + $this->resourceConnection = $resourceConnection; + $this->categoryRepository = $categoryRepository; + } + + /** + * @param Filter $filter + * @return string + */ + public function build(Filter $filter): string + { + $categorySelect = $this->resourceConnection->getConnection()->select() + ->from( + ['cat' => $this->resourceConnection->getTableName('catalog_category_product')], + 'cat.product_id' + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + 'cat.category_id', + [$this->mapConditionType($filter->getConditionType()) => $this->getCategoryIds($filter)] + ) + ); + + $selectCondition = [ + 'in' => $categorySelect + ]; + + return $this->resourceConnection->getConnection() + ->prepareSqlCondition(Collection::MAIN_TABLE_ALIAS . '.entity_id', $selectCondition); + } + + /** + * Extracts required category ids from Filter + * If category is anchor all children categories will be included too + * If category is root all children categories will be included too + * + * @param Filter $filter + * @return array + */ + private function getCategoryIds(Filter $filter): array + { + $categoryIds = explode(',', $filter->getValue()); + $childCategoryIds = []; + + foreach ($categoryIds as $categoryId) { + try { + $category = $this->categoryRepository->get($categoryId); + } catch (CategoryDoesNotExistException $exception) { + continue; + } + + if ($category->getIsAnchor()) { + $childCategoryIds[] = $category->getAllChildren(true); + } + + // This is the simplest way to check if category is root + if ((int)$category->getLevel() === $this->rootCategoryLevel) { + $childCategoryIds[] = $category->getAllChildren(true); + } + } + + return array_map('intval', array_unique(array_merge($categoryIds, ...$childCategoryIds))); + } + + /** + * Map equal and not equal conditions to in and not in + * + * @param string $conditionType + * @return mixed + */ + private function mapConditionType(string $conditionType): string + { + $conditionsMap = [ + 'eq' => 'in', + 'neq' => 'nin', + 'like' => 'in', + 'nlike' => 'nin', + ]; + return $conditionsMap[$conditionType] ?? $conditionType; + } +} diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php index e0fbc16421f55..1f0f2361df507 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilter.php @@ -21,8 +21,14 @@ class ProductCategoryFilter implements CustomFilterInterface */ public function apply(Filter $filter, AbstractDb $collection) { - $conditionType = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; - $categoryFilter = [$conditionType => [$filter->getValue()]]; + $value = $filter->getValue(); + $conditionType = $filter->getConditionType() ?: 'in'; + if (($conditionType === 'in' || $conditionType === 'nin') && is_string($value)) { + $value = explode(',', $value); + } else { + $value = [$value]; + } + $categoryFilter = [$conditionType => $value]; /** @var Collection $collection */ $collection->addCategoriesFilter($categoryFilter); diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php new file mode 100644 index 0000000000000..3beef80a6ae72 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Framework\Exception\LocalizedException; +use Magento\Catalog\Model\AbstractModel; + +/** + * Custom layout file attribute. + */ +abstract class AbstractLayoutUpdate extends AbstractBackend +{ + const VALUE_USE_UPDATE_XML = '__existing__'; + + const VALUE_NO_UPDATE = '__no_update__'; + + /** + * Extract attribute value. + * + * @param AbstractModel $model + * @return mixed + */ + private function extractAttributeValue(AbstractModel $model) + { + $code = $this->getAttribute()->getAttributeCode(); + + return $model->getData($code); + } + + /** + * Compose list of available files (layout handles) for given entity. + * + * @param AbstractModel $forModel + * @return string[] + */ + abstract protected function listAvailableValues(AbstractModel $forModel): array; + + /** + * Extracts prepare attribute value to be saved. + * + * @throws LocalizedException + * @param AbstractModel $model + * @return string|null + */ + private function prepareValue(AbstractModel $model) + { + $value = $this->extractAttributeValue($model); + if (!is_string($value)) { + $value = null; + } + if ($value + && $value !== self::VALUE_USE_UPDATE_XML + && $value !== self::VALUE_NO_UPDATE + && !in_array($value, $this->listAvailableValues($model), true) + ) { + throw new LocalizedException(__('Selected layout update is not available')); + } + + return $value; + } + + /** + * Set value for the object. + * + * @param string|null $value + * @param AbstractModel $forObject + * @param string|null $attrCode + * @return void + */ + private function setAttributeValue($value, AbstractModel $forObject, $attrCode = null) + { + $attrCode = $attrCode ?? $this->getAttribute()->getAttributeCode(); + if ($forObject->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) { + $forObject->setCustomAttribute($attrCode, $value); + } + $forObject->setData($attrCode, $value); + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + */ + public function validate($object) + { + $valid = parent::validate($object); + if ($valid) { + $this->prepareValue($object); + } + + return $valid; + } + + /** + * @inheritDoc + * @param AbstractModel $object + * @throws LocalizedException + */ + public function beforeSave($object) + { + $value = $this->prepareValue($object); + if ($value && ($value === self::VALUE_NO_UPDATE || $value !== self::VALUE_USE_UPDATE_XML)) { + $this->setAttributeValue(null, $object, 'custom_layout_update'); + } + if (!$value || $value === self::VALUE_USE_UPDATE_XML || $value === self::VALUE_NO_UPDATE) { + $value = null; + } + $this->setAttributeValue($value, $object); + + return $this; + } +} diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php index a994446881189..e34e355b321cf 100644 --- a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php @@ -5,8 +5,10 @@ */ namespace Magento\Catalog\Model\Attribute\Backend; +use Magento\Catalog\Model\AbstractModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; -use Magento\Eav\Model\Entity\Attribute\Exception; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; /** * Layout update attribute backend @@ -16,18 +18,15 @@ * @SuppressWarnings(PHPMD.LongVariable) * @since 100.0.2 */ -class Customlayoutupdate extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend +class Customlayoutupdate extends AbstractBackend { /** - * Layout update validator factory - * * @var ValidatorFactory + * @deprecated Is not used anymore. */ protected $_layoutUpdateValidatorFactory; /** - * Construct the custom layout update class - * * @param ValidatorFactory $layoutUpdateValidatorFactory */ public function __construct(ValidatorFactory $layoutUpdateValidatorFactory) @@ -36,31 +35,95 @@ public function __construct(ValidatorFactory $layoutUpdateValidatorFactory) } /** - * Validate the custom layout update + * Extract an attribute value. * - * @param \Magento\Framework\DataObject $object - * @return bool - * @throws Exception + * @param AbstractModel $object + * @return mixed */ - public function validate($object) + private function extractValue(AbstractModel $object) + { + $attributeCode = $attributeCode ?? $this->getAttribute()->getName(); + $value = $object->getData($attributeCode); + if (!$value || !is_string($value)) { + $value = null; + } + + return $value; + } + + /** + * Extract old attribute value. + * + * @param AbstractModel $object + * @return mixed Old value or null. + */ + private function extractOldValue(AbstractModel $object) { - $attributeName = $this->getAttribute()->getName(); - $xml = trim($object->getData($attributeName)); + if (!empty($object->getId())) { + $attr = $this->getAttribute()->getAttributeCode(); + + if ($object->getOrigData()) { + return $object->getOrigData($attr); + } - if (!$this->getAttribute()->getIsRequired() && empty($xml)) { - return true; + $oldObject = clone $object; + $oldObject->unsetData(); + $oldObject->load($object->getId()); + + return $oldObject->getData($attr); } - /** @var $validator \Magento\Framework\View\Model\Layout\Update\Validator */ - $validator = $this->_layoutUpdateValidatorFactory->create(); - if (!$validator->isValid($xml)) { - $messages = $validator->getMessages(); - //Add first message to exception - $message = array_shift($messages); - $eavExc = new Exception(__($message)); - $eavExc->setAttributeCode($attributeName); - throw $eavExc; + return null; + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + */ + public function validate($object) + { + if (parent::validate($object)) { + if ($object instanceof AbstractModel) { + $value = $this->extractValue($object); + $oldValue = $this->extractOldValue($object); + if ($value && $oldValue !== $value) { + throw new LocalizedException(__('Custom layout update text cannot be changed, only removed')); + } + } } + return true; } + + /** + * Put an attribute value. + * + * @param AbstractModel $object + * @param string|null $value + * @return void + */ + private function putValue(AbstractModel $object, $value) + { + $attributeCode = $this->getAttribute()->getName(); + if ($object->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) { + $object->setCustomAttribute($attributeCode, $value); + } + $object->setData($attributeCode, $value); + } + + /** + * @inheritDoc + * + * @param AbstractModel $object + * @throws LocalizedException + */ + public function beforeSave($object) + { + //Validate first, validation might have been skipped. + $this->validate($object); + $this->putValue($object, $this->extractValue($object)); + + return parent::beforeSave($object); + } } diff --git a/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php new file mode 100644 index 0000000000000..0003b9996c84b --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Source; + +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate as Backend; +use Magento\Framework\Model\AbstractExtensibleModel; + +/** + * List of layout updates available for a catalog entity. + */ +abstract class AbstractLayoutUpdate extends AbstractSource implements SpecificSourceInterface +{ + /** + * @var string[] + */ + private $optionsText; + + /** + * @inheritDoc + */ + public function getAllOptions() + { + $default = Backend::VALUE_NO_UPDATE; + $defaultText = 'No update'; + $this->optionsText[$default] = $defaultText; + + return [['label' => $defaultText, 'value' => $default]]; + } + + /** + * @inheritDoc + */ + public function getOptionText($value) + { + if (is_scalar($value) && array_key_exists($value, $this->optionsText)) { + return $this->optionsText[$value]; + } + + return false; + } + + /** + * Extract attribute value. + * + * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity + * @return mixed + */ + private function extractAttributeValue(CustomAttributesDataInterface $entity) + { + $attrCode = 'custom_layout_update'; + if ($entity instanceof AbstractExtensibleModel + && !$entity->hasData(CustomAttributesDataInterface::CUSTOM_ATTRIBUTES) + ) { + //Custom attributes were not loaded yet, using data array + return $entity->getData($attrCode); + } + //Fallback to customAttribute method + $attr = $entity->getCustomAttribute($attrCode); + + return $attr ? $attr->getValue() : null; + } + + /** + * List available layout update options for the entity. + * + * @param CustomAttributesDataInterface $entity + * @return string[] + */ + abstract protected function listAvailableOptions(CustomAttributesDataInterface $entity): array; + + /** + * @inheritDoc + * + * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity + */ + public function getOptionsFor(CustomAttributesDataInterface $entity): array + { + $options = $this->getAllOptions(); + if ($this->extractAttributeValue($entity)) { + $existingValue = Backend::VALUE_USE_UPDATE_XML; + $existingLabel = 'Use existing'; + $options[] = ['label' => $existingLabel, 'value' => $existingValue]; + $this->optionsText[$existingValue] = $existingLabel; + } + foreach ($this->listAvailableOptions($entity) as $handle) { + $options[] = ['label' => $handle, 'value' => $handle]; + $this->optionsText[$handle] = $handle; + } + + return $options; + } +} diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 571e4646f46a3..f404c31cd0e8e 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -71,6 +71,11 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements const CACHE_TAG = 'cat_c'; + /** + * Category Store Id + */ + const STORE_ID = 'store_id'; + /**#@+ * Constants */ @@ -112,6 +117,11 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements */ protected $_url; + /** + * @var ResourceModel\Category + */ + protected $_resource; + /** * URL rewrite model * @@ -139,6 +149,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements 'page_layout', 'custom_layout_update', 'custom_apply_to_products', + 'custom_layout_update_file', + 'custom_use_parent_settings' ]; /** @@ -332,6 +344,18 @@ protected function getCustomAttributesCodes() return $this->customAttributesCodes; } + /** + * @throws \Magento\Framework\Exception\LocalizedException + * @return \Magento\Catalog\Model\ResourceModel\Category + * @deprecated because resource models should be used directly + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + */ + protected function _getResource() + { + //phpcs:enable Generic.CodeAnalysis.UselessOverridingMethod + return parent::_getResource(); + } + /** * Get flat resource model flag * @@ -484,7 +508,7 @@ public function getProductCollection() * Retrieve all customer attributes * * @param bool $noDesignAttributes - * @return array + * @return \Magento\Eav\Api\Data\AttributeInterface[] * @todo Use with Flat Resource * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -573,12 +597,12 @@ public function getStoreIds() * * If store id is underfined for category return current active store id * - * @return integer + * @return int */ public function getStoreId() { - if ($this->hasData('store_id')) { - return (int)$this->_getData('store_id'); + if ($this->hasData(self::STORE_ID)) { + return (int)$this->_getData(self::STORE_ID); } return (int)$this->_storeManager->getStore()->getId(); } @@ -594,7 +618,7 @@ public function setStoreId($storeId) if (!is_numeric($storeId)) { $storeId = $this->_storeManager->getStore($storeId)->getId(); } - $this->setData('store_id', $storeId); + $this->setData(self::STORE_ID, $storeId); $this->getResource()->setStoreId($storeId); return $this; } @@ -706,7 +730,7 @@ public function getParentId() return $parentId; } $parentIds = $this->getParentIds(); - return intval(array_pop($parentIds)); + return (int)array_pop($parentIds); } /** @@ -736,7 +760,7 @@ public function getCustomDesignDate() /** * Retrieve design attributes array * - * @return array + * @return \Magento\Eav\Api\Data\AttributeInterface[] */ public function getDesignAttributes() { @@ -943,8 +967,11 @@ public function getAnchorsAbove() */ public function getProductCount() { - $count = $this->_getResource()->getProductCount($this); - $this->setData(self::KEY_PRODUCT_COUNT, $count); + if (!$this->hasData(self::KEY_PRODUCT_COUNT)) { + $count = $this->_getResource()->getProductCount($this); + $this->setData(self::KEY_PRODUCT_COUNT, $count); + } + return $this->getData(self::KEY_PRODUCT_COUNT); } @@ -1107,10 +1134,15 @@ public function reindex() } } $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); - if (!$productIndexer->isScheduled() - && (!empty($this->getAffectedProductIds()) || $this->dataHasChangedFor('is_anchor')) - ) { - $productIndexer->reindexList($this->getPathIds()); + + if (!empty($this->getAffectedProductIds()) + || $this->dataHasChangedFor('is_anchor') + || $this->dataHasChangedFor('is_active')) { + if (!$productIndexer->isScheduled()) { + $productIndexer->reindexList($this->getPathIds()); + } else { + $productIndexer->invalidate(); + } } } @@ -1132,16 +1164,24 @@ public function afterDeleteCommit() */ public function getIdentities() { - $identities = [ - self::CACHE_TAG . '_' . $this->getId(), - ]; - if (!$this->getId() || $this->hasDataChanges() - || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU) - ) { - $identities[] = self::CACHE_TAG; - $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); + $identities = []; + if ($this->getId()) { + if ($this->getAffectedCategoryIds()) { + foreach (array_unique($this->getAffectedCategoryIds()) as $affectedCategoryId) { + $identities[] = self::CACHE_TAG . '_' . $affectedCategoryId; + } + } else { + $identities[] = self::CACHE_TAG . '_' . $this->getId(); + } + + $identities = $this->getCategoryRelationIdentities($identities); + + if ($this->isObjectNew()) { + $identities[] = self::CACHE_TAG; + } } - return $identities; + + return array_unique($identities); } /** @@ -1427,5 +1467,25 @@ public function setExtensionAttributes(\Magento\Catalog\Api\Data\CategoryExtensi return $this->_setExtensionAttributes($extensionAttributes); } + /** + * Return category relation identities. + * + * @param array $identities + * @return array + */ + private function getCategoryRelationIdentities(array $identities): array + { + if ($this->hasDataChanges() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); + if ($this->dataHasChangedFor('is_anchor') || $this->dataHasChangedFor('is_active')) { + foreach ($this->getPathIds() as $id) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $id; + } + } + } + + return $identities; + } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Catalog/Model/Category/Attribute.php b/app/code/Magento/Catalog/Model/Category/Attribute.php index 968db224c01f5..b1803a0db947e 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute.php @@ -29,14 +29,15 @@ class Attribute extends \Magento\Catalog\Model\Entity\Attribute implements */ public function getApplyTo() { - if ($this->getData(self::APPLY_TO)) { - if (is_array($this->getData(self::APPLY_TO))) { - return $this->getData(self::APPLY_TO); + $applyTo = $this->_getData(self::APPLY_TO); + if ($applyTo) { + if (is_array($applyTo)) { + return $applyTo; } - return explode(',', $this->getData(self::APPLY_TO)); - } else { - return []; + return explode(',', $applyTo); } + + return []; } /** @@ -59,7 +60,7 @@ public function setApplyTo($applyTo) */ public function getIsWysiwygEnabled() { - return $this->getData(self::IS_WYSIWYG_ENABLED); + return $this->_getData(self::IS_WYSIWYG_ENABLED); } /** @@ -70,7 +71,7 @@ public function getIsWysiwygEnabled() */ public function setIsWysiwygEnabled($isWysiwygEnabled) { - return $this->getData(self::IS_WYSIWYG_ENABLED, $isWysiwygEnabled); + return $this->setData(self::IS_WYSIWYG_ENABLED, $isWysiwygEnabled); } /** @@ -78,7 +79,7 @@ public function setIsWysiwygEnabled($isWysiwygEnabled) */ public function getIsHtmlAllowedOnFront() { - return $this->getData(self::IS_HTML_ALLOWED_ON_FRONT); + return $this->_getData(self::IS_HTML_ALLOWED_ON_FRONT); } /** @@ -97,7 +98,7 @@ public function setIsHtmlAllowedOnFront($isHtmlAllowedOnFront) */ public function getUsedForSortBy() { - return $this->getData(self::USED_FOR_SORT_BY); + return $this->_getData(self::USED_FOR_SORT_BY); } /** @@ -116,7 +117,7 @@ public function setUsedForSortBy($usedForSortBy) */ public function getIsFilterable() { - return $this->getData(self::IS_FILTERABLE); + return $this->_getData(self::IS_FILTERABLE); } /** @@ -135,7 +136,7 @@ public function setIsFilterable($isFilterable) */ public function getIsFilterableInSearch() { - return $this->getData(self::IS_FILTERABLE_IN_SEARCH); + return $this->_getData(self::IS_FILTERABLE_IN_SEARCH); } /** @@ -143,7 +144,7 @@ public function getIsFilterableInSearch() */ public function getIsUsedInGrid() { - return (bool)$this->getData(self::IS_USED_IN_GRID); + return (bool)$this->_getData(self::IS_USED_IN_GRID); } /** @@ -151,7 +152,7 @@ public function getIsUsedInGrid() */ public function getIsVisibleInGrid() { - return (bool)$this->getData(self::IS_VISIBLE_IN_GRID); + return (bool)$this->_getData(self::IS_VISIBLE_IN_GRID); } /** @@ -159,7 +160,7 @@ public function getIsVisibleInGrid() */ public function getIsFilterableInGrid() { - return (bool)$this->getData(self::IS_FILTERABLE_IN_GRID); + return (bool)$this->_getData(self::IS_FILTERABLE_IN_GRID); } /** @@ -170,7 +171,7 @@ public function getIsFilterableInGrid() */ public function setIsFilterableInSearch($isFilterableInSearch) { - return $this->getData(self::IS_FILTERABLE_IN_SEARCH, $isFilterableInSearch); + return $this->setData(self::IS_FILTERABLE_IN_SEARCH, $isFilterableInSearch); } /** @@ -178,7 +179,7 @@ public function setIsFilterableInSearch($isFilterableInSearch) */ public function getPosition() { - return $this->getData(self::POSITION); + return $this->_getData(self::POSITION); } /** @@ -197,7 +198,7 @@ public function setPosition($position) */ public function getIsSearchable() { - return $this->getData(self::IS_SEARCHABLE); + return $this->_getData(self::IS_SEARCHABLE); } /** @@ -216,7 +217,7 @@ public function setIsSearchable($isSearchable) */ public function getIsVisibleInAdvancedSearch() { - return $this->getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH); + return $this->_getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH); } /** @@ -235,7 +236,7 @@ public function setIsVisibleInAdvancedSearch($isVisibleInAdvancedSearch) */ public function getIsComparable() { - return $this->getData(self::IS_COMPARABLE); + return $this->_getData(self::IS_COMPARABLE); } /** @@ -254,7 +255,7 @@ public function setIsComparable($isComparable) */ public function getIsUsedForPromoRules() { - return $this->getData(self::IS_USED_FOR_PROMO_RULES); + return $this->_getData(self::IS_USED_FOR_PROMO_RULES); } /** @@ -273,7 +274,7 @@ public function setIsUsedForPromoRules($isUsedForPromoRules) */ public function getIsVisibleOnFront() { - return $this->getData(self::IS_VISIBLE_ON_FRONT); + return $this->_getData(self::IS_VISIBLE_ON_FRONT); } /** @@ -292,7 +293,7 @@ public function setIsVisibleOnFront($isVisibleOnFront) */ public function getUsedInProductListing() { - return $this->getData(self::USED_IN_PRODUCT_LISTING); + return $this->_getData(self::USED_IN_PRODUCT_LISTING); } /** @@ -311,7 +312,7 @@ public function setUsedInProductListing($usedInProductListing) */ public function getIsVisible() { - return $this->getData(self::IS_VISIBLE); + return $this->_getData(self::IS_VISIBLE); } /** @@ -332,7 +333,7 @@ public function setIsVisible($isVisible) */ public function getScope() { - $scope = $this->getData(self::KEY_IS_GLOBAL); + $scope = $this->_getData(self::KEY_IS_GLOBAL); if ($scope == self::SCOPE_GLOBAL) { return self::SCOPE_GLOBAL_TEXT; } elseif ($scope == self::SCOPE_WEBSITE) { diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php index 19587ce56f592..2613b7865acf9 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Category\Attribute\Backend; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; + /** * Catalog category image attribute backend model * @@ -68,7 +71,8 @@ public function __construct( /** * Gets image name from $value array. - * Will return empty string in a case when $value is not an array + * + * Will return empty string in a case when $value is not an array. * * @param array $value Attribute value * @return string @@ -83,8 +87,28 @@ private function getUploadedImageName($value) } /** - * Avoiding saving potential upload data to DB - * Will set empty image attribute value if image was not uploaded + * Check that image name exists in catalog/category directory and return new image name if it already exists. + * + * @param string $imageName + * @return string + */ + private function checkUniqueImageName(string $imageName): string + { + $imageUploader = $this->getImageUploader(); + $mediaDirectory = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $imageAbsolutePath = $mediaDirectory->getAbsolutePath( + $imageUploader->getBasePath() . DIRECTORY_SEPARATOR . $imageName + ); + + $imageName = Uploader::getNewFilename($imageAbsolutePath); + + return $imageName; + } + + /** + * Avoiding saving potential upload data to DB. + * + * Will set empty image attribute value if image was not uploaded. * * @param \Magento\Framework\DataObject $object * @return $this @@ -96,16 +120,19 @@ public function beforeSave($object) $value = $object->getData($attributeName); if ($imageName = $this->getUploadedImageName($value)) { + $imageName = $this->checkUniqueImageName($imageName); $object->setData($this->additionalData . $attributeName, $value); $object->setData($attributeName, $imageName); } elseif (!is_string($value)) { - $object->setData($attributeName, ''); + $object->setData($attributeName, null); } return parent::beforeSave($object); } /** + * Get image uploader. + * * @return \Magento\Catalog\Model\ImageUploader * * @deprecated 101.0.0 diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php new file mode 100644 index 0000000000000..215fe1c19bd8d --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute\Backend; + +use Magento\Catalog\Model\AbstractModel; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; + +/** + * Allows to select a layout file to merge when rendering the category's page. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + * + * @param AbstractModel|Category $forModel + */ + protected function listAvailableValues(AbstractModel $forModel): array + { + return $this->manager->fetchAvailableFiles($forModel); + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php new file mode 100644 index 0000000000000..a26ccd7989a05 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Framework\App\Area; +use Magento\Framework\DataObject; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * Manage available layout updates for categories. + */ +class LayoutUpdateManager +{ + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param LayoutProcessorFactory $layoutProcessorFactory + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + LayoutProcessorFactory $layoutProcessorFactory + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->layoutProcessorFactory = $layoutProcessorFactory; + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * Fetch list of available files/handles for the category. + * + * @param CategoryInterface $category + * @return string[] + */ + public function fetchAvailableFiles(CategoryInterface $category): array + { + if (!$category->getId()) { + return []; + } + + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($category) { + preg_match( + '/^catalog\_category\_view\_selectable\_' .$category->getId() .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * Extract custom layout attribute value. + * + * @param CategoryInterface $category + * @return mixed + */ + private function extractAttributeValue(CategoryInterface $category) + { + if ($category instanceof Category && !$category->hasData(CategoryInterface::CUSTOM_ATTRIBUTES)) { + return $category->getData('custom_layout_update_file'); + } + if ($attr = $category->getCustomAttribute('custom_layout_update_file')) { + return $attr->getValue(); + } + + return null; + } + + /** + * Extract selected custom layout settings. + * + * If no update is selected none will apply. + * + * @param CategoryInterface $category + * @param DataObject $intoSettings + * @return void + */ + public function extractCustomSettings(CategoryInterface $category, DataObject $intoSettings) + { + if ($category->getId() && $value = $this->extractAttributeValue($category)) { + $handles = $intoSettings->getPageLayoutHandles() ?? []; + $handles = array_merge_recursive( + $handles, + ['selectable' => $category->getId() . '_' . $value] + ); + $intoSettings->setPageLayoutHandles($handles); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php index 1890ea0f7d99e..20ea899a3d0d7 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php new file mode 100644 index 0000000000000..1c307220aa9f8 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Attribute\Source; + +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Catalog\Model\Attribute\Source\AbstractLayoutUpdate; + +/** + * List of layout updates available for a category. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + */ + protected function listAvailableOptions(CustomAttributesDataInterface $entity): array + { + return $this->manager->fetchAvailableFiles($entity); + } +} diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php index 97bc00bc7dd64..4dda2fe5786e3 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Sortby.php @@ -40,7 +40,7 @@ protected function _getCatalogConfig() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllOptions() { @@ -49,7 +49,7 @@ public function getAllOptions() foreach ($this->_getCatalogConfig()->getAttributesUsedForSortBy() as $attribute) { $this->_options[] = [ 'label' => __($attribute['frontend_label']), - 'value' => $attribute['attribute_code'], + 'value' => $attribute['attribute_code'] ]; } } diff --git a/app/code/Magento/Catalog/Model/Category/Authorization.php b/app/code/Magento/Catalog/Model/Category/Authorization.php new file mode 100644 index 0000000000000..f2e988aef97d3 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/Authorization.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; + +/** + * Additional authorization for category operations. + */ +class Authorization +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var CategoryFactory + */ + private $categoryFactory; + + /** + * @param AuthorizationInterface $authorization + * @param CategoryFactory $factory + */ + public function __construct(AuthorizationInterface $authorization, CategoryFactory $factory) + { + $this->authorization = $authorization; + $this->categoryFactory = $factory; + } + + /** + * Extract attribute value from the model. + * + * @param CategoryModel $category + * @param AttributeInterface $attr + * @throws \RuntimeException When no new value is present. + * @return mixed + */ + private function extractAttributeValue(CategoryModel $category, AttributeInterface $attr) + { + if ($category->hasData($attr->getAttributeCode())) { + $newValue = $category->getData($attr->getAttributeCode()); + } elseif ($category->hasData(CategoryModel::CUSTOM_ATTRIBUTES) + && $attrValue = $category->getCustomAttribute($attr->getAttributeCode()) + ) { + $newValue = $attrValue->getValue(); + } else { + throw new \RuntimeException('New value is not set'); + } + + if (empty($newValue) + || ($attr->getBackend() instanceof LayoutUpdate + && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE) + ) + ) { + $newValue = null; + } + + return $newValue; + } + + /** + * Find values to compare the new one. + * + * @param AttributeInterface $attribute + * @param array|null $oldCategory + * @return mixed[] + */ + private function fetchOldValue(AttributeInterface $attribute, $oldCategory): array + { + $oldValues = [null]; + $attrCode = $attribute->getAttributeCode(); + if ($oldCategory) { + //New value must match saved value exactly + $oldValues = [!empty($oldCategory[$attrCode]) ? $oldCategory[$attrCode] : null]; + if (empty($oldValues[0])) { + $oldValues[0] = null; + } + } else { + //New value can be either empty or default value. + $oldValues[] = $attribute->getDefaultValue(); + } + + return $oldValues; + } + + /** + * Determine whether a category has design properties changed. + * + * @param CategoryModel $category + * @param array|null $oldCategory + * @return bool + */ + private function hasChanges(CategoryModel $category, $oldCategory): bool + { + foreach ($category->getDesignAttributes() as $designAttribute) { + $oldValues = $this->fetchOldValue($designAttribute, $oldCategory); + try { + $newValue = $this->extractAttributeValue($category, $designAttribute); + } catch (\RuntimeException $exception) { + //No new value + continue; + } + + if (!in_array($newValue, $oldValues, true)) { + return true; + } + } + + return false; + } + + /** + * Authorize saving of a category. + * + * @throws AuthorizationException + * @throws NoSuchEntityException When a category with invalid ID given. + * @param CategoryInterface|CategoryModel $category + * @return void + */ + public function authorizeSavingOf(CategoryInterface $category) + { + if (!$this->authorization->isAllowed('Magento_Catalog::edit_category_design')) { + $oldData = null; + if ($category->getId()) { + if ($category->getOrigData()) { + $oldData = $category->getOrigData(); + } else { + /** @var CategoryModel $savedCategory */ + $savedCategory = $this->categoryFactory->create(); + $savedCategory->load($category->getId()); + if (!$savedCategory->getName()) { + throw NoSuchEntityException::singleField('id', $category->getId()); + } + $oldData = $savedCategory->getData(); + } + } + + if ($this->hasChanges($category, $oldData)) { + throw new AuthorizationException(__('Not allowed to edit the category\'s design attributes')); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index a1ccfc9f20993..2cda72b620879 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -15,6 +15,7 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; @@ -24,6 +25,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Framework\AuthorizationInterface; /** * Class DataProvider @@ -31,6 +33,7 @@ * @api * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * @since 101.0.0 */ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider @@ -63,6 +66,8 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider 'size' => 'multiline_count', ]; + private $boolMetaProperties = ['visible', 'required']; + /** * Form element mapping * @@ -145,6 +150,11 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ private $fileInfo; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * DataProvider constructor * @@ -160,6 +170,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param CategoryFactory $categoryFactory * @param array $meta * @param array $data + * @param AuthorizationInterface|null $authorization * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -174,7 +185,8 @@ public function __construct( \Magento\Framework\App\RequestInterface $request, CategoryFactory $categoryFactory, array $meta = [], - array $data = [] + array $data = [], + AuthorizationInterface $authorization = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -184,6 +196,7 @@ public function __construct( $this->storeManager = $storeManager; $this->request = $request; $this->categoryFactory = $categoryFactory; + $this->authorization = $authorization ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); } @@ -207,6 +220,8 @@ public function getMeta() } /** + * Add 'use default checkbox' to attributes that can have it. + * * @param Category $category * @param array $meta * @return array @@ -274,11 +289,20 @@ public function prepareMeta($meta) */ private function prepareFieldsMeta($fieldsMap, $fieldsMeta) { + $canEditDesign = $this->authorization->isAllowed('Magento_Catalog::edit_category_design'); + $result = []; foreach ($fieldsMap as $fieldSet => $fields) { foreach ($fields as $field) { if (isset($fieldsMeta[$field])) { - $result[$fieldSet]['children'][$field]['arguments']['data']['config'] = $fieldsMeta[$field]; + $config = $fieldsMeta[$field]; + if (($fieldSet === 'design' || $fieldSet === 'schedule_design_update') && !$canEditDesign) { + $config['required'] = 1; + $config['disabled'] = 1; + $config['serviceDisabled'] = true; + } + + $result[$fieldSet]['children'][$field]['arguments']['data']['config'] = $config; } } } @@ -329,13 +353,26 @@ public function getAttributesMeta(Type $entityType) foreach ($this->metaProperties as $metaName => $origName) { $value = $attribute->getDataUsingMethod($origName); $meta[$code][$metaName] = $value; + if (in_array($metaName, $this->boolMetaProperties, true)) { + $meta[$code][$metaName] = (bool)$meta[$code][$metaName]; + } if ('frontend_input' === $origName) { $meta[$code]['formElement'] = isset($this->formElement[$value]) ? $this->formElement[$value] : $value; } if ($attribute->usesSource()) { - $meta[$code]['options'] = $attribute->getSource()->getAllOptions(); + $source = $attribute->getSource(); + $currentCategory = $this->getCurrentCategory(); + if ($source instanceof SpecificSourceInterface && $currentCategory) { + $options = $source->getOptionsFor($currentCategory); + } else { + $options = $source->getAllOptions(); + } + foreach ($options as &$option) { + $option['__disableTmpl'] = true; + } + $meta[$code]['options'] = $options; } } @@ -480,6 +517,12 @@ protected function filterFields($categoryData) private function convertValues($category, $categoryData) { foreach ($category->getAttributes() as $attributeCode => $attribute) { + if ($attributeCode === 'custom_layout_update_file') { + if (!empty($categoryData['custom_layout_update'])) { + $categoryData['custom_layout_update_file'] + = \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML; + } + } if (!isset($categoryData[$attributeCode])) { continue; } @@ -494,8 +537,8 @@ private function convertValues($category, $categoryData) $categoryData[$attributeCode][0]['name'] = $fileName; $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode); - $categoryData['image'][0]['size'] = isset($stat) ? $stat['size'] : 0; - $categoryData['image'][0]['type'] = $mime; + $categoryData[$attributeCode][0]['size'] = isset($stat) ? $stat['size'] : 0; + $categoryData[$attributeCode][0]['type'] = $mime; } } } @@ -521,6 +564,8 @@ public function getDefaultMetaData($result) } /** + * List form field sets and fields. + * * @return array * @since 101.0.0 */ @@ -565,6 +610,7 @@ protected function getFieldsMap() 'custom_design', 'page_layout', 'custom_layout_update', + 'custom_layout_update_file' ], 'schedule_design_update' => [ 'custom_design_from', diff --git a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php index f22c6903a230c..fd80d754a5b57 100644 --- a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php @@ -60,7 +60,7 @@ public function execute($entity, $arguments = []) if ($dtoCategoryLinks !== null) { $hydrator = $this->hydratorPool->getHydrator(CategoryLinkInterface::class); $dtoCategoryLinks = array_map(function ($categoryLink) use ($hydrator) { - return $hydrator->extract($categoryLink) ; + return $hydrator->extract($categoryLink); }, $dtoCategoryLinks); $processLinks = $this->mergeCategoryLinks($dtoCategoryLinks, $modelCategoryLinks); } else { @@ -106,27 +106,19 @@ private function getCategoryLinksPositions($entity) */ private function mergeCategoryLinks($newCategoryPositions, $oldCategoryPositions) { - $result = []; if (empty($newCategoryPositions)) { - return $result; + return []; } + $categoryPositions = array_combine(array_column($oldCategoryPositions, 'category_id'), $oldCategoryPositions); foreach ($newCategoryPositions as $newCategoryPosition) { - $key = array_search( - $newCategoryPosition['category_id'], - array_column($oldCategoryPositions, 'category_id') - ); - - if ($key === false) { - $result[] = $newCategoryPosition; - } elseif (isset($oldCategoryPositions[$key]) - && $oldCategoryPositions[$key]['position'] != $newCategoryPosition['position'] - ) { - $result[] = $newCategoryPositions[$key]; - unset($oldCategoryPositions[$key]); + $categoryId = $newCategoryPosition['category_id']; + if (!isset($categoryPositions[$categoryId])) { + $categoryPositions[$categoryId] = ['category_id' => $categoryId]; } + $categoryPositions[$categoryId]['position'] = $newCategoryPosition['position']; } - $result = array_merge($result, $oldCategoryPositions); + $result = array_values($categoryPositions); return $result; } diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 97941f2d23b9f..edaac39864c5a 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -52,6 +52,8 @@ public function getPositions(int $categoryId) $categoryId )->order( 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC ); return array_flip($connection->fetchCol($select)); diff --git a/app/code/Magento/Catalog/Model/Category/StoreCategories.php b/app/code/Magento/Catalog/Model/Category/StoreCategories.php new file mode 100644 index 0000000000000..c92f8d44c6887 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Category/StoreCategories.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Store\Api\GroupRepositoryInterface; + +/** + * Fetcher for associated with store group categories. + */ +class StoreCategories +{ + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @var GroupRepositoryInterface + */ + private $groupRepository; + + /** + * @param CategoryRepositoryInterface $categoryRepository + * @param GroupRepositoryInterface $groupRepository + */ + public function __construct( + CategoryRepositoryInterface $categoryRepository, + GroupRepositoryInterface $groupRepository + ) { + $this->categoryRepository = $categoryRepository; + $this->groupRepository = $groupRepository; + } + + /** + * Get all category ids for store. + * + * @param int|null $storeGroupId + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getCategoryIds($storeGroupId = null): array + { + $rootCategoryId = $storeGroupId + ? $this->groupRepository->get($storeGroupId)->getRootCategoryId() + : Category::TREE_ROOT_ID; + /** @var Category $rootCategory */ + $rootCategory = $this->categoryRepository->get($rootCategoryId); + $categoriesIds = $rootCategory->getAllChildren(true); + + return (array) $categoriesIds; + } +} diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index 790ea6b921fbe..cab8e013d9ba1 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -15,6 +15,9 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +/** + * Class for getting category list. + */ class CategoryList implements CategoryListInterface { /** @@ -64,7 +67,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 505c729ac1001..3739b91d2348e 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -129,7 +129,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) */ public function get($categoryId, $storeId = null) { - $cacheKey = null !== $storeId ? $storeId : 'all'; + $cacheKey = $storeId ?? 'all'; if (!isset($this->instances[$categoryId][$cacheKey])) { /** @var Category $category */ $category = $this->categoryFactory->create(); diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php index 227821463b7f0..5dce940308a4f 100644 --- a/app/code/Magento/Catalog/Model/Config.php +++ b/app/code/Magento/Catalog/Model/Config.php @@ -4,9 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model; + use Magento\Framework\Serialize\SerializerInterface; /** @@ -382,7 +381,7 @@ public function getProductTypeName($id) $this->loadProductTypes(); - return isset($this->_productTypesById[$id]) ? $this->_productTypesById[$id] : false; + return $this->_productTypesById[$id] ?? false; } /** @@ -407,7 +406,7 @@ public function getSourceOptionId($source, $value) */ public function getProductAttributes() { - if (is_null($this->_productAttributes)) { + if ($this->_productAttributes === null) { $this->_productAttributes = array_keys($this->getAttributesUsedInProductListing()); } return $this->_productAttributes; @@ -430,7 +429,7 @@ protected function _getResource() */ public function getAttributesUsedInProductListing() { - if (is_null($this->_usedInProductListing)) { + if ($this->_usedInProductListing === null) { $this->_usedInProductListing = []; $entityType = \Magento\Catalog\Model\Product::ENTITY; $attributesData = $this->_getResource()->setStoreId($this->getStoreId())->getAttributesUsedInListing(); @@ -453,7 +452,7 @@ public function getAttributesUsedInProductListing() */ public function getAttributesUsedForSortBy() { - if (is_null($this->_usedForSortBy)) { + if ($this->_usedForSortBy === null) { $this->_usedForSortBy = []; $entityType = \Magento\Catalog\Model\Product::ENTITY; $attributesData = $this->_getResource()->getAttributesUsedForSortBy(); @@ -491,6 +490,10 @@ public function getAttributeUsedForSortByArray() */ public function getProductListDefaultSortBy($store = null) { - return $this->_scopeConfig->getValue(self::XML_PATH_LIST_DEFAULT_SORT_BY, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return $this->_scopeConfig->getValue( + self::XML_PATH_LIST_DEFAULT_SORT_BY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } } diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php index e2b0a91574021..d7d342f357519 100644 --- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php +++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php @@ -5,10 +5,14 @@ */ namespace Magento\Catalog\Model\Config\CatalogClone\Media; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; + /** * Clone model for media images related config fields * * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image extends \Magento\Framework\App\Config\Value { @@ -26,6 +30,11 @@ class Image extends \Magento\Framework\App\Config\Value */ protected $_attributeCollectionFactory; + /** + * @var Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +45,9 @@ class Image extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param Escaper|null $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,8 +58,10 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + Escaper $escaper = null ) { + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); @@ -74,7 +88,7 @@ public function getPrefixes() /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ $prefixes[] = [ 'field' => $attribute->getAttributeCode() . '_', - 'label' => $attribute->getFrontend()->getLabel(), + 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), ]; } diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php index bd7cdabb40856..8185b50379618 100644 --- a/app/code/Magento/Catalog/Model/Design.php +++ b/app/code/Magento/Catalog/Model/Design.php @@ -5,6 +5,11 @@ */ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager as CategoryLayoutManager; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager as ProductLayoutManager; +use Magento\Framework\App\ObjectManager; +use \Magento\Framework\TranslateInterface; + /** * Catalog Custom Category design Model * @@ -12,6 +17,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Design extends \Magento\Framework\Model\AbstractModel { @@ -31,6 +37,21 @@ class Design extends \Magento\Framework\Model\AbstractModel */ protected $_localeDate; + /** + * @var TranslateInterface + */ + private $translator; + + /** + * @var CategoryLayoutManager + */ + private $categoryLayoutUpdates; + + /** + * @var ProductLayoutManager + */ + private $productLayoutUpdates; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -39,6 +60,10 @@ class Design extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param TranslateInterface|null $translator + * @param CategoryLayoutManager|null $categoryLayoutManager + * @param ProductLayoutManager|null $productLayoutManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -47,10 +72,18 @@ public function __construct( \Magento\Framework\View\DesignInterface $design, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + TranslateInterface $translator = null, + CategoryLayoutManager $categoryLayoutManager = null, + ProductLayoutManager $productLayoutManager = null ) { $this->_localeDate = $localeDate; $this->_design = $design; + $this->translator = $translator ?? ObjectManager::getInstance()->get(TranslateInterface::class); + $this->categoryLayoutUpdates = $categoryLayoutManager + ?? ObjectManager::getInstance()->get(CategoryLayoutManager::class); + $this->productLayoutUpdates = $productLayoutManager + ?? ObjectManager::getInstance()->get(ProductLayoutManager::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -63,6 +96,7 @@ public function __construct( public function applyCustomDesign($design) { $this->_design->setDesignTheme($design); + $this->translator->loadData(null, true); return $this; } @@ -128,6 +162,11 @@ protected function _extractSettings($object) )->setLayoutUpdates( (array)$object->getCustomLayoutUpdate() ); + if ($object instanceof \Magento\Catalog\Model\Category) { + $this->categoryLayoutUpdates->extractCustomSettings($object, $settings); + } elseif ($object instanceof \Magento\Catalog\Model\Product) { + $this->productLayoutUpdates->extractCustomSettings($object, $settings); + } } return $settings; } diff --git a/app/code/Magento/Catalog/Model/ImageExtractor.php b/app/code/Magento/Catalog/Model/ImageExtractor.php index 7888d8de1c2ff..f13d682f505cd 100644 --- a/app/code/Magento/Catalog/Model/ImageExtractor.php +++ b/app/code/Magento/Catalog/Model/ImageExtractor.php @@ -32,12 +32,16 @@ public function process(\DOMElement $mediaNode, $mediaParentTag) continue; } $attributeTagName = $attribute->tagName; - if ($attributeTagName === 'background') { - $nodeValue = $this->processImageBackground($attribute->nodeValue); - } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { - $nodeValue = intval($attribute->nodeValue); + if ((bool)$attribute->getAttribute('xsi:nil') !== true) { + if ($attributeTagName === 'background') { + $nodeValue = $this->processImageBackground($attribute->nodeValue); + } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { + $nodeValue = (int)$attribute->nodeValue; + } else { + $nodeValue = $attribute->nodeValue; + } } else { - $nodeValue = $attribute->nodeValue; + $nodeValue = null; } $result[$mediaParentTag][$moduleNameImage][Image::MEDIA_TYPE_CONFIG_NODE][$imageId][$attribute->tagName] = $nodeValue; diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index 6aa76ca8c1e43..852d6ba5c3efc 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model; +use Magento\Framework\File\Uploader; + /** * Catalog image uploader */ @@ -64,6 +68,13 @@ class ImageUploader */ protected $allowedExtensions; + /** + * List of allowed image mime types + * + * @var string[] + */ + private $allowedMimeTypes; + /** * ImageUploader constructor * @@ -75,6 +86,7 @@ class ImageUploader * @param string $baseTmpPath * @param string $basePath * @param string[] $allowedExtensions + * @param string[] $allowedMimeTypes */ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, @@ -84,7 +96,8 @@ public function __construct( \Psr\Log\LoggerInterface $logger, $baseTmpPath, $basePath, - $allowedExtensions + $allowedExtensions, + $allowedMimeTypes = [] ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); @@ -94,6 +107,7 @@ public function __construct( $this->baseTmpPath = $baseTmpPath; $this->basePath = $basePath; $this->allowedExtensions = $allowedExtensions; + $this->allowedMimeTypes = $allowedMimeTypes; } /** @@ -153,7 +167,7 @@ public function getBasePath() } /** - * Retrieve base path + * Retrieve allowed extensions * * @return string[] */ @@ -189,7 +203,14 @@ public function moveFileFromTmp($imageName) $baseTmpPath = $this->getBaseTmpPath(); $basePath = $this->getBasePath(); - $baseImagePath = $this->getFilePath($basePath, $imageName); + $baseImagePath = $this->getFilePath( + $basePath, + Uploader::getNewFileName( + $this->mediaDirectory->getAbsolutePath( + $this->getFilePath($basePath, $imageName) + ) + ) + ); $baseTmpImagePath = $this->getFilePath($baseTmpPath, $imageName); try { @@ -218,6 +239,7 @@ public function moveFileFromTmp($imageName) * @return string[] * * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Exception */ public function saveFileToTmpDir($fileId) { @@ -227,7 +249,9 @@ public function saveFileToTmpDir($fileId) $uploader = $this->uploaderFactory->create(['fileId' => $fileId]); $uploader->setAllowedExtensions($this->getAllowedExtensions()); $uploader->setAllowRenameFiles(true); - + if (!$uploader->checkMimeType($this->allowedMimeTypes)) { + throw new \Magento\Framework\Exception\LocalizedException(__('File validation failed.')); + } $result = $uploader->save($this->mediaDirectory->getAbsolutePath($baseTmpPath)); unset($result['path']); diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php index 24d81f0054c5a..16f08ce59e91b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Indexer\Category\Flat; use Magento\Framework\App\ResourceConnection; @@ -113,7 +111,7 @@ public function getColumns() public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) { if (is_string($storeId)) { - $storeId = intval($storeId); + $storeId = (int)$storeId; } $suffix = sprintf('store_%d', $storeId); @@ -139,7 +137,9 @@ protected function getFlatTableStructure($tableName) //Adding columns foreach ($this->getColumns() as $fieldName => $fieldProp) { $default = $fieldProp['default']; - if ($fieldProp['type'][0] == \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP && $default == 'CURRENT_TIMESTAMP') { + if ($fieldProp['type'][0] == \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP + && $default == 'CURRENT_TIMESTAMP' + ) { $default = \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT; } $table->addColumn( @@ -369,25 +369,55 @@ protected function getAttributeValues($entityIds, $storeId) } $values = []; - foreach ($entityIds as $entityId) { - $values[$entityId] = []; + $linkIds = $this->getLinkIds($entityIds); + foreach ($linkIds as $linkId) { + $values[$linkId] = []; } + $attributes = $this->getAttributes(); $attributesType = ['varchar', 'int', 'decimal', 'text', 'datetime']; + $linkField = $this->getCategoryMetadata()->getLinkField(); foreach ($attributesType as $type) { foreach ($this->getAttributeTypeValues($type, $entityIds, $storeId) as $row) { - if (isset($row[$this->getCategoryMetadata()->getLinkField()]) && isset($row['attribute_id'])) { + if (isset($row[$linkField]) && isset($row['attribute_id'])) { $attributeId = $row['attribute_id']; if (isset($attributes[$attributeId])) { $attributeCode = $attributes[$attributeId]['attribute_code']; - $values[$row[$this->getCategoryMetadata()->getLinkField()]][$attributeCode] = $row['value']; + $values[$row[$linkField]][$attributeCode] = $row['value']; } } } } + return $values; } + /** + * Translate entity ids into link ids + * + * Used for rows with no EAV attributes set. + * + * @param array $entityIds + * @return array + */ + private function getLinkIds(array $entityIds) + { + $linkField = $this->getCategoryMetadata()->getLinkField(); + if ($linkField === 'entity_id') { + return $entityIds; + } + + $select = $this->connection->select()->from( + ['e' => $this->connection->getTableName($this->getTableName('catalog_category_entity'))], + [$linkField] + )->where( + 'e.entity_id IN (?)', + $entityIds + ); + + return $this->connection->fetchCol($select); + } + /** * Return attribute values for given entities and store of specific attribute type * diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php index eeac2e80af97e..64a8f930d83ee 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php @@ -64,18 +64,22 @@ protected function populateFlatTables(array $stores) } /** @TODO Do something with chunks */ $categoriesIdsChunks = array_chunk($categoriesIds[$store->getRootCategoryId()], 500); + foreach ($categoriesIdsChunks as $categoriesIdsChunk) { $attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId()); + $linkField = $this->categoryMetadata->getLinkField(); + $data = []; foreach ($categories[$store->getRootCategoryId()] as $category) { - if (!isset($attributesData[$category[$this->categoryMetadata->getLinkField()]])) { + if (!isset($attributesData[$category[$linkField]])) { continue; } $category['store_id'] = $store->getId(); $data[] = $this->prepareValuesToInsert( - array_merge($category, $attributesData[$category[$this->categoryMetadata->getLinkField()]]) + array_merge($category, $attributesData[$category[$linkField]]) ); } + $this->connection->insertMultiple( $this->addTemporaryTableSuffix($this->getMainStoreTable($store->getId())), $data diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php index 2368c27e02d72..bc17d731f04c0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php @@ -69,22 +69,24 @@ public function reindex(array $entityIds = [], $useTempTable = false) $categoriesIdsChunk = $this->filterIdsByStore($categoriesIdsChunk, $store); $attributesData = $this->getAttributeValues($categoriesIdsChunk, $store->getId()); + $linkField = $this->categoryMetadata->getLinkField(); $data = []; foreach ($categoriesIdsChunk as $categoryId) { - if (!isset($attributesData[$categoryId])) { - continue; - } - try { $category = $this->categoryRepository->get($categoryId); } catch (NoSuchEntityException $e) { continue; } + $categoryData = $category->getData(); + if (!isset($attributesData[$categoryData[$linkField]])) { + continue; + } + $data[] = $this->prepareValuesToInsert( array_merge( - $category->getData(), - $attributesData[$categoryId], + $categoryData, + $attributesData[$categoryData[$linkField]], ['store_id' => $store->getId()] ) ); diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index c7ddb14a7649d..a944bdb29d7f4 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -4,13 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Indexer\Category\Product; -use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Class AbstractAction @@ -39,27 +43,28 @@ abstract class AbstractAction /** * Suffix for table to show it is temporary + * @deprecated */ const TEMPORARY_TABLE_SUFFIX = '_tmp'; /** * Cached non anchor categories select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $nonAnchorSelects = []; /** * Cached anchor categories select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $anchorSelects = []; /** * Cached all product select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $productsSelects = []; @@ -103,6 +108,11 @@ abstract class AbstractAction */ protected $metadataPool; + /** + * @var TableMaintainer + */ + protected $tableMaintainer; + /** * @var string * @since 101.0.0 @@ -114,24 +124,35 @@ abstract class AbstractAction */ private $queryGenerator; + /** + * Current store id. + * @var int + */ + private $currentStoreId = 0; + /** * @param ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Config $config * @param QueryGenerator $queryGenerator + * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Config $config, - QueryGenerator $queryGenerator = null + QueryGenerator $queryGenerator = null, + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); $this->storeManager = $storeManager; $this->config = $config; - $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(QueryGenerator::class); + $this->queryGenerator = $queryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); + $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); } /** @@ -150,6 +171,7 @@ protected function reindex() { foreach ($this->storeManager->getStores() as $store) { if ($this->getPathFromCategoryId($store->getRootCategoryId())) { + $this->currentStoreId = $store->getId(); $this->reindexRootCategory($store); $this->reindexAnchorCategories($store); $this->reindexNonAnchorCategories($store); @@ -175,6 +197,7 @@ protected function getTable($table) * The name is switched between 'catalog_category_product_index' and 'catalog_category_product_index_replica' * * @return string + * @deprecated */ protected function getMainTable() { @@ -185,12 +208,26 @@ protected function getMainTable() * Return temporary index table name * * @return string + * @deprecated */ protected function getMainTmpTable() { - return $this->useTempTable ? $this->getTable( - self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX - ) : $this->getMainTable(); + return $this->useTempTable + ? $this->getTable(self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX) + : $this->getMainTable(); + } + + /** + * Return index table name + * + * @param int $storeId + * @return string + */ + protected function getIndexTable($storeId) + { + return $this->useTempTable + ? $this->tableMaintainer->getMainReplicaTable($storeId) + : $this->tableMaintainer->getMainTable($storeId); } /** @@ -218,24 +255,25 @@ protected function getPathFromCategoryId($categoryId) /** * Retrieve select for reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface */ - protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) + protected function getNonAnchorCategoriesSelect(Store $store) { if (!isset($this->nonAnchorSelects[$store->getId()])) { $statusAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'status' )->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); $rootPath = $this->getPathFromCategoryId($store->getRootCategoryId()); - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); $select = $this->connection->select()->from( ['cc' => $this->getTable('catalog_category_entity')], @@ -304,12 +342,65 @@ protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $stor ] ); + $this->addFilteringByChildProductsToSelect($select, $store); + $this->nonAnchorSelects[$store->getId()] = $select; } return $this->nonAnchorSelects[$store->getId()]; } + /** + * Add filtering by child products to select + * + * It's used for correct handling of composite products. + * This method makes assumption that select already joins `catalog_product_entity` as `cpe`. + * + * @param Select $select + * @param Store $store + * @return void + * @throws \Exception when metadata not found for ProductInterface + */ + private function addFilteringByChildProductsToSelect(Select $select, Store $store) + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + + $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId(); + + $select->joinLeft( + ['relation' => $this->getTable('catalog_product_relation')], + 'cpe.' . $linkField . ' = relation.parent_id', + [] + )->joinLeft( + ['relation_product_entity' => $this->getTable('catalog_product_entity')], + 'relation.child_id = relation_product_entity.entity_id', + [] + )->joinLeft( + ['child_cpsd' => $this->getTable('catalog_product_entity_int')], + 'child_cpsd.' . $linkField . ' = '. 'relation_product_entity.' . $linkField + . ' AND child_cpsd.store_id = 0' + . ' AND child_cpsd.attribute_id = ' . $statusAttributeId, + [] + )->joinLeft( + ['child_cpss' => $this->getTable('catalog_product_entity_int')], + 'child_cpss.' . $linkField . ' = '. 'relation_product_entity.' . $linkField . '' + . ' AND child_cpss.attribute_id = child_cpsd.attribute_id' + . ' AND child_cpss.store_id = ' . $store->getId(), + [] + )->where( + 'relation.child_id IS NULL OR ' + . $this->connection->getIfNullSql('child_cpss.value', 'child_cpsd.value') . ' = ?', + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + )->group( + [ + 'cc.entity_id', + 'ccp.product_id', + 'visibility' + ] + ); + } + /** * Check whether select ranging is needed * @@ -323,16 +414,13 @@ protected function isRangingNeeded() /** * Return selects cut by min and max * - * @param \Magento\Framework\DB\Select $select + * @param Select $select * @param string $field * @param int $range - * @return \Magento\Framework\DB\Select[] + * @return Select[] */ - protected function prepareSelectsByRange( - \Magento\Framework\DB\Select $select, - $field, - $range = self::RANGE_CATEGORY_STEP - ) { + protected function prepareSelectsByRange(Select $select, $field, $range = self::RANGE_CATEGORY_STEP) + { if ($this->isRangingNeeded()) { $iterator = $this->queryGenerator->generate( $field, @@ -353,17 +441,17 @@ protected function prepareSelectsByRange( /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store) { $selects = $this->prepareSelectsByRange($this->getNonAnchorCategoriesSelect($store), 'entity_id'); foreach ($selects as $select) { $this->connection->query( $this->connection->insertFromSelect( $select, - $this->getMainTmpTable(), + $this->getIndexTable($store->getId()), ['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'], \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE ) @@ -374,10 +462,10 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) /** * Check if anchor select isset * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return bool */ - protected function hasAnchorSelect(\Magento\Store\Model\Store $store) + protected function hasAnchorSelect(Store $store) { return isset($this->anchorSelects[$store->getId()]); } @@ -385,19 +473,20 @@ protected function hasAnchorSelect(\Magento\Store\Model\Store $store) /** * Create anchor select * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface or CategoryInterface * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function createAnchorSelect(\Magento\Store\Model\Store $store) + protected function createAnchorSelect(Store $store) { $isAnchorAttributeId = $this->config->getAttribute( \Magento\Catalog\Model\Category::ENTITY, 'is_anchor' )->getId(); - $statusAttributeId = $this->config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId(); + $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); $rootCatIds = explode('/', $this->getPathFromCategoryId($store->getRootCategoryId())); @@ -405,12 +494,12 @@ protected function createAnchorSelect(\Magento\Store\Model\Store $store) $temporaryTreeTable = $this->makeTempCategoryTreeIndex(); - $productMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $categoryMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); $productLinkField = $productMetadata->getLinkField(); $categoryLinkField = $categoryMetadata->getLinkField(); - return $this->connection->select()->from( + $select = $this->connection->select()->from( ['cc' => $this->getTable('catalog_category_entity')], [] )->joinInner( @@ -492,6 +581,10 @@ protected function createAnchorSelect(\Magento\Store\Model\Store $store) 'visibility' => new \Zend_Db_Expr($this->connection->getIfNullSql('cpvs.value', 'cpvd.value')), ] ); + + $this->addFilteringByChildProductsToSelect($select, $store); + + return $select; } /** @@ -506,7 +599,7 @@ protected function getTemporaryTreeIndexTableName() if (empty($this->tempTreeIndexTableName)) { $this->tempTreeIndexTableName = $this->connection->getTableName('temp_catalog_category_tree_index') . '_' - . substr(md5(time() . rand(0, 999999999)), 0, 8); + . substr(sha1(time() . random_int(0, 999999999)), 0, 8); } return $this->tempTreeIndexTableName; @@ -545,6 +638,12 @@ protected function makeTempCategoryTreeIndex() ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY] ); + $temporaryTable->addIndex( + 'child_id', + ['child_id'], + ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_INDEX] + ); + // Drop the temporary table in case it already exists on this (persistent?) connection. $this->connection->dropTemporaryTable($temporaryName); $this->connection->createTemporaryTable($temporaryTable); @@ -555,41 +654,67 @@ protected function makeTempCategoryTreeIndex() } /** - * Populate the temporary category tree index table + * Populate the temporary category tree index table. * * @param string $temporaryName + * @return void * @since 101.0.0 */ protected function fillTempCategoryTreeIndex($temporaryName) { - // This finds all children (cc2) that descend from a parent (cc) by path. - // For example, cc.path may be '1/2', and cc2.path may be '1/2/3/4/5'. - $temporarySelect = $this->connection->select()->from( - ['cc' => $this->getTable('catalog_category_entity')], - ['parent_id' => 'entity_id'] - )->joinInner( - ['cc2' => $this->getTable('catalog_category_entity')], - 'cc2.path LIKE ' . $this->connection->getConcatSql( - [$this->connection->quoteIdentifier('cc.path'), $this->connection->quote('/%')] - ), - ['child_id' => 'entity_id'] + $isActiveAttributeId = $this->config->getAttribute( + \Magento\Catalog\Model\Category::ENTITY, + 'is_active' + )->getId(); + $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); + $categoryLinkField = $categoryMetadata->getLinkField(); + $selects = $this->prepareSelectsByRange( + $this->connection->select() + ->from( + ['c' => $this->getTable('catalog_category_entity')], + ['entity_id', 'path'] + )->joinInner( + ['ccacd' => $this->getTable('catalog_category_entity_int')], + 'ccacd.' . $categoryLinkField . ' = c.' . $categoryLinkField + . ' AND ccacd.store_id = 0' . ' AND ccacd.attribute_id = ' . $isActiveAttributeId, + [] + )->joinLeft( + ['ccacs' => $this->getTable('catalog_category_entity_int')], + 'ccacs.' . $categoryLinkField . ' = c.' . $categoryLinkField + . ' AND ccacs.attribute_id = ccacd.attribute_id AND ccacs.store_id = ' + . $this->currentStoreId, + [] + )->where( + $this->connection->getIfNullSql('ccacs.value', 'ccacd.value') . ' = ?', + 1 + ), + 'entity_id' ); - $this->connection->query( - $temporarySelect->insertFromSelect( - $temporaryName, - ['parent_id', 'child_id'] - ) - ); + foreach ($selects as $select) { + $values = []; + + foreach ($this->connection->fetchAll($select) as $category) { + foreach (explode('/', $category['path']) as $parentId) { + if ($parentId !== $category['entity_id']) { + $values[] = [$parentId, $category['entity_id']]; + } + } + } + + if (count($values) > 0) { + $this->connection->insertArray($temporaryName, ['parent_id', 'child_id'], $values); + } + } } /** * Retrieve select for reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select */ - protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store) + protected function getAnchorCategoriesSelect(Store $store) { if (!$this->hasAnchorSelect($store)) { $this->anchorSelects[$store->getId()] = $this->createAnchorSelect($store); @@ -600,10 +725,10 @@ protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store) /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store) { $selects = $this->prepareSelectsByRange($this->getAnchorCategoriesSelect($store), 'entity_id'); @@ -611,7 +736,7 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) $this->connection->query( $this->connection->insertFromSelect( $select, - $this->getMainTmpTable(), + $this->getIndexTable($store->getId()), ['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'], \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE ) @@ -622,22 +747,23 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) /** * Get select for all products * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface */ - protected function getAllProducts(\Magento\Store\Model\Store $store) + protected function getAllProducts(Store $store) { if (!isset($this->productsSelects[$store->getId()])) { $statusAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'status' )->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); $select = $this->connection->select()->from( @@ -726,10 +852,10 @@ protected function isIndexRootCategoryNeeded() /** * Reindex all products to root category * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store) { if ($this->isIndexRootCategoryNeeded()) { $selects = $this->prepareSelectsByRange( @@ -742,7 +868,7 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) $this->connection->query( $this->connection->insertFromSelect( $select, - $this->getMainTmpTable(), + $this->getIndexTable($store->getId()), ['category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'], \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE ) @@ -750,16 +876,4 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) } } } - - /** - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() - { - if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); - } - return $this->metadataPool; - } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php index e61acc2aa09e5..b12ffe1ac1f87 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -5,30 +5,41 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\BatchSizeManagementInterface; +use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\Indexer\BatchSizeManagementInterface + * @var BatchSizeManagementInterface */ private $batchSizeManagement; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; @@ -44,27 +55,35 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $activeTableSwitcher; + /** + * @var ProcessManager + */ + private $processManager; + /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Config $config + * @param StoreManagerInterface $storeManager + * @param Config $config * @param QueryGenerator|null $queryGenerator - * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param BatchSizeManagementInterface|null $batchSizeManagement + * @param BatchProviderInterface|null $batchProvider + * @param MetadataPool|null $metadataPool * @param int|null $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param ProcessManager $processManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, QueryGenerator $queryGenerator = null, - \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + BatchSizeManagementInterface $batchSizeManagement = null, + BatchProviderInterface $batchProvider = null, + MetadataPool $metadataPool = null, $batchRowsCount = null, - ActiveTableSwitcher $activeTableSwitcher = null + ActiveTableSwitcher $activeTableSwitcher = null, + ProcessManager $processManager = null ) { parent::__construct( $resource, @@ -72,186 +91,202 @@ public function __construct( $config, $queryGenerator ); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( - \Magento\Framework\Indexer\BatchSizeManagementInterface::class + BatchSizeManagementInterface::class ); $this->batchProvider = $batchProvider ?: $objectManager->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->metadataPool = $metadataPool ?: $objectManager->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); + $this->processManager = $processManager ?: $objectManager->get(ProcessManager::class); } /** - * Clear the table we'll be writing de-normalized data into - * to prevent archived data getting in the way of actual data. + * Create the store tables * * @return void */ - private function clearCurrentTable() + private function createTables() { - $this->connection->delete( - $this->activeTableSwitcher - ->getAdditionalTableName($this->getMainTable()) - ); + foreach ($this->storeManager->getStores() as $store) { + $this->tableMaintainer->createTablesForStore((int)$store->getId()); + } } /** - * Refresh entities index + * Truncates the replica tables * - * @return $this + * @return void + */ + private function clearReplicaTables() + { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); + } + } + + /** + * Switches the active table + * + * @return void + */ + private function switchTables() + { + $tablesToSwitch = []; + foreach ($this->storeManager->getStores() as $store) { + $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); + } + $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); + } + + /** + * @inheritdoc */ public function execute() { - $this->clearCurrentTable(); + $this->createTables(); + $this->clearReplicaTables(); $this->reindex(); - $this->activeTableSwitcher->switchTable($this->connection, [$this->getMainTable()]); + $this->switchTables(); + return $this; } /** - * Return select for remove unnecessary data + * Run reindexation * - * @return \Magento\Framework\DB\Select - * @deprecated 102.0.1 Not needed anymore. + * @return void */ - protected function getSelectUnnecessaryData() + protected function reindex() { - return $this->connection->select()->from( - $this->getMainTable(), - [] - )->joinLeft( - ['t' => $this->getMainTable()], - $this->getMainTable() . - '.category_id = t.category_id AND ' . - $this->getMainTable() . - '.store_id = t.store_id AND ' . - $this->getMainTable() . - '.product_id = t.product_id', - [] - )->where( - 't.category_id IS NULL' - ); + $userFunctions = []; + + foreach ($this->storeManager->getStores() as $store) { + if ($this->getPathFromCategoryId($store->getRootCategoryId())) { + $userFunctions[$store->getId()] = function () use ($store) { + return $this->reindexStore($store); + }; + } + } + + $this->processManager->execute($userFunctions); } /** - * Remove unnecessary data + * Execute indexation by store * - * @return void - * - * @deprecated 102.0.1 Not needed anymore. + * @param Store $store */ - protected function removeUnnecessaryData() + private function reindexStore($store) { - //Called for backwards compatibility. - $this->getSelectUnnecessaryData(); - //This method is useless, - //left it here just in case somebody's using it in child classes. + $this->reindexRootCategory($store); + $this->reindexAnchorCategories($store); + $this->reindexNonAnchorCategories($store); } /** - * Publish data from tmp to index + * Publish data from tmp to replica table * + * @param Store $store * @return void */ - protected function publishData() + private function publishData($store) { - $select = $this->connection->select()->from($this->getMainTmpTable()); - $columns = array_keys($this->connection->describeTable($this->getMainTable())); - $tableName = $this->activeTableSwitcher->getAdditionalTableName($this->getMainTable()); + $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $columns = array_keys( + $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) + ); + $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); $this->connection->query( $this->connection->insertFromSelect( $select, $tableName, $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * Clear all index data - * - * @return void - */ - protected function clearTmpData() - { - $this->connection->delete($this->getMainTmpTable()); - } - - /** - * {@inheritdoc} + * @inheritdoc */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store) { if ($this->isIndexRootCategoryNeeded()) { - $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)'); + $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); } } /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store) { - $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)'); + $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store) { - $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)'); + $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } /** * Reindex categories using given SQL select and condition. * - * @param \Magento\Framework\DB\Select $basicSelect + * @param Select $basicSelect * @param string $whereCondition + * @param Store $store * @return void */ - private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition) + private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store) { - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $columns = array_keys($this->connection->describeTable($this->getMainTmpTable())); + $this->tableMaintainer->createMainTmpTable((int)$store->getId()); + + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $columns = array_keys( + $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) + ); $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); - $batches = $this->batchProvider->getBatches( - $this->connection, - $entityMetadata->getEntityTable(), + + $select = $this->connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->prepareSelectsByRange( + $select, $entityMetadata->getIdentifierField(), - $this->batchRowsCount + (int)$this->batchRowsCount ); - foreach ($batches as $batch) { - $this->clearTmpData(); + + foreach ($batchQueries as $query) { + $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $select = $this->connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); $resultSelect->where($whereCondition, $entityIds); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, - $this->getMainTmpTable(), + $this->tableMaintainer->getMainTmpTable((int)$store->getId()), $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); - $this->publishData(); - $this->removeUnnecessaryData(); + $this->publishData($store); } } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index 248ec970d2250..3bd4910767587 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -36,17 +36,16 @@ public function execute(array $entityIds = [], $useTempTable = false) /** * Return array of all category root IDs + tree root ID * - * @return int[] + * @param \Magento\Store\Model\Store $store + * @return int */ - protected function getRootCategoryIds() + private function getRootCategoryId($store) { - $rootIds = [\Magento\Catalog\Model\Category::TREE_ROOT_ID]; - foreach ($this->storeManager->getStores() as $store) { - if ($this->getPathFromCategoryId($store->getRootCategoryId())) { - $rootIds[] = $store->getRootCategoryId(); - } + $rootId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; + if ($this->getPathFromCategoryId($store->getRootCategoryId())) { + $rootId = $store->getRootCategoryId(); } - return $rootIds; + return $rootId; } /** @@ -54,10 +53,15 @@ protected function getRootCategoryIds() * * @return void */ - protected function removeEntries() + private function removeEntries() { - $removalCategoryIds = array_diff($this->limitationByCategories, $this->getRootCategoryIds()); - $this->connection->delete($this->getMainTable(), ['category_id IN (?)' => $removalCategoryIds]); + foreach ($this->storeManager->getStores() as $store) { + $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); + $this->connection->delete( + $this->getIndexTable($store->getId()), + ['category_id IN (?)' => $removalCategoryIds] + ); + } } /** diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/MviewState.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/MviewState.php index 16323b762c122..3987f69e35f48 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/MviewState.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/MviewState.php @@ -1,15 +1,14 @@ <?php /** - * Plugin for \Magento\Framework\Mview\View\StateInterface model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; +/** + * Plugin for \Magento\Framework\Mview\View\StateInterface model + */ class MviewState { /** @@ -54,7 +53,9 @@ public function afterSetStatus(\Magento\Framework\Mview\View\StateInterface $sta { if (in_array($state->getViewId(), $this->viewIds)) { $viewId = $state->getViewId() == - \Magento\Catalog\Model\Indexer\Category\Product::INDEXER_ID ? \Magento\Catalog\Model\Indexer\Product\Category::INDEXER_ID : \Magento\Catalog\Model\Indexer\Category\Product::INDEXER_ID; + \Magento\Catalog\Model\Indexer\Category\Product::INDEXER_ID + ? \Magento\Catalog\Model\Indexer\Product\Category::INDEXER_ID + : \Magento\Catalog\Model\Indexer\Category\Product::INDEXER_ID; $relatedViewState = $this->state->loadByView($viewId); diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php index 2ee46b3a6096b..7770b90dda0a5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreGroup.php @@ -9,6 +9,8 @@ use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\Indexer\Category\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; class StoreGroup { @@ -22,12 +24,21 @@ class StoreGroup */ protected $indexerRegistry; + /** + * @var TableMaintainer + */ + protected $tableMaintainer; + /** * @param IndexerRegistry $indexerRegistry + * @param TableMaintainer $tableMaintainer */ - public function __construct(IndexerRegistry $indexerRegistry) - { + public function __construct( + IndexerRegistry $indexerRegistry, + TableMaintainer $tableMaintainer + ) { $this->indexerRegistry = $indexerRegistry; + $this->tableMaintainer = $tableMaintainer; } /** @@ -73,4 +84,22 @@ protected function validate(AbstractModel $group) return ($group->dataHasChangedFor('website_id') || $group->dataHasChangedFor('root_category_id')) && !$group->isObjectNew(); } + + /** + * Delete catalog_category_product indexer tables for deleted store group + * + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $storeGroup + * + * @return AbstractDb + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $storeGroup) + { + foreach ($storeGroup->getStores() as $store) { + $this->tableMaintainer->dropTablesForStore($store->getId()); + } + return $objectResource; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php index f49b685ba6f7f..114d2a94f5b35 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/StoreView.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\AbstractModel; + class StoreView extends StoreGroup { /** @@ -17,4 +20,38 @@ protected function validate(\Magento\Framework\Model\AbstractModel $store) { return $store->isObjectNew() || $store->dataHasChangedFor('group_id'); } + + /** + * Invalidate catalog_category_product indexer + * + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $store + * + * @return AbstractDb + */ + public function afterSave(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $store = null) + { + if ($store->isObjectNew()) { + $this->tableMaintainer->createTablesForStore($store->getId()); + } + + return parent::afterSave($subject, $objectResource); + } + + /** + * Delete catalog_category_product indexer table for deleted store + * + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $store + * + * @return AbstractDb + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $store) + { + $this->tableMaintainer->dropTablesForStore($store->getId()); + return $objectResource; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php new file mode 100644 index 0000000000000..84c0eb46429cd --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; + +use Magento\Framework\App\ResourceConnection; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +use Magento\Framework\Search\Request\Dimension; + +/** + * Class that replace catalog_category_product_index table name on the table name segmented per store + */ +class TableResolver +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var IndexScopeResolver + */ + private $tableResolver; + + /** + * @param StoreManagerInterface $storeManager + * @param IndexScopeResolver $tableResolver + */ + public function __construct( + StoreManagerInterface $storeManager, + IndexScopeResolver $tableResolver + ) { + $this->storeManager = $storeManager; + $this->tableResolver = $tableResolver; + } + + /** + * replacing catalog_category_product_index table name on the table name segmented per store + * + * @param ResourceConnection $subject + * @param string $result + * @param string|string[] $modelEntity + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @return string + */ + public function afterGetTableName( + \Magento\Framework\App\ResourceConnection $subject, + string $result, + $modelEntity + ) { + if (!is_array($modelEntity) && $modelEntity === AbstractAction::MAIN_INDEX_TABLE) { + $catalogCategoryProductDimension = new Dimension( + \Magento\Store\Model\Store::ENTITY, + $this->storeManager->getStore()->getId() + ); + + $tableName = $this->tableResolver->resolve( + AbstractAction::MAIN_INDEX_TABLE, + [ + $catalogCategoryProductDimension + ] + ); + return $tableName; + } + return $result; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php new file mode 100644 index 0000000000000..90abb0415288d --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; + +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\AbstractModel; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; + +class Website +{ + /** + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * @param TableMaintainer $tableMaintainer + */ + public function __construct( + TableMaintainer $tableMaintainer + ) { + $this->tableMaintainer = $tableMaintainer; + } + + /** + * Delete catalog_category_product indexer tables for deleted website + * + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $website + * + * @return AbstractDb + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website) + { + foreach ($website->getStoreIds() as $storeId) { + $this->tableMaintainer->dropTablesForStore($storeId); + } + return $objectResource; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php new file mode 100644 index 0000000000000..3c2629bc570f2 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); +namespace Magento\Catalog\Model\Indexer\Category\Product; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Search\Request\Dimension; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver as TableResolver; + +/** + * Class encapsulate logic of work with tables per store in Category Product indexer + */ +class TableMaintainer +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var TableResolver + */ + private $tableResolver; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * Catalog tmp category index table name + */ + private $tmpTableSuffix = '_tmp'; + + /** + * Catalog tmp category index table name + */ + private $additionalTableSuffix = '_replica'; + + /** + * @var string[] + */ + private $mainTmpTable; + + /** + * @param ResourceConnection $resource + * @param TableResolver $tableResolver + */ + public function __construct( + ResourceConnection $resource, + TableResolver $tableResolver + ) { + $this->resource = $resource; + $this->tableResolver = $tableResolver; + } + + /** + * Get connection + * + * @return AdapterInterface + */ + private function getConnection() + { + if (!isset($this->connection)) { + $this->connection = $this->resource->getConnection(); + } + return $this->connection; + } + + /** + * Return validated table name + * + * @param string|string[] $table + * @return string + */ + private function getTable($table) + { + return $this->resource->getTableName($table); + } + + /** + * Create table based on main table + * + * @param string $mainTableName + * @param string $newTableName + * + * @return void + * + * @throws \Zend_Db_Exception + */ + private function createTable($mainTableName, $newTableName) + { + if (!$this->getConnection()->isTableExists($newTableName)) { + $this->getConnection()->createTable( + $this->getConnection()->createTableByDdl($mainTableName, $newTableName) + ); + } + } + + /** + * Drop table + * + * @param string $tableName + * + * @return void + */ + private function dropTable($tableName) + { + if ($this->getConnection()->isTableExists($tableName)) { + $this->getConnection()->dropTable($tableName); + } + } + + /** + * Return main index table name + * + * @param $storeId + * + * @return string + */ + public function getMainTable(int $storeId) + { + $catalogCategoryProductDimension = new Dimension(\Magento\Store\Model\Store::ENTITY, $storeId); + + return $this->tableResolver->resolve(AbstractAction::MAIN_INDEX_TABLE, [$catalogCategoryProductDimension]); + } + + /** + * Create main and replica index tables for store + * + * @param $storeId + * + * @return void + * + * @throws \Zend_Db_Exception + */ + public function createTablesForStore(int $storeId) + { + $mainTableName = $this->getMainTable($storeId); + //Create index table for store based on main replica table + //Using main replica table is necessary for backward capability and TableResolver plugin work + $this->createTable( + $this->getTable(AbstractAction::MAIN_INDEX_TABLE . $this->additionalTableSuffix), + $mainTableName + ); + + $mainReplicaTableName = $this->getMainTable($storeId) . $this->additionalTableSuffix; + //Create replica table for store based on main replica table + $this->createTable( + $this->getTable(AbstractAction::MAIN_INDEX_TABLE . $this->additionalTableSuffix), + $mainReplicaTableName + ); + } + + /** + * Drop main and replica index tables for store + * + * @param $storeId + * + * @return void + */ + public function dropTablesForStore(int $storeId) + { + $mainTableName = $this->getMainTable($storeId); + $this->dropTable($mainTableName); + + $mainReplicaTableName = $this->getMainTable($storeId) . $this->additionalTableSuffix; + $this->dropTable($mainReplicaTableName); + } + + /** + * Return replica index table name + * + * @param $storeId + * + * @return string + */ + public function getMainReplicaTable(int $storeId) + { + return $this->getMainTable($storeId) . $this->additionalTableSuffix; + } + + /** + * Create temporary index table for store + * + * @param $storeId + * + * @return void + */ + public function createMainTmpTable(int $storeId) + { + if (!isset($this->mainTmpTable[$storeId])) { + $originTableName = $this->getMainTable($storeId); + $temporaryTableName = $this->getMainTable($storeId) . $this->tmpTableSuffix; + $this->getConnection()->createTemporaryTableLike($temporaryTableName, $originTableName, true); + $this->mainTmpTable[$storeId] = $temporaryTableName; + } + } + + /** + * Return temporary index table name + * + * @param $storeId + * + * @return string + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getMainTmpTable(int $storeId) + { + if (!isset($this->mainTmpTable[$storeId])) { + throw new \Magento\Framework\Exception\NoSuchEntityException('Temporary table does not exist'); + } + return $this->mainTmpTable[$storeId]; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 1b988534328e9..182f04de4ab0e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -6,9 +6,19 @@ namespace Magento\Catalog\Model\Indexer\Product\Category\Action; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; use Magento\Framework\Indexer\CacheContext; +use Magento\Store\Model\StoreManagerInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction { /** @@ -19,32 +29,102 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio protected $limitationByProducts; /** - * @var \Magento\Framework\Indexer\CacheContext + * @var CacheContext */ private $cacheContext; + /** + * @var EventManagerInterface|null + */ + private $eventManager; + + /** + * @param ResourceConnection $resource + * @param StoreManagerInterface $storeManager + * @param Config $config + * @param QueryGenerator|null $queryGenerator + * @param MetadataPool|null $metadataPool + * @param CacheContext|null $cacheContext + * @param EventManagerInterface|null $eventManager + */ + public function __construct( + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, + QueryGenerator $queryGenerator = null, + MetadataPool $metadataPool = null, + CacheContext $cacheContext = null, + EventManagerInterface $eventManager = null + ) { + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); + } + /** * Refresh entities index * * @param int[] $entityIds * @param bool $useTempTable * @return $this + * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface + * @throws \DomainException */ public function execute(array $entityIds = [], $useTempTable = false) { - $this->limitationByProducts = $entityIds; + $idsToBeReIndexed = $this->getProductIdsWithParents($entityIds); + + $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; + $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + $this->removeEntries(); $this->reindex(); - $this->registerProducts($entityIds); - $this->registerCategories($entityIds); + $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); + + $this->registerProducts($idsToBeReIndexed); + $this->registerCategories($affectedCategories); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); return $this; } + /** + * Get IDs of parent products by their child IDs. + * + * Returns identifiers of parent product from the catalog_product_relation. + * Please note that returned ids don't contain ids of passed child products. + * + * @param int[] $childProductIds + * @return int[] + * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface + * @throws \DomainException + */ + private function getProductIdsWithParents(array $childProductIds) + { + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $fieldForParent = $metadata->getLinkField(); + + $select = $this->connection + ->select() + ->from(['relation' => $this->getTable('catalog_product_relation')], []) + ->distinct(true) + ->where('child_id IN (?)', $childProductIds) + ->join( + ['cpe' => $this->getTable('catalog_product_entity')], + 'relation.parent_id = cpe.' . $fieldForParent, + ['cpe.entity_id'] + ); + + $parentProductIds = $this->connection->fetchCol($select); + + return array_unique(array_merge($childProductIds, $parentProductIds)); + } + /** * Register affected products * @@ -53,26 +133,19 @@ public function execute(array $entityIds = [], $useTempTable = false) */ private function registerProducts($entityIds) { - $this->getCacheContext()->registerEntities(Product::CACHE_TAG, $entityIds); + $this->cacheContext->registerEntities(Product::CACHE_TAG, $entityIds); } /** * Register categories assigned to products * - * @param array $entityIds + * @param array $categoryIds * @return void */ - private function registerCategories($entityIds) + private function registerCategories(array $categoryIds) { - $categories = $this->connection->fetchCol( - $this->connection->select() - ->from($this->getMainTable(), ['category_id']) - ->where('product_id IN (?)', $entityIds) - ->distinct() - ); - - if ($categories) { - $this->getCacheContext()->registerEntities(Category::CACHE_TAG, $categories); + if ($categoryIds) { + $this->cacheContext->registerEntities(Category::CACHE_TAG, $categoryIds); } } @@ -83,10 +156,12 @@ private function registerCategories($entityIds) */ protected function removeEntries() { - $this->connection->delete( - $this->getMainTable(), - ['product_id IN (?)' => $this->limitationByProducts] - ); + foreach ($this->storeManager->getStores() as $store) { + $this->connection->delete( + $this->getIndexTable($store->getId()), + ['product_id IN (?)' => $this->limitationByProducts] + ); + }; } /** @@ -98,7 +173,7 @@ protected function removeEntries() protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getNonAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?) OR relation.child_id IN (?)', $this->limitationByProducts); } /** @@ -136,16 +211,31 @@ protected function isRangingNeeded() } /** - * Get cache context + * Returns a list of category ids which are assigned to product ids in the index * * @return \Magento\Framework\Indexer\CacheContext - * @deprecated 101.0.0 */ - private function getCacheContext() + private function getCategoryIdsFromIndex(array $productIds) { - if ($this->cacheContext === null) { - $this->cacheContext = \Magento\Framework\App\ObjectManager::getInstance()->get(CacheContext::class); + $categoryIds = []; + foreach ($this->storeManager->getStores() as $store) { + $categoryIds = array_merge( + $categoryIds, + $this->connection->fetchCol( + $this->connection->select() + ->from($this->getIndexTable($store->getId()), ['category_id']) + ->where('product_id IN (?)', $productIds) + ->distinct() + ) + ); + }; + $parentCategories = $categoryIds; + foreach ($categoryIds as $categoryId) { + $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); + $parentCategories = array_merge($parentCategories, $parentIds); } - return $this->cacheContext; + $categoryIds = array_unique($parentCategories); + + return $categoryIds; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php index 6a2642a8568f4..43b4b5518c16f 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Eav; /** @@ -10,6 +12,11 @@ */ abstract class AbstractAction { + /** + * Config path for enable EAV indexer + */ + const ENABLE_EAV_INDEXER = 'catalog/search/enable_eav_indexer'; + /** * EAV Indexers by type * @@ -27,17 +34,27 @@ abstract class AbstractAction */ protected $_eavDecimalFactory; + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + /** * AbstractAction constructor. * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory + * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null ) { $this->_eavDecimalFactory = $eavDecimalFactory; $this->_eavSourceFactory = $eavSourceFactory; + $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\App\Config\ScopeConfigInterface::class + ); } /** @@ -90,6 +107,9 @@ public function getIndexer($type) */ public function reindex($ids = null) { + if (!$this->isEavIndexerEnabled()) { + return; + } foreach ($this->getIndexers() as $indexer) { if ($ids === null) { $indexer->reindexAll(); @@ -143,7 +163,23 @@ protected function syncData($indexer, $destinationTable, $ids) protected function processRelations($indexer, $ids, $onlyParents = false) { $parentIds = $indexer->getRelationsByChild($ids); + $parentIds = array_unique(array_merge($parentIds, $ids)); $childIds = $onlyParents ? [] : $indexer->getRelationsByParent($parentIds); return array_unique(array_merge($ids, $childIds, $parentIds)); } + + /** + * Get EAV indexer status + * + * @return bool + */ + private function isEavIndexerEnabled(): bool + { + $eavIndexerStatus = $this->scopeConfig->getValue( + self::ENABLE_EAV_INDEXER, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + return (bool)$eavIndexerStatus; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index bc747e62f641e..b9ca4f342b45b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -3,27 +3,45 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Class Full reindex action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; @@ -33,34 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction private $activeTableSwitcher; /** - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param DecimalFactory $eavDecimalFactory + * @param SourceFactory $eavSourceFactory + * @param MetadataPool|null $metadataPool + * @param BatchProviderInterface|null $batchProvider + * @param BatchSizeCalculator $batchSizeCalculator * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param ScopeConfigInterface|null $scopeConfig + * @param QueryGenerator|null $batchQueryGenerator */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, - ActiveTableSwitcher $activeTableSwitcher = null + DecimalFactory $eavDecimalFactory, + SourceFactory $eavSourceFactory, + MetadataPool $metadataPool = null, + BatchProviderInterface $batchProvider = null, + BatchSizeCalculator $batchSizeCalculator = null, + ActiveTableSwitcher $activeTableSwitcher = null, + ScopeConfigInterface $scopeConfig = null, + QueryGenerator $batchQueryGenerator = null ) { - parent::__construct($eavDecimalFactory, $eavSourceFactory); - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( + ScopeConfigInterface::class + ); + parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( + MetadataPool::class ); - $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( + BatchProviderInterface::class ); - $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class + $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( + BatchSizeCalculator::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( ActiveTableSwitcher::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( + QueryGenerator::class + ); } /** @@ -68,30 +106,34 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($ids = null) { + if (!$this->isEavIndexerEnabled()) { + return; + } try { foreach ($this->getIndexers() as $indexerName => $indexer) { $connection = $indexer->getConnection(); $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); $connection->truncateTable($mainTable); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) + $select, + $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { - /** @var \Magento\Framework\DB\Select $select */ - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + foreach ($batchQueries as $query) { + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); @@ -100,7 +142,7 @@ public function execute($ids = null) $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); } } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + throw new LocalizedException(__($e->getMessage()), $e); } } @@ -120,7 +162,7 @@ protected function syncData($indexer, $destinationTable, $ids = null) $select, $destinationTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); $connection->commit(); @@ -129,4 +171,19 @@ protected function syncData($indexer, $destinationTable, $ids = null) throw $e; } } + + /** + * Get EAV indexer status + * + * @return bool + */ + private function isEavIndexerEnabled(): bool + { + $eavIndexerStatus = $this->scopeConfig->getValue( + self::ENABLE_EAV_INDEXER, + ScopeInterface::SCOPE_STORE + ); + + return (bool)$eavIndexerStatus; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php index 9dd312e9da801..25a9eebb5a0a9 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\Store; /** * Class Indexer @@ -56,39 +57,36 @@ public function __construct( * @return \Magento\Catalog\Model\Indexer\Product\Flat * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function write($storeId, $productId, $valueFieldSuffix = '') { $flatTable = $this->_productIndexerHelper->getFlatTableName($storeId); + $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); $attributes = $this->_productIndexerHelper->getAttributes(); $eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes); $updateData = []; $describe = $this->_connection->describeTable($flatTable); + $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); foreach ($eavAttributes as $tableName => $tableColumns) { $columnsChunks = array_chunk($tableColumns, self::ATTRIBUTES_CHUNK_SIZE, true); foreach ($columnsChunks as $columns) { $select = $this->_connection->select(); - $selectValue = $this->_connection->select(); - $keyColumns = [ - 'entity_id' => 'e.entity_id', - 'attribute_id' => 't.attribute_id', - 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'), - ]; - - if ($tableName != $this->_productIndexerHelper->getTable('catalog_product_entity')) { + + if ($tableName != $entityTableName) { $valueColumns = []; $ids = []; $select->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], - $keyColumns - ); - - $selectValue->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], - $keyColumns + ['e' => $entityTableName], + [ + 'entity_id' => 'e.entity_id', + 'attribute_id' => 't.attribute_id', + 'value' => 't.value' + ] ); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ @@ -97,40 +95,35 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $ids[$attribute->getId()] = $columnName; } } - $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); - $select->joinLeft( + $select->joinInner( ['t' => $tableName], sprintf('e.%s = t.%s ', $linkField, $linkField) . $this->_connection->quoteInto( ' AND t.attribute_id IN (?)', array_keys($ids) - ) . ' AND t.store_id = 0', - [] - )->joinLeft( - ['t2' => $tableName], - sprintf('t.%s = t2.%s ', $linkField, $linkField) . - ' AND t.attribute_id = t2.attribute_id ' . - $this->_connection->quoteInto( - ' AND t2.store_id = ?', - $storeId - ), + ) . ' AND ' . $this->_connection->quoteInto('t.store_id IN(?)', [ + Store::DEFAULT_STORE_ID, + $storeId + ]), [] )->where( 'e.entity_id = ' . $productId - )->where( - 't.attribute_id IS NOT NULL' - ); + )->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { $updateData[$ids[$row['attribute_id']]] = $row['value']; $valueColumnName = $ids[$row['attribute_id']] . $valueFieldSuffix; if (isset($describe[$valueColumnName])) { - $valueColumns[$row['value']] = $valueColumnName; + $valueColumns[$row['attribute_id']] = [ + 'value' => $row['value'], + 'column_name' => $valueColumnName + ]; } } //Update not simple attributes (eg. dropdown) if (!empty($valueColumns)) { - $valueIds = array_keys($valueColumns); + $valueIds = array_column($valueColumns, 'value'); + $optionIdToAttributeName = array_column($valueColumns, 'column_name', 'value'); $select = $this->_connection->select()->from( ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], @@ -139,14 +132,14 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $this->_connection->quoteInto('t.option_id IN (?)', $valueIds) )->where( $this->_connection->quoteInto('t.store_id IN(?)', [ - \Magento\Store\Model\Store::DEFAULT_STORE_ID, + Store::DEFAULT_STORE_ID, $storeId ]) ) ->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { - $valueColumnName = $valueColumns[$row['option_id']]; + $valueColumnName = $optionIdToAttributeName[$row['option_id']]; if (isset($describe[$valueColumnName])) { $updateData[$valueColumnName] = $row['value']; } @@ -156,8 +149,9 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $columnNames = array_keys($columns); $columnNames[] = 'attribute_set_id'; $columnNames[] = 'type_id'; + $columnNames[] = $linkField; $select->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], + ['e' => $entityTableName], $columnNames )->where( 'e.entity_id = ' . $productId @@ -165,6 +159,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $cursor = $this->_connection->query($select); $row = $cursor->fetch(\Zend_Db::FETCH_ASSOC); if (!empty($row)) { + $linkFieldId = $linkField; foreach ($row as $columnName => $value) { $updateData[$columnName] = $value; } @@ -175,6 +170,9 @@ public function write($storeId, $productId, $valueFieldSuffix = '') if (!empty($updateData)) { $updateData += ['entity_id' => $productId]; + if ($linkField !== $metadata->getIdentifierField()) { + $updateData += [$linkField => $linkFieldId]; + } $updateFields = []; foreach ($updateData as $key => $value) { $updateFields[$key] = $key; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php index b5dbdb68606ff..8ccd48f1360c0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php @@ -5,16 +5,20 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder; use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder; +use Magento\Framework\EntityManager\MetadataPool; /** - * Class Row reindex action + * Class Row reindex action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer + * @var Indexer */ protected $flatItemWriter; @@ -23,6 +27,11 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction */ protected $flatItemEraser; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -32,6 +41,7 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction * @param FlatTableBuilder $flatTableBuilder * @param Indexer $flatItemWriter * @param Eraser $flatItemEraser + * @param MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, @@ -41,7 +51,8 @@ public function __construct( TableBuilder $tableBuilder, FlatTableBuilder $flatTableBuilder, Indexer $flatItemWriter, - Eraser $flatItemEraser + Eraser $flatItemEraser, + MetadataPool $metadataPool = null ) { parent::__construct( $resource, @@ -53,6 +64,8 @@ public function __construct( ); $this->flatItemWriter = $flatItemWriter; $this->flatItemEraser = $flatItemEraser; + $this->metadataPool = $metadataPool ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class); } /** @@ -70,24 +83,49 @@ public function execute($id = null) ); } $ids = [$id]; - foreach ($this->_storeManager->getStores() as $store) { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $stores = $this->_storeManager->getStores(); + foreach ($stores as $store) { $tableExists = $this->_isFlatTableExists($store->getId()); if ($tableExists) { $this->flatItemEraser->removeDeletedProducts($ids, $store->getId()); } - if (isset($ids[0])) { + + /* @var $status \Magento\Eav\Model\Entity\Attribute */ + $status = $this->_productIndexerHelper->getAttribute(ProductInterface::STATUS); + $statusTable = $status->getBackend()->getTable(); + $catalogProductEntityTable = $this->_productIndexerHelper->getTable('catalog_product_entity'); + $statusConditions = [ + 's.store_id IN(0,' . (int)$store->getId() . ')', + 's.attribute_id = ' . (int)$status->getId(), + 'e.entity_id = ' . (int)$id, + ]; + $select = $this->_connection->select(); + $select->from(['e' => $catalogProductEntityTable], ['s.value']) + ->where(implode(' AND ', $statusConditions)) + ->joinLeft(['s' => $statusTable], "e.{$linkField} = s.{$linkField}", []) + ->order('s.store_id DESC') + ->limit(1); + $result = $this->_connection->query($select); + $status = $result->fetchColumn(0); + + if ($status == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) { if (!$tableExists) { $this->_flatTableBuilder->build( $store->getId(), - [$ids[0]], + $ids, $this->_valueFieldSuffix, $this->_tableDropSuffix, false ); } - $this->flatItemWriter->write($store->getId(), $ids[0], $this->_valueFieldSuffix); + $this->flatItemWriter->write($store->getId(), $id, $this->_valueFieldSuffix); + } else { + $this->flatItemEraser->deleteProductsFromStore($id, $store->getId()); } } + return $this; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index fbe0d4b550fa6..a7273ff37860b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -11,6 +11,7 @@ /** * Class FlatTableBuilder + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FlatTableBuilder @@ -341,20 +342,36 @@ protected function _updateTemporaryTableByStoreValues( if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } + + /* + * According to \Magento\Framework\DB\SelectRendererInterface select rendering may be updated + * so we need to trigger select renderer for correct update + */ + $select->assemble(); $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); } //Update not simple attributes (eg. dropdown) - if (isset($flatColumns[$attributeCode . $valueFieldSuffix])) { - $select = $this->_connection->select()->joinInner( - ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], - 't.option_id = et.' . $attributeCode . ' AND t.store_id=' . $storeId, - [$attributeCode . $valueFieldSuffix => 't.value'] - ); + $columnName = $attributeCode . $valueFieldSuffix; + if (isset($flatColumns[$columnName])) { + $columnValue = $this->_connection->getIfNullSql('ts.value', 't0.value'); + $select = $this->_connection->select(); + $select->joinLeft( + ['t0' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 't0.option_id = et.' . $attributeCode . ' AND t0.store_id = 0', + [] + )->joinLeft( + ['ts' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 'ts.option_id = et.' . $attributeCode . ' AND ts.store_id = ' . $storeId, + [] + )->columns( + [$columnName => $columnValue] + )->where($columnValue . ' IS NOT NULL'); if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } + $select->assemble(); $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); } @@ -374,6 +391,8 @@ protected function _getTemporaryTableName($tableName) } /** + * Get metadata pool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php index 8cf57b7d8fefd..ef7919193e609 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/Store.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Indexer\Product\Flat\Plugin; class Store @@ -34,8 +32,10 @@ public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processo * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave(\Magento\Store\Model\ResourceModel\Store $subject, \Magento\Framework\Model\AbstractModel $object) - { + public function beforeSave( + \Magento\Store\Model\ResourceModel\Store $subject, + \Magento\Framework\Model\AbstractModel $object + ) { if (!$object->getId() || $object->dataHasChangedFor('group_id')) { $this->_productFlatIndexerProcessor->markIndexerAsInvalid(); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php index 027c21a956590..df62fe8d349e4 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Plugin/StoreGroup.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Indexer\Product\Flat\Plugin; class StoreGroup @@ -34,8 +32,10 @@ public function __construct(\Magento\Catalog\Model\Indexer\Product\Flat\Processo * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSave(\Magento\Store\Model\ResourceModel\Group $subject, \Magento\Framework\Model\AbstractModel $object) - { + public function beforeSave( + \Magento\Store\Model\ResourceModel\Group $subject, + \Magento\Framework\Model\AbstractModel $object + ) { if (!$object->getId() || $object->dataHasChangedFor('root_category_id')) { $this->_productFlatIndexerProcessor->markIndexerAsInvalid(); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index 5f8be83872021..c1384fed86f2d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -6,7 +6,11 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat; use Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterfaceFactory; +use Magento\Store\Model\Store; +/** + * Flat product indexer temporary table builder. + */ class TableBuilder { /** @@ -34,13 +38,6 @@ class TableBuilder */ private $tableBuilderFactory; - /** - * Check whether builder was executed - * - * @var bool - */ - protected $_isExecuted = false; - /** * Constructor * @@ -70,9 +67,6 @@ public function __construct( */ public function build($storeId, $changedIds, $valueFieldSuffix) { - if ($this->_isExecuted) { - return; - } $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); $attributes = $this->_productIndexerHelper->getAttributes(); $eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes); @@ -117,7 +111,6 @@ public function build($storeId, $changedIds, $valueFieldSuffix) //Fill temporary tables with attributes grouped by it type $this->_fillTemporaryTable($tableName, $columns, $changedIds, $valueFieldSuffix, $storeId); } - $this->_isExecuted = true; } /** @@ -268,70 +261,70 @@ protected function _fillTemporaryTable( $valueFieldSuffix, $storeId ) { - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); if (!empty($tableColumns)) { - $columnsChunks = array_chunk( - $tableColumns, - Action\Indexer::ATTRIBUTES_CHUNK_SIZE, - true - ); + $columnsChunks = array_chunk($tableColumns, Action\Indexer::ATTRIBUTES_CHUNK_SIZE / 2, true); + + $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); + $entityTemporaryTableName = $this->_getTemporaryTableName($entityTableName); + $temporaryTableName = $this->_getTemporaryTableName($tableName); + $temporaryValueTableName = $temporaryTableName . $valueFieldSuffix; + $attributeOptionValueTableName = $this->_productIndexerHelper->getTable('eav_attribute_option_value'); + + $flatColumns = $this->_productIndexerHelper->getFlatColumns(); + $defaultStoreId = Store::DEFAULT_STORE_ID; + $linkField = $this->getMetadataPool() + ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + foreach ($columnsChunks as $columnsList) { $select = $this->_connection->select(); $selectValue = $this->_connection->select(); - $entityTableName = $this->_getTemporaryTableName( - $this->_productIndexerHelper->getTable('catalog_product_entity') - ); - $temporaryTableName = $this->_getTemporaryTableName($tableName); - $temporaryValueTableName = $temporaryTableName . $valueFieldSuffix; - $keyColumn = array_unique([$metadata->getLinkField(), 'entity_id']); + $keyColumn = array_unique([$linkField, 'entity_id']); $columns = array_merge($keyColumn, array_keys($columnsList)); $valueColumns = $keyColumn; - $flatColumns = $this->_productIndexerHelper->getFlatColumns(); $iterationNum = 1; - $select->from(['et' => $entityTableName], $keyColumn) - ->join( - ['e' => $this->resource->getTableName('catalog_product_entity')], - 'e.entity_id = et.entity_id', - [] - ); + $select->from(['et' => $entityTemporaryTableName], $keyColumn) + ->join(['e' => $entityTableName], 'e.entity_id = et.entity_id', []); $selectValue->from(['e' => $temporaryTableName], $keyColumn); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ foreach ($columnsList as $columnName => $attribute) { - $countTableName = 't' . $iterationNum++; - $joinCondition = sprintf( - 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = 0', - $countTableName, - $attribute->getId(), - $metadata->getLinkField() - ); - + $countTableName = 't' . ($iterationNum++); + $joinCondition = 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = %4$d'; $select->joinLeft( [$countTableName => $tableName], - $joinCondition, - [$columnName => 'value'] + sprintf($joinCondition, $countTableName, $attribute->getId(), $linkField, $defaultStoreId), + [] + )->joinLeft( + ['s' . $countTableName => $tableName], + sprintf($joinCondition, 's' . $countTableName, $attribute->getId(), $linkField, $storeId), + [] + ); + + $columnValue = $this->_connection->getIfNullSql( + 's' . $countTableName . '.value', + $countTableName . '.value' ); + $select->columns([$columnName => $columnValue]); if ($attribute->getFlatUpdateSelect($storeId) instanceof \Magento\Framework\DB\Select) { $attributeCode = $attribute->getAttributeCode(); $columnValueName = $attributeCode . $valueFieldSuffix; if (isset($flatColumns[$columnValueName])) { - $valueJoinCondition = sprintf( - 'e.%1$s = %2$s.option_id AND %2$s.store_id = 0', - $attributeCode, - $countTableName - ); + $valueJoinCondition = 'e.%1$s = %2$s.option_id AND %2$s.store_id = %3$d'; $selectValue->joinLeft( - [ - $countTableName => $this->_productIndexerHelper->getTable( - 'eav_attribute_option_value' - ), - ], - $valueJoinCondition, - [$columnValueName => $countTableName . '.value'] + [$countTableName => $attributeOptionValueTableName], + sprintf($valueJoinCondition, $attributeCode, $countTableName, $defaultStoreId), + [] + )->joinLeft( + ['s' . $countTableName => $attributeOptionValueTableName], + sprintf($valueJoinCondition, $attributeCode, 's' . $countTableName, $storeId), + [] ); + + $selectValue->columns([$columnValueName => $columnValue]); $valueColumns[] = $columnValueName; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price.php index c9936f7e6c691..b703ba82a4052 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price.php @@ -5,43 +5,56 @@ */ namespace Magento\Catalog\Model\Indexer\Product; +use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Full as FullAction; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Row as RowAction; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Rows as RowsAction; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Framework\Indexer\ActionInterface as IndexerActionInterface; use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Mview\ActionInterface as MviewActionInterface; -class Price implements \Magento\Framework\Indexer\ActionInterface, \Magento\Framework\Mview\ActionInterface +/** + * Price indexer + */ +class Price implements IndexerActionInterface, MviewActionInterface { /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Row + * @var RowAction */ protected $_productPriceIndexerRow; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Rows + * @var RowsAction */ protected $_productPriceIndexerRows; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Action\Full + * @var FullAction */ protected $_productPriceIndexerFull; /** - * @var \Magento\Framework\Indexer\CacheContext + * @var CacheContext */ private $cacheContext; /** - * @param Price\Action\Row $productPriceIndexerRow - * @param Price\Action\Rows $productPriceIndexerRows - * @param Price\Action\Full $productPriceIndexerFull + * @param RowAction $productPriceIndexerRow + * @param RowsAction $productPriceIndexerRows + * @param FullAction $productPriceIndexerFull + * @param CacheContext $cacheContext */ public function __construct( - \Magento\Catalog\Model\Indexer\Product\Price\Action\Row $productPriceIndexerRow, - \Magento\Catalog\Model\Indexer\Product\Price\Action\Rows $productPriceIndexerRows, - \Magento\Catalog\Model\Indexer\Product\Price\Action\Full $productPriceIndexerFull + RowAction $productPriceIndexerRow, + RowsAction $productPriceIndexerRows, + FullAction $productPriceIndexerFull, + CacheContext $cacheContext ) { $this->_productPriceIndexerRow = $productPriceIndexerRow; $this->_productPriceIndexerRows = $productPriceIndexerRows; $this->_productPriceIndexerFull = $productPriceIndexerFull; + $this->cacheContext = $cacheContext; } /** @@ -53,7 +66,7 @@ public function __construct( public function execute($ids) { $this->_productPriceIndexerRows->execute($ids); - $this->getCacheContext()->registerEntities(\Magento\Catalog\Model\Product::CACHE_TAG, $ids); + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, $ids); } /** @@ -64,10 +77,10 @@ public function execute($ids) public function executeFull() { $this->_productPriceIndexerFull->execute(); - $this->getCacheContext()->registerTags( + $this->cacheContext->registerTags( [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Catalog\Model\Product::CACHE_TAG + CategoryModel::CACHE_TAG, + ProductModel::CACHE_TAG ] ); } @@ -81,6 +94,7 @@ public function executeFull() public function executeList(array $ids) { $this->_productPriceIndexerRows->execute($ids); + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, $ids); } /** @@ -92,20 +106,6 @@ public function executeList(array $ids) public function executeRow($id) { $this->_productPriceIndexerRow->execute($id); - } - - /** - * Get cache context - * - * @return \Magento\Framework\Indexer\CacheContext - * @deprecated 100.0.11 - */ - protected function getCacheContext() - { - if (!($this->cacheContext instanceof CacheContext)) { - return \Magento\Framework\App\ObjectManager::getInstance()->get(CacheContext::class); - } else { - return $this->cacheContext; - } + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, [$id]); } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index 6db52f969d273..e9a907f0b5097 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -5,6 +5,12 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Price; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; + /** * Abstract action reindex class * @@ -15,7 +21,7 @@ abstract class AbstractAction /** * Default Product Type Price indexer resource model * - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice + * @var DefaultPrice */ protected $_defaultIndexerResource; @@ -71,9 +77,19 @@ abstract class AbstractAction protected $_indexers; /** - * @var \Magento\Catalog\Model\ResourceModel\Product + * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice + */ + private $tierPriceIndexResource; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + */ + private $dimensionCollectionFactory; + + /** + * @var TableMaintainer */ - private $productResource; + private $tableMaintainer; /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $config @@ -83,7 +99,13 @@ abstract class AbstractAction * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Catalog\Model\Product\Type $catalogProductType * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource + * @param DefaultPrice $defaultIndexerResource + * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice|null $tierPriceIndexResource + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $tableMaintainer + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $config, @@ -93,7 +115,10 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Catalog\Model\Product\Type $catalogProductType, \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource + DefaultPrice $defaultIndexerResource, + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice $tierPriceIndexResource = null, + \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, + \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $tableMaintainer = null ) { $this->_config = $config; $this->_storeManager = $storeManager; @@ -104,6 +129,15 @@ public function __construct( $this->_indexerPriceFactory = $indexerPriceFactory; $this->_defaultIndexerResource = $defaultIndexerResource; $this->_connection = $this->_defaultIndexerResource->getConnection(); + $this->tierPriceIndexResource = $tierPriceIndexResource ?? ObjectManager::getInstance()->get( + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice::class + ); + $this->dimensionCollectionFactory = $dimensionCollectionFactory ?? ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + ); + $this->tableMaintainer = $tableMaintainer ?? ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + ); } /** @@ -119,30 +153,29 @@ abstract public function execute($ids); * * @param array $processIds * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated Used only for backward compatibility for indexer, which not support indexation by dimensions */ protected function _syncData(array $processIds = []) { - // delete invalid rows - $select = $this->_connection->select()->from( - ['index_price' => $this->getIndexTargetTable()], - null - )->joinLeft( - ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()], - 'index_price.entity_id = ip_tmp.entity_id AND index_price.website_id = ip_tmp.website_id', - [] - )->where( - 'ip_tmp.entity_id IS NULL' - ); - if (!empty($processIds)) { - $select->where('index_price.entity_id IN(?)', $processIds); - } - $sql = $select->deleteFromSelect('index_price'); - $this->_connection->query($sql); + // for backward compatibility split data from old idx table on dimension tables + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $insertSelect = $this->getConnection()->select()->from( + ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()] + ); - $this->_insertFromTable( - $this->_defaultIndexerResource->getIdxTable(), - $this->getIndexTargetTable() - ); + foreach ($dimensions as $dimension) { + if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) { + $insertSelect->where('ip_tmp.website_id = ?', $dimension->getValue()); + } + if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) { + $insertSelect->where('ip_tmp.customer_group_id = ?', $dimension->getValue()); + } + } + + $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTable($dimensions)); + $this->getConnection()->query($query); + } return $this; } @@ -150,12 +183,15 @@ protected function _syncData(array $processIds = []) * Prepare website current dates table * * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _prepareWebsiteDateTable() { $baseCurrency = $this->_config->getValue(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE); - $select = $this->_connection->select()->from( + $select = $this->getConnection()->select()->from( ['cw' => $this->_defaultIndexerResource->getTable('store_website')], ['website_id'] )->join( @@ -167,7 +203,7 @@ protected function _prepareWebsiteDateTable() ); $data = []; - foreach ($this->_connection->fetchAll($select) as $item) { + foreach ($this->getConnection()->fetchAll($select) as $item) { /** @var $website \Magento\Store\Model\Website */ $website = $this->_storeManager->getWebsite($item['website_id']); @@ -192,6 +228,7 @@ protected function _prepareWebsiteDateTable() 'website_id' => $website->getId(), 'website_date' => $this->_dateTime->formatDate($timestamp, false), 'rate' => $rate, + 'default_store_id' => $store->getId() ]; } } @@ -200,7 +237,7 @@ protected function _prepareWebsiteDateTable() $this->_emptyTable($table); if ($data) { foreach ($data as $row) { - $this->_connection->insertOnDuplicate($table, $row, array_keys($row)); + $this->getConnection()->insertOnDuplicate($table, $row, array_keys($row)); } } @@ -215,101 +252,21 @@ protected function _prepareWebsiteDateTable() */ protected function _prepareTierPriceIndex($entityIds = null) { - $table = $this->_defaultIndexerResource->getTable('catalog_product_index_tier_price'); - $this->_emptyTable($table); - if (empty($entityIds)) { - return $this; - } - $linkField = $this->getProductIdFieldName(); - $priceAttribute = $this->getProductResource()->getAttribute('price'); - $baseColumns = [ - 'cpe.entity_id', - 'tp.customer_group_id', - 'tp.website_id' - ]; - if ($linkField !== 'entity_id') { - $baseColumns[] = 'cpe.' . $linkField; - }; - $subSelect = $this->_connection->select()->from( - ['cpe' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], - array_merge_recursive( - $baseColumns, - [ - 'min(tp.value) AS value', - 'min(tp.percentage_value) AS percentage_value' - ] - ) - )->joinInner( - ['tp' => $this->_defaultIndexerResource->getTable(['catalog_product_entity', 'tier_price'])], - 'tp.' . $linkField . ' = cpe.' . $linkField, - [] - )->where("cpe.entity_id IN(?)", $entityIds) - ->where("tp.website_id != 0") - ->group(['cpe.entity_id', 'tp.customer_group_id', 'tp.website_id']); - - $subSelect2 = $this->_connection->select() - ->from( - ['cpe' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], - array_merge_recursive( - $baseColumns, - [ - 'MIN(ROUND(tp.value * cwd.rate, 4)) AS value', - 'MIN(ROUND(tp.percentage_value * cwd.rate, 4)) AS percentage_value' + $this->tierPriceIndexResource->reindexEntity((array) $entityIds); - ] - ) - ) - ->joinInner( - ['tp' => $this->_defaultIndexerResource->getTable(['catalog_product_entity', 'tier_price'])], - 'tp.' . $linkField . ' = cpe.' . $linkField, - [] - )->join( - ['cw' => $this->_defaultIndexerResource->getTable('store_website')], - true, - [] - ) - ->joinInner( - ['cwd' => $this->_defaultIndexerResource->getTable('catalog_product_index_website')], - 'cw.website_id = cwd.website_id', - [] - ) - ->where("cpe.entity_id IN(?)", $entityIds) - ->where("tp.website_id = 0") - ->group( - ['cpe.entity_id', 'tp.customer_group_id', 'tp.website_id'] - ); - - $unionSelect = $this->_connection->select() - ->union([$subSelect, $subSelect2], \Magento\Framework\DB\Select::SQL_UNION_ALL); - $select = $this->_connection->select() - ->from( - ['b' => new \Zend_Db_Expr(sprintf('(%s)', $unionSelect->assemble()))], - [ - 'b.entity_id', - 'b.customer_group_id', - 'b.website_id', - 'MIN(IF(b.value = 0, product_price.value * (1 - b.percentage_value / 100), b.value))' - ] - ) - ->joinInner( - ['product_price' => $priceAttribute->getBackend()->getTable()], - 'b.' . $linkField . ' = product_price.' . $linkField, - [] - ) - ->group(['b.entity_id', 'b.customer_group_id', 'b.website_id']); - - $query = $select->insertFromSelect($table, [], false); - - $this->_connection->query($query); return $this; } /** * Retrieve price indexers per product type * + * @param bool $fullReindexAction + * * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface[] + * + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getTypeIndexers() + public function getTypeIndexers($fullReindexAction = false) { if ($this->_indexers === null) { $this->_indexers = []; @@ -319,14 +276,20 @@ public function getTypeIndexers() $typeInfo['price_indexer'] ) ? $typeInfo['price_indexer'] : get_class($this->_defaultIndexerResource); - $isComposite = !empty($typeInfo['composite']); $indexer = $this->_indexerPriceFactory->create( - $modelName - )->setTypeId( - $typeId - )->setIsComposite( - $isComposite + $modelName, + [ + 'fullReindexAction' => $fullReindexAction + ] ); + // left setters for backward compatibility + if ($indexer instanceof DefaultPrice) { + $indexer->setTypeId( + $typeId + )->setIsComposite( + !empty($typeInfo['composite']) + ); + } $this->_indexers[$typeId] = $indexer; } } @@ -339,7 +302,9 @@ public function getTypeIndexers() * * @param string $productTypeId * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface + * * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getIndexer($productTypeId) { @@ -360,19 +325,19 @@ protected function _getIndexer($productTypeId) */ protected function _insertFromTable($sourceTable, $destTable, $where = null) { - $sourceColumns = array_keys($this->_connection->describeTable($sourceTable)); - $targetColumns = array_keys($this->_connection->describeTable($destTable)); - $select = $this->_connection->select()->from($sourceTable, $sourceColumns); + $sourceColumns = array_keys($this->getConnection()->describeTable($sourceTable)); + $targetColumns = array_keys($this->getConnection()->describeTable($destTable)); + $select = $this->getConnection()->select()->from($sourceTable, $sourceColumns); if ($where) { $select->where($where); } - $query = $this->_connection->insertFromSelect( + $query = $this->getConnection()->insertFromSelect( $select, $destTable, $targetColumns, \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE ); - $this->_connection->query($query); + $this->getConnection()->query($query); } /** @@ -383,7 +348,7 @@ protected function _insertFromTable($sourceTable, $destTable, $where = null) */ protected function _emptyTable($table) { - $this->_connection->delete($table); + $this->getConnection()->delete($table); } /** @@ -391,74 +356,64 @@ protected function _emptyTable($table) * * @param array $changedIds * @return array Affected ids - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _reindexRows($changedIds = []) { - $this->_emptyTable($this->_defaultIndexerResource->getIdxTable()); $this->_prepareWebsiteDateTable(); - $select = $this->_connection->select()->from( - $this->_defaultIndexerResource->getTable('catalog_product_entity'), - ['entity_id', 'type_id'] - )->where( - 'entity_id IN(?)', - $changedIds - ); - $pairs = $this->_connection->fetchPairs($select); - $byType = []; - foreach ($pairs as $productId => $productType) { - $byType[$productType][$productId] = $productId; - } + $productsTypes = $this->getProductsTypes($changedIds); + $parentProductsTypes = $this->getParentProductsTypes($changedIds); - $compositeIds = []; - $notCompositeIds = []; + $changedIds = array_merge($changedIds, ...array_values($parentProductsTypes)); + $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); - foreach ($byType as $productType => $entityIds) { - $indexer = $this->_getIndexer($productType); - if ($indexer->getIsComposite()) { - $compositeIds += $entityIds; - } else { - $notCompositeIds += $entityIds; - } + if ($changedIds) { + $this->deleteIndexData($changedIds); } - - if (!empty($notCompositeIds)) { - $select = $this->_connection->select()->from( - ['l' => $this->_defaultIndexerResource->getTable('catalog_product_relation')], - '' - )->join( - ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], - 'e.' . $this->getProductIdFieldName() . ' = l.parent_id', - ['e.entity_id as parent_id', 'type_id'] - )->where( - 'l.child_id IN(?)', - $notCompositeIds - ); - $pairs = $this->_connection->fetchPairs($select); - foreach ($pairs as $productId => $productType) { - if (!in_array($productId, $changedIds)) { - $changedIds[] = (string) $productId; - $byType[$productType][$productId] = $productId; - $compositeIds[$productId] = $productId; + foreach ($productsTypes as $productType => $entityIds) { + $indexer = $this->_getIndexer($productType); + if ($indexer instanceof DimensionalIndexerInterface) { + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $this->tableMaintainer->createMainTmpTable($dimensions); + $temporaryTable = $this->tableMaintainer->getMainTmpTable($dimensions); + $this->_emptyTable($temporaryTable); + $indexer->executeByDimensions($dimensions, \SplFixedArray::fromArray($entityIds, false)); + // copy to index + $this->_insertFromTable( + $temporaryTable, + $this->tableMaintainer->getMainTable($dimensions) + ); } + } else { + // handle 3d-party indexers for backward compatibility + $this->_emptyTable($this->_defaultIndexerResource->getIdxTable()); + $this->_copyRelationIndexData($entityIds); + $indexer->reindexEntity($entityIds); + $this->_syncData($entityIds); } } - if (!empty($compositeIds)) { - $this->_copyRelationIndexData($compositeIds, $notCompositeIds); - } - $this->_prepareTierPriceIndex($compositeIds + $notCompositeIds); + return $changedIds; + } - $indexers = $this->getTypeIndexers(); - foreach ($indexers as $indexer) { - if (!empty($byType[$indexer->getTypeId()])) { - $indexer->reindexEntity($byType[$indexer->getTypeId()]); - } + /** + * @param array $entityIds + * @return void + */ + private function deleteIndexData(array $entityIds) + { + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $select = $this->getConnection()->select()->from( + ['index_price' => $this->tableMaintainer->getMainTable($dimensions)], + null + )->where('index_price.entity_id IN (?)', $entityIds); + $query = $select->deleteFromSelect('index_price'); + $this->getConnection()->query($query); } - $this->_syncData($changedIds); - - return $compositeIds + $notCompositeIds; } /** @@ -467,16 +422,21 @@ protected function _reindexRows($changedIds = []) * @param null|array $parentIds * @param array $excludeIds * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * @deprecated Used only for backward compatibility for do not broke custom indexer implementation + * which do not work by dimensions. + * For indexers, which support dimensions all composite products read data directly from main price indexer table + * or replica table for partial or full reindex correspondingly. */ protected function _copyRelationIndexData($parentIds, $excludeIds = null) { $linkField = $this->getProductIdFieldName(); - $select = $this->_connection->select()->from( + $select = $this->getConnection()->select()->from( $this->_defaultIndexerResource->getTable('catalog_product_relation'), ['child_id'] )->join( ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], - 'e.' . $linkField . ' = parent_id' + 'e.' . $linkField . ' = parent_id', + [] )->where( 'e.entity_id IN(?)', $parentIds @@ -485,22 +445,45 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) $select->where('child_id NOT IN(?)', $excludeIds); } - $children = $this->_connection->fetchCol($select); + $children = $this->getConnection()->fetchCol($select); if ($children) { - $select = $this->_connection->select()->from( - $this->getIndexTargetTable() - )->where( - 'entity_id IN(?)', - $children - ); - $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false); - $this->_connection->query($query); + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $select = $this->getConnection()->select()->from( + $this->getIndexTargetTableByDimension($dimensions) + )->where( + 'entity_id IN(?)', + $children + ); + $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false); + $this->getConnection()->query($query); + } } return $this; } + /** + * Retrieve index table by dimension that will be used for write operations. + * + * This method is used during both partial and full reindex to identify the table. + * + * @param \Magento\Framework\Search\Request\Dimension[] $dimensions + * + * @return string + */ + private function getIndexTargetTableByDimension(array $dimensions) + { + $indexTargetTable = $this->getIndexTargetTable(); + if ($indexTargetTable === self::getIndexTargetTable()) { + $indexTargetTable = $this->tableMaintainer->getMainTable($dimensions); + } + if ($indexTargetTable === self::getIndexTargetTable() . '_replica') { + $indexTargetTable = $this->tableMaintainer->getMainReplicaTable($dimensions); + } + return $indexTargetTable; + } + /** * Retrieve index table that will be used for write operations. * @@ -519,20 +502,72 @@ protected function getIndexTargetTable() protected function getProductIdFieldName() { $table = $this->_defaultIndexerResource->getTable('catalog_product_entity'); - $indexList = $this->_connection->getIndexList($table); - return $indexList[$this->_connection->getPrimaryKeyName($table)]['COLUMNS_LIST'][0]; + $indexList = $this->getConnection()->getIndexList($table); + return $indexList[$this->getConnection()->getPrimaryKeyName($table)]['COLUMNS_LIST'][0]; + } + + /** + * Get products types. + * + * @param array $changedIds + * @return array + */ + private function getProductsTypes(array $changedIds = []) + { + $select = $this->getConnection()->select()->from( + $this->_defaultIndexerResource->getTable('catalog_product_entity'), + ['entity_id', 'type_id'] + ); + if ($changedIds) { + $select->where('entity_id IN (?)', $changedIds); + } + $pairs = $this->getConnection()->fetchPairs($select); + + $byType = []; + foreach ($pairs as $productId => $productType) { + $byType[$productType][$productId] = $productId; + } + + return $byType; } /** - * @return \Magento\Catalog\Model\ResourceModel\Product - * @deprecated 101.1.0 + * Get parent products types + * Used for add composite products to reindex if we have only simple products in changed ids set + * + * @param array $productsIds + * @return array */ - private function getProductResource() + private function getParentProductsTypes(array $productsIds) { - if (null === $this->productResource) { - $this->productResource = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\ResourceModel\Product::class); + $select = $this->getConnection()->select()->from( + ['l' => $this->_defaultIndexerResource->getTable('catalog_product_relation')], + '' + )->join( + ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], + 'e.' . $this->getProductIdFieldName() . ' = l.parent_id', + ['e.entity_id as parent_id', 'type_id'] + )->where( + 'l.child_id IN(?)', + $productsIds + ); + $pairs = $this->getConnection()->fetchPairs($select); + + $byType = []; + foreach ($pairs as $productId => $productType) { + $byType[$productType][$productId] = $productId; } - return $this->productResource; + + return $byType; + } + + /** + * Get connection + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ + private function getConnection() + { + return $this->_defaultIndexerResource->getConnection(); } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index eb15833a7d0b2..79eeb3cc3225d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -3,65 +3,128 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIterator; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var ActiveTableSwitcher */ private $activeTableSwitcher; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher - * + * @var EntityMetadataInterface + */ + private $productMetaDataCached; + + /** + * @var DimensionCollectionFactory + */ + private $dimensionCollectionFactory; + + /** + * @var TableMaintainer + */ + private $dimensionTableMaintainer; + + /** + * @var ProcessManager + */ + private $processManager; + + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param MetadataPool|null $metadataPool + * @param BatchSizeCalculator|null $batchSizeCalculator + * @param BatchProviderInterface|null $batchProvider + * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $dimensionTableMaintainer + * @param ProcessManager $processManager + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + MetadataPool $metadataPool = null, + BatchSizeCalculator $batchSizeCalculator = null, + BatchProviderInterface $batchProvider = null, + ActiveTableSwitcher $activeTableSwitcher = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $dimensionTableMaintainer = null, + ProcessManager $processManager = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $config, @@ -74,17 +137,27 @@ public function __construct( $defaultIndexerResource ); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class + BatchSizeCalculator::class ); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class + ActiveTableSwitcher::class + ); + $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( + DimensionCollectionFactory::class + ); + $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( + TableMaintainer::class + ); + $this->processManager = $processManager ?: ObjectManager::getInstance()->get( + ProcessManager::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -92,78 +165,322 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Exception * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($ids = null) { try { - $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); - $this->_prepareWebsiteDateTable(); + //Prepare indexer tables before full reindex + $this->prepareTables(); + + /** @var DefaultPrice $indexer */ + foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { + if ($priceIndexer instanceof DimensionalIndexerInterface) { + //New price reindex mechanism + $this->reindexProductTypeWithDimensions($priceIndexer, $typeId); + continue; + } + + $priceIndexer->getTableStrategy()->setUseIdxTable(false); + + //Old price reindex mechanism + $this->reindexProductType($priceIndexer, $typeId); + } + + //Final replacement of tables from replica to main + $this->switchTables(); + } catch (\Exception $e) { + throw new LocalizedException(__($e->getMessage()), $e); + } + } + + /** + * Prepare indexer tables before full reindex + * + * @return void + * @throws \Exception + */ + private function prepareTables() + { + $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); + + $this->_prepareWebsiteDateTable(); + + $this->truncateReplicaTables(); + } + + /** + * Truncate replica tables by dimensions + * + * @return void + * @throws \Exception + */ + private function truncateReplicaTables() + { + foreach ($this->dimensionCollectionFactory->create() as $dimension) { + $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension); + $this->_defaultIndexerResource->getConnection()->truncateTable($dimensionTable); + } + } + + /** + * Reindex new 'Dimensional' price indexer by product type + * + * @param DimensionalIndexerInterface $priceIndexer + * @param string $typeId + * + * @return void + * @throws \Exception + */ + private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId) + { + $userFunctions = []; + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { + $this->reindexByBatches($priceIndexer, $dimensions, $typeId); + }; + } + $this->processManager->execute($userFunctions); + } + + /** + * Reindex new 'Dimensional' price indexer by batches + * + * @param DimensionalIndexerInterface $priceIndexer + * @param array $dimensions + * @param string $typeId + * + * @return void + * @throws \Exception + */ + private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) + { + foreach ($this->getBatchesForIndexer($typeId) as $batch) { + $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); + } + } + + /** + * Get batches for new 'Dimensional' price indexer + * + * @param string $typeId + * + * @return BatchIterator + */ + private function getBatchesForIndexer(string $typeId): BatchIterator + { + $connection = $this->_defaultIndexerResource->getConnection(); + $entityMetadata = $this->getProductMetaData(); + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + return $this->batchQueryGenerator->generate( + $entityMetadata->getIdentifierField(), + $select, + $this->batchSizeCalculator->estimateBatchSize( + $connection, + $typeId + ) + ); + } + + /** + * Reindex by batch for new 'Dimensional' price indexer + * + * @param DimensionalIndexerInterface $priceIndexer + * @param Select $batchQuery + * @param array $dimensions + * + * @return void + * @throws \Exception + */ + private function reindexByBatchWithDimensions( + DimensionalIndexerInterface $priceIndexer, + Select $batchQuery, + array $dimensions + ) { + $entityIds = $this->getEntityIdsFromBatch($batchQuery); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $replicaTable = $this->activeTableSwitcher->getAdditionalTableName( - $this->_defaultIndexerResource->getMainTable() + if (!empty($entityIds)) { + $this->dimensionTableMaintainer->createMainTmpTable($dimensions); + $temporaryTable = $this->dimensionTableMaintainer->getMainTmpTable($dimensions); + $this->_emptyTable($temporaryTable); + + $priceIndexer->executeByDimensions($dimensions, \SplFixedArray::fromArray($entityIds, false)); + + // Sync data from temp table to index table + $this->_insertFromTable( + $temporaryTable, + $this->dimensionTableMaintainer->getMainReplicaTable($dimensions) ); + } + } - // Prepare replica table for indexation. - $this->_defaultIndexerResource->getConnection()->truncateTable($replicaTable); - - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer $indexer */ - foreach ($this->getTypeIndexers() as $indexer) { - $indexer->getTableStrategy()->setUseIdxTable(false); - $connection = $indexer->getConnection(); - - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), - $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexer->getTypeId()) - ); - - foreach ($batches as $batch) { - // Get entity ids from batch - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $select->where('type_id = ?', $indexer->getTypeId()); - - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); - - if (!empty($entityIds)) { - // Temporary table will created if not exists - $idxTableName = $this->_defaultIndexerResource->getIdxTable(); - $this->_emptyTable($idxTableName); - - if ($indexer->getIsComposite()) { - $this->_copyRelationIndexData($entityIds); - } - $this->_prepareTierPriceIndex($entityIds); - - // Reindex entities by id - $indexer->reindexEntity($entityIds); - - // Sync data from temp table to index table - $this->_insertFromTable($idxTableName, $replicaTable); - - // Drop temporary index table - $connection->dropTable($idxTableName); - } - } + /** + * Reindex old price indexer by product type + * + * @param PriceInterface $priceIndexer + * @param string $typeId + * + * @return void + * @throws \Exception + */ + private function reindexProductType(PriceInterface $priceIndexer, string $typeId) + { + foreach ($this->getBatchesForIndexer($typeId) as $batch) { + $this->reindexBatch($priceIndexer, $batch); + } + } + + /** + * Reindex by batch for old price indexer + * + * @param PriceInterface $priceIndexer + * @param Select $batch + * @return void + * @throws \Exception + */ + private function reindexBatch(PriceInterface $priceIndexer, Select $batch) + { + $entityIds = $this->getEntityIdsFromBatch($batch); + + if (!empty($entityIds)) { + // Temporary table will created if not exists + $idxTableName = $this->_defaultIndexerResource->getIdxTable(); + $this->_emptyTable($idxTableName); + + if ($priceIndexer->getIsComposite()) { + $this->_copyRelationIndexData($entityIds); } + + // Reindex entities by id + $priceIndexer->reindexEntity($entityIds); + + // Sync data from temp table to index table + $this->_insertFromTable($idxTableName, $this->getReplicaTable()); + + // Drop temporary index table + $this->_defaultIndexerResource->getConnection()->dropTable($idxTableName); + } + } + + /** + * Get Entity Ids from batch + * + * @param Select $batch + * @return array + */ + private function getEntityIdsFromBatch(Select $batch): array + { + $connection = $this->_defaultIndexerResource->getConnection(); + + return $connection->fetchCol($batch); + } + + /** + * Get product meta data + * + * @return EntityMetadataInterface + */ + private function getProductMetaData(): EntityMetadataInterface + { + if ($this->productMetaDataCached === null) { + $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); + } + + return $this->productMetaDataCached; + } + + /** + * Get replica table + * + * @return string + */ + private function getReplicaTable(): string + { + return $this->activeTableSwitcher->getAdditionalTableName( + $this->_defaultIndexerResource->getMainTable() + ); + } + + /** + * Replacement of tables from replica to main + * + * @return void + */ + private function switchTables() + { + // Switch dimension tables + $mainTablesByDimension = []; + + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTable($dimensions); + + //Move data from indexers with old realisation + $this->moveDataFromReplicaTableToReplicaTables($dimensions); + } + + if (count($mainTablesByDimension) > 0) { $this->activeTableSwitcher->switchTable( $this->_defaultIndexerResource->getConnection(), - [$this->_defaultIndexerResource->getMainTable()] + $mainTablesByDimension ); - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); } } /** - * @inheritdoc + * Move data from old price indexer mechanism to new indexer mechanism by dimensions. + * + * Used only for backward compatibility + * + * @param array $dimensions + * @return void + */ + private function moveDataFromReplicaTableToReplicaTables(array $dimensions) + { + if (!$dimensions) { + return; + } + $select = $this->dimensionTableMaintainer->getConnection()->select()->from( + $this->dimensionTableMaintainer->getMainReplicaTable([]) + ); + + $check = clone $select; + $check->reset('columns')->columns('count(*)'); + + if (!$this->dimensionTableMaintainer->getConnection()->query($check)->fetchColumn()) { + return; + } + + $replicaTablesByDimension = $this->dimensionTableMaintainer->getMainReplicaTable($dimensions); + + foreach ($dimensions as $dimension) { + if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) { + $select->where('website_id = ?', $dimension->getValue()); + } + if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) { + $select->where('customer_group_id = ?', $dimension->getValue()); + } + } + + $this->dimensionTableMaintainer->getConnection()->query( + $this->dimensionTableMaintainer->getConnection()->insertFromSelect( + $select, + $replicaTablesByDimension, + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } + + /** + * Retrieves the index table that should be used + * + * @deprecated */ - protected function getIndexTargetTable() + protected function getIndexTargetTable(): string { return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php new file mode 100644 index 0000000000000..62a129f0ff448 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionCollectionFactory.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\MultiDimensionProvider; + +class DimensionCollectionFactory +{ + /** + * @var \Magento\Framework\Indexer\MultiDimensionProviderFactory + */ + private $multiDimensionProviderFactory; + + /** + * @var DimensionProviderInterface[] + */ + private $dimensionProviders; + + /** + * @var DimensionModeConfiguration + */ + private $dimensionModeConfiguration; + + /** + * @param \Magento\Framework\Indexer\MultiDimensionProviderFactory $multiDimensionProviderFactory + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param array $dimensionProviders + */ + public function __construct( + \Magento\Framework\Indexer\MultiDimensionProviderFactory $multiDimensionProviderFactory, + DimensionModeConfiguration $dimensionModeConfiguration, + array $dimensionProviders + ) { + $this->multiDimensionProviderFactory = $multiDimensionProviderFactory; + $this->dimensionProviders = $dimensionProviders; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + } + + /** + * Create MultiDimensionProvider for specified "dimension mode". + * By default return multiplication of dimensions by current set mode + * + * @param string|null $dimensionsMode + * @return MultiDimensionProvider + */ + public function create(string $dimensionsMode = null): MultiDimensionProvider + { + $dimensionConfiguration = $this->dimensionModeConfiguration->getDimensionConfiguration($dimensionsMode); + + $providers = []; + foreach ($dimensionConfiguration as $dimensionName) { + if (!isset($this->dimensionProviders[$dimensionName])) { + throw new \LogicException( + 'Dimension Provider is missing. Cannot handle unknown dimension: ' . $dimensionName + ); + } + $providers[] = clone $this->dimensionProviders[$dimensionName]; + } + + return $this->multiDimensionProviderFactory->create( + [ + 'dimensionProviders' => $providers + ] + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php new file mode 100644 index 0000000000000..7a4d8e313462d --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/DimensionModeConfiguration.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; + +class DimensionModeConfiguration +{ + /**#@+ + * Available modes of dimensions for product price indexer + */ + const DIMENSION_NONE = 'none'; + const DIMENSION_WEBSITE = 'website'; + const DIMENSION_CUSTOMER_GROUP = 'customer_group'; + const DIMENSION_WEBSITE_AND_CUSTOMER_GROUP = 'website_and_customer_group'; + /**#@-*/ + + /** + * Mapping between dimension mode and dimension provider name + * + * @var array + */ + private $modesMapping = [ + self::DIMENSION_NONE => [ + ], + self::DIMENSION_WEBSITE => [ + WebsiteDimensionProvider::DIMENSION_NAME + ], + self::DIMENSION_CUSTOMER_GROUP => [ + CustomerGroupDimensionProvider::DIMENSION_NAME + ], + self::DIMENSION_WEBSITE_AND_CUSTOMER_GROUP => [ + WebsiteDimensionProvider::DIMENSION_NAME, + CustomerGroupDimensionProvider::DIMENSION_NAME + ], + ]; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var string + */ + private $currentMode; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Return dimension modes configuration. + * + * @return array + */ + public function getDimensionModes(): array + { + return $this->modesMapping; + } + + /** + * Get names of dimensions which used for provided mode. + * By default return dimensions for current enabled mode + * + * @param string|null $mode + * @return string[] + * @throws \InvalidArgumentException + */ + public function getDimensionConfiguration(string $mode = null): array + { + if ($mode && !isset($this->modesMapping[$mode])) { + throw new \InvalidArgumentException( + sprintf('Undefined dimension mode "%s".', $mode) + ); + } + return $this->modesMapping[$mode ?? $this->getCurrentMode()]; + } + + /** + * @return string + */ + private function getCurrentMode(): string + { + if (null === $this->currentMode) { + $this->currentMode = $this->scopeConfig->getValue(ModeSwitcherConfiguration::XML_PATH_PRICE_DIMENSIONS_MODE) + ?: self::DIMENSION_NONE; + } + + return $this->currentMode; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php new file mode 100644 index 0000000000000..e71031489fa0e --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\Search\Request\Dimension; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Indexer\Model\DimensionModes; +use Magento\Indexer\Model\DimensionMode; + +/** + * Class to prepare new tables for new indexer mode + */ +class ModeSwitcher implements \Magento\Indexer\Model\ModeSwitcherInterface +{ + /** + * TableMaintainer + * + * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer + */ + private $tableMaintainer; + + /** + * DimensionCollectionFactory + * + * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + */ + private $dimensionCollectionFactory; + + /** + * @var array|null + */ + private $dimensionsArray; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration + */ + private $dimensionModeConfiguration; + + /** + * @var ModeSwitcherConfiguration + */ + private $modeSwitcherConfiguration; + + /** + * @param TableMaintainer $tableMaintainer + * @param DimensionCollectionFactory $dimensionCollectionFactory + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param ModeSwitcherConfiguration $modeSwitcherConfiguration + */ + public function __construct( + TableMaintainer $tableMaintainer, + DimensionCollectionFactory $dimensionCollectionFactory, + DimensionModeConfiguration $dimensionModeConfiguration, + ModeSwitcherConfiguration $modeSwitcherConfiguration + ) { + $this->tableMaintainer = $tableMaintainer; + $this->dimensionCollectionFactory = $dimensionCollectionFactory; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + $this->modeSwitcherConfiguration = $modeSwitcherConfiguration; + } + + /** + * @inheritdoc + */ + public function getDimensionModes(): DimensionModes + { + $dimensionsList = []; + foreach ($this->dimensionModeConfiguration->getDimensionModes() as $dimension => $modes) { + $dimensionsList[] = new DimensionMode($dimension, $modes); + } + + return new DimensionModes($dimensionsList); + } + + /** + * @inheritdoc + */ + public function switchMode(string $currentMode, string $previousMode) + { + //Create new tables and move data + $this->createTables($currentMode); + $this->moveData($currentMode, $previousMode); + + //Change config options + $this->modeSwitcherConfiguration->saveMode($currentMode); + + //Delete old tables + $this->dropTables($previousMode); + } + + /** + * Create new tables + * + * @param string $currentMode + * + * @return void + * @throws \Zend_Db_Exception + */ + public function createTables(string $currentMode) + { + foreach ($this->getDimensionsArray($currentMode) as $dimensions) { + if (!empty($dimensions)) { + $this->tableMaintainer->createTablesForDimensions($dimensions); + } + } + } + + /** + * Move data from old tables to new + * + * @param string $currentMode + * @param string $previousMode + * + * @return void + */ + public function moveData(string $currentMode, string $previousMode) + { + $dimensionsArrayForCurrentMode = $this->getDimensionsArray($currentMode); + $dimensionsArrayForPreviousMode = $this->getDimensionsArray($previousMode); + + foreach ($dimensionsArrayForCurrentMode as $dimensionsForCurrentMode) { + $newTable = $this->tableMaintainer->getMainTable($dimensionsForCurrentMode); + if (empty($dimensionsForCurrentMode)) { + // new mode is 'none' + foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { + $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $this->insertFromOldTablesToNew($newTable, $oldTable); + } + } else { + // new mode is not 'none' + foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { + $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $this->insertFromOldTablesToNew($newTable, $oldTable, $dimensionsForCurrentMode); + } + } + } + } + + /** + * Drop old tables + * + * @param string $previousMode + * + * @return void + */ + public function dropTables(string $previousMode) + { + foreach ($this->getDimensionsArray($previousMode) as $dimensions) { + if (empty($dimensions)) { + $this->tableMaintainer->truncateTablesForDimensions($dimensions); + } else { + $this->tableMaintainer->dropTablesForDimensions($dimensions); + } + } + } + + /** + * Get dimensions array + * + * @param string $mode + * + * @return \Magento\Framework\Indexer\MultiDimensionProvider + */ + private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider + { + if (isset($this->dimensionsArray[$mode])) { + return $this->dimensionsArray[$mode]; + } + + $this->dimensionsArray[$mode] = $this->dimensionCollectionFactory->create($mode); + + return $this->dimensionsArray[$mode]; + } + + /** + * Insert from old tables data to new + * + * @param string $newTable + * @param string $oldTable + * @param Dimension[] $dimensions + * + * @return void + */ + private function insertFromOldTablesToNew(string $newTable, string $oldTable, array $dimensions = []) + { + $select = $this->tableMaintainer->getConnection()->select()->from($oldTable); + + foreach ($dimensions as $dimension) { + if ($dimension->getName() === WebsiteDimensionProvider::DIMENSION_NAME) { + $select->where('website_id = ?', $dimension->getValue()); + } + if ($dimension->getName() === CustomerGroupDimensionProvider::DIMENSION_NAME) { + $select->where('customer_group_id = ?', $dimension->getValue()); + } + } + $this->tableMaintainer->getConnection()->query( + $this->tableMaintainer->getConnection()->insertFromSelect( + $select, + $newTable, + [], + \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php new file mode 100644 index 0000000000000..66b7147a8db76 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcherConfiguration.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Indexer\Model\Indexer; + +/** + * Class to configure indexers and system config after modes has been switched + */ +class ModeSwitcherConfiguration +{ + const XML_PATH_PRICE_DIMENSIONS_MODE = 'indexer/catalog_product_price/dimensions_mode'; + + /** + * ConfigInterface + * + * @var ConfigInterface + */ + private $configWriter; + + /** + * TypeListInterface + * + * @var TypeListInterface + */ + private $cacheTypeList; + + /** + * @var Indexer $indexer + */ + private $indexer; + + /** + * @param ConfigInterface $configWriter + * @param TypeListInterface $cacheTypeList + * @param Indexer $indexer + */ + public function __construct( + ConfigInterface $configWriter, + TypeListInterface $cacheTypeList, + Indexer $indexer + ) { + $this->configWriter = $configWriter; + $this->cacheTypeList = $cacheTypeList; + $this->indexer = $indexer; + } + + /** + * Save switcher mode and invalidate reindex. + * + * @param string $mode + * @return void + * @throws \InvalidArgumentException + */ + public function saveMode(string $mode) + { + //Change config options + $this->configWriter->saveConfig(self::XML_PATH_PRICE_DIMENSIONS_MODE, $mode); + $this->cacheTypeList->cleanType('config'); + $this->indexer->load(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID); + $this->indexer->invalidate(); + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php index 32b2db8a7008c..9b99ee8c8dc8c 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/CustomerGroup.php @@ -5,9 +5,15 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Price\Plugin; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Api\Data\GroupInterface; -use \Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface; +use Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Framework\Indexer\Dimension; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; class CustomerGroup { @@ -17,14 +23,46 @@ class CustomerGroup private $updateIndex; /** - * Constructor + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * DimensionFactory * + * @var DimensionFactory + */ + private $dimensionFactory; + + /** + * @var DimensionModeConfiguration + */ + private $dimensionModeConfiguration; + + /** + * @var WebsiteDimensionProvider + */ + private $websiteDimensionProvider; + + /** * @param UpdateIndexInterface $updateIndex + * @param TableMaintainer $tableMaintainer + * @param DimensionFactory $dimensionFactory + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param WebsiteDimensionProvider $websiteDimensionProvider */ public function __construct( - UpdateIndexInterface $updateIndex + UpdateIndexInterface $updateIndex, + TableMaintainer $tableMaintainer, + DimensionFactory $dimensionFactory, + DimensionModeConfiguration $dimensionModeConfiguration, + WebsiteDimensionProvider $websiteDimensionProvider ) { $this->updateIndex = $updateIndex; + $this->tableMaintainer = $tableMaintainer; + $this->dimensionFactory = $dimensionFactory; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + $this->websiteDimensionProvider = $websiteDimensionProvider; } /** @@ -32,7 +70,8 @@ public function __construct( * * @param GroupRepositoryInterface $subject * @param \Closure $proceed - * @param GroupInterface $result + * @param GroupInterface $group + * * @return GroupInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -43,7 +82,64 @@ public function aroundSave( ) { $isGroupNew = !$group->getId(); $group = $proceed($group); + if ($isGroupNew) { + foreach ($this->getAffectedDimensions((string)$group->getId()) as $dimensions) { + $this->tableMaintainer->createTablesForDimensions($dimensions); + } + } $this->updateIndex->update($group, $isGroupNew); return $group; } + + /** + * Update price index after customer group deleted + * + * @param GroupRepositoryInterface $subject + * @param bool $result + * @param string $groupId + * + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDeleteById(GroupRepositoryInterface $subject, bool $result, string $groupId) + { + foreach ($this->getAffectedDimensions($groupId) as $dimensions) { + $this->tableMaintainer->dropTablesForDimensions($dimensions); + } + + return $result; + } + + /** + * Get affected dimensions + * + * @param string $groupId + * @return Dimension[][] + */ + private function getAffectedDimensions(string $groupId): array + { + $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration(); + // do not return dimensions if Customer Group dimension is not present in configuration + if (!in_array(CustomerGroupDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) { + return []; + } + $customerGroupDimension = $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + $groupId + ); + + $dimensions = []; + if (in_array(WebsiteDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) { + foreach ($this->websiteDimensionProvider as $websiteDimension) { + $dimensions[] = [ + $customerGroupDimension, + $websiteDimension + ]; + } + } else { + $dimensions[] = [$customerGroupDimension]; + } + + return $dimensions; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php new file mode 100644 index 0000000000000..fbeec22783090 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/TableResolver.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Price\Plugin; + +use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; +use Magento\Framework\App\Http\Context; +use Magento\Framework\Indexer\Dimension; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Context as CustomerContext; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; + +/** + * Replace catalog_product_index_price table name on the table name segmented per dimension. + * Used only for backward compatibility + */ +class TableResolver +{ + /** + * @var IndexScopeResolverInterface + */ + private $priceTableResolver; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Context + */ + private $httpContext; + + /** + * @var DimensionFactory + */ + private $dimensionFactory; + + /** + * @var DimensionModeConfiguration + */ + private $dimensionModeConfiguration; + + /** + * @param IndexScopeResolverInterface $priceTableResolver + * @param StoreManagerInterface $storeManager + * @param Context $context + * @param DimensionFactory $dimensionFactory + * @param DimensionModeConfiguration $dimensionModeConfiguration + */ + public function __construct( + IndexScopeResolverInterface $priceTableResolver, + StoreManagerInterface $storeManager, + Context $context, + DimensionFactory $dimensionFactory, + DimensionModeConfiguration $dimensionModeConfiguration + ) { + $this->priceTableResolver = $priceTableResolver; + $this->storeManager = $storeManager; + $this->httpContext = $context; + $this->dimensionFactory = $dimensionFactory; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + } + + /** + * Replacing catalog_product_index_price table name on the table name segmented per dimension. + * + * @param ResourceConnection $subject + * @param string $result + * @param string|string[] $tableName + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetTableName( + ResourceConnection $subject, + string $result, + $tableName + ) { + if (!is_array($tableName) + && $tableName === 'catalog_product_index_price' + && $this->dimensionModeConfiguration->getDimensionConfiguration() + ) { + return $this->priceTableResolver->resolve('catalog_product_index_price', $this->getDimensions()); + } + + return $result; + } + + /** + * @return Dimension[] + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getDimensions(): array + { + $dimensions = []; + foreach ($this->dimensionModeConfiguration->getDimensionConfiguration() as $dimensionName) { + if ($dimensionName === WebsiteDimensionProvider::DIMENSION_NAME) { + $dimensions[] = $this->createDimensionFromWebsite(); + } + if ($dimensionName === CustomerGroupDimensionProvider::DIMENSION_NAME) { + $dimensions[] = $this->createDimensionFromCustomerGroup(); + } + } + + return $dimensions; + } + + /** + * @return Dimension + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function createDimensionFromWebsite(): Dimension + { + $storeKey = $this->httpContext->getValue(StoreManagerInterface::CONTEXT_STORE); + return $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + (string)$this->storeManager->getStore($storeKey)->getWebsiteId() + ); + } + + /** + * @return Dimension + */ + private function createDimensionFromCustomerGroup(): Dimension + { + return $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP) + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php index 269515e292e17..4831680f07c33 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Plugin/Website.php @@ -5,33 +5,128 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Price\Plugin; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Framework\Indexer\Dimension; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\AbstractModel; + class Website { /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * DimensionFactory + * + * @var DimensionFactory + */ + private $dimensionFactory; + + /** + * @var DimensionModeConfiguration */ - protected $_processor; + private $dimensionModeConfiguration; /** - * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $processor + * @var CustomerGroupDimensionProvider */ - public function __construct(\Magento\Catalog\Model\Indexer\Product\Price\Processor $processor) + private $customerGroupDimensionProvider; + + /** + * @param TableMaintainer $tableMaintainer + * @param DimensionFactory $dimensionFactory + * @param DimensionModeConfiguration $dimensionModeConfiguration + * @param CustomerGroupDimensionProvider $customerGroupDimensionProvider + */ + public function __construct( + TableMaintainer $tableMaintainer, + DimensionFactory $dimensionFactory, + DimensionModeConfiguration $dimensionModeConfiguration, + CustomerGroupDimensionProvider $customerGroupDimensionProvider + ) { + $this->tableMaintainer = $tableMaintainer; + $this->dimensionFactory = $dimensionFactory; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + $this->customerGroupDimensionProvider = $customerGroupDimensionProvider; + } + + /** + * Update price index after website deleted + * + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $website + * + * @return AbstractDb + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website) { - $this->_processor = $processor; + foreach ($this->getAffectedDimensions($website->getId()) as $dimensions) { + $this->tableMaintainer->dropTablesForDimensions($dimensions); + } + + return $objectResource; } /** - * Invalidate price indexer + * Update price index after website created * - * @param \Magento\Store\Model\ResourceModel\Website $subject - * @param \Magento\Store\Model\ResourceModel\Website $result - * @return \Magento\Store\Model\ResourceModel\Website + * @param AbstractDb $subject + * @param AbstractDb $objectResource + * @param AbstractModel $website * + * @return AbstractDb * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterDelete(\Magento\Store\Model\ResourceModel\Website $subject, $result) + public function afterSave(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website) + { + if ($website->isObjectNew()) { + foreach ($this->getAffectedDimensions($website->getId()) as $dimensions) { + $this->tableMaintainer->createTablesForDimensions($dimensions); + } + } + + return $objectResource; + } + + /** + * Get affected dimensions + * + * @param string $websiteId + * + * @return Dimension[][] + */ + private function getAffectedDimensions(string $websiteId): array { - $this->_processor->markIndexerAsInvalid(); - return $result; + $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration(); + // do not return dimensions if Website dimension is not present in configuration + if (!in_array(WebsiteDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) { + return []; + } + $websiteDimension = $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + $websiteId + ); + + $dimensions = []; + if (in_array(CustomerGroupDimensionProvider::DIMENSION_NAME, $currentDimensions, true)) { + foreach ($this->customerGroupDimensionProvider as $customerGroupDimension) { + $dimensions[] = [ + $customerGroupDimension, + $websiteDimension + ]; + } + } else { + $dimensions[] = [$websiteDimension]; + } + + return $dimensions; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php new file mode 100644 index 0000000000000..0e4850d1f9541 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/PriceTableResolver.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\Indexer\Dimension; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; + +/** + * Class return price table name based on dimension + * use only on the frontend area + */ +class PriceTableResolver implements IndexScopeResolverInterface +{ + /** + * @var IndexScopeResolver + */ + private $indexScopeResolver; + + /** + * @var DimensionModeConfiguration + */ + private $dimensionModeConfiguration; + + /** + * @param IndexScopeResolver $indexScopeResolver + * @param DimensionModeConfiguration $dimensionModeConfiguration + */ + public function __construct( + IndexScopeResolver $indexScopeResolver, + DimensionModeConfiguration $dimensionModeConfiguration + ) { + $this->indexScopeResolver = $indexScopeResolver; + $this->dimensionModeConfiguration = $dimensionModeConfiguration; + } + + /** + * Return price table name based on dimension + * @param string $index + * @param array $dimensions + * @return string + */ + public function resolve($index, array $dimensions) + { + if ($index === 'catalog_product_index_price') { + $dimensions = $this->filterDimensions($dimensions); + } + return $this->indexScopeResolver->resolve($index, $dimensions); + } + + /** + * @param Dimension[] $dimensions + * @return array + * @throws \Exception + */ + private function filterDimensions($dimensions): array + { + $existDimensions = []; + $currentDimensions = $this->dimensionModeConfiguration->getDimensionConfiguration(); + foreach ($dimensions as $dimension) { + if ((string)$dimension->getValue() === '') { + throw new \InvalidArgumentException( + sprintf('Dimension value of "%s" can not be empty', $dimension->getName()) + ); + } + if (in_array($dimension->getName(), $currentDimensions, true)) { + $existDimensions[] = $dimension; + } + } + + return $existDimensions; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php new file mode 100644 index 0000000000000..999eaaa2a8025 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php @@ -0,0 +1,280 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); +namespace Magento\Catalog\Model\Indexer\Product\Price; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Search\Request\Dimension; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; + +/** + * Class encapsulate logic of work with tables per store in Product Price indexer + */ +class TableMaintainer +{ + /** + * Catalog product price index table name + */ + const MAIN_INDEX_TABLE = 'catalog_product_index_price'; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var TableResolver + */ + private $tableResolver; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * Catalog tmp category index table name + */ + private $tmpTableSuffix = '_temp'; + + /** + * Catalog tmp category index table name + */ + private $additionalTableSuffix = '_replica'; + + /** + * @var string[] + */ + private $mainTmpTable; + + /** + * @var null|string + */ + private $connectionName; + + /** + * @param ResourceConnection $resource + * @param TableResolver $tableResolver + * @param null $connectionName + */ + public function __construct( + ResourceConnection $resource, + TableResolver $tableResolver, + $connectionName = null + ) { + $this->resource = $resource; + $this->tableResolver = $tableResolver; + $this->connectionName = $connectionName; + } + + /** + * Get connection for work with price indexer + * + * @return AdapterInterface + */ + public function getConnection(): AdapterInterface + { + if (null === $this->connection) { + $this->connection = $this->resource->getConnection($this->connectionName); + } + return $this->connection; + } + + /** + * Return validated table name + * + * @param string $table + * @return string + */ + private function getTable(string $table): string + { + return $this->resource->getTableName($table); + } + + /** + * Create table based on main table + * + * @param string $mainTableName + * @param string $newTableName + * + * @return void + * + * @throws \Zend_Db_Exception + */ + private function createTable(string $mainTableName, string $newTableName) + { + if (!$this->getConnection()->isTableExists($newTableName)) { + $this->getConnection()->createTable( + $this->getConnection()->createTableByDdl($mainTableName, $newTableName) + ); + } + } + + /** + * Drop table + * + * @param string $tableName + * + * @return void + */ + private function dropTable(string $tableName) + { + if ($this->getConnection()->isTableExists($tableName)) { + $this->getConnection()->dropTable($tableName); + } + } + + /** + * Truncate table + * + * @param string $tableName + * + * @return void + */ + private function truncateTable(string $tableName) + { + if ($this->getConnection()->isTableExists($tableName)) { + $this->getConnection()->truncateTable($tableName); + } + } + + /** + * Get array key for tmp table + * + * @param Dimension[] $dimensions + * + * @return string + */ + private function getArrayKeyForTmpTable(array $dimensions): string + { + $key = 'temp'; + foreach ($dimensions as $dimension) { + $key .= $dimension->getName() . '_' . $dimension->getValue(); + } + return $key; + } + + /** + * Return main index table name + * + * @param Dimension[] $dimensions + * + * @return string + */ + public function getMainTable(array $dimensions): string + { + return $this->tableResolver->resolve(self::MAIN_INDEX_TABLE, $dimensions); + } + + /** + * Create main and replica index tables for dimensions + * + * @param Dimension[] $dimensions + * + * @return void + * + * @throws \Zend_Db_Exception + */ + public function createTablesForDimensions(array $dimensions) + { + $mainTableName = $this->getMainTable($dimensions); + //Create index table for dimensions based on main replica table + //Using main replica table is necessary for backward capability and TableResolver plugin work + $this->createTable( + $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix), + $mainTableName + ); + + $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + //Create replica table for dimensions based on main replica table + $this->createTable( + $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix), + $mainReplicaTableName + ); + } + + /** + * Drop main and replica index tables for dimensions + * + * @param Dimension[] $dimensions + * + * @return void + */ + public function dropTablesForDimensions(array $dimensions) + { + $mainTableName = $this->getMainTable($dimensions); + $this->dropTable($mainTableName); + + $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $this->dropTable($mainReplicaTableName); + } + + /** + * Truncate main and replica index tables for dimensions + * + * @param Dimension[] $dimensions + * + * @return void + */ + public function truncateTablesForDimensions(array $dimensions) + { + $mainTableName = $this->getMainTable($dimensions); + $this->truncateTable($mainTableName); + + $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $this->truncateTable($mainReplicaTableName); + } + + /** + * Return replica index table name + * + * @param Dimension[] $dimensions + * + * @return string + */ + public function getMainReplicaTable(array $dimensions): string + { + return $this->getMainTable($dimensions) . $this->additionalTableSuffix; + } + + /** + * Create temporary index table for dimensions + * + * @param Dimension[] $dimensions + * + * @return void + */ + public function createMainTmpTable(array $dimensions) + { + // Create temporary table based on template table catalog_product_index_price_tmp without indexes + $templateTableName = $this->resource->getTableName(self::MAIN_INDEX_TABLE . '_tmp'); + $temporaryTableName = $this->getMainTable($dimensions) . $this->tmpTableSuffix; + $this->getConnection()->createTemporaryTableLike($temporaryTableName, $templateTableName, true); + $this->mainTmpTable[$this->getArrayKeyForTmpTable($dimensions)] = $temporaryTableName; + } + + /** + * Return temporary index table name + * + * @param Dimension[] $dimensions + * + * @return string + * + * @throws \LogicException + */ + public function getMainTmpTable(array $dimensions): string + { + $cacheKey = $this->getArrayKeyForTmpTable($dimensions); + if (!isset($this->mainTmpTable[$cacheKey])) { + throw new \LogicException( + sprintf('Temporary table for provided dimensions "%s" does not exist', $cacheKey) + ); + } + return $this->mainTmpTable[$cacheKey]; + } +} diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php index a4db630f0234b..d21a8666ec0ac 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php @@ -241,7 +241,7 @@ protected function _createItem($label, $value, $count = 0) } /** - * Get all product ids from from collection with applied filters + * Get all product ids from collection with applied filters * * @return array */ diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Decimal.php b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Decimal.php index 36caa148b2e4e..1389d2789a158 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Decimal.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Decimal.php @@ -56,7 +56,7 @@ public function getRange(FilterInterface $filter) $index = 1; do { $range = pow(10, strlen(floor($maxValue)) - $index); - $items = $this->getRangeItemCounts($range, $filter); + $items = $this->getRangeItemCounts($range, $filter) ?: []; $index++; } while ($range > self::MIN_RANGE_POWER && count($items) < 2); $this->range = $range; diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php index 8a17a3b6c8cfa..d1aee8c4c5ba6 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php @@ -282,7 +282,8 @@ public function getMaxPrice() public function getPriorFilters($filterParams) { $priorFilters = []; - for ($i = 1; $i < count($filterParams); ++$i) { + $count = count($filterParams); + for ($i = 1; $i < $count; ++$i) { $priorFilter = $this->validateFilter($filterParams[$i]); if ($priorFilter) { $priorFilters[] = $priorFilter; diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Item.php b/app/code/Magento/Catalog/Model/Layer/Filter/Item.php index 95a8a781bdc6c..b37d248a3f2f8 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Item.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Item.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Filter item model * diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price.php index ec5e2bff81ab3..6a62854a19e88 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Price.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Layer\Filter; /** @@ -150,7 +148,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) public function getCustomerGroupId() { $customerGroupId = $this->_getData('customer_group_id'); - if (is_null($customerGroupId)) { + if ($customerGroupId === null) { $customerGroupId = $this->_customerSession->getCustomerGroupId(); } @@ -176,7 +174,7 @@ public function setCustomerGroupId($customerGroupId) public function getCurrencyRate() { $rate = $this->_getData('currency_rate'); - if (is_null($rate)) { + if ($rate === null) { $rate = $this->_storeManager->getStore($this->getStoreId()) ->getCurrentCurrencyRate(); } @@ -276,7 +274,10 @@ protected function _getItemsData() { $algorithm = $this->algorithmFactory->create(); - return $algorithm->getItemsData((array)$this->dataProvider->getInterval(), $this->dataProvider->getAdditionalRequestData()); + return $algorithm->getItemsData( + (array)$this->dataProvider->getInterval(), + $this->dataProvider->getAdditionalRequestData() + ); } /** diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index cf1392a7e9e8c..9a5124458346e 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Pricing\SaleableInterface; @@ -117,6 +118,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ protected $_urlModel = null; + /** + * @var ResourceModel\Product + */ + protected $_resource; + /** * @var string */ @@ -270,6 +276,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + * @deprecated Not used anymore due to performance issue (loaded all product attributes) */ protected $metadataService; @@ -346,6 +353,11 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ protected $linkTypeProvider; + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * Product constructor. * @param \Magento\Framework\Model\Context $context @@ -383,7 +395,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor * @param array $data - * + * @param \Magento\Eav\Model\Config|null $config * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -422,7 +434,8 @@ public function __construct( EntryConverterPool $mediaGalleryEntryConverterPool, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor, - array $data = [] + array $data = [], + \Magento\Eav\Model\Config $config = null ) { $this->metadataService = $metadataService; $this->_itemOptionFactory = $itemOptionFactory; @@ -461,6 +474,7 @@ public function __construct( $resourceCollection, $data ); + $this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class); } /** @@ -474,12 +488,30 @@ protected function _construct() } /** - * {@inheritdoc} + * Get resource instance + * + * @throws \Magento\Framework\Exception\LocalizedException + * @return \Magento\Catalog\Model\ResourceModel\Product + * @deprecated because resource models should be used directly + */ + protected function _getResource() + { + return parent::_getResource(); + } + + /** + * Get a list of custom attribute codes that belongs to product attribute set. If attribute set not specified for + * product will return all attribute codes + * + * @return string[] */ protected function getCustomAttributesCodes() { if ($this->customAttributesCodes === null) { - $this->customAttributesCodes = $this->getEavAttributesCodes($this->metadataService); + $this->customAttributesCodes = array_keys($this->eavConfig->getEntityAttributes( + self::ENTITY, + $this + )); $this->customAttributesCodes = array_diff($this->customAttributesCodes, $this->interfaceAttributes); } return $this->customAttributesCodes; @@ -493,22 +525,9 @@ protected function getCustomAttributesCodes() public function getStoreId() { if ($this->hasData(self::STORE_ID)) { - return $this->getData(self::STORE_ID); + return (int)$this->getData(self::STORE_ID); } - return $this->_storeManager->getStore()->getId(); - } - - /** - * Get collection instance - * - * @return object - * @deprecated 101.1.0 because collections should be used directly via factory - */ - public function getResourceCollection() - { - $collection = parent::getResourceCollection(); - $collection->setStoreId($this->getStoreId()); - return $collection; + return (int)$this->_storeManager->getStore()->getId(); } /** @@ -610,6 +629,7 @@ public function getUpdatedAt() * * @param bool $calculate * @return void + * @deprecated */ public function setPriceCalculation($calculate = true) { @@ -694,7 +714,7 @@ public function getIdBySku($sku) public function getCategoryId() { $category = $this->_registry->registry('current_category'); - if ($category) { + if ($category && in_array($category->getId(), $this->getCategoryIds())) { return $category->getId(); } return false; @@ -789,11 +809,17 @@ public function getStoreIds() if (!$this->hasStoreIds()) { $storeIds = []; if ($websiteIds = $this->getWebsiteIds()) { + if ($this->_storeManager->isSingleStoreMode()) { + $websiteIds = array_keys($websiteIds); + } foreach ($websiteIds as $websiteId) { $websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds(); - $storeIds = array_merge($storeIds, $websiteStores); + $storeIds[] = $websiteStores; } } + if ($storeIds) { + $storeIds = array_merge(...$storeIds); + } $this->setStoreIds($storeIds); } return $this->getData('store_ids'); @@ -962,7 +988,7 @@ public function setQty($qty) */ public function getQty() { - return $this->getData('qty'); + return (float)$this->getData('qty'); } /** @@ -1061,12 +1087,13 @@ protected function _afterLoad() /** * Clear cache related with product id * + * @deprecated + * @see \Magento\Framework\Model\AbstractModel::cleanModelCache * @return $this */ public function cleanCache() { - $this->_cacheManager->clean('catalog_product_' . $this->getId()); - return $this; + return $this->cleanModelCache(); } /** @@ -1128,11 +1155,24 @@ public function getTierPrice($qty = null) /** * Get formatted by currency product price * - * @return array || double + * @return array|double + */ + public function getFormattedPrice() + { + return $this->getPriceModel()->getFormattedPrice($this); + } + + /** + * Get formatted by currency product price + * + * @return array|double + * + * @deprecated + * @see getFormattedPrice() */ public function getFormatedPrice() { - return $this->getPriceModel()->getFormatedPrice($this); + return $this->getFormattedPrice(); } /** @@ -1159,10 +1199,11 @@ public function setFinalPrice($price) */ public function getFinalPrice($qty = null) { - if ($this->_getData('final_price') === null) { - $this->setFinalPrice($this->getPriceModel()->getFinalPrice($qty, $this)); + if ($this->_calculatePrice || $this->_getData('final_price') === null) { + return $this->getPriceModel()->getFinalPrice($qty, $this); + } else { + return $this->_getData('final_price'); } - return $this->_getData('final_price'); } /** @@ -1712,7 +1753,7 @@ public function isInStock() * Get attribute text by its code * * @param string $attributeCode Code of the attribute - * @return string + * @return string|array|null */ public function getAttributeText($attributeCode) { @@ -1991,7 +2032,7 @@ public function getIsVirtual() */ public function addCustomOption($code, $value, $product = null) { - $product = $product ? $product : $this; + $product = $product ?: $this; $option = $this->_itemOptionFactory->create()->addData( ['product_id' => $product->getId(), 'product' => $product, 'code' => $code, 'value' => $value] ); @@ -2094,6 +2135,8 @@ public function reset() /** * Get cache tags associated with object id * + * @deprecated + * @see \Magento\Catalog\Model\Product::getIdentities * @return string[] */ public function getCacheIdTags() @@ -2287,7 +2330,12 @@ public function getImage() */ public function getIdentities() { - $identities = [self::CACHE_TAG . '_' . $this->getId()]; + $identities = []; + + if ($this->getId()) { + $identities[] = self::CACHE_TAG . '_' . $this->getId(); + } + if ($this->getIsChangedCategories()) { foreach ($this->getAffectedCategoryIds() as $categoryId) { $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; @@ -2299,6 +2347,7 @@ public function getIdentities() $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; } } + if ($this->_appState->getAreaCode() == \Magento\Framework\App\Area::AREA_FRONTEND) { $identities[] = self::CACHE_TAG; } @@ -2718,4 +2767,18 @@ public function setStockData($stockData) $this->setData('stock_data', $stockData); return $this; } + + /** + * {@inheritDoc} + */ + public function getCacheTags() + { + //Preferring individual tags over broad ones. + $individualTags = $this->getIdentities(); + if ($individualTags) { + return $individualTags; + } + + return parent::getCacheTags(); + } } diff --git a/app/code/Magento/Catalog/Model/Product/Action.php b/app/code/Magento/Catalog/Model/Product/Action.php index f5d7dff53132d..f78048424b42c 100644 --- a/app/code/Magento/Catalog/Model/Product/Action.php +++ b/app/code/Magento/Catalog/Model/Product/Action.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product; /** diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php index c2108b0273bdb..dbefb09f06bd1 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php @@ -25,7 +25,9 @@ public function beforeSave($object) $attributeCode = $this->getAttribute()->getName(); if ($object->getData('use_config_' . $attributeCode)) { $object->setData($attributeCode, BooleanSource::VALUE_USE_CONFIG); + return $this; } - return $this; + + return parent::beforeSave($object); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php index cd686c05908ce..208f7912e7273 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product\Attribute\Backend\GroupPrice; use Magento\Catalog\Api\Data\ProductInterface; @@ -91,7 +89,7 @@ public function __construct( */ protected function _getWebsiteCurrencyRates() { - if (is_null($this->_rates)) { + if ($this->_rates === null) { $this->_rates = []; $baseCurrency = $this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, @@ -375,118 +373,10 @@ protected function modifyPriceData($object, $data) * * @param \Magento\Catalog\Model\Product $object * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave($object) { - $websiteId = $this->_storeManager->getStore($object->getStoreId())->getWebsiteId(); - $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0; - - $priceRows = $object->getData($this->getAttribute()->getName()); - if (null === $priceRows) { - return $this; - } - - $priceRows = array_filter((array)$priceRows); - - $old = []; - $new = []; - - // prepare original data for compare - $origPrices = $object->getOrigData($this->getAttribute()->getName()); - if (!is_array($origPrices)) { - $origPrices = []; - } - foreach ($origPrices as $data) { - if ($data['website_id'] > 0 || $data['website_id'] == '0' && $isGlobal) { - $key = implode( - '-', - array_merge( - [$data['website_id'], $data['cust_group']], - $this->_getAdditionalUniqueFields($data) - ) - ); - $old[$key] = $data; - } - } - - // prepare data for save - foreach ($priceRows as $data) { - $hasEmptyData = false; - foreach ($this->_getAdditionalUniqueFields($data) as $field) { - if (empty($field)) { - $hasEmptyData = true; - break; - } - } - - if ($hasEmptyData || !isset($data['cust_group']) || !empty($data['delete'])) { - continue; - } - if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) { - continue; - } - if (!$isGlobal && (int)$data['website_id'] == 0) { - continue; - } - - $key = implode( - '-', - array_merge([$data['website_id'], $data['cust_group']], $this->_getAdditionalUniqueFields($data)) - ); - - $useForAllGroups = $data['cust_group'] == $this->_groupManagement->getAllCustomersGroup()->getId(); - $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0; - $new[$key] = array_merge( - $this->getAdditionalFields($data), - [ - 'website_id' => $data['website_id'], - 'all_groups' => $useForAllGroups ? 1 : 0, - 'customer_group_id' => $customerGroupId, - 'value' => isset($data['price']) ? $data['price'] : null, - ], - $this->_getAdditionalUniqueFields($data) - ); - } - - $delete = array_diff_key($old, $new); - $insert = array_diff_key($new, $old); - $update = array_intersect_key($new, $old); - - $isChanged = false; - $productId = $object->getData($this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField()); - - if (!empty($delete)) { - foreach ($delete as $data) { - $this->_getResource()->deletePriceData($productId, null, $data['price_id']); - $isChanged = true; - } - } - - if (!empty($insert)) { - foreach ($insert as $data) { - $price = new \Magento\Framework\DataObject($data); - $price->setData( - $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(), - $productId - ); - $this->_getResource()->savePriceData($price); - - $isChanged = true; - } - } - - if (!empty($update)) { - $isChanged |= $this->updateValues($update, $old); - } - - if ($isChanged) { - $valueChangedKey = $this->getAttribute()->getName() . '_changed'; - $object->setData($valueChangedKey, 1); - } - return $this; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php new file mode 100644 index 0000000000000..fa5a218824eea --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Backend; + +use Magento\Catalog\Model\AbstractModel; +use Magento\Catalog\Model\Attribute\Backend\AbstractLayoutUpdate; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; + +/** + * Allows to select a layout file to merge when rendering the product's page. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + * + * @param AbstractModel|Product $forModel + */ + protected function listAvailableValues(AbstractModel $forModel): array + { + return $this->manager->fetchAvailableFiles($forModel); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php index a9da2a3400de9..ee480bd48c32c 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Catalog product SKU backend attribute model * @@ -52,7 +50,9 @@ public function validate($object) $attrCode = $this->getAttribute()->getAttributeCode(); $value = $object->getData($attrCode); if ($this->getAttribute()->getIsRequired() && strlen($value) === 0) { - throw new \Magento\Framework\Exception\LocalizedException(__('The value of attribute "%1" must be set', $attrCode)); + throw new \Magento\Framework\Exception\LocalizedException( + __('The value of attribute "%1" must be set', $attrCode) + ); } if ($this->string->strlen($object->getSku()) > self::SKU_MAX_LENGTH) { @@ -97,6 +97,7 @@ protected function _generateUniqueSku($object) public function beforeSave($object) { $this->_generateUniqueSku($object); + $this->trimValue($object); return parent::beforeSave($object); } @@ -127,4 +128,17 @@ protected function _getLastSimilarAttributeValueIncrement($attribute, $object) $data = $connection->fetchOne($select, $bind); return abs((int)str_replace($value, '', $data)); } + + /** + * @param Product $object + * @return void + */ + private function trimValue($object) + { + $attrCode = $this->getAttribute()->getAttributeCode(); + $value = $object->getData($attrCode); + if ($value) { + $object->setData($attrCode, trim($value)); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php new file mode 100644 index 0000000000000..6439374accec7 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Tier price data abstract handler. + */ +abstract class AbstractHandler implements ExtensionInterface +{ + /** + * @var \Magento\Customer\Api\GroupManagementInterface + */ + protected $groupManagement; + + /** + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + */ + public function __construct( + GroupManagementInterface $groupManagement + ) { + $this->groupManagement = $groupManagement; + } + + /** + * Get additional tier price fields. + * + * @return array + */ + protected function getAdditionalFields(array $objectArray): array + { + $percentageValue = $this->getPercentage($objectArray); + + return [ + 'value' => $percentageValue ? null : $objectArray['price'], + 'percentage_value' => $percentageValue ?: null, + ]; + } + + /** + * Check whether price has percentage value. + * + * @param array $priceRow + * @return integer|null + */ + protected function getPercentage(array $priceRow) + { + return isset($priceRow['percentage_value']) && is_numeric($priceRow['percentage_value']) + ? (int)$priceRow['percentage_value'] + : null; + } + + /** + * Prepare tier price data by provided price row data. + * + * @param array $data + * @return array + */ + protected function prepareTierPrice(array $data): array + { + $useForAllGroups = (int)$data['cust_group'] === $this->groupManagement->getAllCustomersGroup()->getId(); + $customerGroupId = $useForAllGroups ? 0 : $data['cust_group']; + $tierPrice = array_merge( + $this->getAdditionalFields($data), + [ + 'website_id' => $data['website_id'], + 'all_groups' => (int)$useForAllGroups, + 'customer_group_id' => $customerGroupId, + 'value' => $data['price'] ?? null, + 'qty' => $this->parseQty($data['price_qty']), + ] + ); + + return $tierPrice; + } + + /** + * Parse quantity value into float. + * + * @param mixed $value + * @return float|int + */ + protected function parseQty($value) + { + return $value * 1; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php new file mode 100644 index 0000000000000..92046bf15d2e4 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Process tier price data for handled new product + */ +class SaveHandler extends AbstractHandler +{ + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPoll; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice + */ + private $tierPriceResource; + + /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource + */ + public function __construct( + StoreManagerInterface $storeManager, + ProductAttributeRepositoryInterface $attributeRepository, + GroupManagementInterface $groupManagement, + MetadataPool $metadataPool, + Tierprice $tierPriceResource + ) { + parent::__construct($groupManagement); + + $this->storeManager = $storeManager; + $this->attributeRepository = $attributeRepository; + $this->metadataPoll = $metadataPool; + $this->tierPriceResource = $tierPriceResource; + } + + /** + * Set tier price data for product entity + * + * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity + * @param array $arguments + * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @throws \Magento\Framework\Exception\InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + $attribute = $this->attributeRepository->get('tier_price'); + $priceRows = $entity->getData($attribute->getName()); + if (null !== $priceRows) { + if (!is_array($priceRows)) { + throw new \Magento\Framework\Exception\InputException( + __('Tier prices data should be array, but actually other type is received') + ); + } + $websiteId = $this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); + $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + $priceRows = array_filter($priceRows); + $productId = $entity->getData($identifierField); + + // prepare and save data + foreach ($priceRows as $data) { + $isPriceWebsiteGlobal = (int)$data['website_id'] === 0; + if ($isGlobal === $isPriceWebsiteGlobal + || !empty($data['price_qty']) + || isset($data['cust_group']) + ) { + $tierPrice = $this->prepareTierPrice($data); + $price = new \Magento\Framework\DataObject($tierPrice); + $price->setData( + $identifierField, + $productId + ); + $this->tierPriceResource->savePriceData($price); + $valueChangedKey = $attribute->getName() . '_changed'; + $entity->setData($valueChangedKey, 1); + } + } + } + + return $entity; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php new file mode 100644 index 0000000000000..bda92fbeab9ed --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php @@ -0,0 +1,261 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\FormatInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Process tier price data for handled existing product. + */ +class UpdateHandler extends AbstractHandler +{ + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPoll; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice + */ + private $tierPriceResource; + + /** + * @var FormatInterface + */ + private $localeFormat; + + /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource + * @param FormatInterface|null $localeFormat + */ + public function __construct( + StoreManagerInterface $storeManager, + ProductAttributeRepositoryInterface $attributeRepository, + GroupManagementInterface $groupManagement, + MetadataPool $metadataPool, + Tierprice $tierPriceResource, + FormatInterface $localeFormat = null + ) { + parent::__construct($groupManagement); + + $this->storeManager = $storeManager; + $this->attributeRepository = $attributeRepository; + $this->metadataPoll = $metadataPool; + $this->tierPriceResource = $tierPriceResource; + $this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(FormatInterface::class); + } + + /** + * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity + * @param array $arguments + * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @throws \Magento\Framework\Exception\InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + $attribute = $this->attributeRepository->get('tier_price'); + $priceRows = $entity->getData($attribute->getName()); + if (null !== $priceRows) { + if (!is_array($priceRows)) { + throw new \Magento\Framework\Exception\InputException( + __('Tier prices data should be array, but actually other type is received') + ); + } + $websiteId = (int)$this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); + $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + $productId = (int)$entity->getData($identifierField); + + // prepare original data to compare + $origPrices = $entity->getOrigData($attribute->getName()); + $old = $this->prepareOldTierPriceToCompare($origPrices); + // prepare data for save + $new = $this->prepareNewDataForSave($priceRows, $isGlobal); + + $delete = array_diff_key($old, $new); + $insert = array_diff_key($new, $old); + $update = array_intersect_key($new, $old); + + $isAttributeChanged = $this->deleteValues($productId, $delete); + $isAttributeChanged |= $this->insertValues($productId, $insert); + $isAttributeChanged |= $this->updateValues($update, $old); + + if ($isAttributeChanged) { + $valueChangedKey = $attribute->getName() . '_changed'; + $entity->setData($valueChangedKey, 1); + } + } + + return $entity; + } + + /** + * Update existing tier prices for processed product + * + * @param array $valuesToUpdate + * @param array $oldValues + * @return boolean + */ + private function updateValues(array $valuesToUpdate, array $oldValues): bool + { + $isChanged = false; + foreach ($valuesToUpdate as $key => $value) { + if ((!empty($value['value']) + && (float)$oldValues[$key]['price'] !== $this->localeFormat->getNumber($value['value']) + ) || $this->getPercentage($oldValues[$key]) !== $this->getPercentage($value) + ) { + $price = new \Magento\Framework\DataObject( + [ + 'value_id' => $oldValues[$key]['price_id'], + 'value' => $value['value'], + 'percentage_value' => $this->getPercentage($value) + ] + ); + $this->tierPriceResource->savePriceData($price); + $isChanged = true; + } + } + + return $isChanged; + } + + /** + * Insert new tier prices for processed product. + * + * @param int $productId + * @param array $valuesToInsert + * @return bool + */ + private function insertValues(int $productId, array $valuesToInsert): bool + { + $isChanged = false; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + foreach ($valuesToInsert as $data) { + $price = new \Magento\Framework\DataObject($data); + $price->setData( + $identifierField, + $productId + ); + $this->tierPriceResource->savePriceData($price); + $isChanged = true; + } + + return $isChanged; + } + + /** + * Delete tier price values for processed product. + * + * @param int $productId + * @param array $valuesToDelete + * @return bool + */ + private function deleteValues(int $productId, array $valuesToDelete): bool + { + $isChanged = false; + foreach ($valuesToDelete as $data) { + $this->tierPriceResource->deletePriceData($productId, null, $data['price_id']); + $isChanged = true; + } + + return $isChanged; + } + + /** + * Get generated price key based on price data. + * + * @param array $priceData + * @return string + */ + private function getPriceKey(array $priceData): string + { + $qty = $this->parseQty($priceData['price_qty']); + $key = implode( + '-', + array_merge([$priceData['website_id'], $priceData['cust_group']], [$qty]) + ); + + return $key; + } + + /** + * Check by id is website global. + * + * @param int $websiteId + * @return bool + */ + private function isWebsiteGlobal(int $websiteId): bool + { + return $websiteId === 0; + } + + /** + * Prepare old data to compare. + * + * @param array|null $origPrices + * @return array + */ + private function prepareOldTierPriceToCompare($origPrices): array + { + $old = []; + if (is_array($origPrices)) { + foreach ($origPrices as $data) { + $key = $this->getPriceKey($data); + $old[$key] = $data; + } + } + + return $old; + } + + /** + * Prepare new data for save. + * + * @param array $priceRows + * @param bool $isGlobal + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function prepareNewDataForSave($priceRows, $isGlobal = true): array + { + $new = []; + $priceRows = array_filter($priceRows); + foreach ($priceRows as $data) { + if (empty($data['delete']) + && (!empty($data['price_qty']) + || isset($data['cust_group']) + || $isGlobal === $this->isWebsiteGlobal((int)$data['website_id'])) + ) { + $key = $this->getPriceKey($data); + $new[$key] = $this->prepareTierPrice($data); + } + } + + return $new; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php index 92b9a2e4239b2..23b2dfa01bfbd 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php @@ -159,8 +159,22 @@ protected function validatePrice(array $priceRow) */ protected function modifyPriceData($object, $data) { + /** @var \Magento\Catalog\Model\Product $object */ $data = parent::modifyPriceData($object, $data); $price = $object->getPrice(); + + $specialPrice = $object->getSpecialPrice(); + $specialPriceFromDate = $object->getSpecialFromDate(); + $specialPriceToDate = $object->getSpecialToDate(); + $today = time(); + + if ($specialPrice && ($object->getPrice() > $object->getFinalPrice())) { + if ($today >= strtotime($specialPriceFromDate) && $today <= strtotime($specialPriceToDate) || + $today >= strtotime($specialPriceFromDate) && $specialPriceToDate === null) { + $price = $specialPrice; + } + } + foreach ($data as $key => $tierPrice) { $percentageValue = $this->getPercentage($tierPrice); if ($percentageValue) { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php index 6173a76eca421..cdd6da7019da5 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Image.php @@ -9,23 +9,28 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\Catalog\Model\Product\Attribute\Frontend; -class Image extends \Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; + +class Image extends AbstractFrontend { /** * Store manager * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** * Construct * - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param StoreManagerInterface $storeManager */ - public function __construct(\Magento\Store\Model\StoreManagerInterface $storeManager) + public function __construct(StoreManagerInterface $storeManager) { $this->_storeManager = $storeManager; } @@ -42,9 +47,9 @@ public function getUrl($product) $image = $product->getData($this->getAttribute()->getAttributeCode()); $url = false; if (!empty($image)) { - $url = $this->_storeManager->getStore($product->getStore()) - ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) - . 'catalog/product/' . $image; + $url = $this->_storeManager + ->getStore($product->getStore()) + ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . 'catalog/product/' . ltrim($image, '/'); } return $url; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php new file mode 100644 index 0000000000000..c5628937f25c4 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Area; +use Magento\Framework\DataObject; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * Manage available layout updates for products. + */ +class LayoutUpdateManager +{ + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param LayoutProcessorFactory $layoutProcessorFactory + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + LayoutProcessorFactory $layoutProcessorFactory + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->layoutProcessorFactory = $layoutProcessorFactory; + } + + /** + * Adopt product's SKU to be used as layout handle. + * + * @param ProductInterface $product + * @return string + */ + private function sanitizeSku(ProductInterface $product): string + { + return rawurlencode($product->getSku()); + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * Fetch list of available files/handles for the product. + * + * @param ProductInterface $product + * @return string[] + */ + public function fetchAvailableFiles(ProductInterface $product): array + { + if (!$product->getSku()) { + return []; + } + + $identifier = $this->sanitizeSku($product); + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($identifier) { + preg_match( + '/^catalog\_product\_view\_selectable\_' .preg_quote($identifier) .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * Extract custom layout attribute value. + * + * @param ProductInterface $product + * @return mixed + */ + private function extractAttributeValue(ProductInterface $product) + { + if ($product instanceof Product && !$product->hasData(ProductInterface::CUSTOM_ATTRIBUTES)) { + return $product->getData('custom_layout_update_file'); + } + if ($attr = $product->getCustomAttribute('custom_layout_update_file')) { + return $attr->getValue(); + } + + return null; + } + + /** + * Extract selected custom layout settings. + * + * If no update is selected none will apply. + * + * @param ProductInterface $product + * @param DataObject $intoSettings + * @return void + */ + public function extractCustomSettings(ProductInterface $product, DataObject $intoSettings) + { + if ($product->getSku() && $value = $this->extractAttributeValue($product)) { + $handles = $intoSettings->getPageLayoutHandles() ?? []; + $handles = array_merge_recursive( + $handles, + ['selectable' => $this->sanitizeSku($product) . '_' . $value] + ); + $intoSettings->setPageLayoutHandles($handles); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php index f2039a5002dcc..6cca2c07e2dd8 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php @@ -47,7 +47,7 @@ public function add($attributeCode, $option) /** @var \Magento\Eav\Api\Data\AttributeOptionInterface $attributeOption */ $attributeOption = $attributeOption->getLabel(); }); - if (in_array($option->getLabel(), $currentOptions)) { + if (in_array($option->getLabel(), $currentOptions, true)) { return false; } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php index 270a2f229678b..c36d5ffcaa9e9 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php @@ -118,17 +118,11 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib $attribute->setAttributeId($existingModel->getAttributeId()); $attribute->setIsUserDefined($existingModel->getIsUserDefined()); $attribute->setFrontendInput($existingModel->getFrontendInput()); - - if (is_array($attribute->getFrontendLabels())) { - $defaultFrontendLabel = $attribute->getDefaultFrontendLabel(); - $frontendLabel[0] = !empty($defaultFrontendLabel) - ? $defaultFrontendLabel - : $existingModel->getDefaultFrontendLabel(); - foreach ($attribute->getFrontendLabels() as $item) { - $frontendLabel[$item->getStoreId()] = $item->getLabel(); - } - $attribute->setDefaultFrontendLabel($frontendLabel); + if ($attribute->getIsUserDefined()) { + $this->processAttributeData($attribute); } + + $this->updateDefaultFrontendLabel($attribute, $existingModel); } else { $attribute->setAttributeId(null); @@ -136,35 +130,15 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib throw InputException::requiredField('frontend_label'); } - $frontendLabels = []; - if ($attribute->getDefaultFrontendLabel()) { - $frontendLabels[0] = $attribute->getDefaultFrontendLabel(); - } - if ($attribute->getFrontendLabels() && is_array($attribute->getFrontendLabels())) { - foreach ($attribute->getFrontendLabels() as $label) { - $frontendLabels[$label->getStoreId()] = $label->getLabel(); - } - if (!isset($frontendLabels[0]) || !$frontendLabels[0]) { - throw InputException::invalidFieldValue('frontend_label', null); - } + $frontendLabel = $this->updateDefaultFrontendLabel($attribute, null); - $attribute->setDefaultFrontendLabel($frontendLabels); - } $attribute->setAttributeCode( - $attribute->getAttributeCode() ?: $this->generateCode($frontendLabels[0]) + $attribute->getAttributeCode() ?: $this->generateCode($frontendLabel) ); $this->validateCode($attribute->getAttributeCode()); $this->validateFrontendInput($attribute->getFrontendInput()); - $attribute->setBackendType( - $attribute->getBackendTypeByInput($attribute->getFrontendInput()) - ); - $attribute->setSourceModel( - $this->productHelper->getAttributeSourceModelByInputType($attribute->getFrontendInput()) - ); - $attribute->setBackendModel( - $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput()) - ); + $this->processAttributeData($attribute); $attribute->setEntityTypeId( $this->eavConfig ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) @@ -275,4 +249,71 @@ protected function validateFrontendInput($frontendInput) throw InputException::invalidFieldValue('frontend_input', $frontendInput); } } + + /** + * Process attribute data based on attribute frontend input type. + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return void + */ + private function processAttributeData(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) + { + $attribute->setBackendType( + $attribute->getBackendTypeByInput($attribute->getFrontendInput()) + ); + $attribute->setSourceModel( + $this->productHelper->getAttributeSourceModelByInputType($attribute->getFrontendInput()) + ); + $attribute->setBackendModel( + $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput()) + ); + } + + /** + * This method sets default frontend value using given default frontend value or frontend value from admin store + * if default frontend value is not presented. + * If both default frontend label and admin store frontend label are not given it throws exception + * for attribute creation process or sets existing attribute value for attribute update action. + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface|null $existingModel + * @return string|null + * @throws InputException + */ + private function updateDefaultFrontendLabel($attribute, $existingModel) + { + $frontendLabel = $attribute->getDefaultFrontendLabel(); + if (empty($frontendLabel)) { + $frontendLabel = $this->extractAdminStoreFrontendLabel($attribute); + if (empty($frontendLabel)) { + if ($existingModel) { + $frontendLabel = $existingModel->getDefaultFrontendLabel(); + } else { + throw InputException::invalidFieldValue('frontend_label', null); + } + } + $attribute->setDefaultFrontendLabel($frontendLabel); + } + return $frontendLabel; + } + + /** + * This method extracts frontend label from FrontendLabel object for admin store. + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return string|null + */ + private function extractAdminStoreFrontendLabel($attribute) + { + $frontendLabel = []; + $frontendLabels = $attribute->getFrontendLabels(); + if (isset($frontendLabels[0]) + && $frontendLabels[0] instanceof \Magento\Eav\Api\Data\AttributeFrontendLabelInterface + ) { + foreach ($attribute->getFrontendLabels() as $label) { + $frontendLabel[$label->getStoreId()] = $label->getLabel(); + } + } + return $frontendLabel[0] ?? null; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php b/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php index 14774103b8cd2..73a8a1e25957f 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/SetRepository.php @@ -6,8 +6,6 @@ */ namespace Magento\Catalog\Model\Product\Attribute; -use Magento\Framework\Exception\InputException; - class SetRepository implements \Magento\Catalog\Api\AttributeSetRepositoryInterface { /** @@ -53,7 +51,7 @@ public function __construct( */ public function save(\Magento\Eav\Api\Data\AttributeSetInterface $attributeSet) { - $this->validate($attributeSet); + $this->validateBeforeSave($attributeSet); return $this->attributeSetRepository->save($attributeSet); } @@ -127,4 +125,29 @@ protected function validate(\Magento\Eav\Api\Data\AttributeSetInterface $attribu ); } } + + /** + * Validate attribute set entity type id. + * + * @param \Magento\Eav\Api\Data\AttributeSetInterface $attributeSet + * @return void + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function validateBeforeSave(\Magento\Eav\Api\Data\AttributeSetInterface $attributeSet) + { + $productEntityId = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getId(); + $result = $attributeSet->getEntityTypeId() === $productEntityId; + if (!$result && $attributeSet->getAttributeSetId()) { + $existingAttributeSet = $this->attributeSetRepository->get($attributeSet->getAttributeSetId()); + $attributeSet->setEntityTypeId($existingAttributeSet->getEntityTypeId()); + $result = $existingAttributeSet->getEntityTypeId() === $productEntityId; + } + if (!$result) { + throw new \Magento\Framework\Exception\StateException( + __('Provided Attribute set non product Attribute set.') + ); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Inputtype.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Inputtype.php index e669efa19ed87..adbd6579e6828 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Inputtype.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Inputtype.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Product attribute source input types */ @@ -31,8 +29,10 @@ class Inputtype extends \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputt * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Registry $coreRegistry */ - public function __construct(\Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Framework\Registry $coreRegistry) - { + public function __construct( + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Registry $coreRegistry + ) { $this->_eventManager = $eventManager; $this->_coreRegistry = $coreRegistry; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php index 63b1444d1db07..dbc7535dccfa9 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * @return array + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php new file mode 100644 index 0000000000000..0ddb528e768cc --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Source; + +use Magento\Catalog\Model\Attribute\Source\AbstractLayoutUpdate; +use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager; +use Magento\Framework\Api\CustomAttributesDataInterface; + +/** + * List of layout updates available for a product. + */ +class LayoutUpdate extends AbstractLayoutUpdate +{ + /** + * @var LayoutUpdateManager + */ + private $manager; + + /** + * @param LayoutUpdateManager $manager + */ + public function __construct(LayoutUpdateManager $manager) + { + $this->manager = $manager; + } + + /** + * @inheritDoc + */ + protected function listAvailableOptions(CustomAttributesDataInterface $entity): array + { + return $this->manager->fetchAvailableFiles($entity); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php b/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php index d0c7103851499..57d08916bcd40 100644 --- a/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php +++ b/app/code/Magento/Catalog/Model/Product/AttributeSet/Options.php @@ -5,10 +5,13 @@ */ namespace Magento\Catalog\Model\Product\AttributeSet; +/** + * Attribute Set Options + */ class Options implements \Magento\Framework\Data\OptionSourceInterface { /** - * @var null|array + * @var array */ protected $options; @@ -25,7 +28,7 @@ public function __construct( } /** - * @return array|null + * @inheritDoc */ public function toOptionArray() { @@ -33,7 +36,15 @@ public function toOptionArray() $this->options = $this->collectionFactory->create() ->setEntityTypeFilter($this->product->getTypeId()) ->toOptionArray(); + + array_walk( + $this->options, + function (&$option) { + $option['__disableTmpl'] = true; + } + ); } + return $this->options; } } diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php new file mode 100644 index 0000000000000..442fe9b8a3b6f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Authorization.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Catalog\Model\ProductFactory; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate; + +/** + * Additional authorization for product operations. + */ +class Authorization +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ProductFactory + */ + private $productFactory; + + /** + * @param AuthorizationInterface $authorization + * @param ProductFactory $factory + */ + public function __construct(AuthorizationInterface $authorization, ProductFactory $factory) + { + $this->authorization = $authorization; + $this->productFactory = $factory; + } + + /** + * Extract attribute value from the model. + * + * @param ProductModel $product + * @param AttributeInterface $attr + * @return mixed + * @throws \RuntimeException When no new value is present. + */ + private function extractAttributeValue(ProductModel $product, AttributeInterface $attr) + { + if ($product->hasData($attr->getAttributeCode())) { + $newValue = $product->getData($attr->getAttributeCode()); + } elseif ($product->hasData(ProductModel::CUSTOM_ATTRIBUTES) + && $attrValue = $product->getCustomAttribute($attr->getAttributeCode()) + ) { + $newValue = $attrValue->getValue(); + } else { + throw new \RuntimeException('No new value is present'); + } + + if (empty($newValue) + || ($attr->getBackend() instanceof LayoutUpdate + && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE) + ) + ) { + $newValue = null; + } + + return $newValue; + } + + /** + * Prepare old values to compare to. + * + * @param AttributeInterface $attribute + * @param array|null $oldProduct + * @return array + */ + private function fetchOldValues(AttributeInterface $attribute, $oldProduct): array + { + $attrCode = $attribute->getAttributeCode(); + if ($oldProduct) { + //New value may only be the saved value + $oldValues = [!empty($oldProduct[$attrCode]) ? $oldProduct[$attrCode] : null]; + if (empty($oldValues[0])) { + $oldValues[0] = null; + } + } else { + //New value can be empty or default + $oldValues[] = $attribute->getDefaultValue(); + } + + return $oldValues; + } + + /** + * Check whether the product has changed. + * + * @param ProductModel $product + * @param array|null $oldProduct + * @return bool + */ + private function hasProductChanged(ProductModel $product, $oldProduct = null): bool + { + $designAttributes = [ + 'custom_design', + 'page_layout', + 'options_container', + 'custom_layout_update', + 'custom_design_from', + 'custom_design_to', + 'custom_layout_update_file' + ]; + $attributes = $product->getAttributes(); + + foreach ($designAttributes as $designAttribute) { + if (!array_key_exists($designAttribute, $attributes)) { + continue; + } + $attribute = $attributes[$designAttribute]; + $oldValues = $this->fetchOldValues($attribute, $oldProduct); + try { + $newValue = $this->extractAttributeValue($product, $attribute); + } catch (\RuntimeException $exception) { + //No new value + continue; + } + if (!in_array($newValue, $oldValues, true)) { + return true; + } + } + + return false; + } + + /** + * Authorize saving of a product. + * + * @throws AuthorizationException + * @throws NoSuchEntityException When product with invalid ID given. + * @param ProductInterface|ProductModel $product + * @return void + */ + public function authorizeSavingOf(ProductInterface $product) + { + if (!$this->authorization->isAllowed('Magento_Catalog::edit_product_design')) { + $oldData = null; + if ($product->getId()) { + if ($product->getOrigData()) { + $oldData = $product->getOrigData(); + } else { + /** @var ProductModel $savedProduct */ + $savedProduct = $this->productFactory->create(); + $savedProduct->load($product->getId()); + if (!$savedProduct->getSku()) { + throw NoSuchEntityException::singleField('id', $product->getId()); + } + $oldData = $product->getOrigData(); + } + } + if ($this->hasProductChanged($product, $oldData)) { + throw new AuthorizationException(__('Not allowed to edit the product\'s design attributes')); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/Product/Compare/Item.php index 8f18780ef8f71..777ed0fbe393b 100644 --- a/app/code/Magento/Catalog/Model/Product/Compare/Item.php +++ b/app/code/Magento/Catalog/Model/Product/Compare/Item.php @@ -158,8 +158,8 @@ public function addProductData($product) { if ($product instanceof Product) { $this->setProductId($product->getId()); - } elseif (intval($product)) { - $this->setProductId(intval($product)); + } elseif ((int)$product) { + $this->setProductId((int)$product); } return $this; diff --git a/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php index 12dbaf0c29f4e..bfd36360ee559 100644 --- a/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Model/Product/Compare/ListCompare.php @@ -5,7 +5,10 @@ */ namespace Magento\Catalog\Model\Product\Compare; +use Magento\Catalog\Model\ProductRepository; use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; /** * Product Compare List Model @@ -51,6 +54,11 @@ class ListCompare extends \Magento\Framework\DataObject */ protected $_compareItemFactory; + /** + * @var ProductRepository + */ + private $productRepository; + /** * Constructor * @@ -60,6 +68,7 @@ class ListCompare extends \Magento\Framework\DataObject * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Visitor $customerVisitor * @param array $data + * @param ProductRepository|null $productRepository */ public function __construct( \Magento\Catalog\Model\Product\Compare\ItemFactory $compareItemFactory, @@ -67,13 +76,15 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Visitor $customerVisitor, - array $data = [] + array $data = [], + ProductRepository $productRepository = null ) { $this->_compareItemFactory = $compareItemFactory; $this->_itemCollectionFactory = $itemCollectionFactory; $this->_catalogProductCompareItem = $catalogProductCompareItem; $this->_customerSession = $customerSession; $this->_customerVisitor = $customerVisitor; + $this->productRepository = $productRepository ?: ObjectManager::getInstance()->create(ProductRepository::class); parent::__construct($data); } @@ -82,6 +93,7 @@ public function __construct( * * @param int|\Magento\Catalog\Model\Product $product * @return $this + * @throws \Exception */ public function addProduct($product) { @@ -90,7 +102,7 @@ public function addProduct($product) $this->_addVisitorToItem($item); $item->loadByProduct($product); - if (!$item->getId()) { + if (!$item->getId() && $this->productExists($product)) { $item->addProductData($product); $item->save(); } @@ -98,6 +110,25 @@ public function addProduct($product) return $this; } + /** + * Check product exists. + * + * @param int|\Magento\Catalog\Model\Product $product + * @return bool + */ + private function productExists($product) + { + if ($product instanceof \Magento\Catalog\Model\Product && $product->getId()) { + return true; + } + try { + $product = $this->productRepository->getById((int)$product); + return !empty($product->getId()); + } catch (NoSuchEntityException $e) { + return false; + } + } + /** * Add products to compare list * diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php new file mode 100644 index 0000000000000..ec500ac2f2030 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Configuration\Item; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\ObjectManager; + +/** + * {@inheritdoc} + */ +class ItemResolverComposite implements ItemResolverInterface +{ + /** + * @var string[] + */ + private $itemResolvers = []; + + /** + * @var ItemResolverInterface[] + */ + private $itemResolversInstances = []; + + /** + * @param string[] $itemResolvers + */ + public function __construct(array $itemResolvers) + { + $this->itemResolvers = $itemResolvers; + } + + /** + * {@inheritdoc} + */ + public function getFinalProduct(ItemInterface $item): ProductInterface + { + $finalProduct = $item->getProduct(); + + foreach ($this->itemResolvers as $resolver) { + $resolvedProduct = $this->getItemResolverInstance($resolver)->getFinalProduct($item); + if ($resolvedProduct !== $finalProduct) { + $finalProduct = $resolvedProduct; + break; + } + } + + return $finalProduct; + } + + /** + * Get the instance of the item resolver by class name. + * + * @param string $className + * @return ItemResolverInterface + */ + private function getItemResolverInstance(string $className): ItemResolverInterface + { + if (!isset($this->itemResolversInstances[$className])) { + $this->itemResolversInstances[$className] = ObjectManager::getInstance()->get($className); + } + + return $this->itemResolversInstances[$className]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php new file mode 100644 index 0000000000000..9e6de01bb5112 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Configuration\Item; + +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * Resolves the product from a configured item. + * + * @api + */ +interface ItemResolverInterface +{ + /** + * Get the final product from a configured item by product type and selection. + * + * @param ItemInterface $item + * @return ProductInterface + */ + public function getFinalProduct(ItemInterface $item): ProductInterface; +} diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index e94104ae473a0..a7941ba5c0a79 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -1,18 +1,24 @@ <?php /** - * Catalog product copier. Creates product duplicate - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\Product\Option\Repository as OptionRepository; +/** + * Catalog product copier. Creates product duplicate + */ class Copier { /** - * @var Option\Repository + * @var OptionRepository */ protected $optionRepository; @@ -22,22 +28,22 @@ class Copier protected $copyConstructor; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; /** * @param CopyConstructorInterface $copyConstructor - * @param \Magento\Catalog\Model\ProductFactory $productFactory + * @param ProductFactory $productFactory */ public function __construct( CopyConstructorInterface $copyConstructor, - \Magento\Catalog\Model\ProductFactory $productFactory + ProductFactory $productFactory ) { $this->productFactory = $productFactory; $this->copyConstructor = $copyConstructor; @@ -46,18 +52,16 @@ public function __construct( /** * Create product duplicate * - * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Catalog\Model\Product + * @param Product $product + * @return Product */ - public function copy(\Magento\Catalog\Model\Product $product) + public function copy(Product $product) { $product->getWebsiteIds(); $product->getCategoryIds(); - /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - /** @var \Magento\Catalog\Model\Product $duplicate */ $duplicate = $this->productFactory->create(); $productData = $product->getData(); $productData = $this->removeStockItem($productData); @@ -83,6 +87,7 @@ public function copy(\Magento\Catalog\Model\Product $product) $duplicate->save(); $isDuplicateSaved = true; } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { + } catch (\Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException $e) { } } while (!$isDuplicateSaved); $this->getOptionRepository()->duplicate($product, $duplicate); @@ -94,27 +99,25 @@ public function copy(\Magento\Catalog\Model\Product $product) } /** - * @return Option\Repository + * @return OptionRepository * @deprecated 101.0.0 */ private function getOptionRepository() { if (null === $this->optionRepository) { - $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\Option\Repository::class); + $this->optionRepository = ObjectManager::getInstance()->get(OptionRepository::class); } return $this->optionRepository; } /** - * @return \Magento\Framework\EntityManager\MetadataPool + * @return MetadataPool * @deprecated 101.0.0 */ private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->metadataPool = ObjectManager::getInstance()->get(MetadataPool::class); } return $this->metadataPool; } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 03d418f3ba0d9..8b9703b6623ad 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -167,23 +167,19 @@ public function execute($product, $arguments = []) if (empty($attrData) && empty($clearImages) && empty($newImages) && empty($existImages)) { continue; } - if (in_array($attrData, $clearImages)) { - $product->setData($mediaAttrCode, 'no_selection'); - } - - if (in_array($attrData, array_keys($newImages))) { - $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); - $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); - } - - if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { - $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); - } - if (!empty($product->getData($mediaAttrCode))) { - $product->addAttributeUpdate( + $this->processMediaAttribute( + $product, + $mediaAttrCode, + $clearImages, + $newImages + ); + if (in_array($mediaAttrCode, ['image', 'small_image', 'thumbnail'])) { + $this->processMediaAttributeLabel( + $product, $mediaAttrCode, - $product->getData($mediaAttrCode), - $product->getStoreId() + $clearImages, + $newImages, + $existImages ); } } @@ -245,12 +241,13 @@ protected function processNewAndExistingImages($product, array &$images) if (empty($image['removed'])) { $data = $this->processNewImage($product, $image); - $this->resourceModel->deleteGalleryValueInStore( - $image['value_id'], - $product->getData($this->metadata->getLinkField()), - $product->getStoreId() - ); - + if (!$product->isObjectNew()) { + $this->resourceModel->deleteGalleryValueInStore( + $image['value_id'], + $product->getData($this->metadata->getLinkField()), + $product->getStoreId() + ); + } // Add per store labels, position, disabled $data['value_id'] = $image['value_id']; $data['label'] = isset($image['label']) ? $image['label'] : ''; @@ -311,7 +308,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), - isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], + $mediaGalleryData['duplicate'] ?? [], $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); @@ -448,4 +445,77 @@ private function getMediaAttributeCodes() } return $this->mediaAttributeCodes; } + + /** + * @param \Magento\Catalog\Model\Product $product + * @param $mediaAttrCode + * @param array $clearImages + * @param array $newImages + */ + private function processMediaAttribute( + \Magento\Catalog\Model\Product $product, + $mediaAttrCode, + array $clearImages, + array $newImages + ) { + $attrData = $product->getData($mediaAttrCode); + if (in_array($attrData, $clearImages)) { + $product->setData($mediaAttrCode, 'no_selection'); + } + + if (in_array($attrData, array_keys($newImages))) { + $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); + } + if (!empty($product->getData($mediaAttrCode))) { + $product->addAttributeUpdate( + $mediaAttrCode, + $product->getData($mediaAttrCode), + $product->getStoreId() + ); + } + } + + /** + * @param \Magento\Catalog\Model\Product $product + * @param $mediaAttrCode + * @param array $clearImages + * @param array $newImages + * @param array $existImages + */ + private function processMediaAttributeLabel( + \Magento\Catalog\Model\Product $product, + $mediaAttrCode, + array $clearImages, + array $newImages, + array $existImages + ) { + $resetLabel = false; + $attrData = $product->getData($mediaAttrCode); + if (in_array($attrData, $clearImages)) { + $product->setData($mediaAttrCode . '_label', null); + $resetLabel = true; + } + + if (in_array($attrData, array_keys($newImages))) { + $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); + } + + if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { + $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); + } + + if ($attrData === 'no_selection' && !empty($product->getData($mediaAttrCode . '_label'))) { + $product->setData($mediaAttrCode . '_label', null); + $resetLabel = true; + } + if (!empty($product->getData($mediaAttrCode . '_label')) + || $resetLabel === true + ) { + $product->addAttributeUpdate( + $mediaAttrCode . '_label', + $product->getData($mediaAttrCode . '_label'), + $product->getStoreId() + ); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 1b5f96baeaf9f..33813d5079215 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -68,8 +68,10 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $inputException) { throw $inputException; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new StateException(__('Cannot save product.')); } @@ -100,7 +102,10 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) if ($existingEntry->getId() == $entry->getId()) { $found = true; - if ($entry->getFile()) { + + $file = $entry->getContent(); + + if ($file && $file->getBase64EncodedData() || $entry->getFile()) { $entry->setId(null); } $existingMediaGalleryEntries[$key] = $entry; diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php index 31e322f4e38f2..9eb5edd65ba89 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php @@ -5,9 +5,10 @@ */ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\App\ObjectManager; /** * Catalog product Media Gallery attribute processor. @@ -55,28 +56,39 @@ class Processor */ protected $resourceModel; + /** + * @var \Magento\Framework\File\Mime + */ + private $mime; + /** * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel + * @param \Magento\Framework\File\Mime|null $mime + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, \Magento\Framework\Filesystem $filesystem, - \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel + \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel, + \Magento\Framework\File\Mime $mime = null ) { $this->attributeRepository = $attributeRepository; $this->fileStorageDb = $fileStorageDb; $this->mediaConfig = $mediaConfig; $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->resourceModel = $resourceModel; + $this->mime = $mime ?: ObjectManager::getInstance()->get(\Magento\Framework\File\Mime::class); } /** + * Return media_gallery attribute + * * @return \Magento\Catalog\Api\Data\ProductAttributeInterface * @since 101.0.0 */ @@ -149,10 +161,10 @@ public function addImage( } $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($pathinfo['basename']); - $dispretionPath = \Magento\MediaStorage\Model\File\Uploader::getDispretionPath($fileName); - $fileName = $dispretionPath . '/' . $fileName; + $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); + $fileName = $dispersionPath . '/' . $fileName; - $fileName = $this->getNotDuplicatedFilename($fileName, $dispretionPath); + $fileName = $this->getNotDuplicatedFilename($fileName, $dispersionPath); $destinationFile = $this->mediaConfig->getTmpMediaPath($fileName); @@ -178,6 +190,13 @@ public function addImage( $attrCode = $this->getAttribute()->getAttributeCode(); $mediaGalleryData = $product->getData($attrCode); $position = 0; + + $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($destinationFile); + $imageMimeType = $this->mime->getMimeType($absoluteFilePath); + $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); + $imageBase64 = base64_encode($imageContent); + $imageName = $pathinfo['filename']; + if (!is_array($mediaGalleryData)) { $mediaGalleryData = ['images' => []]; } @@ -192,9 +211,17 @@ public function addImage( $mediaGalleryData['images'][] = [ 'file' => $fileName, 'position' => $position, - 'media_type' => 'image', 'label' => '', 'disabled' => (int)$exclude, + 'media_type' => 'image', + 'types' => $mediaAttribute, + 'content' => [ + 'data' => [ + ImageContentInterface::NAME => $imageName, + ImageContentInterface::BASE64_ENCODED_DATA => $imageBase64, + ImageContentInterface::TYPE => $imageMimeType, + ] + ] ]; $product->setData($attrCode, $mediaGalleryData); @@ -340,9 +367,9 @@ public function setMediaAttribute(\Magento\Catalog\Model\Product $product, $medi $mediaAttributeCodes = $this->mediaConfig->getMediaAttributeCodes(); if (is_array($mediaAttribute)) { - foreach ($mediaAttribute as $atttribute) { - if (in_array($atttribute, $mediaAttributeCodes)) { - $product->setData($atttribute, $value); + foreach ($mediaAttribute as $attribute) { + if (in_array($attribute, $mediaAttributeCodes)) { + $product->setData($attribute, $value); } } } elseif (in_array($mediaAttribute, $mediaAttributeCodes)) { @@ -353,7 +380,8 @@ public function setMediaAttribute(\Magento\Catalog\Model\Product $product, $medi } /** - * get media attribute codes + * Get media attribute codes + * * @return array * @since 101.0.0 */ @@ -363,6 +391,8 @@ public function getMediaAttributeCodes() } /** + * Trim .tmp ending from filename + * * @param string $file * @return string * @since 101.0.0 @@ -428,27 +458,27 @@ protected function getUniqueFileName($file, $forTmp = false) * Get filename which is not duplicated with other files in media temporary and media directories * * @param string $fileName - * @param string $dispretionPath + * @param string $dispersionPath * @return string * @since 101.0.0 */ - protected function getNotDuplicatedFilename($fileName, $dispretionPath) + protected function getNotDuplicatedFilename($fileName, $dispersionPath) { - $fileMediaName = $dispretionPath . '/' + $fileMediaName = $dispersionPath . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getMediaPath($fileName)); - $fileTmpMediaName = $dispretionPath . '/' + $fileTmpMediaName = $dispersionPath . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName($this->mediaConfig->getTmpMediaPath($fileName)); if ($fileMediaName != $fileTmpMediaName) { if ($fileMediaName != $fileName) { return $this->getNotDuplicatedFilename( $fileMediaName, - $dispretionPath + $dispersionPath ); } elseif ($fileTmpMediaName != $fileName) { return $this->getNotDuplicatedFilename( $fileTmpMediaName, - $dispretionPath + $dispersionPath ); } } @@ -459,7 +489,7 @@ protected function getNotDuplicatedFilename($fileName, $dispretionPath) /** * Retrieve data for update attribute * - * @param \Magento\Catalog\Model\Product $object + * @param \Magento\Catalog\Model\Product $object * @return array * @since 101.0.0 */ @@ -484,7 +514,6 @@ public function getAffectedFields($object) /** * Attribute value is not to be saved in a conventional way, separate table is used to store the complex value * - * {@inheritdoc} * @since 101.0.0 */ public function isScalar() diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php index c785d08e64b7f..4ad275bc70f90 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php @@ -55,9 +55,6 @@ public function __construct( */ public function execute($entity, $arguments = []) { - $value = []; - $value['images'] = []; - $mediaEntries = $this->resourceModel->loadProductGalleryByAttributeId( $entity, $this->getAttribute()->getAttributeId() @@ -79,37 +76,13 @@ public function execute($entity, $arguments = []) */ public function addMediaDataToProduct(Product $product, array $mediaEntries) { - $attrCode = $this->getAttribute()->getAttributeCode(); - $value = []; - $value['images'] = []; - $value['values'] = []; - - foreach ($mediaEntries as $mediaEntry) { - $mediaEntry = $this->substituteNullsWithDefaultValues($mediaEntry); - $value['images'][$mediaEntry['value_id']] = $mediaEntry; - } - $product->setData($attrCode, $value); - } - - /** - * @param array $rawData - * @return array - */ - private function substituteNullsWithDefaultValues(array $rawData) - { - $processedData = []; - foreach ($rawData as $key => $rawValue) { - if (null !== $rawValue) { - $processedValue = $rawValue; - } elseif (isset($rawData[$key . '_default'])) { - $processedValue = $rawData[$key . '_default']; - } else { - $processedValue = null; - } - $processedData[$key] = $processedValue; - } - - return $processedData; + $product->setData( + $this->getAttribute()->getAttributeCode(), + [ + 'images' => array_column($mediaEntries, null, 'value_id'), + 'values' => [] + ] + ); } /** diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 0dbad386f8b2f..9a4f3892fe115 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -16,7 +16,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler { /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processDeletedImages($product, array &$images) @@ -31,7 +32,10 @@ protected function processDeletedImages($product, array &$images) foreach ($images as &$image) { if (!empty($image['removed'])) { - if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { + if (!empty($image['value_id'])) { + if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { + continue; + } $recordsToDelete[] = $image['value_id']; $catalogPath = $this->mediaConfig->getBaseMediaPath(); $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']); @@ -49,7 +53,8 @@ protected function processDeletedImages($product, array &$images) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processNewImage($product, array &$image) @@ -76,6 +81,8 @@ protected function processNewImage($product, array &$image) } /** + * Retrieve store ids from product. + * * @param \Magento\Catalog\Model\Product $product * @return array * @since 101.0.0 @@ -94,6 +101,8 @@ protected function extractStoreIds($product) } /** + * Remove deleted images. + * * @param array $files * @return null * @since 101.0.0 diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index abd28ed3bf1ec..2463734503d88 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -5,9 +5,11 @@ */ namespace Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Image as MagentoImage; +use Magento\Framework\Serialize\SerializerInterface; /** * @method string getFile() @@ -34,7 +36,7 @@ class Image extends \Magento\Framework\Model\AbstractModel * * @var int */ - protected $_quality = 80; + protected $_quality = null; /** * @var bool @@ -171,6 +173,16 @@ class Image extends \Magento\Framework\Model\AbstractModel */ private $imageAsset; + /** + * @var string + */ + private $cachePrefix = 'IMG_INFO'; + + /** + * @var SerializerInterface + */ + private $serializer; + /** * Constructor * @@ -189,6 +201,7 @@ class Image extends \Magento\Framework\Model\AbstractModel * @param array $data * @param \Magento\Catalog\Model\View\Asset\ImageFactory|null $viewAssetImageFactory * @param \Magento\Catalog\Model\View\Asset\PlaceholderFactory|null $viewAssetPlaceholderFactory + * @param SerializerInterface|null $serializer * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -207,7 +220,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], \Magento\Catalog\Model\View\Asset\ImageFactory $viewAssetImageFactory = null, - \Magento\Catalog\Model\View\Asset\PlaceholderFactory $viewAssetPlaceholderFactory = null + \Magento\Catalog\Model\View\Asset\PlaceholderFactory $viewAssetPlaceholderFactory = null, + SerializerInterface $serializer = null ) { $this->_storeManager = $storeManager; $this->_catalogProductMediaConfig = $catalogProductMediaConfig; @@ -222,6 +236,7 @@ public function __construct( ->get(\Magento\Catalog\Model\View\Asset\ImageFactory::class); $this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory ?: ObjectManager::getInstance() ->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); } /** @@ -279,7 +294,8 @@ public function setQuality($quality) */ public function getQuality() { - return $this->_quality; + return $this->_quality === null + ? $this->_scopeConfig->getValue('system/upload_configuration/jpeg_quality') : $this->_quality; } /** @@ -355,86 +371,6 @@ public function setSize($size) return $this; } - /** - * @param string|null $file - * @return bool - */ - protected function _checkMemory($file = null) - { - return $this->_getMemoryLimit() > $this->_getMemoryUsage() + $this->_getNeedMemoryForFile( - $file - ) - || $this->_getMemoryLimit() == -1; - } - - /** - * @return string - */ - protected function _getMemoryLimit() - { - $memoryLimit = trim(strtoupper(ini_get('memory_limit'))); - - if (!isset($memoryLimit[0])) { - $memoryLimit = "128M"; - } - - if (substr($memoryLimit, -1) == 'K') { - return substr($memoryLimit, 0, -1) * 1024; - } - if (substr($memoryLimit, -1) == 'M') { - return substr($memoryLimit, 0, -1) * 1024 * 1024; - } - if (substr($memoryLimit, -1) == 'G') { - return substr($memoryLimit, 0, -1) * 1024 * 1024 * 1024; - } - return $memoryLimit; - } - - /** - * @return int - */ - protected function _getMemoryUsage() - { - if (function_exists('memory_get_usage')) { - return memory_get_usage(); - } - return 0; - } - - /** - * @param string|null $file - * @return float|int - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - protected function _getNeedMemoryForFile($file = null) - { - $file = $file === null ? $this->getBaseFile() : $file; - if (!$file) { - return 0; - } - - if (!$this->_mediaDirectory->isExist($file)) { - return 0; - } - - $imageInfo = getimagesize($this->_mediaDirectory->getAbsolutePath($file)); - - if (!isset($imageInfo[0]) || !isset($imageInfo[1])) { - return 0; - } - if (!isset($imageInfo['channels'])) { - // if there is no info about this parameter lets set it for maximum - $imageInfo['channels'] = 4; - } - if (!isset($imageInfo['bits'])) { - // if there is no info about this parameter lets set it for maximum - $imageInfo['bits'] = 8; - } - return round( - ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + Pow(2, 16)) * 1.65 - ); - } - /** * Convert array of 3 items (decimal r, g, b) to string of their hex values * @@ -471,9 +407,7 @@ public function setBaseFile($file) 'filePath' => $file, ] ); - if ($file == 'no_selection' || !$this->_fileExists($this->imageAsset->getSourceFile()) - || !$this->_checkMemory($this->imageAsset->getSourceFile()) - ) { + if ($file == 'no_selection' || !$this->_fileExists($this->imageAsset->getSourceFile())) { $this->_isBaseFilePlaceholder = true; $this->imageAsset = $this->viewAssetPlaceholderFactory->create( [ @@ -538,7 +472,7 @@ public function getImageProcessor() $this->_processor->keepTransparency($this->_keepTransparency); $this->_processor->constrainOnly($this->_constrainOnly); $this->_processor->backgroundColor($this->_backgroundColor); - $this->_processor->quality($this->_quality); + $this->_processor->quality($this->getQuality()); return $this->_processor; } @@ -561,7 +495,7 @@ public function resize() */ public function rotate($angle) { - $angle = intval($angle); + $angle = (int)$angle; $this->getImageProcessor()->rotate($angle); return $this; } @@ -681,11 +615,14 @@ public function getDestinationSubdir() } /** - * @return bool|void + * @return bool */ public function isCached() { - return file_exists($this->imageAsset->getPath()); + return ( + is_array($this->loadImageInfoFromCache($this->imageAsset->getPath())) || + file_exists($this->imageAsset->getPath()) + ); } /** @@ -855,6 +792,7 @@ public function clearCache() $this->_mediaDirectory->delete($directory); $this->_coreFileStorageDatabase->deleteFolder($this->_mediaDirectory->getAbsolutePath($directory)); + $this->clearImageInfoFromCache(); } /** @@ -877,17 +815,26 @@ protected function _fileExists($filename) /** * Return resized product image information - * * @return array + * @throws NotLoadInfoImageException */ public function getResizedImageInfo() { - if ($this->isBaseFilePlaceholder() == true) { - $image = $this->imageAsset->getSourceFile(); - } else { - $image = $this->imageAsset->getPath(); + try { + if ($this->isBaseFilePlaceholder() == true) { + $image = $this->imageAsset->getSourceFile(); + } else { + $image = $this->imageAsset->getPath(); + } + + $imageProperties = $this->getImageSize($image); + + return $imageProperties; + } finally { + if (empty($imageProperties)) { + throw new NotLoadInfoImageException(__('Can\'t get information about the picture: %1', $image)); + } } - return getimagesize($image); } /** @@ -908,7 +855,7 @@ private function getMiscParams() 'constrain_only' => ($this->_constrainOnly ? 'do' : 'not') . 'constrainonly', 'background' => $this->_rgbToString($this->_backgroundColor), 'angle' => $this->_angle, - 'quality' => $this->_quality, + 'quality' => $this->getQuality(), ]; // if has watermark add watermark params to hash @@ -922,4 +869,66 @@ private function getMiscParams() return $miscParams; } + + /** + * Get image size + * + * @param string $imagePath + * @return array + */ + private function getImageSize($imagePath) + { + $imageInfo = $this->loadImageInfoFromCache($imagePath); + if (!isset($imageInfo['size'])) { + $imageSize = getimagesize($imagePath); + $this->saveImageInfoToCache(['size' => $imageSize], $imagePath); + return $imageSize; + } else { + return $imageInfo['size']; + } + } + + /** + * Save image data to cache + * + * @param array $imageInfo + * @param string $imagePath + * @return void + */ + private function saveImageInfoToCache(array $imageInfo, string $imagePath) + { + $imagePath = $this->cachePrefix . $imagePath; + $this->_cacheManager->save( + $this->serializer->serialize($imageInfo), + $imagePath, + [$this->cachePrefix] + ); + } + + /** + * Load image data from cache + * + * @param string $imagePath + * @return array|false + */ + private function loadImageInfoFromCache(string $imagePath) + { + $imagePath = $this->cachePrefix . $imagePath; + $cacheData = $this->_cacheManager->load($imagePath); + if (!$cacheData) { + return false; + } else { + return $this->serializer->unserialize($cacheData); + } + } + + /** + * Clear image data from cache + * + * @return void + */ + private function clearImageInfoFromCache() + { + $this->_cacheManager->clean([$this->cachePrefix]); + } } diff --git a/app/code/Magento/Catalog/Model/Product/Image/Cache.php b/app/code/Magento/Catalog/Model/Product/Image/Cache.php index c5e5e0ecac4c0..cd66257657cb1 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/Cache.php +++ b/app/code/Magento/Catalog/Model/Product/Image/Cache.php @@ -10,6 +10,7 @@ use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection; use Magento\Framework\App\Area; use Magento\Framework\View\ConfigInterface; +use Psr\Log\LoggerInterface; class Cache { @@ -33,19 +34,29 @@ class Cache */ protected $data = []; + /** + * Logger. + * + * @var LoggerInterface + */ + private $logger; + /** * @param ConfigInterface $viewConfig * @param ThemeCollection $themeCollection * @param ImageHelper $imageHelper + * @param LoggerInterface $logger */ public function __construct( ConfigInterface $viewConfig, ThemeCollection $themeCollection, - ImageHelper $imageHelper + ImageHelper $imageHelper, + LoggerInterface $logger = null ) { $this->viewConfig = $viewConfig; $this->themeCollection = $themeCollection; $this->imageHelper = $imageHelper; + $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -74,21 +85,37 @@ protected function getData() } /** - * Resize product images and save results to image cache + * Resize product images and save results to image cache. * * @param Product $product + * * @return $this + * @throws \Exception */ public function generate(Product $product) { + $isException = false; $galleryImages = $product->getMediaGalleryImages(); if ($galleryImages) { foreach ($galleryImages as $image) { foreach ($this->getData() as $imageData) { - $this->processImageData($product, $imageData, $image->getFile()); + try { + $this->processImageData($product, $imageData, $image->getFile()); + } catch (\Exception $e) { + $isException = true; + $this->logger->error($e->getMessage()); + $this->logger->error(__('The image could not be resized: ') . $image->getPath()); + } } } } + + if ($isException === true) { + throw new \Magento\Framework\Exception\RuntimeException( + __('Some images could not be resized. See log file for details.') + ); + } + return $this; } diff --git a/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php b/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php new file mode 100644 index 0000000000000..0feec11c62a59 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Image/NotLoadInfoImageException.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\Product\Image; + +class NotLoadInfoImageException extends \Exception +{ +} diff --git a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php index 13c6a13a50407..a7468bcb1e77f 100644 --- a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php @@ -31,6 +31,7 @@ class SaveHandler private $linkResource; /** + * SaveHandler constructor. * @param MetadataPool $metadataPool * @param Link $linkResource * @param ProductLinkRepositoryInterface $productLinkRepository @@ -55,12 +56,30 @@ public function execute($entityType, $entity) { $link = $entity->getData($this->metadataPool->getMetadata($entityType)->getLinkField()); if ($this->linkResource->hasProductLinks($link)) { - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity*/ + /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ foreach ($this->productLinkRepository->getList($entity) as $link) { $this->productLinkRepository->delete($link); } } - $productLinks = $entity->getProductLinks(); + + // Build links per type + $linksByType = []; + foreach ($entity->getProductLinks() as $link) { + $linksByType[$link->getLinkType()][] = $link; + } + + // Set array position as a fallback position if necessary + foreach ($linksByType as $linkType => $links) { + if (!$this->hasPosition($links)) { + array_walk($linksByType[$linkType], function ($productLink, $position) { + $productLink->setPosition(++$position); + }); + } + } + + // Flatten multi-dimensional linksByType in ProductLinks + $productLinks = array_reduce($linksByType, 'array_merge', []); + if (count($productLinks) > 0) { foreach ($entity->getProductLinks() as $link) { $this->productLinkRepository->save($link); @@ -68,4 +87,19 @@ public function execute($entityType, $entity) } return $entity; } + + /** + * Check if at least one link without position + * @param array $links + * @return bool + */ + private function hasPosition(array $links) + { + foreach ($links as $link) { + if (!array_key_exists('position', $link->getData())) { + return false; + } + } + return true; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 330da9107b280..2e1f30d5a05ff 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; @@ -76,7 +74,8 @@ public function getBaseMediaPath() */ public function getBaseMediaUrl() { - return $this->storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . 'catalog/product'; + return $this->storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) + . 'catalog/product'; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 67d9074d91382..2b72a6ca4e8ae 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; @@ -102,6 +103,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $metadataPool; + /** + * @var ProductCustomOptionValuesInterfaceFactory + */ + private $customOptionValuesFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -114,6 +120,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -127,12 +134,16 @@ public function __construct( Option\Validator\Pool $validatorPool, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->validatorPool = $validatorPool; $this->string = $string; + $this->customOptionValuesFactory = $customOptionValuesFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + parent::__construct( $context, $registry, @@ -312,7 +323,7 @@ public function getGroupByType($type = null) self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, ]; - return isset($optionGroupsToTypes[$type]) ? $optionGroupsToTypes[$type] : ''; + return $optionGroupsToTypes[$type] ?? ''; } /** @@ -377,6 +388,11 @@ public function beforeSave() } } } + + if ($this->getGroupByType($this->getData('type')) === self::OPTION_GROUP_FILE) { + $this->cleanFileExtensions(); + } + return $this; } @@ -386,20 +402,21 @@ public function beforeSave() */ public function afterSave() { - $this->getValueInstance()->unsetValues(); $values = $this->getValues() ?: $this->getData('values'); if (is_array($values)) { foreach ($values as $value) { - if ($value instanceof \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface) { + if ($value instanceof ProductCustomOptionValuesInterface) { $data = $value->getData(); } else { $data = $value; } - $this->getValueInstance()->addValue($data); - } - $this->getValueInstance()->setOption($this)->saveValues(); - } elseif ($this->getGroupByType($this->getType()) == self::OPTION_GROUP_SELECT) { + $this->customOptionValuesFactory->create() + ->addValue($data) + ->setOption($this) + ->saveValues(); + } + } elseif ($this->getGroupByType($this->getType()) === self::OPTION_GROUP_SELECT) { throw new LocalizedException(__('Select type options required values rows.')); } @@ -800,7 +817,7 @@ public function setImageSizeY($imageSizeY) } /** - * @param \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface[] $values + * @param ProductCustomOptionValuesInterface[] $values * @return $this */ public function setValues(array $values = null) @@ -901,5 +918,22 @@ private function getMetadataPool() return $this->metadataPool; } + /** + * Clears all non-accepted characters from file_extension field. + * + * @return void + */ + private function cleanFileExtensions() + { + $extensions = ''; + $rawExtensions = $this->getFileExtension(); + $matches = []; + preg_match_all('/(?<extensions>[a-z0-9]+)/i', strtolower($rawExtensions), $matches); + if (!empty($matches)) { + $extensions = implode(', ', array_unique($matches['extensions'])); + } + $this->setFileExtension($extensions); + } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index c4a2d60414a7b..6eec110e76c10 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository; @@ -28,6 +30,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute. + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +39,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; @@ -52,11 +60,28 @@ public function execute($entity, $arguments = []) } } if ($options) { - foreach ($options as $option) { - $this->optionRepository->save($option); - } + $this->processOptionsSaving($options, $entity->dataHasChangedFor('sku'), $entity->getSku()); } return $entity; } + + /** + * Save custom options + * + * @param array $options + * @param bool $hasChangedSku + * @param string $newSku + * + * @return void + */ + private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku) + { + foreach ($options as $option) { + if ($hasChangedSku && $option->hasData('product_sku')) { + $option->setProductSku($newSku); + } + $this->optionRepository->save($option); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index cb6e76aebaadb..f6884a8d17f1f 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -101,11 +101,11 @@ public function validateUserValue($values) $this->setUserValue( [ 'date' => isset($value['date']) ? $value['date'] : '', - 'year' => isset($value['year']) ? intval($value['year']) : 0, - 'month' => isset($value['month']) ? intval($value['month']) : 0, - 'day' => isset($value['day']) ? intval($value['day']) : 0, - 'hour' => isset($value['hour']) ? intval($value['hour']) : 0, - 'minute' => isset($value['minute']) ? intval($value['minute']) : 0, + 'year' => isset($value['year']) ? (int)$value['year'] : 0, + 'month' => isset($value['month']) ? (int)$value['month'] : 0, + 'day' => isset($value['day']) ? (int)$value['day'] : 0, + 'hour' => isset($value['hour']) ? (int)$value['hour'] : 0, + 'minute' => isset($value['minute']) ? (int)$value['minute'] : 0, 'day_part' => isset($value['day_part']) ? $value['day_part'] : '', 'date_internal' => isset($value['date_internal']) ? $value['date_internal'] : '', ] diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 690809332de4a..b7bd507eb73f4 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product\Option\Type; use Magento\Framework\Exception\LocalizedException; @@ -193,7 +191,10 @@ public function getRequest() */ public function getConfigData($key) { - return $this->_scopeConfig->getValue('catalog/custom_options/' . $key, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return $this->_scopeConfig->getValue( + 'catalog/custom_options/' . $key, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** @@ -276,7 +277,7 @@ public function getFormattedOptionValue($optionValue) */ public function getCustomizedView($optionInfo) { - return isset($optionInfo['value']) ? $optionInfo['value'] : $optionInfo; + return $optionInfo['value'] ?? $optionInfo; } /** @@ -338,7 +339,7 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->_getChargableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); + return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); } /** @@ -392,14 +393,27 @@ public function getProductOptions() } /** - * Return final chargable price for option - * * @param float $price Price of option * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float + * @deprecated 102.0.4 typo in method name + * @see _getChargeableOptionPrice */ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) + { + return $this->_getChargeableOptionPrice($price, $isPercent, $basePrice); + } + + /** + * Return final chargeable price for option + * + * @param float $price Price of option + * @param boolean $isPercent Price type - percent or fixed + * @param float $basePrice For percent price type + * @return float + */ + protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) { if ($isPercent) { return $basePrice * $price / 100; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php index a7304c9b67bb2..26e6a92852720 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php @@ -126,7 +126,7 @@ public function __construct( $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->validatorInfo = $validatorInfo; $this->validatorFile = $validatorFile; - $this->serializer = $serializer ? $serializer : ObjectManager::getInstance()->get(Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($checkoutSession, $scopeConfig, $data); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php new file mode 100644 index 0000000000000..8d4aea135eabb --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\Product\Option\Type\File; + +/** + * Validator for existing (already saved) files. + */ +class ExistingValidate extends \Zend_Validate +{ + /** + * @inheritDoc + * + * @param string $value File's full path. + * @param string|null $originalName Original file's name (when uploaded). + */ + public function isValid($value, string $originalName = null) + { + $this->_messages = []; + $this->_errors = []; + + if (!is_string($value)) { + $this->_messages[] = __('Full file path is expected.')->render(); + return false; + } + + $result = true; + $fileInfo = null; + if ($originalName) { + $fileInfo = ['name' => $originalName]; + } + foreach ($this->_validators as $element) { + $validator = $element['instance']; + if ($validator->isValid($value, $fileInfo)) { + continue; + } + $result = false; + $messages = $validator->getMessages(); + $this->_messages = array_merge($this->_messages, $messages); + $this->_errors = array_merge($this->_errors, array_keys($messages)); + if ($element['breakChainOnFailure']) { + break; + } + } + return $result; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php index 32c901afe8e74..c0d10c720f6f6 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php @@ -13,6 +13,6 @@ class ValidateFactory */ public function create() { - return new \Zend_Validate(); + return new ExistingValidate(); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php index af6c4dba784f0..49e062c5fd465 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php @@ -10,6 +10,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Math\Random; +use Magento\Framework\App\ObjectManager; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -62,12 +64,18 @@ class ValidatorFile extends Validator */ protected $isImageValidator; + /** + * @var Random + */ + private $random; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\File\Size $fileSize * @param \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory * @param \Magento\Framework\Validator\File\IsImage $isImageValidator + * @param Random|null $random * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( @@ -75,12 +83,15 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\File\Size $fileSize, \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory, - \Magento\Framework\Validator\File\IsImage $isImageValidator + \Magento\Framework\Validator\File\IsImage $isImageValidator, + Random $random = null ) { $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->filesystem = $filesystem; $this->httpFactory = $httpFactory; $this->isImageValidator = $isImageValidator; + $this->random = $random + ?? ObjectManager::getInstance()->get(Random::class); parent::__construct($scopeConfig, $filesystem, $fileSize); } @@ -147,16 +158,15 @@ public function validate($processingParams, $option) $userValue = []; if ($upload->isUploaded($file) && $upload->isValid($file)) { - $extension = pathinfo(strtolower($fileInfo['name']), PATHINFO_EXTENSION); - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); - $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispretionPath($fileName); + $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); $filePath = $dispersion; $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); - $filePath .= '/' . $fileHash . '.' . $extension; + $fileRandomName = $this->random->getRandomString(32); + $filePath .= '/' .$fileRandomName; $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php index 30c3de932c3e6..b9946b99c5fdc 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php @@ -6,6 +6,9 @@ namespace Magento\Catalog\Model\Product\Option\Type\File; +/** + * Validator for existing files. + */ class ValidatorInfo extends Validator { /** @@ -90,7 +93,7 @@ public function validate($optionValue, $option) } $result = false; - if ($validatorChain->isValid($this->fileFullPath)) { + if ($validatorChain->isValid($this->fileFullPath, $optionValue['title'])) { $result = $this->rootDirectory->isReadable($this->fileRelativePath) && isset($optionValue['secret_key']) && $this->buildSecretKey($this->fileRelativePath) == $optionValue['secret_key']; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 78cce7bd76163..5fda0f6f7c05c 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -29,23 +29,35 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ protected $string; + /** + * @var array + */ + private $singleSelectionTypes; + /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\Escaper $escaper * @param array $data + * @param array $singleSelectionTypes */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Escaper $escaper, - array $data = [] + array $data = [], + array $singleSelectionTypes = [] ) { $this->string = $string; $this->_escaper = $escaper; parent::__construct($checkoutSession, $scopeConfig, $data); + + $this->singleSelectionTypes = $singleSelectionTypes ?: [ + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, + ]; } /** @@ -68,7 +80,8 @@ public function validateUserValue($values) } if (!$this->_isSingleSelection()) { $valuesCollection = $option->getOptionValuesByOptionId($value, $this->getProduct()->getStoreId())->load(); - if ($valuesCollection->count() != count($value)) { + $valueCount = is_array($value) ? count($value) : 0; + if ($valuesCollection->count() != $valueCount) { $this->setIsValid(false); throw new LocalizedException(__('Please specify product\'s required option(s).')); } @@ -222,7 +235,7 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->_getChargableOptionPrice( + $result += $this->_getChargeableOptionPrice( $_result->getPrice(), $_result->getPriceType() == 'percent', $basePrice @@ -237,7 +250,7 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->_getChargableOptionPrice( + $result = $this->_getChargeableOptionPrice( $_result->getPrice(), $_result->getPriceType() == 'percent', $basePrice @@ -301,10 +314,6 @@ public function getOptionSku($optionValue, $skuDelimiter) */ protected function _isSingleSelection() { - $single = [ - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, - ]; - return in_array($this->getOption()->getType(), $single); + return in_array($this->getOption()->getType(), $this->singleSelectionTypes, true); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php index 79ee37c51671d..5624733831c1a 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php @@ -81,7 +81,7 @@ public function validateUserValue($values) */ public function prepareForCart() { - if ($this->getIsValid() && strlen($this->getUserValue()) > 0) { + if ($this->getIsValid() && ($this->getUserValue() !== '')) { return $this->getUserValue(); } else { return null; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php index 1e5c7f76d829b..73bbc9cc88d3d 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php @@ -106,7 +106,9 @@ protected function isValidOptionTitle($title, $storeId) if ($storeId > \Magento\Store\Model\Store::DEFAULT_STORE_ID && $title === null) { return true; } - if ($this->isEmpty($title)) { + + // checking whether title is null and is empty string + if ($title === null || $title === '') { return false; } @@ -132,7 +134,7 @@ protected function validateOptionType(Option $option) */ protected function validateOptionValue(Option $option) { - return $this->isInRange($option->getPriceType(), $this->priceTypes) && !$this->isNegative($option->getPrice()); + return $this->isInRange($option->getPriceType(), $this->priceTypes); } /** @@ -166,6 +168,6 @@ protected function isInRange($value, array $range) */ protected function isNegative($value) { - return intval($value) < 0; + return (int)$value < 0; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php index 1e00654249556..2256f031098f1 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php @@ -29,6 +29,6 @@ public function __construct(array $validators) */ public function get($type) { - return isset($this->validators[$type]) ? $this->validators[$type] : $this->validators['default']; + return $this->validators[$type] ?? $this->validators['default']; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php index f04ab497e1d4f..44756890b6ed7 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php @@ -83,7 +83,7 @@ protected function isValidOptionPrice($priceType, $price, $storeId) if (!$priceType && !$price) { return true; } - if (!$this->isInRange($priceType, $this->priceTypes) || $this->isNegative($price)) { + if (!$this->isInRange($priceType, $this->priceTypes)) { return false; } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 0e86510ebcee7..ebbc060c99edf 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -4,13 +4,14 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; use Magento\Framework\Model\AbstractModel; +use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; +use Magento\Catalog\Pricing\Price\RegularPrice; /** * Catalog product option select type model @@ -20,6 +21,9 @@ * @method \Magento\Catalog\Model\Product\Option\Value setOptionId(int $value) * * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - added use of constants instead of string literals: + * BasePrice::PRICE_CODE - instead of 'base_price' + * RegularPrice::PRICE_CODE - instead of 'regular_price' * @since 100.0.2 */ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface @@ -60,6 +64,11 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu */ protected $_valueCollectionFactory; + /** + * @var CustomOptionPriceCalculator + */ + private $customOptionPriceCalculator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -67,6 +76,7 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -74,9 +84,13 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory $valueCollectionFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + CustomOptionPriceCalculator $customOptionPriceCalculator = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; + $this->customOptionPriceCalculator = $customOptionPriceCalculator + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + parent::__construct( $context, $registry, @@ -178,7 +192,7 @@ public function setProduct($product) */ public function getProduct() { - if (is_null($this->_product)) { + if ($this->_product === null) { $this->_product = $this->getOption()->getProduct(); } return $this->_product; @@ -189,18 +203,15 @@ public function getProduct() */ public function saveValues() { + $option = $this->getOption(); + foreach ($this->getValues() as $value) { - $this->setData( - $value - )->setData( - 'option_id', - $this->getOption()->getId() - )->setData( - 'store_id', - $this->getOption()->getStoreId() - ); - - if ($this->getData('is_delete') == '1') { + $this->isDeleted(false); + $this->setData($value) + ->setData('option_id', $option->getId()) + ->setData('store_id', $option->getStoreId()); + + if ((bool) $this->getData('is_delete') === true) { if ($this->getId()) { $this->deleteValues($this->getId()); $this->delete(); @@ -209,7 +220,7 @@ public function saveValues() $this->save(); } } - //eof foreach() + return $this; } @@ -222,10 +233,8 @@ public function saveValues() */ public function getPrice($flag = false) { - if ($flag && $this->getPriceType() == self::TYPE_PERCENT) { - $basePrice = $this->getOption()->getProduct()->getFinalPrice(); - $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); - return $price; + if ($flag) { + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); } return $this->_getData(self::KEY_PRICE); } @@ -237,12 +246,7 @@ public function getPrice($flag = false) */ public function getRegularPrice() { - if ($this->getPriceType() == self::TYPE_PERCENT) { - $basePrice = $this->getOption()->getProduct()->getPriceInfo()->getPrice('regular_price')->getAmount()->getValue(); - $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); - return $price; - } - return $this->_getData(self::KEY_PRICE); + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, RegularPrice::PRICE_CODE); } /** diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 3bb6bba69bfb4..af274f2fce6c5 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -7,11 +7,15 @@ namespace Magento\Catalog\Model\Product\Price; use Magento\Catalog\Api\Data\TierPriceInterface; +use Magento\Catalog\Api\TierPriceStorageInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; +use Magento\Catalog\Model\ProductIdLocatorInterface; /** * Tier price storage. */ -class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface +class TierPriceStorage implements TierPriceStorageInterface { /** * Tier price resource model. @@ -23,7 +27,7 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface /** * Tier price validator. * - * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator + * @var TierPriceValidator */ private $tierPriceValidator; @@ -35,100 +39,78 @@ class TierPriceStorage implements \Magento\Catalog\Api\TierPriceStorageInterface private $tierPriceFactory; /** - * Price indexer. + * Price index processor. * - * @var \Magento\Catalog\Model\Indexer\Product\Price + * @var PriceIndexerProcessor */ - private $priceIndexer; + private $priceIndexProcessor; /** * Product ID locator. * - * @var \Magento\Catalog\Model\ProductIdLocatorInterface + * @var ProductIdLocatorInterface */ private $productIdLocator; - /** - * Page cache config. - * - * @var \Magento\PageCache\Model\Config - */ - private $config; - - /** - * Cache type list. - * - * @var \Magento\Framework\App\Cache\TypeListInterface - */ - private $typeList; - - /** - * Indexer chunk value. - * - * @var int - */ - private $indexerChunkValue = 500; - /** * @param TierPricePersistence $tierPricePersistence - * @param \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator + * @param TierPriceValidator $tierPriceValidator * @param TierPriceFactory $tierPriceFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer - * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator - * @param \Magento\PageCache\Model\Config $config - * @param \Magento\Framework\App\Cache\TypeListInterface $typeList + * @param PriceIndexerProcessor $priceIndexProcessor + * @param ProductIdLocatorInterface $productIdLocator */ public function __construct( TierPricePersistence $tierPricePersistence, - \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator $tierPriceValidator, + TierPriceValidator $tierPriceValidator, TierPriceFactory $tierPriceFactory, - \Magento\Catalog\Model\Indexer\Product\Price $priceIndexer, - \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, - \Magento\PageCache\Model\Config $config, - \Magento\Framework\App\Cache\TypeListInterface $typeList + PriceIndexerProcessor $priceIndexProcessor, + ProductIdLocatorInterface $productIdLocator ) { $this->tierPricePersistence = $tierPricePersistence; $this->tierPriceValidator = $tierPriceValidator; $this->tierPriceFactory = $tierPriceFactory; - $this->priceIndexer = $priceIndexer; + $this->priceIndexProcessor = $priceIndexProcessor; $this->productIdLocator = $productIdLocator; - $this->config = $config; - $this->typeList = $typeList; } /** - * {@inheritdoc} + * @inheritdoc */ public function get(array $skus) { $skus = $this->tierPriceValidator->validateSkus($skus); + $skuByIdLookup = $this->buildSkuByIdLookup($skus); + $prices = $this->getExistingPrices($skuByIdLookup); - return $this->getExistingPrices($skus); + return $prices; } /** - * {@inheritdoc} + * @inheritdoc */ public function update(array $prices) { - $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); $skus = array_unique( - array_map(function ($price) { - return $price->getSku(); - }, $prices) + array_map( + function (TierPriceInterface $price) { + return $price->getSku(); + }, + $prices + ) ); - $result = $this->tierPriceValidator->retrieveValidationResult($prices, $this->getExistingPrices($skus, true)); + $skuByIdLookup = $this->buildSkuByIdLookup($skus); + $existingPrices = $this->getExistingPrices($skuByIdLookup, true); + $result = $this->tierPriceValidator->retrieveValidationResult($prices, $existingPrices); $prices = $this->removeIncorrectPrices($prices, $result->getFailedRowIds()); $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->update($formattedPrices); - $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); + $this->reindexPrices(array_keys($skuByIdLookup)); return $result->getFailedItems(); } /** - * {@inheritdoc} + * @inheritdoc */ public function replace(array $prices) { @@ -138,13 +120,12 @@ public function replace(array $prices) $formattedPrices = $this->retrieveFormattedPrices($prices); $this->tierPricePersistence->replace($formattedPrices, $affectedIds); $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); return $result->getFailedItems(); } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(array $prices) { @@ -154,7 +135,6 @@ public function delete(array $prices) $priceIds = $this->retrieveAffectedPriceIds($prices); $this->tierPricePersistence->delete($priceIds); $this->reindexPrices($affectedIds); - $this->invalidateFullPageCache(); return $result->getFailedItems(); } @@ -162,18 +142,18 @@ public function delete(array $prices) /** * Get existing prices by SKUs. * - * @param array $skus + * @param array $skuByIdLookup * @param bool $groupBySku [optional] * @return array */ - private function getExistingPrices(array $skus, $groupBySku = false) + private function getExistingPrices(array $skuByIdLookup, $groupBySku = false): array { - $ids = $this->retrieveAffectedIds($skus); - $rawPrices = $this->tierPricePersistence->get($ids); + $rawPrices = $this->tierPricePersistence->get(array_keys($skuByIdLookup)); $prices = []; + $linkField = $this->tierPricePersistence->getEntityLinkField(); foreach ($rawPrices as $rawPrice) { - $sku = $this->retrieveSkuById($rawPrice[$this->tierPricePersistence->getEntityLinkField()], $skus); + $sku = $skuByIdLookup[$rawPrice[$linkField]]; $price = $this->tierPriceFactory->create($rawPrice, $sku); if ($groupBySku) { $prices[$sku][] = $price; @@ -191,7 +171,7 @@ private function getExistingPrices(array $skus, $groupBySku = false) * @param array $prices * @return array */ - private function retrieveFormattedPrices(array $prices) + private function retrieveFormattedPrices(array $prices): array { $formattedPrices = []; @@ -212,12 +192,15 @@ private function retrieveFormattedPrices(array $prices) * @param TierPriceInterface[] $prices * @return array */ - private function retrieveAffectedProductIdsForPrices(array $prices) + private function retrieveAffectedProductIdsForPrices(array $prices): array { $skus = array_unique( - array_map(function ($price) { - return $price->getSku(); - }, $prices) + array_map( + function (TierPriceInterface $price) { + return $price->getSku(); + }, + $prices + ) ); return $this->retrieveAffectedIds($skus); @@ -229,7 +212,7 @@ private function retrieveAffectedProductIdsForPrices(array $prices) * @param array $skus * @return array */ - private function retrieveAffectedIds(array $skus) + private function retrieveAffectedIds(array $skus): array { $affectedIds = []; @@ -246,7 +229,7 @@ private function retrieveAffectedIds(array $skus) * @param array $prices * @return array */ - private function retrieveAffectedPriceIds(array $prices) + private function retrieveAffectedPriceIds(array $prices): array { $affectedIds = $this->retrieveAffectedProductIdsForPrices($prices); $formattedPrices = $this->retrieveFormattedPrices($prices); @@ -292,7 +275,7 @@ private function retrievePriceId(array $price, array $existingPrices) * @param array $price * @return bool */ - private function isCorrectPriceValue(array $existingPrice, array $price) + private function isCorrectPriceValue(array $existingPrice, array $price): bool { return ($existingPrice['value'] != 0 && $existingPrice['value'] == $price['value']) || ($existingPrice['percentage_value'] !== null @@ -300,33 +283,21 @@ private function isCorrectPriceValue(array $existingPrice, array $price) } /** - * Retrieve SKU by product ID. + * Generate lookup to retrieve SKU by product ID. * - * @param int $id * @param array $skus - * @return string|null + * @return array */ - private function retrieveSkuById($id, $skus) + private function buildSkuByIdLookup($skus): array { + $lookup = []; foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { - if (isset($ids[$id])) { - return $sku; + foreach (array_keys($ids) as $id) { + $lookup[$id] = $sku; } } - return null; - } - - /** - * Invalidate full page cache. - * - * @return void - */ - private function invalidateFullPageCache() - { - if ($this->config->isEnabled()) { - $this->typeList->invalidate('full_page'); - } + return $lookup; } /** @@ -337,8 +308,8 @@ private function invalidateFullPageCache() */ private function reindexPrices(array $ids) { - foreach (array_chunk($ids, $this->indexerChunkValue) as $affectedIds) { - $this->priceIndexer->execute($affectedIds); + if (!empty($ids)) { + $this->priceIndexProcessor->reindexList($ids); } } @@ -349,7 +320,7 @@ private function reindexPrices(array $ids) * @param array $ids * @return array */ - private function removeIncorrectPrices(array $prices, array $ids) + private function removeIncorrectPrices(array $prices, array $ids): array { foreach ($ids as $id) { unset($prices[$id]); diff --git a/app/code/Magento/Catalog/Model/Product/PriceModifier.php b/app/code/Magento/Catalog/Model/Product/PriceModifier.php index 4d81000501cff..ed9b3597c0dc9 100644 --- a/app/code/Magento/Catalog/Model/Product/PriceModifier.php +++ b/app/code/Magento/Catalog/Model/Product/PriceModifier.php @@ -46,11 +46,11 @@ public function removeTierPrice(\Magento\Catalog\Model\Product $product, $custom foreach ($prices as $key => $tierPrice) { if ($customerGroupId == 'all' && $tierPrice['price_qty'] == $qty - && $tierPrice['all_groups'] == 1 && intval($tierPrice['website_id']) === intval($websiteId) + && $tierPrice['all_groups'] == 1 && (int)$tierPrice['website_id'] === (int)$websiteId ) { unset($prices[$key]); } elseif ($tierPrice['price_qty'] == $qty && $tierPrice['cust_group'] == $customerGroupId - && intval($tierPrice['website_id']) === intval($websiteId) + && (int)$tierPrice['website_id'] === (int)$websiteId ) { unset($prices[$key]); } diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php index 7a1926cf642ec..5d2490e01dc3a 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -138,7 +138,9 @@ private function getProductIdsByActions(array $actions) $productIds = []; foreach ($actions as $action) { - $productIds[] = $action['product_id']; + if (isset($action['product_id']) && (int)$action['product_id']) { + $productIds[] = (int)$action['product_id']; + } } return $productIds; @@ -159,33 +161,37 @@ public function syncActions(array $productsData, $typeId) $customerId = $this->session->getCustomerId(); $visitorId = $this->visitor->getId(); $collection = $this->getActionsByType($typeId); - $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); - - /** - * Note that collection is also filtered by visitor id and customer id - * This collection shouldnt be flushed when visitor has products and then login - * It can remove only products for visitor, or only products for customer - * - * ['product_id' => 'added_at'] - * @var ProductFrontendActionInterface $item - */ - foreach ($collection as $item) { - $this->entityManager->delete($item); - } - - foreach ($productsData as $productId => $productData) { - /** @var ProductFrontendActionInterface $action */ - $action = $this->productFrontendActionFactory->create([ - 'data' => [ - 'visitor_id' => $customerId ? null : $visitorId, - 'customer_id' => $this->session->getCustomerId(), - 'added_at' => $productData['added_at'], - 'product_id' => $productId, - 'type_id' => $typeId - ] - ]); - - $this->entityManager->save($action); + $productIds = $this->getProductIdsByActions($productsData); + + if ($productIds) { + $collection->addFieldToFilter('product_id', $productIds); + + /** + * Note that collection is also filtered by visitor id and customer id + * This collection shouldnt be flushed when visitor has products and then login + * It can remove only products for visitor, or only products for customer + * + * ['product_id' => 'added_at'] + * @var ProductFrontendActionInterface $item + */ + foreach ($collection as $item) { + $this->entityManager->delete($item); + } + + foreach ($productsData as $productId => $productData) { + /** @var ProductFrontendActionInterface $action */ + $action = $this->productFrontendActionFactory->create([ + 'data' => [ + 'visitor_id' => $customerId ? null : $visitorId, + 'customer_id' => $this->session->getCustomerId(), + 'added_at' => $productData['added_at'], + 'product_id' => $productId, + 'type_id' => $typeId + ] + ]); + + $this->entityManager->save($action); + } } } diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php index c2046bea550e6..af0772251e235 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Model/Product/ProductList/Toolbar.php @@ -102,6 +102,6 @@ public function getLimit() public function getCurrentPage() { $page = (int) $this->request->getParam(self::PAGE_PARM_NAME); - return $page ? $page : 1; + return $page ?: 1; } } diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php new file mode 100644 index 0000000000000..46a73e104b87f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php @@ -0,0 +1,183 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\ProductList; + +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Responds for saving toolbar settings to catalog session. + */ +class ToolbarMemorizer +{ + /** + * XML PATH to enable/disable saving toolbar parameters to session + */ + const XML_PATH_CATALOG_REMEMBER_PAGINATION = 'catalog/frontend/remember_pagination'; + + /** + * @var CatalogSession + */ + private $catalogSession; + + /** + * @var Toolbar + */ + private $toolbarModel; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var string|bool + */ + private $order; + + /** + * @var string|bool + */ + private $direction; + + /** + * @var string|bool + */ + private $mode; + + /** + * @var string|bool + */ + private $limit; + + /** + * @var bool + */ + private $isMemorizingAllowed; + + /** + * @param Toolbar $toolbarModel + * @param CatalogSession $catalogSession + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + Toolbar $toolbarModel, + CatalogSession $catalogSession, + ScopeConfigInterface $scopeConfig + ) { + $this->toolbarModel = $toolbarModel; + $this->catalogSession = $catalogSession; + $this->scopeConfig = $scopeConfig; + } + + /** + * Get sort order. + * + * @return string|bool|null + */ + public function getOrder() + { + if ($this->order === null) { + $this->order = $this->toolbarModel->getOrder() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::ORDER_PARAM_NAME) : null); + } + + return $this->order; + } + + /** + * Get sort direction. + * + * @return string|bool|null + */ + public function getDirection() + { + if ($this->direction === null) { + $this->direction = $this->toolbarModel->getDirection() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::DIRECTION_PARAM_NAME) : null); + } + + return $this->direction; + } + + /** + * Get sort mode. + * + * @return string|bool|null + */ + public function getMode() + { + if ($this->mode === null) { + $this->mode = $this->toolbarModel->getMode() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::MODE_PARAM_NAME) : null); + } + + return $this->mode; + } + + /** + * Get products per page limit. + * + * @return string|bool|null + */ + public function getLimit() + { + if ($this->limit === null) { + $this->limit = $this->toolbarModel->getLimit() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::LIMIT_PARAM_NAME) : null); + } + + return $this->limit; + } + + /** + * Method to save all catalog parameters in catalog session. + * + * @return void + */ + public function memorizeParams() + { + if (!$this->catalogSession->getParamsMemorizeDisabled() && $this->isMemorizingAllowed()) { + $this->memorizeParam(Toolbar::ORDER_PARAM_NAME, $this->getOrder()) + ->memorizeParam(Toolbar::DIRECTION_PARAM_NAME, $this->getDirection()) + ->memorizeParam(Toolbar::MODE_PARAM_NAME, $this->getMode()) + ->memorizeParam(Toolbar::LIMIT_PARAM_NAME, $this->getLimit()); + } + } + + /** + * Check configuration for enabled/disabled toolbar memorizing. + * + * @return bool + */ + public function isMemorizingAllowed(): bool + { + if ($this->isMemorizingAllowed === null) { + $this->isMemorizingAllowed = $this->scopeConfig->isSetFlag(self::XML_PATH_CATALOG_REMEMBER_PAGINATION); + } + + return $this->isMemorizingAllowed; + } + + /** + * Memorize parameter value for session. + * + * @param string $param parameter name + * @param mixed $value parameter value + * @return ToolbarMemorizer + */ + private function memorizeParam(string $param, $value): ToolbarMemorizer + { + if ($value && $this->catalogSession->getData($param) != $value) { + $this->catalogSession->setData($param, $value); + } + + return $this; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php new file mode 100644 index 0000000000000..404760a51eff5 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class to check that product is saleable. + */ +class SalabilityChecker +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ProductRepositoryInterface $productRepository + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductRepositoryInterface $productRepository, + StoreManagerInterface $storeManager + ) { + $this->productRepository = $productRepository; + $this->storeManager = $storeManager; + } + + /** + * Check if product is salable. + * + * @param int|string $productId + * @param int|null $storeId + * @return bool + */ + public function isSalable($productId, $storeId = null): bool + { + if ($storeId === null) { + $storeId = $this->storeManager->getStore()->getId(); + } + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->getById($productId, false, $storeId); + + return $product->isSalable(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php index 7cecd2f37bb84..18dc49a852a94 100644 --- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php +++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php @@ -181,7 +181,7 @@ public function getList($sku, $customerGroupId) $prices = []; foreach ($product->getData('tier_price') as $price) { - if ((is_numeric($customerGroupId) && intval($price['cust_group']) === intval($customerGroupId)) + if ((is_numeric($customerGroupId) && (int)$price['cust_group'] === (int)$customerGroupId) || ($customerGroupId === 'all' && $price['all_groups']) ) { /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ diff --git a/app/code/Magento/Catalog/Model/Product/Type.php b/app/code/Magento/Catalog/Model/Product/Type.php index dc3971397acb2..0bc5216d2d238 100644 --- a/app/code/Magento/Catalog/Model/Product/Type.php +++ b/app/code/Magento/Catalog/Model/Product/Type.php @@ -232,7 +232,7 @@ public function getOptions() public function getOptionText($optionId) { $options = $this->getOptionArray(); - return isset($options[$optionId]) ? $options[$optionId] : null; + return $options[$optionId] ?? null; } /** @@ -285,7 +285,7 @@ public function getTypesByPriority() $types = $this->getTypes(); foreach ($types as $typeId => $typeInfo) { - $priority = isset($typeInfo['index_priority']) ? abs(intval($typeInfo['index_priority'])) : 0; + $priority = isset($typeInfo['index_priority']) ? abs((int)$typeInfo['index_priority']) : 0; if (!empty($typeInfo['composite'])) { $compositePriority[$typeId] = $priority; } else { diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index 33ff3ecccd4dd..83057ad00c668 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -251,7 +251,7 @@ public function getChildrenIds($parentId, $required = true) } /** - * Retrieve parent ids array by requered child + * Retrieve parent ids array by required child * * @param int|array $childId * @return array @@ -291,13 +291,7 @@ public function attributesCompare($attributeOne, $attributeTwo) $sortOne = $attributeOne->getGroupSortPath() * 1000 + $attributeOne->getSortPath() * 0.0001; $sortTwo = $attributeTwo->getGroupSortPath() * 1000 + $attributeTwo->getSortPath() * 0.0001; - if ($sortOne > $sortTwo) { - return 1; - } elseif ($sortOne < $sortTwo) { - return -1; - } - - return 0; + return $sortOne <=> $sortTwo; } /** @@ -938,7 +932,7 @@ public function getForceChildItemQtyChanges($product) */ public function prepareQuoteItemQty($qty, $product) { - return floatval($qty); + return (float)$qty; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php new file mode 100644 index 0000000000000..f6893a41113e6 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Type; + +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice; +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Store\Api\Data\WebsiteInterface; + +/** + * Product special price model. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FrontSpecialPrice extends Price +{ + /** + * @var SpecialPrice + */ + private $specialPrice; + + /** + * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory + * @param \Magento\Framework\App\Config\ScopeConfigInterface $config + * @param SpecialPrice $specialPrice + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + \Magento\Customer\Api\GroupManagementInterface $groupManagement, + \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, + \Magento\Framework\App\Config\ScopeConfigInterface $config, + SpecialPrice $specialPrice + ) { + $this->specialPrice = $specialPrice; + parent::__construct( + $ruleFactory, + $storeManager, + $localeDate, + $customerSession, + $eventManager, + $priceCurrency, + $groupManagement, + $tierPriceFactory, + $config + ); + } + + /** + * @inheritdoc + */ + protected function _applySpecialPrice($product, $finalPrice) + { + if (!$product->getSpecialPrice()) { + return $finalPrice; + } + + $specialPrices = $this->getSpecialPrices($product); + $specialPrice = !(empty($specialPrices)) ? min($specialPrices) : $product->getSpecialPrice(); + + $specialPrice = $this->calculateSpecialPrice( + $finalPrice, + $specialPrice, + $product->getSpecialFromDate(), + $product->getSpecialToDate(), + WebsiteInterface::ADMIN_CODE + ); + $product->setData('special_price', $specialPrice); + + return $specialPrice; + } + + /** + * Get special prices. + * + * @param mixed $product + * @return array + */ + private function getSpecialPrices($product): array + { + $allSpecialPrices = $this->specialPrice->get([$product->getSku()]); + $specialPrices = []; + foreach ($allSpecialPrices as $price) { + if ($this->isSuitableSpecialPrice($product, $price)) { + $specialPrices[] = $price['value']; + } + } + + return $specialPrices; + } + + /** + * Price is suitable from default and current store + start and end date are equal. + * + * @param mixed $product + * @param array $price + * @return bool + */ + private function isSuitableSpecialPrice($product, array $price): bool + { + $priceStoreId = $price[Store::STORE_ID]; + if (($priceStoreId == Store::DEFAULT_STORE_ID || $product->getStoreId() == $priceStoreId) + && $price[SpecialPriceInterface::PRICE_FROM] == $product->getSpecialFromDate() + && $price[SpecialPriceInterface::PRICE_TO] == $product->getSpecialToDate()) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index aa28a3478ebf7..a4ee944a9bff2 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -11,6 +11,7 @@ use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Api\Data\WebsiteInterface; /** * Product type price model @@ -184,6 +185,8 @@ public function getFinalPrice($qty, $product) } /** + * Retrieve final price for child product. + * * @param Product $product * @param float $productQty * @param Product $childProduct @@ -341,7 +344,7 @@ public function getTierPrice($qty, $product) } } - return $prices ? $prices : []; + return $prices ?: []; } /** @@ -428,6 +431,8 @@ public function setTierPrices($product, array $tierPrices = null) } /** + * Retrieve customer group id from product. + * * @param Product $product * @return int */ @@ -453,7 +458,7 @@ protected function _applySpecialPrice($product, $finalPrice) $product->getSpecialPrice(), $product->getSpecialFromDate(), $product->getSpecialToDate(), - $product->getStore() + WebsiteInterface::ADMIN_CODE ); } @@ -474,14 +479,15 @@ public function getTierPriceCount($product) * * @param float $qty * @param Product $product + * * @return array|float */ - public function getFormatedTierPrice($qty, $product) + public function getFormattedTierPrice($qty, $product) { $price = $product->getTierPrice($qty); if (is_array($price)) { foreach (array_keys($price) as $index) { - $price[$index]['formated_price'] = $this->priceCurrency->convertAndFormat( + $price[$index]['formatted_price'] = $this->priceCurrency->convertAndFormat( $price[$index]['website_price'] ); } @@ -492,15 +498,45 @@ public function getFormatedTierPrice($qty, $product) return $price; } + /** + * Get formatted by currency tier price + * + * @param float $qty + * @param Product $product + * + * @return array|float + * + * @deprecated + * @see getFormattedTierPrice() + */ + public function getFormatedTierPrice($qty, $product) + { + return $this->getFormattedTierPrice($qty, $product); + } + + /** + * Get formatted by currency product price + * + * @param Product $product + * @return array|float + */ + public function getFormattedPrice($product) + { + return $this->priceCurrency->format($product->getFinalPrice()); + } + /** * Get formatted by currency product price * * @param Product $product * @return array || float + * + * @deprecated + * @see getFormattedPrice() */ public function getFormatedPrice($product) { - return $this->priceCurrency->format($product->getFinalPrice()); + return $this->getFormattedPrice($product); } /** @@ -570,7 +606,7 @@ public function calculatePrice( $specialPrice, $specialPriceFrom, $specialPriceTo, - $sId + WebsiteInterface::ADMIN_CODE ); if ($rulePrice === false) { diff --git a/app/code/Magento/Catalog/Model/Product/TypeTransitionManager.php b/app/code/Magento/Catalog/Model/Product/TypeTransitionManager.php index 65fbdb9dc1f0c..0152293b202bc 100644 --- a/app/code/Magento/Catalog/Model/Product/TypeTransitionManager.php +++ b/app/code/Magento/Catalog/Model/Product/TypeTransitionManager.php @@ -1,18 +1,17 @@ <?php /** - * Product type transition manager - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type; +/** + * Product type transition manager + */ class TypeTransitionManager { /** diff --git a/app/code/Magento/Catalog/Model/Product/Url.php b/app/code/Magento/Catalog/Model/Product/Url.php index c291dc33fedab..a295f3fbe1a0f 100644 --- a/app/code/Magento/Catalog/Model/Product/Url.php +++ b/app/code/Magento/Catalog/Model/Product/Url.php @@ -7,6 +7,7 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Product Url model @@ -45,6 +46,11 @@ class Url extends \Magento\Framework\DataObject */ protected $urlFinder; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param \Magento\Framework\UrlFactory $urlFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -52,6 +58,7 @@ class Url extends \Magento\Framework\DataObject * @param \Magento\Framework\Session\SidResolverInterface $sidResolver * @param UrlFinderInterface $urlFinder * @param array $data + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Framework\UrlFactory $urlFactory, @@ -59,7 +66,8 @@ public function __construct( \Magento\Framework\Filter\FilterManager $filter, \Magento\Framework\Session\SidResolverInterface $sidResolver, UrlFinderInterface $urlFinder, - array $data = [] + array $data = [], + ScopeConfigInterface $scopeConfig = null ) { parent::__construct($data); $this->urlFactory = $urlFactory; @@ -67,16 +75,8 @@ public function __construct( $this->filter = $filter; $this->sidResolver = $sidResolver; $this->urlFinder = $urlFinder; - } - - /** - * Retrieve URL Instance - * - * @return \Magento\Framework\UrlInterface - */ - private function getUrlInstance() - { - return $this->urlFactory->create(); + $this->scopeConfig = $scopeConfig ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -157,10 +157,19 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) UrlRewrite::ENTITY_TYPE => \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE, UrlRewrite::STORE_ID => $storeId, ]; - if ($categoryId) { + $useCategories = $this->scopeConfig->getValue( + \Magento\Catalog\Helper\Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + if ($useCategories && $categoryId) { $filterData[UrlRewrite::METADATA]['category_id'] = $categoryId; + } elseif (!$useCategories) { + $filterData[UrlRewrite::METADATA]['category_id'] = ''; } + $rewrite = $this->urlFinder->findOneByData($filterData); + if ($rewrite) { $requestPath = $rewrite->getRequestPath(); $product->setRequestPath($requestPath); @@ -194,6 +203,8 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) $routeParams['_query'] = []; } - return $this->getUrlInstance()->setScope($storeId)->getUrl($routePath, $routeParams); + $url = $this->urlFactory->create()->setScope($storeId); + + return $url->getUrl($routePath, $routeParams); } } diff --git a/app/code/Magento/Catalog/Model/Product/Website.php b/app/code/Magento/Catalog/Model/Product/Website.php index 890e789082a70..148c2c9e9e267 100644 --- a/app/code/Magento/Catalog/Model/Product/Website.php +++ b/app/code/Magento/Catalog/Model/Product/Website.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Catalog Product Website Model * diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php index ae875453be938..f65d9b4c0bf7a 100644 --- a/app/code/Magento/Catalog/Model/ProductCategoryList.php +++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Framework\DB\Select; use Magento\Framework\DB\Sql\UnionExpression; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Provides info about product categories. @@ -29,16 +32,32 @@ class ProductCategoryList */ private $category; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var TableMaintainer + */ + private $tableMaintainer; + /** * @param ResourceModel\Product $productResource * @param ResourceModel\Category $category + * @param StoreManagerInterface $storeManager + * @param TableMaintainer|null $tableMaintainer */ public function __construct( ResourceModel\Product $productResource, - ResourceModel\Category $category + ResourceModel\Category $category, + StoreManagerInterface $storeManager = null, + TableMaintainer $tableMaintainer = null ) { $this->productResource = $productResource; $this->category = $category; + $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -50,18 +69,22 @@ public function __construct( public function getCategoryIds($productId) { if (!isset($this->categoryIdList[$productId])) { + $unionTables[] = $this->getCategorySelect($productId, $this->category->getCategoryProductTable()); + foreach ($this->storeManager->getStores() as $store) { + $unionTables[] = $this->getCategorySelect( + $productId, + $this->tableMaintainer->getMainTable($store->getId()) + ); + } $unionSelect = new UnionExpression( - [ - $this->getCategorySelect($productId, $this->category->getCategoryProductTable()), - $this->getCategorySelect( - $productId, - $this->productResource->getTable(AbstractAction::MAIN_INDEX_TABLE) - ) - ], + $unionTables, Select::SQL_UNION_ALL ); - $this->categoryIdList[$productId] = $this->productResource->getConnection()->fetchCol($unionSelect); + $this->categoryIdList[$productId] = array_map( + 'intval', + $this->productResource->getConnection()->fetchCol($unionSelect) + ); } return $this->categoryIdList[$productId]; diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php index b635d854c9e2b..d397dc515db61 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php +++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php @@ -47,22 +47,20 @@ public function getCollection(\Magento\Catalog\Model\Product $product, $type) $products = $this->providers[$type]->getLinkedProducts($product); $converter = $this->converterPool->getConverter($type); - $output = []; $sorterItems = []; foreach ($products as $item) { - $output[$item->getId()] = $converter->convert($item); + $itemId = $item->getId(); + $sorterItems[$itemId] = $converter->convert($item); + $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0; } - foreach ($output as $item) { - $itemPosition = $item['position']; - if (!isset($sorterItems[$itemPosition])) { - $sorterItems[$itemPosition] = $item; - } else { - $newPosition = $itemPosition + 1; - $sorterItems[$newPosition] = $item; - } - } - ksort($sorterItems); + usort($sorterItems, function ($itemA, $itemB) { + $posA = intval($itemA['position']); + $posB = intval($itemB['position']); + + return $posA <=> $posB; + }); + return $sorterItems; } } diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php index f8dee9216ddcf..9571ab91c3278 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer; use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\EntityManager\MetadataPool; @@ -170,7 +171,7 @@ public function getList(\Magento\Catalog\Api\Data\ProductInterface $product) foreach ($item['custom_attributes'] as $option) { $name = $option['attribute_code']; $value = $option['value']; - $setterName = 'set'.ucfirst($name); + $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); // Check if setter exists if (method_exists($productLinkExtension, $setterName)) { call_user_func([$productLinkExtension, $setterName], $value); diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 4927a19146723..fb6e8a6c8a311 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -6,10 +6,11 @@ */ namespace Magento\Catalog\Model; +use Magento\Catalog\Api\Data\ProductExtension; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageContentValidatorInterface; use Magento\Framework\Api\ImageProcessorInterface; @@ -17,14 +18,16 @@ use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\DB\Adapter\DeadlockException; use Magento\Framework\DB\Adapter\LockWaitException; +use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\StateException; +use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; /** + * Product Repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -116,11 +119,15 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $fileSystem; /** + * @deprecated + * @see \Magento\Catalog\Model\MediaGalleryProcessor * @var ImageContentInterfaceFactory */ protected $contentFactory; /** + * @deprecated + * @see \Magento\Catalog\Model\MediaGalleryProcessor * @var ImageProcessorInterface */ protected $imageProcessor; @@ -131,7 +138,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $extensionAttributesJoinProcessor; /** - * @var \Magento\Catalog\Model\Product\Gallery\Processor + * @var ProductRepository\MediaGalleryProcessor */ protected $mediaGalleryProcessor; @@ -150,6 +157,11 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa */ private $serializer; + /** + * @var ReadExtensions + */ + private $readExtensions; + /** * ProductRepository constructor. * @param ProductFactory $productFactory @@ -175,6 +187,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa * @param CollectionProcessorInterface $collectionProcessor [optional] * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param int $cacheLimit [optional] + * @param ReadExtensions|null $readExtensions * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -201,7 +214,8 @@ public function __construct( \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, - $cacheLimit = 1000 + $cacheLimit = 1000, + ReadExtensions $readExtensions = null ) { $this->productFactory = $productFactory; $this->collectionFactory = $collectionFactory; @@ -224,38 +238,34 @@ public function __construct( $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); $this->cacheLimit = (int)$cacheLimit; + $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ReadExtensions::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId]); - if (!isset($this->instances[$sku][$cacheKey]) || $forceReload) { - $product = $this->productFactory->create(); - + $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey); + if ($cachedProduct === null || $forceReload) { $productId = $this->resourceModel->getIdBySku($sku); if (!$productId) { throw new NoSuchEntityException(__('Requested product doesn\'t exist')); } - if ($editMode) { - $product->setData('_edit_mode', true); - } - if ($storeId !== null) { - $product->setData('store_id', $storeId); - } - $product->load($productId); + + $product = $this->getById($productId, $editMode, $storeId, $forceReload); + $this->cacheProduct($cacheKey, $product); + $cachedProduct = $product; } - if (!isset($this->instances[$sku])) { - $sku = trim($sku); - } - return $this->instances[$sku][$cacheKey]; + + return $cachedProduct; } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { @@ -274,6 +284,7 @@ public function getById($productId, $editMode = false, $storeId = null, $forceRe } $this->cacheProduct($cacheKey, $product); } + return $this->instancesById[$productId][$cacheKey]; } @@ -294,6 +305,7 @@ protected function getCacheKey($data) } } $serializeData = $this->serializer->serialize($serializeData); + return sha1($serializeData); } @@ -301,13 +313,13 @@ protected function getCacheKey($data) * Add product to internal cache and truncate cache if it has more than cacheLimit elements. * * @param string $cacheKey - * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param ProductInterface $product * @return void */ - private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterface $product) + private function cacheProduct($cacheKey, ProductInterface $product) { $this->instancesById[$product->getId()][$cacheKey] = $product; - $this->instances[$product->getSku()][$cacheKey] = $product; + $this->saveProductInLocalCache($product, $cacheKey); if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); @@ -321,7 +333,7 @@ private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterf * * @param array $productData * @param bool $createNew - * @return \Magento\Catalog\Api\Data\ProductInterface|Product + * @return ProductInterface|Product * @throws NoSuchEntityException */ protected function initializeProductData(array $productData, $createNew) @@ -329,102 +341,53 @@ protected function initializeProductData(array $productData, $createNew) unset($productData['media_gallery']); if ($createNew) { $product = $this->productFactory->create(); - if ($this->storeManager->hasSingleStore()) { - $product->setWebsiteIds([$this->storeManager->getStore(true)->getWebsiteId()]); + $this->assignProductToWebsites($product); + if (isset($productData['price']) && !isset($productData['product_type'])) { + $product->setTypeId(Product\Type::TYPE_SIMPLE); } } else { - unset($this->instances[$productData['sku']]); - $product = $this->get($productData['sku']); + if (!empty($productData['id'])) { + unset($this->instancesById[$productData['id']]); + $product = $this->getById($productData['id']); + } else { + $this->removeProductFromLocalCache($productData['sku']); + $product = $this->get($productData['sku']); + } } foreach ($productData as $key => $value) { $product->setData($key, $value); } - $this->assignProductToWebsites($product); return $product; } /** + * Assign product to websites. + * * @param \Magento\Catalog\Model\Product $product * @return void */ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product) { - $websiteIds = $product->getWebsiteIds(); - - if (!$this->storeManager->hasSingleStore()) { - $websiteIds = array_unique( - array_merge( - $websiteIds, - [$this->storeManager->getStore()->getWebsiteId()] - ) - ); - } - - if ($this->storeManager->getStore(true)->getCode() == \Magento\Store\Model\Store::ADMIN_CODE) { + if ($this->storeManager->getStore(true)->getCode() === \Magento\Store\Model\Store::ADMIN_CODE) { $websiteIds = array_keys($this->storeManager->getWebsites()); + } else { + $websiteIds = [$this->storeManager->getStore()->getWebsiteId()]; } $product->setWebsiteIds($websiteIds); } - /** - * @param ProductInterface $product - * @param array $newEntry - * @return $this - * @throws InputException - * @throws StateException - * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function processNewMediaGalleryEntry( - ProductInterface $product, - array $newEntry - ) { - /** @var ImageContentInterface $contentDataObject */ - $contentDataObject = $newEntry['content']; - - /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */ - $mediaConfig = $product->getMediaConfig(); - $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); - - $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); - $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); - - if (!$product->hasGalleryAttribute()) { - throw new StateException(__('Requested product does not support images.')); - } - - $imageFileUri = $this->getMediaGalleryProcessor()->addImage( - $product, - $tmpFilePath, - isset($newEntry['types']) ? $newEntry['types'] : [], - true, - isset($newEntry['disabled']) ? $newEntry['disabled'] : true - ); - // Update additional fields that are still empty after addImage call - $this->getMediaGalleryProcessor()->updateImage( - $product, - $imageFileUri, - [ - 'label' => $newEntry['label'], - 'position' => $newEntry['position'], - 'disabled' => $newEntry['disabled'], - 'media_type' => $newEntry['media_type'], - ] - ); - return $this; - } - /** * Process product links, creating new links, updating and deleting existing links * - * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param ProductInterface $product * @param \Magento\Catalog\Api\Data\ProductLinkInterface[] $newLinks * @return $this * @throws NoSuchEntityException */ - private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $product, $newLinks) + private function processLinks(ProductInterface $product, $newLinks) { if ($newLinks === null) { // If product links were not specified, don't do anything @@ -471,97 +434,21 @@ private function processLinks(\Magento\Catalog\Api\Data\ProductInterface $produc } $product->setProductLinks($newLinks); - return $this; - } - /** - * Process Media gallery data before save product. - * - * Compare Media Gallery Entries Data with existing Media Gallery - * * If Media entry has not value_id set it as new - * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag - * * Merge Existing and new media gallery - * - * @param ProductInterface $product contains only existing media gallery items - * @param array $mediaGalleryEntries array which contains all media gallery items - * @return $this - * @throws InputException - * @throws StateException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries) - { - $existingMediaGallery = $product->getMediaGallery('images'); - $newEntries = []; - $entriesById = []; - if (!empty($existingMediaGallery)) { - foreach ($mediaGalleryEntries as $entry) { - if (isset($entry['value_id'])) { - $entriesById[$entry['value_id']] = $entry; - } else { - $newEntries[] = $entry; - } - } - foreach ($existingMediaGallery as $key => &$existingEntry) { - if (isset($entriesById[$existingEntry['value_id']])) { - $updatedEntry = $entriesById[$existingEntry['value_id']]; - if ($updatedEntry['file'] === null) { - unset($updatedEntry['file']); - } - $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); - } else { - //set the removed flag - $existingEntry['removed'] = true; - } - } - $product->setData('media_gallery', ["images" => $existingMediaGallery]); - } else { - $newEntries = $mediaGalleryEntries; - } - - $this->getMediaGalleryProcessor()->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); - $images = $product->getMediaGallery('images'); - if ($images) { - foreach ($images as $image) { - if (!isset($image['removed']) && !empty($image['types'])) { - $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']); - } - } - } - - foreach ($newEntries as $newEntry) { - if (!isset($newEntry['content'])) { - throw new InputException(__('The image content is not valid.')); - } - /** @var ImageContentInterface $contentDataObject */ - $contentDataObject = $this->contentFactory->create() - ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) - ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) - ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); - $newEntry['content'] = $contentDataObject; - $this->processNewMediaGalleryEntry($product, $newEntry); - - $finalGallery = $product->getData('media_gallery'); - $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); - $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); - $entriesById[$newEntryId] = $newEntry; - $finalGallery['images'][$newEntryId] = $newEntry; - $product->setData('media_gallery', $finalGallery); - } return $this; } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveOptions = false) + public function save(ProductInterface $product, $saveOptions = false) { $tierPrices = $product->getData('tier_price'); try { - $existingProduct = $this->get($product->getSku()); + $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku()); $product->setData( $this->resourceModel->getLinkField(), @@ -570,24 +457,35 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO if (!$product->hasData(Product::STATUS)) { $product->setStatus($existingProduct->getStatus()); } + + /** @var ProductExtension $extensionAttributes */ + $extensionAttributes = $product->getExtensionAttributes(); + if (empty($extensionAttributes->__toArray())) { + $product->setExtensionAttributes($existingProduct->getExtensionAttributes()); + } } catch (NoSuchEntityException $e) { $existingProduct = null; } $productDataArray = $this->extensibleDataObjectConverter - ->toNestedArray($product, [], \Magento\Catalog\Api\Data\ProductInterface::class); + ->toNestedArray($product, [], ProductInterface::class); $productDataArray = array_replace($productDataArray, $product->getData()); $ignoreLinksFlag = $product->getData('ignore_links_flag'); $productLinks = null; if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { $productLinks = $product->getProductLinks(); } - $productDataArray['store_id'] = (int)$this->storeManager->getStore()->getId(); + if (!isset($productDataArray['store_id'])) { + $productDataArray['store_id'] = (int)$this->storeManager->getStore()->getId(); + } $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); - if (isset($productDataArray['media_gallery'])) { - $this->processMediaGallery($product, $productDataArray['media_gallery']['images']); + if (isset($productDataArray['media_gallery_entries'])) { + $this->getMediaGalleryProcessor()->processMediaGallery( + $product, + $productDataArray['media_gallery_entries'] + ); } if (!$product->getOptionsReadonly()) { @@ -601,58 +499,26 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO ); } - try { - if ($tierPrices !== null) { - $product->setData('tier_price', $tierPrices); - } - unset($this->instances[$product->getSku()]); - unset($this->instancesById[$product->getId()]); - $this->resourceModel->save($product); - } catch (ConnectionException $exception) { - throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( - __('Database connection error'), - $exception, - $exception->getCode() - ); - } catch (DeadlockException $exception) { - throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( - __('Database deadlock found when trying to get lock'), - $exception, - $exception->getCode() - ); - } catch (LockWaitException $exception) { - throw new \Magento\Framework\Exception\TemporaryState\CouldNotSaveException( - __('Database lock wait timeout exceeded'), - $exception, - $exception->getCode() - ); - } catch (\Magento\Eav\Model\Entity\Attribute\Exception $exception) { - throw \Magento\Framework\Exception\InputException::invalidFieldValue( - $exception->getAttributeCode(), - $product->getData($exception->getAttributeCode()), - $exception - ); - } catch (ValidatorException $e) { - throw new CouldNotSaveException(__($e->getMessage())); - } catch (LocalizedException $e) { - throw $e; - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\CouldNotSaveException(__('Unable to save product'), $e); + if ($tierPrices !== null) { + $product->setData('tier_price', $tierPrices); } - unset($this->instances[$product->getSku()]); + + $this->saveProduct($product); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); + return $this->get($product->getSku(), false, $product->getStoreId()); } /** - * {@inheritdoc} + * @inheritdoc */ - public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) + public function delete(ProductInterface $product) { $sku = $product->getSku(); $productId = $product->getId(); try { - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); $this->resourceModel->delete($product); } catch (ValidatorException $e) { @@ -662,22 +528,24 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) __('Unable to remove product %1', $sku) ); } - unset($this->instances[$sku]); + $this->removeProductFromLocalCache($sku); unset($this->instancesById[$productId]); + return true; } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($sku) { $product = $this->get($sku); + return $this->delete($product); } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { @@ -694,6 +562,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $collection->load(); $collection->addCategoryIds(); + $this->addExtensionAttributes($collection); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); @@ -704,7 +573,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $this->getCacheKey( [ false, - $product->hasData(\Magento\Catalog\Model\Product::STORE_ID) ? $product->getStoreId() : null + $product->getStoreId() ] ), $product @@ -714,6 +583,21 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr return $searchResult; } + /** + * Add extension attributes to loaded items. + * + * @param Collection $collection + * @return Collection + */ + private function addExtensionAttributes(Collection $collection): Collection + { + foreach ($collection->getItems() as $item) { + $this->readExtensions->execute($item); + } + + return $collection; + } + /** * Helper function that adds a FilterGroup to the collection. * @@ -729,7 +613,7 @@ protected function addFilterGroupToCollection( $fields = []; $categoryFilter = []; foreach ($filterGroup->getFilters() as $filter) { - $conditionType = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; + $conditionType = $filter->getConditionType() ?: 'eq'; if ($filter->getField() == 'category_id') { $categoryFilter[$conditionType][] = $filter->getValue(); @@ -759,14 +643,17 @@ public function cleanCache() } /** - * @return Product\Gallery\Processor + * Retrieve media gallery processor. + * + * @return ProductRepository\MediaGalleryProcessor */ private function getMediaGalleryProcessor() { if (null === $this->mediaGalleryProcessor) { $this->mediaGalleryProcessor = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\Gallery\Processor::class); + ->get(ProductRepository\MediaGalleryProcessor::class); } + return $this->mediaGalleryProcessor; } @@ -783,6 +670,107 @@ private function getCollectionProcessor() 'Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor' ); } + return $this->collectionProcessor; } + + /** + * Gets product from the local cache by SKU. + * + * @param string $sku + * @param string $cacheKey + * @return Product|null + */ + private function getProductFromLocalCache(string $sku, string $cacheKey) + { + $preparedSku = $this->prepareSku($sku); + if (!isset($this->instances[$preparedSku])) { + return null; + } + + return $this->instances[$preparedSku][$cacheKey] ?? null; + } + + /** + * Removes product in the local cache. + * + * @param string $sku + * @return void + */ + private function removeProductFromLocalCache(string $sku) + { + $preparedSku = $this->prepareSku($sku); + unset($this->instances[$preparedSku]); + } + + /** + * Saves product in the local cache. + * + * @param Product $product + * @param string $cacheKey + */ + private function saveProductInLocalCache(Product $product, string $cacheKey) + { + $preparedSku = $this->prepareSku($product->getSku()); + $this->instances[$preparedSku][$cacheKey] = $product; + } + + /** + * Converts SKU to lower case and trims. + * + * @param string $sku + * @return string + */ + private function prepareSku(string $sku): string + { + return mb_strtolower(trim($sku)); + } + + /** + * Save product resource model. + * + * @param ProductInterface|Product $product + * @throws TemporaryCouldNotSaveException + * @throws InputException + * @throws CouldNotSaveException + * @throws LocalizedException + */ + private function saveProduct($product) + { + try { + $this->removeProductFromLocalCache($product->getSku()); + unset($this->instancesById[$product->getId()]); + $this->resourceModel->save($product); + } catch (ConnectionException $exception) { + throw new TemporaryCouldNotSaveException( + __('Database connection error'), + $exception, + $exception->getCode() + ); + } catch (DeadlockException $exception) { + throw new TemporaryCouldNotSaveException( + __('Database deadlock found when trying to get lock'), + $exception, + $exception->getCode() + ); + } catch (LockWaitException $exception) { + throw new TemporaryCouldNotSaveException( + __('Database lock wait timeout exceeded'), + $exception, + $exception->getCode() + ); + } catch (AttributeException $exception) { + throw InputException::invalidFieldValue( + $exception->getAttributeCode(), + $product->getData($exception->getAttributeCode()), + $exception + ); + } catch (ValidatorException $e) { + throw new CouldNotSaveException(__($e->getMessage())); + } catch (LocalizedException $e) { + throw $e; + } catch (\Exception $e) { + throw new CouldNotSaveException(__('Unable to save product'), $e); + } + } } diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php new file mode 100644 index 0000000000000..56d1cfda8bce5 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php @@ -0,0 +1,249 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Model\ProductRepository; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\StateException; + +/** + * Process Media gallery data for ProductRepository before save product. + */ +class MediaGalleryProcessor +{ + /** + * @var Processor + */ + private $processor; + + /** + * @var ImageContentInterfaceFactory + */ + private $contentFactory; + + /** + * @var ImageProcessorInterface + */ + private $imageProcessor; + + /** + * MediaGalleryProcessor constructor. + * + * @param Processor $processor + * @param ImageContentInterfaceFactory $contentFactory + * @param ImageProcessorInterface $imageProcessor + */ + public function __construct( + Processor $processor, + ImageContentInterfaceFactory $contentFactory, + ImageProcessorInterface $imageProcessor + ) { + $this->processor = $processor; + $this->contentFactory = $contentFactory; + $this->imageProcessor = $imageProcessor; + } + + /** + * Process Media gallery data before save product. + * + * Compare Media Gallery Entries Data with existing Media Gallery + * * If Media entry has not value_id set it as new + * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag + * * Merge Existing and new media gallery + * + * @param ProductInterface $product contains only existing media gallery items. + * @param array $mediaGalleryEntries array which contains all media gallery items. + * @return void + * @throws InputException + * @throws StateException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function processMediaGallery(ProductInterface $product, array $mediaGalleryEntries) + { + $existingMediaGallery = $product->getMediaGallery('images'); + $newEntries = []; + $entriesById = []; + if (!empty($existingMediaGallery)) { + foreach ($mediaGalleryEntries as $entry) { + if (isset($entry['id'])) { + $entriesById[$entry['id']] = $entry; + } else { + $newEntries[] = $entry; + } + } + foreach ($existingMediaGallery as $key => &$existingEntry) { + if (isset($entriesById[$existingEntry['value_id']])) { + $updatedEntry = $entriesById[$existingEntry['value_id']]; + if (array_key_exists('file', $updatedEntry) && $updatedEntry['file'] === null) { + unset($updatedEntry['file']); + } + $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); + } else { + //set the removed flag. + $existingEntry['removed'] = true; + } + } + unset($existingEntry); + $product->setData('media_gallery', ["images" => $existingMediaGallery]); + } else { + $newEntries = $mediaGalleryEntries; + } + + $images = $product->getMediaGallery('images'); + + if ($images) { + $images = $this->determineImageRoles($product, $images); + } + + $this->processor->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); + if ($images) { + foreach ($images as $image) { + if (!isset($image['removed']) && !empty($image['types'])) { + $this->processor->setMediaAttribute($product, $image['types'], $image['file']); + } + } + } + $this->processEntries($product, $newEntries, $entriesById); + } + + /** + * Ascertain image roles, if they are not set against the gallery entries + * + * @param ProductInterface $product + * @param array $images + * @return array + */ + private function determineImageRoles(ProductInterface $product, array $images) + { + $imagesWithRoles = []; + foreach ($images as $image) { + if (!isset($image['types'])) { + $image['types'] = []; + if (isset($image['file'])) { + foreach (array_keys($product->getMediaAttributes()) as $attribute) { + if ($image['file'] == $product->getData($attribute)) { + $image['types'][] = $attribute; + } + } + } + } + $imagesWithRoles[] = $image; + } + return $imagesWithRoles; + } + + /** + * Convert entries into product media gallery data and set to product. + * + * @param ProductInterface $product + * @param array $newEntries + * @param array $entriesById + * @throws InputException + * @throws LocalizedException + * @throws StateException + * @return void + */ + private function processEntries(ProductInterface $product, array $newEntries, array $entriesById) + { + foreach ($newEntries as $newEntry) { + if (!isset($newEntry['content'])) { + throw new InputException(__('The image content is not valid.')); + } + /** @var ImageContentInterface $contentDataObject */ + $contentDataObject = $this->contentFactory->create() + ->setName($newEntry['content'][ImageContentInterface::NAME]) + ->setBase64EncodedData($newEntry['content'][ImageContentInterface::BASE64_ENCODED_DATA]) + ->setType($newEntry['content'][ImageContentInterface::TYPE]); + $newEntry['content'] = $contentDataObject; + $this->processNewMediaGalleryEntry($product, $newEntry); + + $finalGallery = $product->getData('media_gallery'); + $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); + if (isset($newEntry['extension_attributes'])) { + $this->processExtensionAttributes($newEntry, $newEntry['extension_attributes']); + } + $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); + $entriesById[$newEntryId] = $newEntry; + $finalGallery['images'][$newEntryId] = $newEntry; + $product->setData('media_gallery', $finalGallery); + } + } + + /** + * Save gallery entry as image. + * + * @param ProductInterface $product + * @param array $newEntry + * @return void + * @throws InputException + * @throws StateException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function processNewMediaGalleryEntry( + ProductInterface $product, + array $newEntry + ) { + /** @var ImageContentInterface $contentDataObject */ + $contentDataObject = $newEntry['content']; + + /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */ + $mediaConfig = $product->getMediaConfig(); + $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); + + $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); + $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); + + if (!$product->hasGalleryAttribute()) { + throw new StateException(__('Requested product does not support images.')); + } + + $imageFileUri = $this->processor->addImage( + $product, + $tmpFilePath, + isset($newEntry['types']) ? $newEntry['types'] : [], + true, + isset($newEntry['disabled']) ? $newEntry['disabled'] : true + ); + // Update additional fields that are still empty after addImage call. + $this->processor->updateImage( + $product, + $imageFileUri, + [ + 'label' => $newEntry['label'], + 'position' => $newEntry['position'], + 'disabled' => $newEntry['disabled'], + 'media_type' => $newEntry['media_type'], + ] + ); + } + + /** + * Convert extension attribute for product media gallery. + * + * @param array $newEntry + * @param array $extensionAttributes + * @return void + */ + private function processExtensionAttributes(array &$newEntry, array $extensionAttributes) + { + foreach ($extensionAttributes as $code => $value) { + if (is_array($value)) { + $this->processExtensionAttributes($newEntry, $value); + } else { + $newEntry[$code] = $value; + } + } + unset($newEntry['extension_attributes']); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index 1296ca62be7ef..fed27f63e1f17 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -4,11 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; /** * Catalog entity abstract model @@ -80,7 +81,7 @@ public function getDefaultStoreId() */ protected function _isApplicableAttribute($object, $attribute) { - $applyTo = $attribute->getApplyTo(); + $applyTo = $attribute->getApplyTo() ?: []; return (count($applyTo) == 0 || in_array($object->getTypeId(), $applyTo)) && $attribute->isInSet($object->getAttributeSetId()); } @@ -88,18 +89,21 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend|\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend|\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance + * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean */ protected function _isCallableAttributeInstance($instance, $method, $args) { - if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend + if ($instance instanceof AbstractBackend && ($method == 'beforeSave' || $method == 'afterSave') ) { $attributeCode = $instance->getAttribute()->getAttributeCode(); - if (isset($args[0]) && $args[0] instanceof \Magento\Framework\DataObject && $args[0]->getData($attributeCode) === false) { + if (isset($args[0]) + && $args[0] instanceof \Magento\Framework\DataObject + && $args[0]->getData($attributeCode) === false + ) { return false; } } @@ -177,7 +181,10 @@ protected function _prepareLoadSelect(array $selects) protected function _saveAttributeValue($object, $attribute, $value) { $connection = $this->getConnection(); - $storeId = (int) $this->_storeManager->getStore($object->getStoreId())->getId(); + $hasSingleStore = $this->_storeManager->hasSingleStore(); + $storeId = $hasSingleStore + ? $this->getDefaultStoreId() + : (int) $this->_storeManager->getStore($object->getStoreId())->getId(); $table = $attribute->getBackend()->getTable(); /** @@ -186,15 +193,18 @@ protected function _saveAttributeValue($object, $attribute, $value) * In this case we clear all not default values */ $entityIdField = $this->getLinkField(); - if ($this->_storeManager->hasSingleStore()) { - $storeId = $this->getDefaultStoreId(); + $conditions = [ + 'attribute_id = ?' => $attribute->getAttributeId(), + "{$entityIdField} = ?" => $object->getData($entityIdField), + 'store_id <> ?' => $storeId + ]; + if ($hasSingleStore + && !$object->isObjectNew() + && $this->isAttributePresentForNonDefaultStore($attribute, $conditions) + ) { $connection->delete( $table, - [ - 'attribute_id = ?' => $attribute->getAttributeId(), - "{$entityIdField} = ?" => $object->getData($entityIdField), - 'store_id <> ?' => $storeId - ] + $conditions ); } @@ -233,6 +243,27 @@ protected function _saveAttributeValue($object, $attribute, $value) return $this; } + /** + * Check if attribute present for non default Store View. + * Prevent "delete" query locking in a case when nothing to delete + * + * @param AbstractAttribute $attribute + * @param array $conditions + * + * @return boolean + */ + private function isAttributePresentForNonDefaultStore($attribute, $conditions) + { + $connection = $this->getConnection(); + $select = $connection->select()->from($attribute->getBackend()->getTable()); + foreach ($conditions as $condition => $conditionValue) { + $select->where($condition, $conditionValue); + } + $select->limit(1); + + return !empty($connection->fetchRow($select)); + } + /** * Insert entity attribute value * @@ -568,8 +599,7 @@ public function getAttributeRawValue($entityId, $attribute, $store) } if (is_array($attributesData) && sizeof($attributesData) == 1) { - $_data = each($attributesData); - $attributesData = $_data[1]; + $attributesData = array_shift($attributesData); } return $attributesData === false ? false : $attributesData; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index bdb3cdab617ac..8457e5d0eaa5c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -141,19 +141,17 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) ->getMetadata(ProductInterface::class) ->getLinkField(); - $select = $this->getConnection()->select()->from( - $attribute->getEntity()->getEntityTable(), - $linkField - )->where( - 'attribute_set_id = ?', - $result['attribute_set_id'] - ); + $backendLinkField = $attribute->getBackend()->getEntityIdField(); - $clearCondition = [ - 'attribute_id =?' => $attribute->getId(), - $linkField . ' IN (?)' => $select, - ]; - $this->getConnection()->delete($backendTable, $clearCondition); + $select = $this->getConnection()->select() + ->from(['b' => $backendTable]) + ->join( + ['e' => $attribute->getEntity()->getEntityTable()], + "b.$backendLinkField = e.$linkField" + )->where('b.attribute_id = ?', $attribute->getId()) + ->where('e.attribute_set_id = ?', $result['attribute_set_id']); + + $this->getConnection()->query($select->deleteFromSelect('b')); } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 6c9867359d40b..fe0e52b8201e3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Model\ResourceModel; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; /** @@ -82,6 +85,11 @@ class Category extends AbstractResource */ protected $aggregateCount; + /** + * @var Processor + */ + private $indexerProcessor; + /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -92,6 +100,7 @@ class Category extends AbstractResource * @param Category\CollectionFactory $categoryCollectionFactory * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param Processor|null $indexerProcessor */ public function __construct( \Magento\Eav\Model\Entity\Context $context, @@ -101,7 +110,8 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $categoryTreeFactory, \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + Processor $indexerProcessor = null ) { parent::__construct( $context, @@ -113,8 +123,10 @@ public function __construct( $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_eventManager = $eventManager; $this->connectionName = 'catalog'; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->indexerProcessor = $indexerProcessor ?: ObjectManager::getInstance() + ->get(Processor::class); } /** @@ -197,6 +209,19 @@ protected function _beforeDelete(\Magento\Framework\DataObject $object) $this->deleteChildren($object); } + /** + * Mark Category indexer as invalid to be picked up by cron. + * + * @param DataObject $object + * @return $this + */ + protected function _afterDelete(DataObject $object): Category + { + $this->indexerProcessor->markIndexerAsInvalid(); + + return parent::_afterDelete($object); + } + /** * Delete children categories of specific category * @@ -244,6 +269,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $object) $object->setAttributeSetId( $object->getAttributeSetId() ?: $this->getEntityType()->getDefaultAttributeSetId() ); + + $this->castPathIdsToInt($object); + if ($object->isObjectNew()) { if ($object->getPosition() === null) { $object->setPosition($this->_getMaxPosition($object->getPath()) + 1); @@ -460,8 +488,20 @@ public function getProductsPosition($category) $this->getCategoryProductTable(), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); @@ -594,10 +634,9 @@ public function getIsActiveAttributeId() */ public function findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValue) { - // @codingStandardsIgnoreStart $serializeData = $this->serializer->serialize($entityIdsFilter); + // phpcs:ignore $entityIdsFilterHash = md5($serializeData); - // @codingStandardsIgnoreEnd if (!isset($this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue])) { $linkField = $this->getLinkField(); @@ -642,7 +681,7 @@ public function getProductCount($category) $bind = ['category_id' => (int)$category->getId()]; $counts = $this->getConnection()->fetchOne($select, $bind); - return intval($counts); + return (int)$counts; } /** @@ -718,6 +757,8 @@ public function getParentDesignCategory($category) 'custom_layout_update' )->addAttributeToSelect( 'custom_apply_to_products' + )->addAttributeToSelect( + 'custom_layout_update_file' )->addFieldToFilter( 'entity_id', ['in' => $pathIds] @@ -896,7 +937,7 @@ public function changeParent( $childrenCount = $this->getChildrenCount($category->getId()) + 1; $table = $this->getEntityTable(); $connection = $this->getConnection(); - $levelFiled = $connection->quoteIdentifier('level'); + $levelField = $connection->quoteIdentifier('level'); $pathField = $connection->quoteIdentifier('path'); /** @@ -936,7 +977,7 @@ public function changeParent( $newPath . '/' ) . ')' ), - 'level' => new \Zend_Db_Expr($levelFiled . ' + ' . $levelDisposition) + 'level' => new \Zend_Db_Expr($levelField . ' + ' . $levelDisposition) ], [$pathField . ' LIKE ?' => $category->getPath() . '/%'] ); @@ -986,7 +1027,7 @@ protected function _processPositions($category, $newParent, $afterCategoryId) if ($afterCategoryId) { $select = $connection->select()->from($table, 'position')->where('entity_id = :entity_id'); $position = $connection->fetchOne($select, ['entity_id' => $afterCategoryId]); - $position += 1; + $position++; } else { $position = 1; } @@ -1088,4 +1129,23 @@ private function getAggregateCount() } return $this->aggregateCount; } + + /** + * Cast category path ids to int. + * + * @param DataObject $object + * @return void + */ + private function castPathIdsToInt(DataObject $object) + { + $pathIds = explode('/', (string)$object->getPath()); + + array_walk( + $pathIds, + function (&$pathId) { + $pathId = (int)$pathId; + } + ); + $object->setPath(implode('/', $pathIds)); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index eed1ab136cce1..e7361c6426b1d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -265,9 +265,7 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr 'main_table.category_id=e.entity_id', [] )->where( - 'e.entity_id = :entity_id' - )->orWhere( - 'e.path LIKE :c_path' + '(e.entity_id = :entity_id OR e.path LIKE :c_path)' ); if ($websiteId) { $select->join( @@ -313,7 +311,7 @@ public function joinUrlRewrite() ['request_path'], sprintf( '{{table}}.is_autogenerated = 1 AND {{table}}.store_id = %d AND {{table}}.entity_type = \'%s\'', - $this->_storeManager->getStore()->getId(), + $this->getStoreId(), CategoryUrlRewriteGenerator::ENTITY_TYPE ), 'left' diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index 01e4b072b0367..05950531e2178 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -173,7 +173,7 @@ public function getMainTable() public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) { if (is_string($storeId)) { - $storeId = intval($storeId); + $storeId = (int) $storeId; } if ($storeId) { @@ -699,8 +699,20 @@ public function getProductsPosition($category) $this->getTable('catalog_category_product'), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php new file mode 100644 index 0000000000000..b683bcd803bd3 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/RedundantCategoryImageChecker.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Category; + +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Check if Image is currently used in any category as Category Image. + */ +class RedundantCategoryImageChecker +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CategoryListInterface + */ + private $categoryList; + + public function __construct( + CategoryListInterface $categoryList, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->categoryList = $categoryList; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Checks if Image is currently used in any category as Category Image. + * + * Returns true if not. + * + * @param string $imageName + * @return bool + */ + public function execute(string $imageName): bool + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteria = $this->searchCriteriaBuilder->addFilter('image', $imageName)->create(); + $categories = $this->categoryList->getList($searchCriteria)->getItems(); + + return empty($categories); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php new file mode 100644 index 0000000000000..fc476ab6ff286 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/StateDependentCollectionFactory.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Catalog\Model\ResourceModel\Category; + +/** + * Factory class for state dependent category collection + */ +class StateDependentCollectionFactory +{ + /** + * Object Manager instance + * + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * Catalog category flat state + * + * @var \Magento\Catalog\Model\Indexer\Category\Flat\State + */ + private $catalogCategoryFlatState; + + /** + * Factory constructor + * + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param \Magento\Catalog\Model\Indexer\Category\Flat\State $catalogCategoryFlatState + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, + \Magento\Catalog\Model\Indexer\Category\Flat\State $catalogCategoryFlatState + ) { + $this->objectManager = $objectManager; + $this->catalogCategoryFlatState = $catalogCategoryFlatState; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\Framework\Data\Collection\AbstractDb + */ + public function create(array $data = []) + { + return $this->objectManager->create( + ($this->catalogCategoryFlatState->isAvailable()) ? Flat\Collection::class : Collection::class, + $data + ); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php index 972f11db7aae3..7fcd07bdb7210 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php @@ -483,6 +483,14 @@ public function loadByIds($ids, $addCollectionData = true, $updateAnchorProductC foreach ($this->_conn->fetchAll($select) as $item) { $pathIds = explode('/', $item['path']); + + array_walk( + $pathIds, + function (&$pathId) { + $pathId = (int)$pathId; + } + ); + $level = (int)$item['level']; while ($level > 0) { $pathIds[count($pathIds) - 1] = '%'; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 0d8c3992ddbb8..9ab863cde2704 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -140,7 +140,7 @@ public function getDefaultStoreId() * * @param string $table * @param array|int $attributeIds - * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection + * @return \Magento\Framework\DB\Select */ protected function _getLoadAttributesSelect($table, $attributeIds = []) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 4b3f81b551b30..863ed9f90806a 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\ResourceModel\Eav; use Magento\Catalog\Model\Attribute\LockValidatorInterface; @@ -239,11 +237,11 @@ public function afterSave() */ protected function _isEnabledInFlat() { - return $this->getData('backend_type') == 'static' + return $this->_getData('backend_type') == 'static' || $this->_productFlatIndexerHelper->isAddFilterableAttributes() - && $this->getData('is_filterable') > 0 - || $this->getData('used_in_product_listing') == 1 - || $this->getData('used_for_sort_by') == 1; + && $this->_getData('is_filterable') > 0 + || $this->_getData('used_in_product_listing') == 1 + || $this->_getData('used_for_sort_by') == 1; } /** @@ -341,7 +339,7 @@ public function getStoreId() if ($dataObject) { return $dataObject->getStoreId(); } - return $this->getData('store_id'); + return $this->_getData('store_id'); } /** @@ -366,7 +364,7 @@ public function getApplyTo() */ public function getSourceModel() { - $model = $this->getData('source_model'); + $model = $this->_getData('source_model'); if (empty($model)) { if ($this->getBackendType() == 'int' && $this->getFrontendInput() == 'select') { return $this->_getDefaultSourceModel(); @@ -462,7 +460,7 @@ protected function _isOriginalIndexable() $backendType = $this->getOrigData('backend_type'); $frontendInput = $this->getOrigData('frontend_input'); - if ($backendType == 'int' && $frontendInput == 'select') { + if ($backendType == 'int' && ($frontendInput == 'select' || $frontendInput == 'boolean')) { return true; } elseif ($backendType == 'varchar' && $frontendInput == 'multiselect') { return true; @@ -496,7 +494,7 @@ public function getIndexType() */ public function getIsWysiwygEnabled() { - return $this->getData(self::IS_WYSIWYG_ENABLED); + return $this->_getData(self::IS_WYSIWYG_ENABLED); } /** @@ -504,7 +502,7 @@ public function getIsWysiwygEnabled() */ public function getIsHtmlAllowedOnFront() { - return $this->getData(self::IS_HTML_ALLOWED_ON_FRONT); + return $this->_getData(self::IS_HTML_ALLOWED_ON_FRONT); } /** @@ -512,7 +510,7 @@ public function getIsHtmlAllowedOnFront() */ public function getUsedForSortBy() { - return $this->getData(self::USED_FOR_SORT_BY); + return $this->_getData(self::USED_FOR_SORT_BY); } /** @@ -520,7 +518,7 @@ public function getUsedForSortBy() */ public function getIsFilterable() { - return $this->getData(self::IS_FILTERABLE); + return $this->_getData(self::IS_FILTERABLE); } /** @@ -528,7 +526,7 @@ public function getIsFilterable() */ public function getIsFilterableInSearch() { - return $this->getData(self::IS_FILTERABLE_IN_SEARCH); + return $this->_getData(self::IS_FILTERABLE_IN_SEARCH); } /** @@ -536,7 +534,7 @@ public function getIsFilterableInSearch() */ public function getIsUsedInGrid() { - return (bool)$this->getData(self::IS_USED_IN_GRID); + return (bool)$this->_getData(self::IS_USED_IN_GRID); } /** @@ -544,7 +542,7 @@ public function getIsUsedInGrid() */ public function getIsVisibleInGrid() { - return (bool)$this->getData(self::IS_VISIBLE_IN_GRID); + return (bool)$this->_getData(self::IS_VISIBLE_IN_GRID); } /** @@ -552,7 +550,7 @@ public function getIsVisibleInGrid() */ public function getIsFilterableInGrid() { - return (bool)$this->getData(self::IS_FILTERABLE_IN_GRID); + return (bool)$this->_getData(self::IS_FILTERABLE_IN_GRID); } /** @@ -560,7 +558,7 @@ public function getIsFilterableInGrid() */ public function getPosition() { - return $this->getData(self::POSITION); + return $this->_getData(self::POSITION); } /** @@ -568,7 +566,7 @@ public function getPosition() */ public function getIsSearchable() { - return $this->getData(self::IS_SEARCHABLE); + return $this->_getData(self::IS_SEARCHABLE); } /** @@ -576,7 +574,7 @@ public function getIsSearchable() */ public function getIsVisibleInAdvancedSearch() { - return $this->getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH); + return $this->_getData(self::IS_VISIBLE_IN_ADVANCED_SEARCH); } /** @@ -584,7 +582,7 @@ public function getIsVisibleInAdvancedSearch() */ public function getIsComparable() { - return $this->getData(self::IS_COMPARABLE); + return $this->_getData(self::IS_COMPARABLE); } /** @@ -592,7 +590,7 @@ public function getIsComparable() */ public function getIsUsedForPromoRules() { - return $this->getData(self::IS_USED_FOR_PROMO_RULES); + return $this->_getData(self::IS_USED_FOR_PROMO_RULES); } /** @@ -600,7 +598,7 @@ public function getIsUsedForPromoRules() */ public function getIsVisibleOnFront() { - return $this->getData(self::IS_VISIBLE_ON_FRONT); + return $this->_getData(self::IS_VISIBLE_ON_FRONT); } /** @@ -608,7 +606,7 @@ public function getIsVisibleOnFront() */ public function getUsedInProductListing() { - return $this->getData(self::USED_IN_PRODUCT_LISTING); + return $this->_getData(self::USED_IN_PRODUCT_LISTING); } /** @@ -616,7 +614,7 @@ public function getUsedInProductListing() */ public function getIsVisible() { - return $this->getData(self::IS_VISIBLE); + return $this->_getData(self::IS_VISIBLE); } //@codeCoverageIgnoreEnd diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php index bed129e19168f..585da2af529a4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php @@ -5,6 +5,15 @@ */ namespace Magento\Catalog\Model\ResourceModel\Layer\Filter; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Context as CustomerContext; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; + /** * Catalog Layer Price Filter resource model * @@ -41,6 +50,21 @@ class Price extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ private $storeManager; + /** + * @var IndexScopeResolverInterface|null + */ + private $priceTableResolver; + + /** + * @var Context + */ + private $httpContext; + + /** + * @var DimensionFactory|null + */ + private $dimensionFactory; + /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -48,6 +72,9 @@ class Price extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Customer\Model\Session $session * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param null $connectionName + * @param IndexScopeResolverInterface|null $priceTableResolver + * @param Context|null $httpContext + * @param DimensionFactory|null $dimensionFactory */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -55,12 +82,19 @@ public function __construct( \Magento\Catalog\Model\Layer\Resolver $layerResolver, \Magento\Customer\Model\Session $session, \Magento\Store\Model\StoreManagerInterface $storeManager, - $connectionName = null + $connectionName = null, + IndexScopeResolverInterface $priceTableResolver = null, + Context $httpContext = null, + DimensionFactory $dimensionFactory = null ) { $this->layer = $layerResolver->get(); $this->session = $session; $this->storeManager = $storeManager; $this->_eventManager = $eventManager; + $this->priceTableResolver = $priceTableResolver + ?? ObjectManager::getInstance()->get(IndexScopeResolverInterface::class); + $this->httpContext = $httpContext ?? ObjectManager::getInstance()->get(Context::class); + $this->dimensionFactory = $dimensionFactory ?? ObjectManager::getInstance()->get(DimensionFactory::class); parent::__construct($context, $connectionName); } @@ -78,7 +112,7 @@ public function getCount($range) /** * Check and set correct variable values to prevent SQL-injections */ - $range = floatval($range); + $range = (float)$range; if ($range == 0) { $range = 1; } @@ -118,11 +152,8 @@ protected function _getSelect() // remove join with main table $fromPart = $select->getPart(\Magento\Framework\DB\Select::FROM); - if (!isset( - $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS] - ) || !isset( - $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS] - ) + if (!isset($fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS]) || + !isset($fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS]) ) { return $select; } @@ -376,6 +407,30 @@ protected function _construct() $this->_init('catalog_product_index_price', 'entity_id'); } + /** + * {@inheritdoc} + * @return string + */ + public function getMainTable() + { + $storeKey = $this->httpContext->getValue(StoreManagerInterface::CONTEXT_STORE); + $priceTableName = $this->priceTableResolver->resolve( + 'catalog_product_index_price', + [ + $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + (string)$this->storeManager->getStore($storeKey)->getWebsiteId() + ), + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP) + ) + ] + ); + + return $this->getTable($priceTableName); + } + /** * Retrieve joined price index table alias * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index a5fdc264aa19a..95f09c7ee80be 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Product entity resource model @@ -83,6 +84,11 @@ class Product extends AbstractResource */ private $productCategoryLink; + /** + * @var TableMaintainer + */ + private $tableMaintainer; + /** * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -94,6 +100,7 @@ class Product extends AbstractResource * @param \Magento\Eav\Model\Entity\TypeFactory $typeFactory * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data + * @param TableMaintainer|null $tableMaintainer * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -107,7 +114,8 @@ public function __construct( \Magento\Eav\Model\Entity\Attribute\SetFactory $setFactory, \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, - $data = [] + $data = [], + TableMaintainer $tableMaintainer = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -122,6 +130,7 @@ public function __construct( $data ); $this->connectionName = 'catalog'; + $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); } /** @@ -280,7 +289,7 @@ protected function _afterSave(\Magento\Framework\DataObject $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -366,22 +375,42 @@ public function getAvailableInCategories($object) // fetching all parent IDs, including those are higher on the tree $entityId = (int)$object->getEntityId(); if (!isset($this->availableCategoryIdsCache[$entityId])) { - $this->availableCategoryIdsCache[$entityId] = $this->getConnection()->fetchCol( - $this->getConnection()->select()->distinct()->from( - $this->getTable('catalog_category_product_index'), - ['category_id'] - )->where( - 'product_id = ? AND is_parent = 1', - $entityId - )->where( - 'visibility != ?', - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE - ) + foreach ($this->_storeManager->getStores() as $store) { + $unionTables[] = $this->getAvailableInCategoriesSelect( + $entityId, + $this->tableMaintainer->getMainTable($store->getId()) + ); + } + $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( + $unionTables, + \Magento\Framework\DB\Select::SQL_UNION_ALL ); + $this->availableCategoryIdsCache[$entityId] = array_unique($this->getConnection()->fetchCol($unionSelect)); } return $this->availableCategoryIdsCache[$entityId]; } + /** + * Returns DB select for available categories. + * + * @param int $entityId + * @param string $tableName + * @return \Magento\Framework\DB\Select + */ + private function getAvailableInCategoriesSelect($entityId, $tableName) + { + return $this->getConnection()->select()->distinct()->from( + $tableName, + ['category_id'] + )->where( + 'product_id = ? AND is_parent = 1', + $entityId + )->where( + 'visibility != ?', + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE + ); + } + /** * Get default attribute source model * @@ -402,7 +431,7 @@ public function getDefaultAttributeSourceModel() public function canBeShowInCategory($product, $categoryId) { $select = $this->getConnection()->select()->from( - $this->getTable('catalog_category_product_index'), + $this->tableMaintainer->getMainTable($product->getStoreId()), 'product_id' )->where( 'product_id = ?', @@ -546,7 +575,7 @@ public function countAll() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate($object) { @@ -586,7 +615,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @since 101.0.0 */ @@ -628,6 +657,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Retrieve entity manager object + * * @return \Magento\Framework\EntityManager\EntityManager */ private function getEntityManager() @@ -640,6 +671,8 @@ private function getEntityManager() } /** + * Retrieve ProductWebsiteLink object + * * @deprecated 101.1.0 * @return ProductWebsiteLink */ @@ -649,6 +682,8 @@ private function getProductWebsiteLink() } /** + * Retrieve CategoryLink object + * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink */ @@ -665,7 +700,7 @@ private function getProductCategoryLink() * Extends parent method to be appropriate for product. * Store id is required to correctly identify attribute value we are working with. * - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ protected function getAttributeRow($entity, $object, $attribute) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php index 6e2642d09910a..69da78543d8eb 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php @@ -83,11 +83,12 @@ public function saveCategoryLinks(ProductInterface $product, array $categoryLink $insertUpdate = $this->processCategoryLinks($categoryLinks, $oldCategoryLinks); $deleteUpdate = $this->processCategoryLinks($oldCategoryLinks, $categoryLinks); - list($delete, $insert) = $this->analyseUpdatedLinks($deleteUpdate, $insertUpdate); + list($delete, $insert, $update) = $this->analyseUpdatedLinks($deleteUpdate, $insertUpdate); return array_merge( - $this->updateCategoryLinks($product, $insert), - $this->deleteCategoryLinks($product, $delete) + $this->deleteCategoryLinks($product, $delete), + $this->updateCategoryLinks($product, $insert, true), + $this->updateCategoryLinks($product, $update) ); } @@ -113,16 +114,16 @@ private function getCategoryLinkMetadata() private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositions) { $result = ['changed' => [], 'updated' => []]; + + $oldCategoryPositions = array_values($oldCategoryPositions); + $oldCategoryList = array_column($oldCategoryPositions, 'category_id'); foreach ($newCategoryPositions as $newCategoryPosition) { - $key = array_search( - $newCategoryPosition['category_id'], - array_column($oldCategoryPositions, 'category_id') - ); + $key = array_search($newCategoryPosition['category_id'], $oldCategoryList); if ($key === false) { $result['changed'][] = $newCategoryPosition; } elseif ($oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']) { - $result['updated'][] = $newCategoryPositions[$key]; + $result['updated'][] = $newCategoryPosition; unset($oldCategoryPositions[$key]); } } @@ -133,16 +134,15 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi /** * @param ProductInterface $product * @param array $insertLinks + * @param bool $insert * @return array */ - private function updateCategoryLinks(ProductInterface $product, array $insertLinks) + private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) { if (empty($insertLinks)) { return []; } - $connection = $this->resourceConnection->getConnection(); - $data = []; foreach ($insertLinks as $categoryLink) { $data[] = [ @@ -153,11 +153,22 @@ private function updateCategoryLinks(ProductInterface $product, array $insertLin } if ($data) { - $connection->insertOnDuplicate( - $this->getCategoryLinkMetadata()->getEntityTable(), - $data, - ['position'] - ); + $connection = $this->resourceConnection->getConnection(); + if ($insert) { + $connection->insertArray( + $this->getCategoryLinkMetadata()->getEntityTable(), + array_keys($data[0]), + $data, + \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_IGNORE + ); + } else { + // for mass update category links with constraint by unique key use insert on duplicate statement + $connection->insertOnDuplicate( + $this->getCategoryLinkMetadata()->getEntityTable(), + $data, + ['position'] + ); + } } return array_column($insertLinks, 'category_id'); @@ -215,7 +226,7 @@ private function verifyCategoryLinks(array $links) } /** - * Analyse category links for update or/and delete + * Analyse category links for update or/and delete. Return array of links for delete, insert and update * * @param array $deleteUpdate * @param array $insertUpdate @@ -226,8 +237,7 @@ private function analyseUpdatedLinks($deleteUpdate, $insertUpdate) $delete = $deleteUpdate['changed'] ?: []; $insert = $insertUpdate['changed'] ?: []; $insert = array_merge_recursive($insert, $deleteUpdate['updated']); - $insert = array_merge_recursive($insert, $insertUpdate['updated']); - return [$delete, $insert]; + return [$delete, $insert, $insertUpdate['updated']]; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 58e8424663c83..7061e29ab6b82 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; @@ -14,10 +12,15 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\Store; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Framework\Indexer\DimensionFactory; /** * Product collection @@ -272,9 +275,23 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ private $backend; + /** + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * @var PriceTableResolver + */ + private $priceTableResolver; + + /** + * @var DimensionFactory + */ + private $dimensionFactory; + /** * Collection constructor - * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -297,7 +314,9 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param TableMaintainer|null $tableMaintainer + * @param PriceTableResolver|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -322,7 +341,10 @@ public function __construct( GroupManagementInterface $groupManagement, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + TableMaintainer $tableMaintainer = null, + PriceTableResolver $priceTableResolver = null, + DimensionFactory $dimensionFactory = null ) { $this->moduleManager = $moduleManager; $this->_catalogProductFlatState = $catalogProductFlatState; @@ -352,6 +374,10 @@ public function __construct( $storeManager, $connection ); + $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); + $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class); + $this->dimensionFactory = $dimensionFactory + ?: ObjectManager::getInstance()->get(DimensionFactory::class); } /** @@ -448,8 +474,7 @@ public function getFlatState() } /** - * Retrieve is flat enabled flag - * Return always false if magento run admin + * Retrieve is flat enabled. Return always false if magento run admin. * * @return bool */ @@ -469,7 +494,10 @@ public function isEnabledFlat() protected function _construct() { if ($this->isEnabledFlat()) { - $this->_init(\Magento\Catalog\Model\Product::class, \Magento\Catalog\Model\ResourceModel\Product\Flat::class); + $this->_init( + \Magento\Catalog\Model\Product::class, + \Magento\Catalog\Model\ResourceModel\Product\Flat::class + ); } else { $this->_init(\Magento\Catalog\Model\Product::class, \Magento\Catalog\Model\ResourceModel\Product::class); } @@ -477,8 +505,7 @@ protected function _construct() } /** - * Standard resource collection initialization - * Needed for child classes + * Standard resource collection initialization. Needed for child classes. * * @param string $model * @param string $entityModel @@ -517,8 +544,7 @@ protected function _prepareStaticFields() } /** - * Retrieve collection empty item - * Redeclared for specifying id field name without getting resource model inside model + * Get collection empty item. Redeclared for specifying id field name without getting resource model inside model. * * @return \Magento\Framework\DataObject */ @@ -604,8 +630,7 @@ public function _loadAttributes($printQuery = false, $logQuery = false) } /** - * Add attribute to entities in collection - * If $attribute=='*' select all attributes + * Add attribute to entities in collection. If $attribute=='*' select all attributes. * * @param array|string|integer|\Magento\Framework\App\Config\Element $attribute * @param bool|string $joinType @@ -641,8 +666,7 @@ public function addAttributeToSelect($attribute, $joinType = false) } /** - * Processing collection items after loading - * Adding url rewrites, minimal prices, final prices, tax percents + * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents. * * @return $this */ @@ -653,6 +677,7 @@ protected function _afterLoad() } $this->_prepareUrlDataObject(); + $this->prepareStoreId(); if (count($this)) { $this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]); @@ -661,6 +686,23 @@ protected function _afterLoad() return $this; } + /** + * Add Store ID to products from collection. + * + * @return $this + */ + protected function prepareStoreId() + { + if ($this->getStoreId() !== null) { + /** @var $item \Magento\Catalog\Model\Product */ + foreach ($this->_items as $item) { + $item->setStoreId($this->getStoreId()); + } + } + + return $this; + } + /** * Prepare Url Data object * @@ -727,8 +769,7 @@ public function addIdFilter($productId, $exclude = false) } /** - * Adding product website names to result collection - * Add for each product websites information + * Adding product website names to result collection. Add for each product websites information. * * @return $this */ @@ -739,7 +780,7 @@ public function addWebsiteNamesToResult() } /** - * {@inheritdoc} + * @inheritdoc */ public function load($printQuery = false, $logQuery = false) { @@ -790,14 +831,14 @@ protected function doAddWebsiteNamesToResult() foreach ($this as $product) { if (isset($productWebsites[$product->getId()])) { $product->setData('websites', $productWebsites[$product->getId()]); + $product->setData('website_ids', $productWebsites[$product->getId()]); } } return $this; } /** - * Add store availability filter. Include availability product - * for store website + * Add store availability filter. Include availability product for store website. * * @param null|string|bool|int|Store $store * @return $this @@ -903,7 +944,7 @@ private function mapConditionType($conditionType) 'eq' => 'in', 'neq' => 'nin' ]; - return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; + return $conditionsMap[$conditionType] ?? $conditionType; } /** @@ -1051,14 +1092,15 @@ public function getAllAttributeValues($attribute) $select = clone $this->getSelect(); $attribute = $this->getEntity()->getAttribute($attribute); - $aiField = $this->getConnection()->getAutoIncrementField($this->getMainTable()); + $fieldMainTable = $this->getConnection()->getAutoIncrementField($this->getMainTable()); + $fieldJoinTable = $attribute->getEntity()->getLinkField(); $select->reset() ->from( ['cpe' => $this->getMainTable()], ['entity_id'] )->join( ['cpa' => $attribute->getBackend()->getTable()], - 'cpe.' . $aiField . ' = cpa.' . $aiField, + 'cpe.' . $fieldMainTable . ' = cpa.' . $fieldJoinTable, ['store_id', 'value'] )->where('attribute_id = ?', (int)$attribute->getId()); @@ -1085,14 +1127,14 @@ public function getSelectCountSql() /** * Get SQL for get record count * - * @param null $select + * @param \Magento\Framework\DB\Select $select * @param bool $resetLeftJoins * @return \Magento\Framework\DB\Select */ protected function _getSelectCountSql($select = null, $resetLeftJoins = true) { $this->_renderFilters(); - $countSelect = is_null($select) ? $this->_getClearSelect() : $this->_buildClearSelect($select); + $countSelect = $select === null ? $this->_getClearSelect() : $this->_buildClearSelect($select); $countSelect->columns('COUNT(DISTINCT e.entity_id)'); if ($resetLeftJoins) { $countSelect->resetJoinLeft(); @@ -1192,7 +1234,7 @@ public function getProductCountSelect() )->distinct( false )->join( - ['count_table' => $this->getTable('catalog_category_product_index')], + ['count_table' => $this->tableMaintainer->getMainTable($this->getStoreId())], 'count_table.product_id = e.entity_id', [ 'count_table.category_id', @@ -1327,8 +1369,7 @@ public function joinUrlRewrite() } /** - * Add URL rewrites data to product - * If collection loadded - run processing else set flag + * Add URL rewrites data to product. If collection loadded - run processing else set flag. * * @param int|string $categoryId * @return $this @@ -1382,6 +1423,11 @@ protected function _addUrlRewrite() ['cu' => $this->getTable('catalog_url_rewrite_product_category')], 'u.url_rewrite_id=cu.url_rewrite_id' )->where('cu.category_id IN (?)', $this->_urlRewriteCategory); + } else { + $select->joinLeft( + ['cu' => $this->getTable('catalog_url_rewrite_product_category')], + 'u.url_rewrite_id=cu.url_rewrite_id' + )->where('cu.url_rewrite_id IS NULL'); } // more priority is data with category id @@ -1435,7 +1481,7 @@ public function getAllIdsCache($resetCache = false) $ids = $this->_allIdsCache; } - if (is_null($ids)) { + if ($ids === null) { $ids = $this->getAllIds(); $this->setAllIdsCache($ids); } @@ -1466,17 +1512,17 @@ public function addPriceData($customerGroupId = null, $websiteId = null) { $this->_productLimitationFilters->setUsePriceIndex(true); - if (!isset($this->_productLimitationFilters['customer_group_id']) && is_null($customerGroupId)) { + if (!isset($this->_productLimitationFilters['customer_group_id']) && $customerGroupId === null) { $customerGroupId = $this->_customerSession->getCustomerGroupId(); } - if (!isset($this->_productLimitationFilters['website_id']) && is_null($websiteId)) { + if (!isset($this->_productLimitationFilters['website_id']) && $websiteId === null) { $websiteId = $this->_storeManager->getStore($this->getStoreId())->getWebsiteId(); } - if (!is_null($customerGroupId)) { + if ($customerGroupId !== null) { $this->_productLimitationFilters['customer_group_id'] = $customerGroupId; } - if (!is_null($websiteId)) { + if ($websiteId !== null) { $this->_productLimitationFilters['website_id'] = $websiteId; } @@ -1525,33 +1571,17 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = $this->_allIdsCache = null; if (is_string($attribute) && $attribute == 'is_saleable') { - $columns = $this->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS); - foreach ($columns as $columnEntry) { - list($correlationName, $column, $alias) = $columnEntry; - if ($alias == 'is_saleable') { - if ($column instanceof \Zend_Db_Expr) { - $field = $column; - } else { - $connection = $this->getSelect()->getConnection(); - if (empty($correlationName)) { - $field = $connection->quoteColumnAs($column, $alias, true); - } else { - $field = $connection->quoteColumnAs([$correlationName, $column], $alias, true); - } - } - $this->getSelect()->where("{$field} = ?", $condition); - break; - } - } - - return $this; + $this->addIsSaleableAttributeToFilter($condition); + } elseif (is_string($attribute) && $attribute == 'tier_price') { + $this->addTierPriceAttributeToFilter($attribute, $condition); } else { return parent::addAttributeToFilter($attribute, $condition, $joinType); } } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity) @@ -1560,7 +1590,7 @@ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $ent } /** - * Add requere tax percent flag for product collection + * Add require tax percent flag for product collection * * @return $this */ @@ -1623,7 +1653,7 @@ public function addOptionsToResult() */ public function addFilterByRequiredOptions() { - $this->addAttributeToFilter('required_options', [['neq' => 1], ['null' => true]], 'left'); + $this->addAttributeToFilter('required_options', [['neq' => 1]], 'left'); return $this; } @@ -1670,7 +1700,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) return $this; } elseif ($attribute == 'is_saleable') { - $this->getSelect()->order("is_saleable " . $dir); + $this->getSelect()->order("is_salable " . $dir); return $this; } @@ -1762,7 +1792,8 @@ protected function _productLimitationJoinWebsite() } $conditions[] = $this->getConnection()->quoteInto( 'product_website.website_id IN(?)', - $filters['website_ids'] + $filters['website_ids'], + 'int' ); } elseif (isset( $filters['store_id'] @@ -1774,7 +1805,7 @@ protected function _productLimitationJoinWebsite() ) { $joinWebsite = true; $websiteId = $this->_storeManager->getStore($filters['store_id'])->getWebsiteId(); - $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId); + $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId, 'int'); } $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); @@ -1853,7 +1884,12 @@ protected function _productLimitationJoinPrice() protected function _productLimitationPrice($joinLeft = false) { $filters = $this->_productLimitationFilters; - if (!$filters->isUsingPriceIndex()) { + if (!$filters->isUsingPriceIndex() || + !isset($filters['website_id']) || + (string)$filters['website_id'] === '' || + !isset($filters['customer_group_id']) || + (string)$filters['customer_group_id'] === '' + ) { return $this; } @@ -1888,7 +1924,23 @@ protected function _productLimitationPrice($joinLeft = false) 'max_price', 'tier_price', ]; - $tableName = ['price_index' => $this->getTable('catalog_product_index_price')]; + + $tableName = [ + 'price_index' => $this->priceTableResolver->resolve( + 'catalog_product_index_price', + [ + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$filters['customer_group_id'] + ), + $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + (string)$filters['website_id'] + ) + ] + ) + ]; + if ($joinLeft) { $select->joinLeft($tableName, $joinCond, $colls); } else { @@ -1949,12 +2001,16 @@ protected function _applyProductLimitations() $conditions = [ 'cat_index.product_id=e.entity_id', - $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id']), + $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id'], 'int'), ]; if (isset($filters['visibility']) && !isset($filters['store_table'])) { - $conditions[] = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility']); + $conditions[] = $this->getConnection()->quoteInto( + 'cat_index.visibility IN(?)', + $filters['visibility'], + 'int' + ); } - $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id']); + $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id'], 'int'); if (isset($filters['category_is_anchor'])) { $conditions[] = $this->getConnection()->quoteInto('cat_index.is_parent=?', $filters['category_is_anchor']); } @@ -1966,7 +2022,7 @@ protected function _applyProductLimitations() $this->getSelect()->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); } else { $this->getSelect()->join( - ['cat_index' => $this->getTable('catalog_category_product_index')], + ['cat_index' => $this->tableMaintainer->getMainTable($this->getStoreId())], $joinCond, ['cat_index_position' => 'position'] ); @@ -2137,7 +2193,7 @@ private function getTierPriceSelect(array $productIds) $this->getLinkField() . ' IN(?)', $productIds )->order( - $this->getLinkField() + 'qty' ); return $select; } @@ -2218,6 +2274,7 @@ public function addPriceDataFieldFilter($comparisonFormat, $fields) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @since 101.0.1 + * @throws \Magento\Framework\Exception\LocalizedException */ public function addMediaGalleryData() { @@ -2229,34 +2286,36 @@ public function addMediaGalleryData() return $this; } - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = $this->getAttribute('media_gallery'); - $select = $this->getMediaGalleryResource()->createBatchBaseSelect( - $this->getStoreId(), - $attribute->getAttributeId() - ); - - $mediaGalleries = []; - $linkField = $this->getProductEntityMetadata()->getLinkField(); $items = $this->getItems(); + $linkField = $this->getProductEntityMetadata()->getLinkField(); - $select->where( - 'entity.' . $linkField . ' IN (?)', - array_map( - function ($item) use ($linkField) { - return $item->getData($linkField); - }, - $items - ) - ); + $select = $this->getMediaGalleryResource() + ->createBatchBaseSelect( + $this->getStoreId(), + $this->getAttribute('media_gallery')->getAttributeId() + )->reset( + Select::ORDER // we don't care what order is in current scenario + )->where( + 'entity.' . $linkField . ' IN (?)', + array_map( + function ($item) use ($linkField) { + return (int) $item->getOrigData($linkField); + }, + $items + ) + ); + + $mediaGalleries = []; foreach ($this->getConnection()->fetchAll($select) as $row) { $mediaGalleries[$row[$linkField]][] = $row; } foreach ($items as $item) { - $mediaEntries = isset($mediaGalleries[$item->getData($linkField)]) ? - $mediaGalleries[$item->getData($linkField)] : []; - $this->getGalleryReadHandler()->addMediaDataToProduct($item, $mediaEntries); + $this->getGalleryReadHandler() + ->addMediaDataToProduct( + $item, + $mediaGalleries[$item->getOrigData($linkField)] ?? [] + ); } $this->setFlag('media_gallery_added', true); @@ -2289,7 +2348,10 @@ private function getGalleryReadHandler() } /** + * Retrieve Media gallery resource. + * * @deprecated 101.0.1 + * * @return \Magento\Catalog\Model\ResourceModel\Product\Gallery */ private function getMediaGalleryResource() @@ -2347,7 +2409,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) */ public function getMaxPrice() { - if (is_null($this->_maxPrice)) { + if ($this->_maxPrice === null) { $this->_prepareStatisticsData(); } @@ -2361,7 +2423,7 @@ public function getMaxPrice() */ public function getMinPrice() { - if (is_null($this->_minPrice)) { + if ($this->_minPrice === null) { $this->_prepareStatisticsData(); } @@ -2375,7 +2437,7 @@ public function getMinPrice() */ public function getPriceStandardDeviation() { - if (is_null($this->_priceStandardDeviation)) { + if ($this->_priceStandardDeviation === null) { $this->_prepareStatisticsData(); } @@ -2389,10 +2451,77 @@ public function getPriceStandardDeviation() */ public function getPricesCount() { - if (is_null($this->_pricesCount)) { + if ($this->_pricesCount === null) { $this->_prepareStatisticsData(); } return $this->_pricesCount; } + + /** + * Add is_saleable attribute to filter + * + * @param array|null $condition + * @return $this + */ + private function addIsSaleableAttributeToFilter($condition) + { + $columns = $this->getSelect()->getPart(Select::COLUMNS); + foreach ($columns as $columnEntry) { + list($correlationName, $column, $alias) = $columnEntry; + if ($alias == 'is_saleable') { + if ($column instanceof \Zend_Db_Expr) { + $field = $column; + } else { + $connection = $this->getSelect()->getConnection(); + if (empty($correlationName)) { + $field = $connection->quoteColumnAs($column, $alias, true); + } else { + $field = $connection->quoteColumnAs([$correlationName, $column], $alias, true); + } + } + $this->getSelect()->where("{$field} = ?", $condition); + break; + } + } + + return $this; + } + + /** + * Add tier price attribute to filter + * + * @param string $attribute + * @param array|null $condition + * @return $this + */ + private function addTierPriceAttributeToFilter($attribute, $condition) + { + $attrCode = $attribute; + $connection = $this->getConnection(); + $attrTable = $this->_getAttributeTableAlias($attrCode); + $entity = $this->getEntity(); + $fKey = 'e.' . $this->getEntityPkName($entity); + $pKey = $attrTable . '.' . $this->getEntityPkName($entity); + $attribute = $entity->getAttribute($attrCode); + $attrFieldName = $attrTable . '.value'; + $fKey = $connection->quoteColumnAs($fKey, null); + $pKey = $connection->quoteColumnAs($pKey, null); + + $condArr = ["{$pKey} = {$fKey}"]; + $this->getSelect()->join( + [$attrTable => $this->getTable('catalog_product_entity_tier_price')], + '(' . implode(') AND (', $condArr) . ')', + [$attrCode => $attrFieldName] + ); + $this->removeAttributeToSelect($attrCode); + $this->_filterAttributes[$attrCode] = $attribute->getId(); + $this->_joinFields[$attrCode] = ['table' => '', 'field' => $attrFieldName]; + $field = $this->_getAttributeTableAlias($attrCode) . '.value'; + $conditionSql = $this->_getConditionSql($field, $condition); + $this->getSelect()->where($conditionSql, null, Select::TYPE_CONDITION); + $this->_totalRecords = null; + + return $this; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index 2868392f85280..9a7af68948a21 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Store\Model\Store; @@ -141,7 +142,7 @@ public function loadProductGalleryByAttributeId($product, $attributeId) */ protected function createBaseLoadSelect($entityId, $storeId, $attributeId) { - $select = $this->createBatchBaseSelect($storeId, $attributeId); + $select = $this->createBatchBaseSelect($storeId, $attributeId); $select = $select->where( 'entity.' . $this->metadata->getLinkField() . ' = ?', @@ -190,7 +191,7 @@ public function createBatchBaseSelect($storeId, $attributeId) 'value.' . $linkField . ' = entity.' . $linkField, ] ), - ['label', 'position', 'disabled'] + [] )->joinLeft( ['default_value' => $this->getTable(self::GALLERY_VALUE_TABLE)], implode( @@ -201,8 +202,15 @@ public function createBatchBaseSelect($storeId, $attributeId) 'default_value.' . $linkField . ' = entity.' . $linkField, ] ), - ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] - )->where( + [] + )->columns([ + 'label' => $this->getConnection()->getIfNullSql('`value`.`label`', '`default_value`.`label`'), + 'position' => $this->getConnection()->getIfNullSql('`value`.`position`', '`default_value`.`position`'), + 'disabled' => $this->getConnection()->getIfNullSql('`value`.`disabled`', '`default_value`.`disabled`'), + 'label_default' => 'default_value.label', + 'position_default' => 'default_value.position', + 'disabled_default' => 'default_value.disabled' + ])->where( $mainTableAlias . '.attribute_id = ?', $attributeId )->where( @@ -355,9 +363,9 @@ public function deleteGalleryValueInStore($valueId, $entityId, $storeId) $conditions = implode( ' AND ', [ - $this->getConnection()->quoteInto('value_id = ?', (int) $valueId), - $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int) $entityId), - $this->getConnection()->quoteInto('store_id = ?', (int) $storeId) + $this->getConnection()->quoteInto('value_id = ?', (int)$valueId), + $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int)$entityId), + $this->getConnection()->quoteInto('store_id = ?', (int)$storeId) ] ); @@ -385,7 +393,7 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu $select = $this->getConnection()->select()->from( [$this->getMainTableAlias() => $this->getMainTable()], - ['value_id', 'value'] + ['value_id', 'value', 'media_type', 'disabled'] )->joinInner( ['entity' => $this->getTable(self::GALLERY_VALUE_TO_ENTITY_TABLE)], $this->getMainTableAlias() . '.value_id = entity.value_id', @@ -402,16 +410,16 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu // Duplicate main entries of gallery foreach ($this->getConnection()->fetchAll($select) as $row) { - $data = [ - 'attribute_id' => $attributeId, - 'value' => isset($newFiles[$row['value_id']]) ? $newFiles[$row['value_id']] : $row['value'], - ]; + $data = $row; + $data['attribute_id'] = $attributeId; + $data['value'] = $newFiles[$row['value_id']] ?? $row['value']; + unset($data['value_id']); $valueIdMap[$row['value_id']] = $this->insertGallery($data); $this->bindValueToEntity($valueIdMap[$row['value_id']], $newProductId); } - if (count($valueIdMap) == 0) { + if (count($valueIdMap) === 0) { return []; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php new file mode 100644 index 0000000000000..77f67480619e0 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\App\ResourceConnection; + +/** + * Class for retrieval of all product images + */ +class Image +{ + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @var Generator + */ + private $batchQueryGenerator; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var int + */ + private $batchSize; + + /** + * @param Generator $generator + * @param ResourceConnection $resourceConnection + * @param int $batchSize + */ + public function __construct( + Generator $generator, + ResourceConnection $resourceConnection, + $batchSize = 100 + ) { + $this->batchQueryGenerator = $generator; + $this->resourceConnection = $resourceConnection; + $this->connection = $this->resourceConnection->getConnection(); + $this->batchSize = $batchSize; + } + + /** + * Returns product images + * + * @return \Generator + */ + public function getAllProductImages(): \Generator + { + $batchSelectIterator = $this->batchQueryGenerator->generate( + 'value_id', + $this->getVisibleImagesSelect(), + $this->batchSize, + \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + ); + + foreach ($batchSelectIterator as $select) { + foreach ($this->connection->fetchAll($select) as $key => $value) { + yield $key => $value; + } + } + } + + /** + * Get the number of unique pictures of products + * + * @return int + */ + public function getCountAllProductImages(): int + { + $select = $this->getVisibleImagesSelect() + ->reset('columns') + ->reset('distinct') + ->columns( + new \Zend_Db_Expr('count(distinct value)') + ); + + return (int) $this->connection->fetchOne($select); + } + + /** + * Return Select to fetch all products images + * + * @return Select + */ + private function getVisibleImagesSelect(): Select + { + return $this->connection->select()->distinct() + ->from( + ['images' => $this->resourceConnection->getTableName(Gallery::GALLERY_TABLE)], + 'value as filepath' + )->where( + 'disabled = 0' + ); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index c4e3fb1bf1e70..183942acaabf7 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -82,6 +82,7 @@ public function reindexEntities($processIds) $this->_prepareIndex($processIds); $this->_prepareRelationIndex($processIds); $this->_removeNotVisibleEntityFromIndex(); + return $this; } @@ -164,6 +165,7 @@ protected function _prepareRelationIndexSelect($parentIds = null) $connection = $this->getConnection(); $idxTable = $this->getIdxTable(); $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); + $select = $connection->select()->from( ['l' => $this->getTable('catalog_product_relation')], [] @@ -179,6 +181,16 @@ protected function _prepareRelationIndexSelect($parentIds = null) ['i' => $idxTable], 'l.child_id = i.entity_id AND cs.store_id = i.store_id', [] + )->join( + ['sw' => $this->getTable('store_website')], + "cs.website_id = sw.website_id", + [] + )->joinLeft( + ['cpw' => $this->getTable('catalog_product_website')], + "i.entity_id = cpw.product_id AND sw.website_id = cpw.website_id", + [] + )->where( + 'cpw.product_id IS NOT NULL' )->group( ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] )->columns( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index 5b68730209b40..77836c58d5070 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -84,7 +84,7 @@ protected function _getIndexableAttributes($multiSelect) if ($multiSelect == true) { $select->where('ea.backend_type = ?', 'varchar')->where('ea.frontend_input = ?', 'multiselect'); } else { - $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input = ?', 'select'); + $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input IN( ? )', ['select', 'boolean']); } return $this->getConnection()->fetchCol($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php index b4f7e43387d0e..ebe04fb63b217 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php @@ -7,9 +7,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuilderInterface { @@ -38,6 +42,16 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild */ private $baseSelectProcessor; + /** + * @var IndexScopeResolverInterface|null + */ + private $priceTableResolver; + + /** + * @var DimensionFactory|null + */ + private $dimensionFactory; + /** * LinkedProductSelectBuilderByIndexPrice constructor. * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -45,13 +59,17 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param BaseSelectProcessorInterface|null $baseSelectProcessor + * @param IndexScopeResolverInterface|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Customer\Model\Session $customerSession, \Magento\Framework\EntityManager\MetadataPool $metadataPool, - BaseSelectProcessorInterface $baseSelectProcessor = null + BaseSelectProcessorInterface $baseSelectProcessor = null, + IndexScopeResolverInterface $priceTableResolver = null, + DimensionFactory $dimensionFactory = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -59,6 +77,9 @@ public function __construct( $this->metadataPool = $metadataPool; $this->baseSelectProcessor = (null !== $baseSelectProcessor) ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); + $this->priceTableResolver = $priceTableResolver + ?? ObjectManager::getInstance()->get(IndexScopeResolverInterface::class); + $this->dimensionFactory = $dimensionFactory ?? ObjectManager::getInstance()->get(DimensionFactory::class); } /** @@ -68,6 +89,8 @@ public function build($productId) { $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); + $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $customerGroupId = $this->customerSession->getCustomerGroupId(); $priceSelect = $this->resource->getConnection()->select() ->from(['parent' => $productTable], '') @@ -80,13 +103,22 @@ public function build($productId) sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( - ['t' => $this->resource->getTableName('catalog_product_index_price')], + [ + 't' => $this->priceTableResolver->resolve('catalog_product_index_price', [ + $this->dimensionFactory->create(WebsiteDimensionProvider::DIMENSION_NAME, (string)$websiteId), + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$customerGroupId + ), + ]) + ], sprintf('t.entity_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] )->where('parent.entity_id = ?', $productId) - ->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId()) - ->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId()) + ->where('t.website_id = ?', $websiteId) + ->where('t.customer_group_id = ?', $customerGroupId) ->order('t.min_price ' . Select::SQL_ASC) + ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC) ->limit(1); $priceSelect = $this->baseSelectProcessor->process($priceSelect); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php new file mode 100644 index 0000000000000..ec967c7c7d04f --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BasePriceModifier.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +/** + * Apply price modifiers to product price indexer which are common for all product types: + * custom options, catalog rule, catalog inventory modifiers + */ +class BasePriceModifier implements PriceModifierInterface +{ + /** + * @var PriceModifierInterface[] + */ + private $priceModifiers; + + /** + * @param array $priceModifiers + */ + public function __construct(array $priceModifiers) + { + $this->priceModifiers = $priceModifiers; + } + + /** + * {@inheritdoc} + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) + { + foreach ($this->priceModifiers as $priceModifier) { + $priceModifier->modifyPrice($priceTable, $entityIds); + } + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php new file mode 100644 index 0000000000000..f7222bd41f42f --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php @@ -0,0 +1,474 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\ColumnValueExpression; + +/** + * Class for modify custom option price. + */ +class CustomOptionPriceModifier implements PriceModifierInterface +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * @var \Magento\Framework\DB\Sql\ColumnValueExpression + */ + private $columnValueExpressionFactory; + + /** + * @var \Magento\Catalog\Helper\Data + */ + private $dataHelper; + + /** + * @var string + */ + private $connectionName; + + /** + * @var bool + */ + private $isPriceGlobalFlag; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * @var \Magento\Framework\Indexer\Table\StrategyInterface + */ + private $tableStrategy; + + /** + * @param \Magento\Framework\App\ResourceConnection $resource + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $columnValueExpressionFactory + * @param \Magento\Catalog\Helper\Data $dataHelper + * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy + * @param string $connectionName + */ + public function __construct( + \Magento\Framework\App\ResourceConnection $resource, + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $columnValueExpressionFactory, + \Magento\Catalog\Helper\Data $dataHelper, + \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, + $connectionName = 'indexer' + ) { + $this->resource = $resource; + $this->metadataPool = $metadataPool; + $this->connectionName = $connectionName; + $this->columnValueExpressionFactory = $columnValueExpressionFactory; + $this->dataHelper = $dataHelper; + $this->tableStrategy = $tableStrategy; + } + + /** + * Apply custom option price to temporary index price table + * + * @param IndexTableStructure $priceTable + * @param array $entityIds + * @return void + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) + { + // no need to run all queries if current products have no custom options + if (!$this->checkIfCustomOptionsExist($priceTable)) { + return; + } + + $connection = $this->getConnection(); + $finalPriceTable = $priceTable->getTableName(); + + $coaTable = $this->getCustomOptionAggregateTable(); + $this->prepareCustomOptionAggregateTable(); + + $copTable = $this->getCustomOptionPriceTable(); + $this->prepareCustomOptionPriceTable(); + + $select = $this->getSelectForOptionsWithMultipleValues($finalPriceTable); + $query = $select->insertFromSelect($coaTable); + $connection->query($query); + + $select = $this->getSelectForOptionsWithOneValue($finalPriceTable); + $query = $select->insertFromSelect($coaTable); + $connection->query($query); + + $select = $this->getSelectAggregated($coaTable); + $query = $select->insertFromSelect($copTable); + $connection->query($query); + + // update tmp price index with prices from custom options (from previous aggregated table) + $select = $this->getSelectForUpdate($copTable); + $query = $select->crossUpdateFromSelect(['i' => $finalPriceTable]); + $connection->query($query); + + $connection->delete($coaTable); + $connection->delete($copTable); + } + + /** + * Check if custom options exist. + * + * @param IndexTableStructure $priceTable + * @return bool + * @throws \Exception + */ + private function checkIfCustomOptionsExist(IndexTableStructure $priceTable): bool + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $this->getConnection() + ->select() + ->from( + ['i' => $priceTable->getTableName()], + ['entity_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] + )->join( + ['o' => $this->getTable('catalog_product_option')], + 'o.product_id = e.' . $metadata->getLinkField(), + ['option_id'] + ); + + return !empty($this->getConnection()->fetchRow($select)); + } + + /** + * Get connection. + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ + private function getConnection() + { + if (null === $this->connection) { + $this->connection = $this->resource->getConnection($this->connectionName); + } + + return $this->connection; + } + + /** + * Prepare prices for products with custom options that has multiple values + * + * @param string $sourceTable + * @return \Magento\Framework\DB\Select + * @throws \Exception + */ + private function getSelectForOptionsWithMultipleValues(string $sourceTable): Select + { + $connection = $this->resource->getConnection($this->connectionName); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select() + ->from( + ['i' => $sourceTable], + ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] + )->join( + ['cwd' => $this->getTable('catalog_product_index_website')], + 'i.website_id = cwd.website_id', + [] + )->join( + ['o' => $this->getTable('catalog_product_option')], + 'o.product_id = e.' . $metadata->getLinkField(), + ['option_id'] + )->join( + ['ot' => $this->getTable('catalog_product_option_type_value')], + 'ot.option_id = o.option_id', + [] + )->join( + ['otpd' => $this->getTable('catalog_product_option_type_price')], + 'otpd.option_type_id = ot.option_type_id AND otpd.store_id = 0', + [] + )->group( + ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] + ); + + if ($this->isPriceGlobal()) { + $optPriceType = 'otpd.price_type'; + $optPriceValue = 'otpd.price'; + } else { + $select->joinLeft( + ['otps' => $this->getTable('catalog_product_option_type_price')], + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cwd.default_store_id', + [] + ); + + $optPriceType = $connection->getCheckSql( + 'otps.option_type_price_id > 0', + 'otps.price_type', + 'otpd.price_type' + ); + $optPriceValue = $connection->getCheckSql('otps.option_type_price_id > 0', 'otps.price', 'otpd.price'); + } + + $minPriceRound = $this->columnValueExpressionFactory + ->create([ + 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)" + ]); + $minPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound); + $minPriceMin = $this->columnValueExpressionFactory + ->create([ + 'expression' => "MIN({$minPriceExpr})" + ]); + $minPrice = $connection->getCheckSql("MIN(o.is_require) = 1", $minPriceMin, '0'); + + $tierPriceRound = $this->columnValueExpressionFactory + ->create([ + 'expression' => "ROUND(i.tier_price * ({$optPriceValue} / 100), 4)" + ]); + $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound); + $tierPriceMin = $this->columnValueExpressionFactory + ->create([ + 'expression' => "MIN({$tierPriceExpr})" + ]); + $tierPriceValue = $connection->getCheckSql("MIN(o.is_require) > 0", $tierPriceMin, 0); + $tierPrice = $connection->getCheckSql("MIN(i.tier_price) IS NOT NULL", $tierPriceValue, "NULL"); + + $maxPriceRound = $this->columnValueExpressionFactory + ->create([ + 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)" + ]); + $maxPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $maxPriceRound); + $maxPrice = $connection->getCheckSql( + "(MIN(o.type)='radio' OR MIN(o.type)='drop_down')", + "MAX({$maxPriceExpr})", + "SUM({$maxPriceExpr})" + ); + + $select->columns( + [ + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'tier_price' => $tierPrice, + ] + ); + + return $select; + } + + /** + * Prepare prices for products with custom options that has single value + * + * @param string $sourceTable + * @return \Magento\Framework\DB\Select + * @throws \Exception + */ + private function getSelectForOptionsWithOneValue(string $sourceTable): Select + { + $connection = $this->resource->getConnection($this->connectionName); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select() + ->from( + ['i' => $sourceTable], + ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] + )->join( + ['cwd' => $this->getTable('catalog_product_index_website')], + 'i.website_id = cwd.website_id', + [] + )->join( + ['o' => $this->getTable('catalog_product_option')], + 'o.product_id = e.' . $metadata->getLinkField(), + ['option_id'] + )->join( + ['opd' => $this->getTable('catalog_product_option_price')], + 'opd.option_id = o.option_id AND opd.store_id = 0', + [] + ); + + if ($this->isPriceGlobal()) { + $optPriceType = 'opd.price_type'; + $optPriceValue = 'opd.price'; + } else { + $select->joinLeft( + ['ops' => $this->getTable('catalog_product_option_price')], + 'ops.option_id = opd.option_id AND ops.store_id = cwd.default_store_id', + [] + ); + + $optPriceType = $connection->getCheckSql('ops.option_price_id > 0', 'ops.price_type', 'opd.price_type'); + $optPriceValue = $connection->getCheckSql('ops.option_price_id > 0', 'ops.price', 'opd.price'); + } + + $minPriceRound = $this->columnValueExpressionFactory + ->create([ + 'expression' => "ROUND(i.final_price * ({$optPriceValue} / 100), 4)" + ]); + $priceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound); + $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require = 1", $priceExpr, 0); + + $maxPrice = $priceExpr; + + $tierPriceRound = $this->columnValueExpressionFactory + ->create([ + 'expression' => "ROUND(i.tier_price * ({$optPriceValue} / 100), 4)" + ]); + $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound); + $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require = 1", $tierPriceExpr, 0); + $tierPrice = $connection->getCheckSql("i.tier_price IS NOT NULL", $tierPriceValue, "NULL"); + + $select->columns( + [ + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'tier_price' => $tierPrice, + ] + ); + + return $select; + } + + /** + * Aggregate prices with one and multiply options into one table + * + * @param string $sourceTable + * @return \Magento\Framework\DB\Select + */ + private function getSelectAggregated(string $sourceTable): Select + { + $connection = $this->resource->getConnection($this->connectionName); + + $select = $connection->select() + ->from( + [$sourceTable], + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'min_price' => 'SUM(min_price)', + 'max_price' => 'SUM(max_price)', + 'tier_price' => 'SUM(tier_price)', + ] + )->group( + ['entity_id', 'customer_group_id', 'website_id'] + ); + + return $select; + } + + /** + * Get select for update. + * + * @param string $sourceTable + * @return \Magento\Framework\DB\Select + */ + private function getSelectForUpdate(string $sourceTable): Select + { + $connection = $this->resource->getConnection($this->connectionName); + + $select = $connection->select()->join( + ['io' => $sourceTable], + 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . + ' AND i.website_id = io.website_id', + [] + ); + $select->columns( + [ + 'min_price' => new ColumnValueExpression('i.min_price + io.min_price'), + 'max_price' => new ColumnValueExpression('i.max_price + io.max_price'), + 'tier_price' => $connection->getCheckSql( + 'i.tier_price IS NOT NULL', + 'i.tier_price + io.tier_price', + 'NULL' + ), + ] + ); + + return $select; + } + + /** + * Get table name. + * + * @param string $tableName + * @return string + */ + private function getTable(string $tableName): string + { + return $this->resource->getTableName($tableName, $this->connectionName); + } + + /** + * Is price scope global. + * + * @return bool + */ + private function isPriceGlobal(): bool + { + if ($this->isPriceGlobalFlag === null) { + $this->isPriceGlobalFlag = $this->dataHelper->isPriceGlobal(); + } + + return $this->isPriceGlobalFlag; + } + + /** + * Retrieve table name for custom option temporary aggregation data + * + * @return string + */ + private function getCustomOptionAggregateTable(): string + { + return $this->tableStrategy->getTableName('catalog_product_index_price_opt_agr'); + } + + /** + * Retrieve table name for custom option prices data + * + * @return string + */ + private function getCustomOptionPriceTable(): string + { + return $this->tableStrategy->getTableName('catalog_product_index_price_opt'); + } + + /** + * Prepare table structure for custom option temporary aggregation data + * + * @return void + */ + private function prepareCustomOptionAggregateTable() + { + $this->getConnection()->delete($this->getCustomOptionAggregateTable()); + } + + /** + * Prepare table structure for custom option prices data + * + * @return void + */ + private function prepareCustomOptionPriceTable() + { + $this->getConnection()->delete($this->getCustomOptionPriceTable()); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 4761b8b1ce896..aab4577de3770 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -6,9 +6,11 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer; +use Magento\Framework\Indexer\DimensionalIndexerInterface; /** * Default Product Type Price Indexer Resource model + * * For correctly work need define product type id * * @api @@ -16,6 +18,8 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * @deprecated Not used anymore for price indexation. Class left for backward compatibility + * @see DimensionalIndexerInterface */ class DefaultPrice extends AbstractIndexer implements PriceInterface { @@ -52,6 +56,16 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface */ private $hasEntity = null; + /** + * @var IndexTableStructureFactory + */ + private $indexTableStructureFactory; + + /** + * @var PriceModifierInterface[] + */ + private $priceModifiers = []; + /** * DefaultPrice constructor. * @@ -61,7 +75,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Module\Manager $moduleManager * @param string|null $connectionName - * @param null|\Magento\Indexer\Model\Indexer\StateFactory $stateFactory + * @param IndexTableStructureFactory $indexTableStructureFactory + * @param PriceModifierInterface[] $priceModifiers */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -69,11 +84,25 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Framework\Module\Manager $moduleManager, - $connectionName = null + $connectionName = null, + IndexTableStructureFactory $indexTableStructureFactory = null, + array $priceModifiers = [] ) { $this->_eventManager = $eventManager; $this->moduleManager = $moduleManager; parent::__construct($context, $tableStrategy, $eavConfig, $connectionName); + + $this->indexTableStructureFactory = $indexTableStructureFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(IndexTableStructureFactory::class); + foreach ($priceModifiers as $priceModifier) { + if (!($priceModifier instanceof PriceModifierInterface)) { + throw new \InvalidArgumentException( + 'Argument \'priceModifiers\' must be of the type ' . PriceModifierInterface::class . '[]' + ); + } + + $this->priceModifiers[] = $priceModifier; + } } /** @@ -180,6 +209,8 @@ public function reindexEntity($entityIds) } /** + * Reindex prices. + * * @param null|int|array $entityIds * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice */ @@ -209,6 +240,8 @@ protected function _getDefaultFinalPriceTable() * Prepare final price temporary index table * * @return $this + * @deprecated + * @see prepareFinalPriceTable() */ protected function _prepareDefaultFinalPriceTable() { @@ -216,6 +249,32 @@ protected function _prepareDefaultFinalPriceTable() return $this; } + /** + * Create (if needed), clean and return structure of final price table + * + * @return IndexTableStructure + */ + private function prepareFinalPriceTable() + { + $tableName = $this->_getDefaultFinalPriceTable(); + $this->getConnection()->delete($tableName); + + $finalPriceTable = $this->indexTableStructureFactory->create([ + 'tableName' => $tableName, + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'orig_price', + 'finalPriceField' => 'price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', + ]); + + return $finalPriceTable; + } + /** * Retrieve website current dates table name * @@ -248,11 +307,14 @@ protected function _prepareFinalPriceData($entityIds = null) */ protected function prepareFinalPriceDataForType($entityIds, $type) { - $this->_prepareDefaultFinalPriceTable(); + $finalPriceTable = $this->prepareFinalPriceTable(); $select = $this->getSelect($entityIds, $type); - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); + $query = $select->insertFromSelect($finalPriceTable->getTableName(), [], false); $this->getConnection()->query($query); + + $this->modifyPriceIndex($finalPriceTable); + return $this; } @@ -271,6 +333,7 @@ protected function prepareFinalPriceDataForType($entityIds, $type) protected function getSelect($entityIds = null, $type = null) { $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $linkField = $metadata->getLinkField(); $connection = $this->getConnection(); $select = $connection->select()->from( ['e' => $this->getTable('catalog_product_entity')], @@ -300,9 +363,38 @@ protected function getSelect($entityIds = null, $type = null) 'pw.product_id = e.entity_id AND pw.website_id = cw.website_id', [] )->joinLeft( - ['tp' => $this->_getTierPriceIndexTable()], - 'tp.entity_id = e.entity_id AND tp.website_id = cw.website_id' . - ' AND tp.customer_group_id = cg.customer_group_id', + // we need this only for BCC in case someone expects table `tp` to be present in query + ['tp' => $this->getTable('catalog_product_index_tier_price')], + 'tp.entity_id = e.entity_id AND tp.customer_group_id = cg.customer_group_id' . + ' AND tp.website_id = pw.website_id', + [] + )->joinLeft( + // calculate tier price specified as Website = `All Websites` and Customer Group = `Specific Customer Group` + ['tier_price_1' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_1.' . $linkField . ' = e.' . $linkField . ' AND tier_price_1.all_groups = 0' . + ' AND tier_price_1.customer_group_id = cg.customer_group_id AND tier_price_1.qty = 1' . + ' AND tier_price_1.website_id = 0', + [] + )->joinLeft( + // calculate tier price specified as Website = `Specific Website` + //and Customer Group = `Specific Customer Group` + ['tier_price_2' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_2.' . $linkField . ' = e.' . $linkField . ' AND tier_price_2.all_groups = 0' . + ' AND tier_price_2.customer_group_id = cg.customer_group_id AND tier_price_2.qty = 1' . + ' AND tier_price_2.website_id = cw.website_id', + [] + )->joinLeft( + // calculate tier price specified as Website = `All Websites` and Customer Group = `ALL GROUPS` + ['tier_price_3' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_3.' . $linkField . ' = e.' . $linkField . ' AND tier_price_3.all_groups = 1' . + ' AND tier_price_3.customer_group_id = 0 AND tier_price_3.qty = 1 AND tier_price_3.website_id = 0', + [] + )->joinLeft( + // calculate tier price specified as Website = `Specific Website` and Customer Group = `ALL GROUPS` + ['tier_price_4' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_4.' . $linkField . ' = e.' . $linkField . ' AND tier_price_4.all_groups = 1' . + ' AND tier_price_4.customer_group_id = 0 AND tier_price_4.qty = 1' . + ' AND tier_price_4.website_id = cw.website_id', [] ); @@ -318,7 +410,7 @@ protected function getSelect($entityIds = null, $type = null) $this->_addAttributeToSelect( $select, 'status', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id', $statusCond, true @@ -327,7 +419,7 @@ protected function getSelect($entityIds = null, $type = null) $taxClassId = $this->_addAttributeToSelect( $select, 'tax_class_id', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id' ); } else { @@ -338,41 +430,46 @@ protected function getSelect($entityIds = null, $type = null) $price = $this->_addAttributeToSelect( $select, 'price', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id' ); $specialPrice = $this->_addAttributeToSelect( $select, 'special_price', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id' ); $specialFrom = $this->_addAttributeToSelect( $select, 'special_from_date', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id' ); $specialTo = $this->_addAttributeToSelect( $select, 'special_to_date', - 'e.' . $metadata->getLinkField(), + 'e.' . $linkField, 'cs.store_id' ); - $currentDate = $connection->getDatePartSql('cwd.website_date'); + $currentDate = 'cwd.website_date'; + $maxUnsignedBigint = '~0'; $specialFromDate = $connection->getDatePartSql($specialFrom); $specialToDate = $connection->getDatePartSql($specialTo); - - $specialFromUse = $connection->getCheckSql("{$specialFromDate} <= {$currentDate}", '1', '0'); - $specialToUse = $connection->getCheckSql("{$specialToDate} >= {$currentDate}", '1', '0'); - $specialFromHas = $connection->getCheckSql("{$specialFrom} IS NULL", '1', "{$specialFromUse}"); - $specialToHas = $connection->getCheckSql("{$specialTo} IS NULL", '1', "{$specialToUse}"); - $finalPrice = $connection->getCheckSql( - "{$specialFromHas} > 0 AND {$specialToHas} > 0" . " AND {$specialPrice} < {$price}", + $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; + $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}"; + $specialPriceExpr = $connection->getCheckSql( + "{$specialPrice} IS NOT NULL AND ({$specialFromExpr}) AND ({$specialToExpr})", $specialPrice, - $price + $maxUnsignedBigint ); + $tierPrice = $this->getTotalTierPriceExpression($price); + $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint); + $finalPrice = $connection->getLeastSql([ + $price, + $specialPriceExpr, + $tierPriceExpr, + ]); $select->columns( [ @@ -380,8 +477,8 @@ protected function getSelect($entityIds = null, $type = null) 'price' => $connection->getIfNullSql($finalPrice, 0), 'min_price' => $connection->getIfNullSql($finalPrice, 0), 'max_price' => $connection->getIfNullSql($finalPrice, 0), - 'tier_price' => new \Zend_Db_Expr('tp.min_price'), - 'base_tier' => new \Zend_Db_Expr('tp.min_price'), + 'tier_price' => $tierPrice, + 'base_tier' => $tierPrice, ] ); @@ -401,6 +498,7 @@ protected function getSelect($entityIds = null, $type = null) 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); + return $select; } @@ -446,6 +544,19 @@ protected function _prepareCustomOptionPriceTable() return $this; } + /** + * Modify data in price index table. + * + * @param IndexTableStructure $finalPriceTable + * @return void + */ + private function modifyPriceIndex(IndexTableStructure $finalPriceTable) + { + foreach ($this->priceModifiers as $priceModifier) { + $priceModifier->modifyPrice($finalPriceTable); + } + } + /** * Apply custom option minimal and maximal price to temporary final price index table * @@ -455,15 +566,21 @@ protected function _prepareCustomOptionPriceTable() protected function _applyCustomOption() { $connection = $this->getConnection(); + $finalPriceTable = $this->_getDefaultFinalPriceTable(); $coaTable = $this->_getCustomOptionAggregateTable(); $copTable = $this->_getCustomOptionPriceTable(); + $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $this->_prepareCustomOptionAggregateTable(); $this->_prepareCustomOptionPriceTable(); $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], + ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] )->join( ['cw' => $this->getTable('store_website')], 'cw.website_id = i.website_id', @@ -478,7 +595,7 @@ protected function _applyCustomOption() [] )->join( ['o' => $this->getTable('catalog_product_option')], - 'o.product_id = i.entity_id', + 'o.product_id = e.' . $metadata->getLinkField(), ['option_id'] )->join( ['ot' => $this->getTable('catalog_product_option_type_value')], @@ -490,7 +607,7 @@ protected function _applyCustomOption() [] )->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cs.store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cs.store_id', [] )->group( ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] @@ -529,8 +646,12 @@ protected function _applyCustomOption() $connection->query($query); $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], + ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] + )->join( + ['e' => $this->getTable('catalog_product_entity')], + 'e.entity_id = i.entity_id', + [] )->join( ['cw' => $this->getTable('store_website')], 'cw.website_id = i.website_id', @@ -545,7 +666,7 @@ protected function _applyCustomOption() [] )->join( ['o' => $this->getTable('catalog_product_option')], - 'o.product_id = i.entity_id', + 'o.product_id = e.' . $metadata->getLinkField(), ['option_id'] )->join( ['opd' => $this->getTable('catalog_product_option_price')], @@ -562,13 +683,13 @@ protected function _applyCustomOption() $minPriceRound = new \Zend_Db_Expr("ROUND(i.price * ({$optPriceValue} / 100), 4)"); $priceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $minPriceRound); - $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require > 1", $priceExpr, 0); + $minPrice = $connection->getCheckSql("{$priceExpr} > 0 AND o.is_require = 1", $priceExpr, 0); $maxPrice = $priceExpr; $tierPriceRound = new \Zend_Db_Expr("ROUND(i.base_tier * ({$optPriceValue} / 100), 4)"); $tierPriceExpr = $connection->getCheckSql("{$optPriceType} = 'fixed'", $optPriceValue, $tierPriceRound); - $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require > 0", $tierPriceExpr, 0); + $tierPriceValue = $connection->getCheckSql("{$tierPriceExpr} > 0 AND o.is_require = 1", $tierPriceExpr, 0); $tierPrice = $connection->getCheckSql("i.base_tier IS NOT NULL", $tierPriceValue, "NULL"); $select->columns( @@ -598,7 +719,7 @@ protected function _applyCustomOption() $query = $select->insertFromSelect($copTable); $connection->query($query); - $table = ['i' => $this->_getDefaultFinalPriceTable()]; + $table = ['i' => $finalPriceTable]; $select = $connection->select()->join( ['io' => $copTable], 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . @@ -684,6 +805,8 @@ public function getIdxTable($table = null) } /** + * Check if product exists. + * * @return bool */ protected function hasEntity() @@ -703,4 +826,66 @@ protected function hasEntity() return $this->hasEntity; } + + /** + * Get total tier price expression. + * + * @param \Zend_Db_Expr $priceExpression + * @return \Zend_Db_Expr + */ + private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) + { + $maxUnsignedBigint = '~0'; + + return $this->getConnection()->getCheckSql( + implode( + ' AND ', + [ + 'tier_price_1.value_id is NULL', + 'tier_price_2.value_id is NULL', + 'tier_price_3.value_id is NULL', + 'tier_price_4.value_id is NULL' + ] + ), + 'NULL', + $this->getConnection()->getLeastSql([ + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_2', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_3', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression), + $maxUnsignedBigint + ), + ]) + ); + } + + /** + * Get tier price expression for table. + * + * @param string $tableAlias + * @param \Zend_Db_Expr $priceExpression + * @return \Zend_Db_Expr + */ + private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression) + { + return $this->getConnection()->getCheckSql( + sprintf('%s.value = 0', $tableAlias), + sprintf( + 'ROUND(%s * (1 - ROUND(%s.percentage_value * cwd.rate, 4) / 100), 4)', + $priceExpression, + $tableAlias + ), + sprintf('ROUND(%s.value * cwd.rate, 4)', $tableAlias) + ); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php index 21a7647214c26..9a310c7365ac9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Factory.php @@ -9,6 +9,8 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; +use Magento\Framework\Indexer\DimensionalIndexerInterface; + class Factory { /** @@ -40,14 +42,17 @@ public function create($className, array $data = []) { $indexerPrice = $this->_objectManager->create($className, $data); - if (!$indexerPrice instanceof \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice) { - throw new \Magento\Framework\Exception\LocalizedException( - __( - '%1 doesn\'t extend \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice', - $className - ) - ); + if ($indexerPrice instanceof PriceInterface || $indexerPrice instanceof DimensionalIndexerInterface) { + return $indexerPrice; } - return $indexerPrice; + + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Price indexer "%1" must implement %2 or %3', + $className, + PriceInterface::class, + DimensionalIndexerInterface::class + ) + ); } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php new file mode 100644 index 0000000000000..fb3eef2bf38eb --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +/** + * Wrapper for structure of price index table. + */ +class IndexTableStructure +{ + /** + * @var string + */ + private $tableName; + + /** + * @var string + */ + private $entityField; + + /** + * @var string + */ + private $customerGroupField; + + /** + * @var string + */ + private $websiteField; + + /** + * @var string + */ + private $taxClassField; + + /** + * @var string + */ + private $originalPriceField; + + /** + * @var string + */ + private $finalPriceField; + + /** + * @var string + */ + private $minPriceField; + + /** + * @var string + */ + private $maxPriceField; + + /** + * @var string + */ + private $tierPriceField; + + /** + * @param string $tableName + * @param string $entityField + * @param string $customerGroupField + * @param string $websiteField + * @param string $taxClassField + * @param string $originalPriceField + * @param string $finalPriceField + * @param string $minPriceField + * @param string $maxPriceField + * @param string $tierPriceField + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $tableName, + string $entityField, + string $customerGroupField, + string $websiteField, + string $taxClassField, + string $originalPriceField, + string $finalPriceField, + string $minPriceField, + string $maxPriceField, + string $tierPriceField + ) { + $this->tableName = $tableName; + $this->entityField = $entityField; + $this->customerGroupField = $customerGroupField; + $this->websiteField = $websiteField; + $this->taxClassField = $taxClassField; + $this->originalPriceField = $originalPriceField; + $this->finalPriceField = $finalPriceField; + $this->minPriceField = $minPriceField; + $this->maxPriceField = $maxPriceField; + $this->tierPriceField = $tierPriceField; + } + + /** + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * @return string + */ + public function getEntityField(): string + { + return $this->entityField; + } + + /** + * @return string + */ + public function getCustomerGroupField(): string + { + return $this->customerGroupField; + } + + /** + * @return string + */ + public function getWebsiteField(): string + { + return $this->websiteField; + } + + /** + * @return string + */ + public function getTaxClassField(): string + { + return $this->taxClassField; + } + + /** + * @return string + */ + public function getOriginalPriceField(): string + { + return $this->originalPriceField; + } + + /** + * @return string + */ + public function getFinalPriceField(): string + { + return $this->finalPriceField; + } + + /** + * @return string + */ + public function getMinPriceField(): string + { + return $this->minPriceField; + } + + /** + * @return string + */ + public function getMaxPriceField(): string + { + return $this->maxPriceField; + } + + /** + * @return string + */ + public function getTierPriceField(): string + { + return $this->tierPriceField; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php new file mode 100644 index 0000000000000..7aa9ec0af3856 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +/** + * Interface for modifying price data in price index table. + */ +interface PriceModifierInterface +{ + /** + * Modify price data. + * + * @param IndexTableStructure $priceTable + * @param array $entityIds + * @return void + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []); +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php new file mode 100644 index 0000000000000..b5aa1fc9e7904 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -0,0 +1,327 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query; + +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\ColumnValueExpression; +use Magento\Framework\Indexer\Dimension; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; + +/** + * Prepare base select for Product Price index limited by specified dimensions: website and customer group + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BaseFinalPrice +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var string + */ + private $connectionName; + + /** + * @var JoinAttributeProcessor + */ + private $joinAttributeProcessor; + + /** + * @var \Magento\Framework\Module\Manager + */ + private $moduleManager; + + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + + /** + * Mapping between dimensions and field in database + * + * @var array + */ + private $dimensionToFieldMapper = [ + WebsiteDimensionProvider::DIMENSION_NAME => 'pw.website_id', + CustomerGroupDimensionProvider::DIMENSION_NAME => 'cg.customer_group_id', + ]; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * BaseFinalPrice constructor. + * @param \Magento\Framework\App\ResourceConnection $resource + * @param JoinAttributeProcessor $joinAttributeProcessor + * @param \Magento\Framework\Module\Manager $moduleManager + * @param string $connectionName + */ + public function __construct( + \Magento\Framework\App\ResourceConnection $resource, + JoinAttributeProcessor $joinAttributeProcessor, + \Magento\Framework\Module\Manager $moduleManager, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + $connectionName = 'indexer' + ) { + $this->resource = $resource; + $this->connectionName = $connectionName; + $this->joinAttributeProcessor = $joinAttributeProcessor; + $this->moduleManager = $moduleManager; + $this->eventManager = $eventManager; + $this->metadataPool = $metadataPool; + } + + /** + * @param Dimension[] $dimensions + * @param string $productType + * @param array $entityIds + * @return Select + * @throws \LogicException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getQuery(array $dimensions, string $productType, array $entityIds = []): Select + { + $connection = $this->getConnection(); + $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $linkField = $metadata->getLinkField(); + + $select = $connection->select()->from( + ['e' => $this->getTable('catalog_product_entity')], + ['entity_id'] + )->joinInner( + ['cg' => $this->getTable('customer_group')], + array_key_exists(CustomerGroupDimensionProvider::DIMENSION_NAME, $dimensions) + ? sprintf( + '%s = %s', + $this->dimensionToFieldMapper[CustomerGroupDimensionProvider::DIMENSION_NAME], + $dimensions[CustomerGroupDimensionProvider::DIMENSION_NAME]->getValue() + ) : '', + ['customer_group_id'] + )->joinInner( + ['pw' => $this->getTable('catalog_product_website')], + 'pw.product_id = e.entity_id', + ['pw.website_id'] + )->joinInner( + ['cwd' => $this->getTable('catalog_product_index_website')], + 'pw.website_id = cwd.website_id', + [] + )->joinLeft( + // we need this only for BCC in case someone expects table `tp` to be present in query + ['tp' => $this->getTable('catalog_product_index_tier_price')], + 'tp.entity_id = e.entity_id AND' . + ' tp.customer_group_id = cg.customer_group_id AND tp.website_id = pw.website_id', + [] + )->joinLeft( + // calculate tier price specified as Website = `All Websites` and Customer Group = `Specific Customer Group` + ['tier_price_1' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_1.' . $linkField . ' = e.' . $linkField . ' AND tier_price_1.all_groups = 0' . + ' AND tier_price_1.customer_group_id = cg.customer_group_id AND tier_price_1.qty = 1' . + ' AND tier_price_1.website_id = 0', + [] + )->joinLeft( + // calculate tier price specified as Website = `Specific Website` + //and Customer Group = `Specific Customer Group` + ['tier_price_2' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_2.' . $linkField . ' = e.' . $linkField . ' AND tier_price_2.all_groups = 0 ' . + 'AND tier_price_2.customer_group_id = cg.customer_group_id AND tier_price_2.qty = 1' . + ' AND tier_price_2.website_id = pw.website_id', + [] + )->joinLeft( + // calculate tier price specified as Website = `All Websites` and Customer Group = `ALL GROUPS` + ['tier_price_3' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_3.' . $linkField . ' = e.' . $linkField . ' AND tier_price_3.all_groups = 1 ' . + 'AND tier_price_3.customer_group_id = 0 AND tier_price_3.qty = 1 AND tier_price_3.website_id = 0', + [] + )->joinLeft( + // calculate tier price specified as Website = `Specific Website` and Customer Group = `ALL GROUPS` + ['tier_price_4' => $this->getTable('catalog_product_entity_tier_price')], + 'tier_price_4.' . $linkField . ' = e.' . $linkField . ' AND tier_price_4.all_groups = 1' . + ' AND tier_price_4.customer_group_id = 0 AND tier_price_4.qty = 1' . + ' AND tier_price_4.website_id = pw.website_id', + [] + ); + + foreach ($dimensions as $dimension) { + if (!isset($this->dimensionToFieldMapper[$dimension->getName()])) { + throw new \LogicException( + 'Provided dimension is not valid for Price indexer: ' . $dimension->getName() + ); + } + $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); + } + + if ($this->moduleManager->isEnabled('Magento_Tax')) { + $taxClassId = $this->joinAttributeProcessor->process($select, 'tax_class_id'); + } else { + $taxClassId = new \Zend_Db_Expr(0); + } + $select->columns(['tax_class_id' => $taxClassId]); + + $this->joinAttributeProcessor->process($select, 'status', Status::STATUS_ENABLED); + + $price = $this->joinAttributeProcessor->process($select, 'price'); + $specialPrice = $this->joinAttributeProcessor->process($select, 'special_price'); + $specialFrom = $this->joinAttributeProcessor->process($select, 'special_from_date'); + $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date'); + $currentDate = 'cwd.website_date'; + + $maxUnsignedBigint = '~0'; + $specialFromDate = $connection->getDatePartSql($specialFrom); + $specialToDate = $connection->getDatePartSql($specialTo); + $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; + $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}"; + $specialPriceExpr = $connection->getCheckSql( + "{$specialPrice} IS NOT NULL AND ({$specialFromExpr}) AND ({$specialToExpr})", + $specialPrice, + $maxUnsignedBigint + ); + $tierPrice = $this->getTotalTierPriceExpression($price); + $tierPriceExpr = $connection->getIfNullSql($tierPrice, $maxUnsignedBigint); + $finalPrice = $connection->getLeastSql([ + $price, + $specialPriceExpr, + $tierPriceExpr, + ]); + + $select->columns( + [ + //orig_price in catalog_product_index_price_final_tmp + 'price' => $connection->getIfNullSql($price, 0), + //price in catalog_product_index_price_final_tmp + 'final_price' => $connection->getIfNullSql($finalPrice, 0), + 'min_price' => $connection->getIfNullSql($finalPrice, 0), + 'max_price' => $connection->getIfNullSql($finalPrice, 0), + 'tier_price' => $tierPrice, + ] + ); + + $select->where("e.type_id = ?", $productType); + + if ($entityIds !== null) { + $select->where(sprintf('e.entity_id BETWEEN %s AND %s', min($entityIds), max($entityIds))); + $select->where('e.entity_id IN(?)', $entityIds); + } + + /** + * throw event for backward compatibility + */ + $this->eventManager->dispatch( + 'prepare_catalog_product_index_select', + [ + 'select' => $select, + 'entity_field' => new ColumnValueExpression('e.entity_id'), + 'website_field' => new ColumnValueExpression('pw.website_id'), + 'store_field' => new ColumnValueExpression('cwd.default_store_id'), + ] + ); + + return $select; + } + + /** + * Get total tier price expression + * + * @param \Zend_Db_Expr $priceExpression + * @return \Zend_Db_Expr + */ + private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) + { + $maxUnsignedBigint = '~0'; + + return $this->getConnection()->getCheckSql( + implode( + ' AND ', + [ + 'tier_price_1.value_id is NULL', + 'tier_price_2.value_id is NULL', + 'tier_price_3.value_id is NULL', + 'tier_price_4.value_id is NULL' + ] + ), + 'NULL', + $this->getConnection()->getLeastSql([ + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_1', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_2', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_3', $priceExpression), + $maxUnsignedBigint + ), + $this->getConnection()->getIfNullSql( + $this->getTierPriceExpressionForTable('tier_price_4', $priceExpression), + $maxUnsignedBigint + ), + ]) + ); + } + + /** + * Get tier price expression for table + * + * @param $tableAlias + * @param \Zend_Db_Expr $priceExpression + * @return \Zend_Db_Expr + */ + private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression): \Zend_Db_Expr + { + return $this->getConnection()->getCheckSql( + sprintf('%s.value = 0', $tableAlias), + sprintf( + 'ROUND(%s * (1 - ROUND(%s.percentage_value * cwd.rate, 4) / 100), 4)', + $priceExpression, + $tableAlias + ), + sprintf('ROUND(%s.value * cwd.rate, 4)', $tableAlias) + ); + } + + /** + * Get connection + * + * return \Magento\Framework\DB\Adapter\AdapterInterface + * @throws \DomainException + */ + private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface + { + if ($this->connection === null) { + $this->connection = $this->resource->getConnection($this->connectionName); + } + + return $this->connection; + } + + /** + * Get table + * + * @param string $tableName + * @return string + */ + private function getTable($tableName) + { + return $this->resource->getTableName($tableName, $this->connectionName); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php new file mode 100644 index 0000000000000..888e68a817081 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/JoinAttributeProcessor.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\Expression; + +/** + * Allows to join product attribute to Select. Used for build price index for specified dimension + */ +class JoinAttributeProcessor +{ + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private $resource; + + /** + * @var string + */ + private $connectionName; + + /** + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Framework\App\ResourceConnection $resource + * @param string $connectionName + */ + public function __construct( + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + \Magento\Framework\App\ResourceConnection $resource, + $connectionName = 'indexer' + ) { + $this->eavConfig = $eavConfig; + $this->metadataPool = $metadataPool; + $this->resource = $resource; + $this->connectionName = $connectionName; + } + + /** + * @param Select $select + * @param string $attributeCode + * @param string|null $attributeValue + * @return \Zend_Db_Expr + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Select_Exception + */ + public function process(Select $select, $attributeCode, $attributeValue = null): \Zend_Db_Expr + { + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + $attributeId = $attribute->getAttributeId(); + $attributeTable = $attribute->getBackend()->getTable(); + $connection = $this->resource->getConnection($this->connectionName); + $joinType = $attributeValue !== null ? 'join' : 'joinLeft'; + $productIdField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + if ($attribute->isScopeGlobal()) { + $alias = 'ta_' . $attributeCode; + $select->{$joinType}( + [$alias => $attributeTable], + "{$alias}.{$productIdField} = e.{$productIdField} AND {$alias}.attribute_id = {$attributeId}" . + " AND {$alias}.store_id = 0", + [] + ); + $whereExpression = new Expression("{$alias}.value"); + } else { + $dAlias = 'tad_' . $attributeCode; + $sAlias = 'tas_' . $attributeCode; + + $select->{$joinType}( + [$dAlias => $attributeTable], + "{$dAlias}.{$productIdField} = e.{$productIdField} AND {$dAlias}.attribute_id = {$attributeId}" . + " AND {$dAlias}.store_id = 0", + [] + ); + $select->joinLeft( + [$sAlias => $attributeTable], + "{$sAlias}.{$productIdField} = e.{$productIdField} AND {$sAlias}.attribute_id = {$attributeId}" . + " AND {$sAlias}.store_id = cwd.default_store_id", + [] + ); + $whereExpression = $connection->getCheckSql( + $connection->getIfNullSql("{$sAlias}.value_id", -1) . ' > 0', + "{$sAlias}.value", + "{$dAlias}.value" + ); + } + + if ($attributeValue !== null) { + $select->where("{$whereExpression} = ?", $attributeValue); + } + + return $whereExpression; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php new file mode 100644 index 0000000000000..5a055e5ed9603 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\BaseFinalPrice; +use Magento\Framework\Indexer\DimensionalIndexerInterface; + +/** + * Simple Product Type Price Indexer + */ +class SimpleProductPrice implements DimensionalIndexerInterface +{ + /** + * @var BaseFinalPrice + */ + private $baseFinalPrice; + + /** + * @var IndexTableStructureFactory + */ + private $indexTableStructureFactory; + + /** + * @var TableMaintainer + */ + private $tableMaintainer; + + /** + * @var string + */ + private $productType; + + /** + * @var BasePriceModifier + */ + private $basePriceModifier; + + /** + * @param BaseFinalPrice $baseFinalPrice + * @param IndexTableStructureFactory $indexTableStructureFactory + * @param TableMaintainer $tableMaintainer + * @param BasePriceModifier $basePriceModifier + * @param string $productType + */ + public function __construct( + BaseFinalPrice $baseFinalPrice, + IndexTableStructureFactory $indexTableStructureFactory, + TableMaintainer $tableMaintainer, + BasePriceModifier $basePriceModifier, + $productType = \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE + ) { + $this->baseFinalPrice = $baseFinalPrice; + $this->indexTableStructureFactory = $indexTableStructureFactory; + $this->tableMaintainer = $tableMaintainer; + $this->productType = $productType; + $this->basePriceModifier = $basePriceModifier; + } + + /** + * {@inheritdoc} + */ + public function executeByDimensions(array $dimensions, \Traversable $entityIds) + { + $this->tableMaintainer->createMainTmpTable($dimensions); + + $temporaryPriceTable = $this->indexTableStructureFactory->create([ + 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'price', + 'finalPriceField' => 'final_price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', + ]); + $select = $this->baseFinalPrice->getQuery($dimensions, $this->productType, iterator_to_array($entityIds)); + $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); + $this->tableMaintainer->getConnection()->query($query); + + $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php new file mode 100644 index 0000000000000..a866c1eaa413f --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php @@ -0,0 +1,220 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price; + +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice as TierPriceResourceModel; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\DB\Select; + +/** + * Class for filling tier price index table. + */ +class TierPrice extends AbstractDb +{ + /** + * @var TierPriceResourceModel + */ + private $tierPriceResourceModel; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @param Context $context + * @param TierPriceResourceModel $tierPriceResourceModel + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $attributeRepository + * @param string|null $connectionName + */ + public function __construct( + Context $context, + TierPriceResourceModel $tierPriceResourceModel, + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $attributeRepository, + string $connectionName = null + ) { + parent::__construct($context, $connectionName); + + $this->tierPriceResourceModel = $tierPriceResourceModel; + $this->metadataPool = $metadataPool; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init('catalog_product_index_tier_price', 'entity_id'); + } + + /** + * Reindex tier price for entities. + * + * @param array $entityIds + * @return void + */ + public function reindexEntity(array $entityIds = []) + { + $this->getConnection()->delete($this->getMainTable(), ['entity_id IN (?)' => $entityIds]); + + //separate by variations for increase performance + $tierPriceVariations = [ + [true, true], //all websites; all customer groups + [true, false], //all websites; specific customer group + [false, true], //specific website; all customer groups + [false, false], //specific website; specific customer group + ]; + foreach ($tierPriceVariations as $variation) { + list ($isAllWebsites, $isAllCustomerGroups) = $variation; + $select = $this->getTierPriceSelect($isAllWebsites, $isAllCustomerGroups, $entityIds); + $query = $select->insertFromSelect($this->getMainTable()); + $this->getConnection()->query($query); + } + } + + /** + * Join websites table. + * If $isAllWebsites is true, for each website will be used default value for all websites, + * otherwise per each website will be used their own values. + * + * @param Select $select + * @param bool $isAllWebsites + */ + private function joinWebsites(Select $select, bool $isAllWebsites) + { + $websiteTable = ['website' => $this->getTable('store_website')]; + if ($isAllWebsites) { + $select->joinCross($websiteTable, []) + ->where('website.website_id > ?', 0) + ->where('tier_price.website_id = ?', 0); + } else { + $select->join($websiteTable, 'website.website_id = tier_price.website_id', []) + ->where('tier_price.website_id > 0'); + } + } + + /** + * Join customer groups table. + * If $isAllCustomerGroups is true, for each customer group will be used default value for all customer groups, + * otherwise per each customer group will be used their own values. + * + * @param Select $select + * @param bool $isAllCustomerGroups + */ + private function joinCustomerGroups(Select $select, bool $isAllCustomerGroups) + { + $customerGroupTable = ['customer_group' => $this->getTable('customer_group')]; + if ($isAllCustomerGroups) { + $select->joinCross($customerGroupTable, []) + ->where('tier_price.all_groups = ?', 1) + ->where('tier_price.customer_group_id = ?', 0); + } else { + $select->join($customerGroupTable, 'customer_group.customer_group_id = tier_price.customer_group_id', []) + ->where('tier_price.all_groups = ?', 0); + } + } + + /** + * Join price table and return price value. + * + * @param Select $select + * @param string $linkField + * @return string + */ + private function joinPrice(Select $select, string $linkField): string + { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */ + $priceAttribute = $this->attributeRepository->get('price'); + $select->joinLeft( + ['entity_price_default' => $priceAttribute->getBackend()->getTable()], + 'entity_price_default.' . $linkField . ' = entity.' . $linkField + . ' AND entity_price_default.attribute_id = ' . $priceAttribute->getAttributeId() + . ' AND entity_price_default.store_id = 0', + [] + ); + $priceValue = 'entity_price_default.value'; + + if (!$priceAttribute->isScopeGlobal()) { + $select->joinLeft( + ['store_group' => $this->getTable('store_group')], + 'store_group.group_id = website.default_group_id', + [] + )->joinLeft( + ['entity_price_store' => $priceAttribute->getBackend()->getTable()], + 'entity_price_store.' . $linkField . ' = entity.' . $linkField + . ' AND entity_price_store.attribute_id = ' . $priceAttribute->getAttributeId() + . ' AND entity_price_store.store_id = store_group.default_store_id', + [] + ); + $priceValue = $this->getConnection() + ->getIfNullSql('entity_price_store.value', 'entity_price_default.value'); + } + + return (string) $priceValue; + } + + /** + * Build select for getting tier price data. + * + * @param bool $isAllWebsites + * @param bool $isAllCustomerGroups + * @param array $entityIds + * @return Select + */ + private function getTierPriceSelect(bool $isAllWebsites, bool $isAllCustomerGroups, array $entityIds = []): Select + { + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $entityMetadata->getLinkField(); + + $select = $this->getConnection()->select(); + $select->from(['tier_price' => $this->tierPriceResourceModel->getMainTable()], []) + ->where('tier_price.qty = ?', 1); + + $select->join( + ['entity' => $this->getTable('catalog_product_entity')], + "entity.{$linkField} = tier_price.{$linkField}", + [] + ); + if (!empty($entityIds)) { + $select->where('entity.entity_id IN (?)', $entityIds); + } + $this->joinWebsites($select, $isAllWebsites); + $this->joinCustomerGroups($select, $isAllCustomerGroups); + + $priceValue = $this->joinPrice($select, $linkField); + $tierPriceValue = 'tier_price.value'; + $tierPricePercentageValue = 'tier_price.percentage_value'; + $tierPriceValueExpr = $this->getConnection()->getCheckSql( + $tierPriceValue, + $tierPriceValue, + sprintf('(1 - %s / 100) * %s', $tierPricePercentageValue, $priceValue) + ); + $select->columns( + [ + 'entity.entity_id', + 'customer_group.customer_group_id', + 'website.website_id', + 'tier_price' => $tierPriceValueExpr, + ] + ); + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php index 4b420329eef99..e6eb4804f56b2 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\DB\Adapter\AdapterInterface; + /** * Catalog product link resource model * @@ -110,7 +112,7 @@ public function hasProductLinks($parentId) ['count' => new \Zend_Db_Expr('COUNT(*)')] )->where( 'product_id = :product_id' - ) ; + ); return $connection->fetchOne( $select, @@ -136,7 +138,6 @@ public function saveProductLinks($parentId, $data, $typeId) $data = []; } - $attributes = $this->getAttributesByType($typeId); $connection = $this->getConnection(); $bind = [':product_id' => (int)$parentId, ':link_type_id' => (int)$typeId]; @@ -151,42 +152,38 @@ public function saveProductLinks($parentId, $data, $typeId) $links = $connection->fetchPairs($select, $bind); - foreach ($data as $linkedProductId => $linkInfo) { - $linkId = null; - if (isset($links[$linkedProductId])) { - $linkId = $links[$linkedProductId]; - unset($links[$linkedProductId]); - } else { - $bind = [ - 'product_id' => $parentId, - 'linked_product_id' => $linkedProductId, - 'link_type_id' => $typeId, - ]; - $connection->insert($this->getMainTable(), $bind); - $linkId = $connection->lastInsertId($this->getMainTable()); - } + list($insertData, $updateData, $deleteConditions) = $this->prepareProductLinksData( + $parentId, + $data, + $typeId, + $links + ); - foreach ($attributes as $attributeInfo) { - $attributeTable = $this->getAttributeTypeTable($attributeInfo['type']); - if ($attributeTable) { - if (isset($linkInfo[$attributeInfo['code']])) { - $value = $this->_prepareAttributeValue( - $attributeInfo['type'], - $linkInfo[$attributeInfo['code']] - ); - $bind = [ - 'product_link_attribute_id' => $attributeInfo['id'], - 'link_id' => $linkId, - 'value' => $value, - ]; - $connection->insertOnDuplicate($attributeTable, $bind, ['value']); - } else { - $connection->delete( - $attributeTable, - ['link_id = ?' => $linkId, 'product_link_attribute_id = ?' => $attributeInfo['id']] - ); - } - } + if ($insertData) { + $insertColumns = [ + 'product_link_attribute_id', + 'link_id', + 'value', + ]; + foreach ($insertData as $table => $values) { + $connection->insertArray($table, $insertColumns, $values, AdapterInterface::INSERT_IGNORE); + } + } + if ($updateData) { + // for mass update product links with constraint by unique key use insert on duplicate statement + foreach ($updateData as $table => $values) { + $connection->insertOnDuplicate($table, $values, ['value']); + } + } + if ($deleteConditions) { + foreach ($deleteConditions as $table => $deleteCondition) { + $connection->delete( + $table, + [ + 'link_id = ?' => $deleteCondition['link_id'], + 'product_link_attribute_id = ?' => $deleteCondition['product_link_attribute_id'] + ] + ); } } @@ -302,4 +299,69 @@ public function getParentIdsByChild($childId, $typeId) return $parentIds; } + + /** + * Prepare data for insert, update or delete product link attributes + * + * @param int $parentId + * @param array $data + * @param int $typeId + * @param array $links + * @return array + */ + private function prepareProductLinksData($parentId, $data, $typeId, $links) + { + $connection = $this->getConnection(); + $attributes = $this->getAttributesByType($typeId); + + $insertData = []; + $updateData = []; + $deleteConditions = []; + + foreach ($data as $linkedProductId => $linkInfo) { + $linkId = null; + if (isset($links[$linkedProductId])) { + $linkId = $links[$linkedProductId]; + } else { + $bind = [ + 'product_id' => $parentId, + 'linked_product_id' => $linkedProductId, + 'link_type_id' => $typeId, + ]; + $connection->insert($this->getMainTable(), $bind); + $linkId = $connection->lastInsertId($this->getMainTable()); + } + + foreach ($attributes as $attributeInfo) { + $attributeTable = $this->getAttributeTypeTable($attributeInfo['type']); + if (!$attributeTable) { + continue; + } + if (isset($linkInfo[$attributeInfo['code']])) { + $value = $this->_prepareAttributeValue( + $attributeInfo['type'], + $linkInfo[$attributeInfo['code']] + ); + if (isset($links[$linkedProductId])) { + $updateData[$attributeTable][] = [ + 'product_link_attribute_id' => $attributeInfo['id'], + 'link_id' => $linkId, + 'value' => $value, + ]; + } else { + $insertData[$attributeTable][] = [ + 'product_link_attribute_id' => $attributeInfo['id'], + 'link_id' => $linkId, + 'value' => $value, + ]; + } + } else { + $deleteConditions[$attributeTable]['link_id'][] = $linkId; + $deleteConditions[$attributeTable]['product_link_attribute_id'][] = $attributeInfo['id']; + } + } + } + + return [$insertData, $updateData, $deleteConditions]; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php index f018e2b148f15..8841b6059c46f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php @@ -95,6 +95,7 @@ public function build($productId) ->where('t.attribute_id = ?', $priceAttribute->getAttributeId()) ->where('t.value IS NOT NULL') ->order('t.value ' . Select::SQL_ASC) + ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC) ->limit(1); $priceSelect = $this->baseSelectProcessor->process($priceSelect); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php index b4459cd1eea07..5c47185a85bf4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php @@ -139,6 +139,7 @@ public function build($productId) 'special_to.value IS NULL OR ' . $connection->getDatePartSql('special_to.value') .' >= ?', $currentDate )->order('t.value ' . Select::SQL_ASC) + ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC) ->limit(1); $specialPrice = $this->baseSelectProcessor->process($specialPrice); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php index 79323e57b033e..37281193d6a1b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php @@ -97,6 +97,7 @@ public function build($productId) ->where('t.all_groups = 1 OR customer_group_id = ?', $this->customerSession->getCustomerGroupId()) ->where('t.qty = ?', 1) ->order('t.value ' . Select::SQL_ASC) + ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC) ->limit(1); $priceSelect = $this->baseSelectProcessor->process($priceSelect); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php index 4775b96e3a448..d592eb8866d3b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php @@ -6,6 +6,9 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\Store; /** * Catalog product custom option resource model @@ -76,10 +79,10 @@ protected function _construct() /** * Save options store data * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ - protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) + protected function _afterSave(AbstractModel $object) { $this->_saveValuePrices($object); $this->_saveValueTitles($object); @@ -90,136 +93,38 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) /** * Save value prices * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $object) + protected function _saveValuePrices(AbstractModel $object) { - $priceTable = $this->getTable('catalog_product_option_price'); - $connection = $this->getConnection(); - /* * Better to check param 'price' and 'price_type' for saving. * If there is not price skip saving price */ - if (in_array($object->getType(), $this->getPriceTypes())) { - //save for store_id = 0 + // save for store_id = 0 if (!$object->getData('scope', 'price')) { - $statement = $connection->select()->from( - $priceTable, - 'option_id' - )->where( - 'option_id = ?', - $object->getId() - )->where( - 'store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); - $optionId = $connection->fetchOne($statement); - - if ($optionId) { - $data = $this->_prepareDataForTable( - new \Magento\Framework\DataObject( - ['price' => $object->getPrice(), 'price_type' => $object->getPriceType()] - ), - $priceTable - ); - - $connection->update( - $priceTable, - $data, - [ - 'option_id = ?' => $object->getId(), - 'store_id = ?' => \Magento\Store\Model\Store::DEFAULT_STORE_ID - ] - ); - } else { - $data = $this->_prepareDataForTable( - new \Magento\Framework\DataObject( - [ - 'option_id' => $object->getId(), - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - 'price' => $object->getPrice(), - 'price_type' => $object->getPriceType(), - ] - ), - $priceTable - ); - $connection->insert($priceTable, $data); - } + $this->savePriceByStore($object, Store::DEFAULT_STORE_ID); } $scope = (int)$this->_config->getValue( - \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, + Store::XML_PATH_PRICE_SCOPE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - if ($object->getStoreId() != '0' && $scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) { - $baseCurrency = $this->_config->getValue( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' - ); - + if ((int)$object->getStoreId() !== Store::DEFAULT_STORE_ID && $scope === Store::PRICE_SCOPE_WEBSITE) { $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); - if (is_array($storeIds)) { - foreach ($storeIds as $storeId) { - if ($object->getPriceType() == 'fixed') { - $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); - $rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency); - if (!$rate) { - $rate = 1; - } - $newPrice = $object->getPrice() * $rate; - } else { - $newPrice = $object->getPrice(); - } - - $statement = $connection->select()->from( - $priceTable - )->where( - 'option_id = ?', - $object->getId() - )->where( - 'store_id = ?', - $storeId - ); - - if ($connection->fetchOne($statement)) { - $data = $this->_prepareDataForTable( - new \Magento\Framework\DataObject( - ['price' => $newPrice, 'price_type' => $object->getPriceType()] - ), - $priceTable - ); - - $connection->update( - $priceTable, - $data, - ['option_id = ?' => $object->getId(), 'store_id = ?' => $storeId] - ); - } else { - $data = $this->_prepareDataForTable( - new \Magento\Framework\DataObject( - [ - 'option_id' => $object->getId(), - 'store_id' => $storeId, - 'price' => $newPrice, - 'price_type' => $object->getPriceType(), - ] - ), - $priceTable - ); - $connection->insert($priceTable, $data); - } - } + if (empty($storeIds)) { + return $this; } - } elseif ($scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price') - ) { - $connection->delete( - $priceTable, + foreach ($storeIds as $storeId) { + $newPrice = $this->calculateStorePrice($object, $storeId); + $this->savePriceByStore($object, (int)$storeId, $newPrice); + } + } elseif ($scope === Store::PRICE_SCOPE_WEBSITE && $object->getData('scope', 'price')) { + $this->getConnection()->delete( + $this->getTable('catalog_product_option_price'), ['option_id = ?' => $object->getId(), 'store_id = ?' => $object->getStoreId()] ); } @@ -228,31 +133,112 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje return $this; } + /** + * Save option price by store + * + * @param AbstractModel $object + * @param int $storeId + * @param float|null $newPrice + */ + private function savePriceByStore(AbstractModel $object, int $storeId, float $newPrice = null) + { + $priceTable = $this->getTable('catalog_product_option_price'); + $connection = $this->getConnection(); + $price = $newPrice ?? $object->getPrice(); + + $statement = $connection->select()->from($priceTable, 'option_id') + ->where('option_id = ?', $object->getId()) + ->where('store_id = ?', $storeId); + $optionId = $connection->fetchOne($statement); + + if (!$optionId) { + $data = $this->_prepareDataForTable( + new DataObject( + [ + 'option_id' => $object->getId(), + 'store_id' => $storeId, + 'price' => $price, + 'price_type' => $object->getPriceType(), + ] + ), + $priceTable + ); + $connection->insert($priceTable, $data); + } else { + // skip to update the default price when the price is saving on other store + if ($storeId === Store::DEFAULT_STORE_ID && (int)$object->getStoreId() !== $storeId) { + return; + } + + $data = $this->_prepareDataForTable( + new DataObject( + [ + 'price' => $price, + 'price_type' => $object->getPriceType() + ] + ), + $priceTable + ); + + $connection->update( + $priceTable, + $data, + [ + 'option_id = ?' => $object->getId(), + 'store_id = ?' => $storeId + ] + ); + } + } + + /** + * Calculate price by store + * + * @param AbstractModel $object + * @param int $storeId + * @return float + */ + private function calculateStorePrice(AbstractModel $object, int $storeId): float + { + $price = $object->getPrice(); + if ($object->getPriceType() === 'fixed') { + $baseCurrency = $this->_config->getValue( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + 'default' + ); + $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); + $rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency); + $price = $object->getPrice() * ($rate ?: 1); + } + + return (float)$price; + } + /** * Save titles * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $object) + protected function _saveValueTitles(AbstractModel $object) { $connection = $this->getConnection(); $titleTableName = $this->getTable('catalog_product_option_title'); - foreach ([\Magento\Store\Model\Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { + foreach ([Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) { $existInCurrentStore = $this->getColFromOptionTable($titleTableName, (int)$object->getId(), (int)$storeId); - $existInDefaultStore = (int)$storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID ? + $existInDefaultStore = (int)$storeId === Store::DEFAULT_STORE_ID ? $existInCurrentStore : $this->getColFromOptionTable( $titleTableName, (int)$object->getId(), - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ); if ($object->getTitle()) { $isDeleteStoreTitle = (bool)$object->getData('is_delete_store_title'); if ($existInCurrentStore) { - if ($isDeleteStoreTitle && (int)$storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID) { + if ($isDeleteStoreTitle && (int)$storeId !== Store::DEFAULT_STORE_ID) { $connection->delete($titleTableName, ['option_title_id = ?' => $existInCurrentStore]); } elseif ($object->getStoreId() == $storeId) { $data = $this->_prepareDataForTable( @@ -270,9 +256,9 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje } } else { // we should insert record into not default store only of if it does not exist in default store - if (($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore) || + if (($storeId == Store::DEFAULT_STORE_ID && !$existInDefaultStore) || ( - $storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && + $storeId != Store::DEFAULT_STORE_ID && !$existInCurrentStore && !$isDeleteStoreTitle ) @@ -291,7 +277,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje } } } else { - if ($object->getId() && $object->getStoreId() > \Magento\Store\Model\Store::DEFAULT_STORE_ID + if ($object->getId() && $object->getStoreId() > Store::DEFAULT_STORE_ID && $storeId ) { $connection->delete( @@ -307,7 +293,7 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje } /** - * Get first col from from first row for option table + * Get first col from first row for option table * * @param string $tableName * @param int $optionId @@ -470,7 +456,7 @@ public function getSearchableData($productId, $storeId) 'option_title_default.option_id=product_option.option_id', $connection->quoteInto( 'option_title_default.store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ) ] ); @@ -517,7 +503,7 @@ public function getSearchableData($productId, $storeId) 'option_title_default.option_type_id=option_type.option_type_id', $connection->quoteInto( 'option_title_default.store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID ) ] ); @@ -582,6 +568,8 @@ public function getPriceTypes() } /** + * Get Metadata Pool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 91bb99ca971a7..3927b37016c1e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -17,11 +17,12 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Helper\Data; /** * Catalog product custom option resource model * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Value extends AbstractDb { @@ -51,6 +52,11 @@ class Value extends AbstractDb */ private $localeFormat; + /** + * @var Data + */ + private $dataHelper; + /** * Class constructor * @@ -59,17 +65,20 @@ class Value extends AbstractDb * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $config * @param string $connectionName + * @param Data $dataHelper */ public function __construct( Context $context, CurrencyFactory $currencyFactory, StoreManagerInterface $storeManager, ScopeConfigInterface $config, - $connectionName = null + $connectionName = null, + Data $dataHelper = null ) { $this->_currencyFactory = $currencyFactory; $this->_storeManager = $storeManager; $this->_config = $config; + $this->dataHelper = $dataHelper ?: ObjectManager::getInstance()->get(Data::class); parent::__construct($context, $connectionName); } @@ -130,7 +139,7 @@ protected function _saveValuePrices(AbstractModel $object) $optionTypeId = $this->getConnection()->fetchOne($select); if ($optionTypeId) { - if ($object->getStoreId() == '0') { + if ($object->getStoreId() == '0' || $this->dataHelper->isPriceGlobal()) { $bind = ['price' => $price, 'price_type' => $priceType]; $where = [ 'option_type_id = ?' => $optionTypeId, @@ -160,19 +169,22 @@ protected function _saveValuePrices(AbstractModel $object) && isset($objectPrice) && $object->getStoreId() != Store::DEFAULT_STORE_ID ) { - $baseCurrency = $this->_config->getValue( + $website = $this->_storeManager->getStore($object->getStoreId())->getWebsite(); + + $websiteBaseCurrency = $this->_config->getValue( Currency::XML_PATH_CURRENCY_BASE, - 'default' + ScopeInterface::SCOPE_WEBSITE, + $website ); - $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); + $storeIds = $website->getStoreIds(); if (is_array($storeIds)) { foreach ($storeIds as $storeId) { if ($priceType == 'fixed') { $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); /** @var $currencyModel Currency */ $currencyModel = $this->_currencyFactory->create(); - $currencyModel->load($baseCurrency); + $currencyModel->load($websiteBaseCurrency); $rate = $currencyModel->getRate($storeCurrency); if (!$rate) { $rate = 1; @@ -256,7 +268,8 @@ protected function _saveValueTitles(AbstractModel $object) $object->unsetData('title'); } - if ($object->getTitle()) { + /*** Checking whether title is not null ***/ + if ($object->getTitle()!= null) { if ($existInCurrentStore) { if ($storeId == $object->getStoreId()) { $where = [ @@ -300,7 +313,7 @@ protected function _saveValueTitles(AbstractModel $object) } /** - * Get first col from from first row for option table + * Get first col from first row for option table * * @param string $tableName * @param int $optionId diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php index 1cb2fed839dde..6ca688f36c265 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php @@ -14,6 +14,7 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Class Url @@ -101,6 +102,11 @@ class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $metadataPool; + /** + * @var TableMaintainer + */ + private $tableMaintainer; + /** * Url constructor. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context @@ -110,6 +116,7 @@ class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Catalog\Model\Category $catalogCategory * @param \Psr\Log\LoggerInterface $logger * @param null $connectionName + * @param TableMaintainer|null $tableMaintainer */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -118,7 +125,8 @@ public function __construct( Product $productResource, \Magento\Catalog\Model\Category $catalogCategory, \Psr\Log\LoggerInterface $logger, - $connectionName = null + $connectionName = null, + TableMaintainer $tableMaintainer = null ) { $this->_storeManager = $storeManager; $this->_eavConfig = $eavConfig; @@ -126,6 +134,7 @@ public function __construct( $this->_catalogCategory = $catalogCategory; $this->_logger = $logger; parent::__construct($context, $connectionName); + $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); } /** @@ -655,43 +664,52 @@ public function getRewriteByProductStore(array $products) } $connection = $this->getConnection(); - $select = $connection->select()->from( - ['i' => $this->getTable('catalog_category_product_index')], - ['product_id', 'store_id', 'visibility'] - )->joinLeft( - ['u' => $this->getMainTable()], - 'i.product_id = u.entity_id AND i.store_id = u.store_id' - . ' AND u.entity_type = "' . ProductUrlRewriteGenerator::ENTITY_TYPE . '"', - ['request_path'] - )->joinLeft( - ['r' => $this->getTable('catalog_url_rewrite_product_category')], - 'u.url_rewrite_id = r.url_rewrite_id AND r.category_id is NULL', - [] - ); - - $bind = []; + $storesProducts = []; foreach ($products as $productId => $storeId) { - $catId = $this->_storeManager->getStore($storeId)->getRootCategoryId(); - $productBind = 'product_id' . $productId; - $storeBind = 'store_id' . $storeId; - $catBind = 'category_id' . $catId; - $cond = '(' . implode( - ' AND ', - ['i.product_id = :' . $productBind, 'i.store_id = :' . $storeBind, 'i.category_id = :' . $catBind] - ) . ')'; - $bind[$productBind] = $productId; - $bind[$storeBind] = $storeId; - $bind[$catBind] = $catId; - $select->orWhere($cond); + $storesProducts[$storeId][] = $productId; } - $rowSet = $connection->fetchAll($select, $bind); - foreach ($rowSet as $row) { - $result[$row['product_id']] = [ - 'store_id' => $row['store_id'], - 'visibility' => $row['visibility'], - 'url_rewrite' => $row['request_path'], - ]; + foreach ($storesProducts as $storeId => $productIds) { + $select = $connection->select()->from( + ['i' => $this->tableMaintainer->getMainTable($storeId)], + ['product_id', 'store_id', 'visibility'] + )->joinLeft( + ['u' => $this->getMainTable()], + 'i.product_id = u.entity_id AND i.store_id = u.store_id' + . ' AND u.entity_type = "' . ProductUrlRewriteGenerator::ENTITY_TYPE . '"', + ['request_path'] + )->joinLeft( + ['r' => $this->getTable('catalog_url_rewrite_product_category')], + 'u.url_rewrite_id = r.url_rewrite_id AND r.category_id is NULL', + [] + ); + + $bind = []; + foreach ($productIds as $productId) { + $catId = $this->_storeManager->getStore($storeId)->getRootCategoryId(); + $productBind = 'product_id' . $productId; + $storeBind = 'store_id' . $storeId; + $catBind = 'category_id' . $catId; + $bindArray = [ + 'i.product_id = :' . $productBind, + 'i.store_id = :' . $storeBind, + 'i.category_id = :' . $catBind + ]; + $cond = '(' . implode(' AND ', $bindArray) . ')'; + $bind[$productBind] = $productId; + $bind[$storeBind] = $storeId; + $bind[$catBind] = $catId; + $select->orWhere($cond); + } + + $rowSet = $connection->fetchAll($select, $bind); + foreach ($rowSet as $row) { + $result[$row['product_id']] = [ + 'store_id' => $row['store_id'], + 'visibility' => $row['visibility'], + 'url_rewrite' => $row['request_path'], + ]; + } } return $result; diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index a3f4d5004182f..b4445510d49e2 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Catalog Template Filter Model * @@ -128,7 +126,8 @@ public function viewDirective($construction) public function mediaDirective($construction) { $params = $this->getParameters($construction[2]); - return $this->_storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; + return $this->_storeManager->getStore() + ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; } /** diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index ce85ba21d211f..dc81236c34cd9 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -19,6 +19,8 @@ */ class Image implements LocalInterface { + private $sourceContentType; + /** * @var string */ @@ -67,6 +69,12 @@ public function __construct( $filePath, array $miscParams = [] ) { + if (array_key_exists('image_type', $miscParams)) { + $this->sourceContentType = $miscParams['image_type']; + unset($miscParams['image_type']); + } else { + $this->sourceContentType = $this->contentType; + } $this->mediaConfig = $mediaConfig; $this->context = $context; $this->filePath = $filePath; @@ -129,7 +137,7 @@ public function getSourceFile() */ public function getSourceContentType() { - return $this->contentType; + return $this->sourceContentType; } /** diff --git a/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php new file mode 100644 index 0000000000000..94977485b95b3 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category\Authorization; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\AuthorizationException; + +/** + * Employ additional authorization logic when a category is saved. + */ +class CategoryDesignAuthorization implements ObserverInterface +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * @inheritDoc + * + * @throws AuthorizationException + */ + public function execute(Observer $observer) + { + /** @var CategoryInterface $category */ + $category = $observer->getEvent()->getData('category'); + $this->authorization->authorizeSavingOf($category); + } +} diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php new file mode 100644 index 0000000000000..1a131ed71973d --- /dev/null +++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * Checks if a category has changed products and depends on indexer configuration + * marks `Category Products` indexer as invalid or reindexes affected products. + */ +class CategoryProductIndexer implements ObserverInterface +{ + /** + * @var Processor + */ + private $processor; + + /** + * @param Processor $processor + */ + public function __construct(Processor $processor) + { + $this->processor = $processor; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + $productIds = $observer->getEvent()->getProductIds(); + if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + $this->processor->markIndexerAsInvalid(); + } + } +} diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index a597b8fddda9f..ed9f89efc6891 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Set value for Special Price start date @@ -13,21 +16,20 @@ class SetSpecialPriceStartDate implements ObserverInterface { /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ private $localeDate; /** - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @codeCoverageIgnore + * @param TimezoneInterface $localeDate */ - public function __construct(\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate) + public function __construct(TimezoneInterface $localeDate) { $this->localeDate = $localeDate; } /** - * Set the current date to Special Price From attribute if it empty + * Set the current date to Special Price From attribute if it's empty. * * @param \Magento\Framework\Event\Observer $observer * @return $this @@ -36,8 +38,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); - if ($product->getSpecialPrice() && !$product->getSpecialFromDate()) { - $product->setData('special_from_date', $this->localeDate->date()); + if ($product->getSpecialPrice() && ! $product->getSpecialFromDate()) { + $product->setData('special_from_date', $this->localeDate->date()->setTime(0, 0)); } return $this; diff --git a/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php b/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php deleted file mode 100644 index 0ba1251edc7d6..0000000000000 --- a/app/code/Magento/Catalog/Observer/UnsetSpecialPrice.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Observer; - -use Magento\Framework\Event\ObserverInterface; - -/** - * Unset value for Special Price if passed as null - */ -class UnsetSpecialPrice implements ObserverInterface -{ - /** - * Unset the Special Price attribute if it is null - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = $observer->getEvent()->getProduct(); - if ($product->getSpecialPrice() === null) { - $product->setData('special_price', ''); - } - - return $this; - } -} diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php index 5119a32d921de..debf7fd002b2d 100644 --- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php +++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php @@ -22,7 +22,7 @@ class Topmenu protected $catalogCategory; /** - * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory + * @var \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory */ private $collectionFactory; @@ -40,13 +40,13 @@ class Topmenu * Initialize dependencies. * * @param \Magento\Catalog\Helper\Category $catalogCategory - * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory + * @param \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver */ public function __construct( \Magento\Catalog\Helper\Category $catalogCategory, - \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, + \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory $categoryCollectionFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Layer\Resolver $layerResolver ) { @@ -162,6 +162,7 @@ private function getCategoryAsArray($category, $currentCategory, $isParentActive 'url' => $this->catalogCategory->getCategoryUrl($category), 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true), 'is_active' => $category->getId() == $currentCategory->getId(), + 'is_category' => true, 'is_parent_active' => $isParentActive ]; } diff --git a/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php new file mode 100644 index 0000000000000..af2dccb96f937 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category\Authorization; +use Magento\Framework\Exception\LocalizedException; + +/** + * Perform additional authorization for category operations. + */ +class CategoryAuthorization +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving of a category. + * + * @param CategoryRepositoryInterface $subject + * @param CategoryInterface $category + * @throws LocalizedException + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(CategoryRepositoryInterface $subject, CategoryInterface $category): array + { + $this->authorization->authorizeSavingOf($category); + + return [$category]; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php new file mode 100644 index 0000000000000..544319e739de5 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Framework\App\Action; + +use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Http\Context as HttpContext; + +/** + * Before dispatch plugin for all frontend controllers to update http context. + */ +class ContextPlugin +{ + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + + /** + * @var CatalogSession + */ + private $catalogSession; + + /** + * @var HttpContext + */ + private $httpContext; + + /** + * @param ToolbarMemorizer $toolbarMemorizer + * @param CatalogSession $catalogSession + * @param HttpContext $httpContext + */ + public function __construct( + ToolbarMemorizer $toolbarMemorizer, + CatalogSession $catalogSession, + HttpContext $httpContext + ) { + $this->toolbarMemorizer = $toolbarMemorizer; + $this->catalogSession = $catalogSession; + $this->httpContext = $httpContext; + } + + /** + * Update http context with catalog sensitive information. + * + * @return void + */ + public function beforeDispatch() + { + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $params = [ + ToolbarModel::ORDER_PARAM_NAME, + ToolbarModel::DIRECTION_PARAM_NAME, + ToolbarModel::MODE_PARAM_NAME, + ToolbarModel::LIMIT_PARAM_NAME, + ]; + + foreach ($params as $param) { + $paramValue = $this->catalogSession->getData($param); + if ($paramValue) { + $this->httpContext->setValue($param, $paramValue, false); + } + } + } + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php index 597a1466a125e..5ccec4c3a4c7b 100644 --- a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php +++ b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php @@ -14,6 +14,11 @@ class AttributeValidation */ private $storeManager; + /** + * @var array + */ + private $allowedEntityTypes; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $allowedEntityTypes @@ -30,6 +35,7 @@ public function __construct( * @param \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject * @param \Closure $proceed * @param \Magento\Framework\DataObject $entity + * @throws \Magento\Framework\Exception\NoSuchEntityException * @return bool */ public function aroundValidate( @@ -41,7 +47,7 @@ public function aroundValidate( return $entity instanceof $allowedEntity; }, $this->allowedEntityTypes))); - if ($isAllowedType && $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { + if ($isAllowedType && (int) $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { $attrCode = $subject->getAttribute()->getAttributeCode(); // Null is meaning "no value" which should be overridden by value from default scope if (array_key_exists($attrCode, $entity->getData()) && $entity->getData($attrCode) === null) { diff --git a/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php b/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php new file mode 100644 index 0000000000000..342b703ded0a5 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/AttributeSetRepository/RemoveProducts.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Plugin\Model\AttributeSetRepository; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Eav\Api\Data\AttributeSetInterface; + +/** + * Delete related products after attribute set successfully removed. + */ +class RemoveProducts +{ + /** + * Retrieve products related to specific attribute set. + * + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * RemoveProducts constructor. + * + * @param CollectionFactory $collectionFactory + */ + public function __construct(CollectionFactory $collectionFactory) + { + $this->collectionFactory = $collectionFactory; + } + + /** + * Delete related to specific attribute set products, if attribute set was removed successfully. + * + * @param AttributeSetRepositoryInterface $subject + * @param bool $result + * @param AttributeSetInterface $attributeSet + * @return bool + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete( + AttributeSetRepositoryInterface $subject, + bool $result, + AttributeSetInterface $attributeSet + ) { + /** @var Collection $productCollection */ + $productCollection = $this->collectionFactory->create(); + $productCollection->addFieldToFilter('attribute_set_id', ['eq' => $attributeSet->getId()]); + $productCollection->delete(); + + return $result; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php new file mode 100644 index 0000000000000..ada96d4fd48f4 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Model\Product\Option; + +/** + * Plugin for updating product 'has_options' and 'required_options' attributes. + */ +class UpdateProductCustomOptionsAttributes +{ + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ + public function __construct(\Magento\Catalog\Api\ProductRepositoryInterface $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Update product 'has_options' and 'required_options' attributes after option save. + * + * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject + * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + * + * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + ) { + $product = $this->productRepository->get($option->getProductSku()); + if (!$product->getHasOptions() + || ($option->getIsRequire() + && !$product->getRequiredOptions()) + ) { + $product->setCanSaveCustomOptions(true); + $product->setOptionsSaved(true); + $optionId = $option->getOptionId(); + $currentOptions = array_filter($product->getOptions(), function ($optionItem) use ($optionId) { + return $optionId != $optionItem->getOptionId(); + }); + $currentOptions[] = $option; + $product->setOptions($currentOptions); + $product->save(); + } + + return $option; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php new file mode 100644 index 0000000000000..59f1051b8ed56 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Category/RemoveRedundantImagePlugin.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Model\ResourceModel\Category; + +use Magento\Catalog\Model\ImageUploader; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Catalog\Model\ResourceModel\Category\RedundantCategoryImageChecker; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Model\AbstractModel; + +/** + * Remove old Category Image file from pub/media/catalog/category directory if such Image is not used anymore. + */ +class RemoveRedundantImagePlugin +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var ImageUploader + */ + private $imageUploader; + + /** + * @var RedundantCategoryImageChecker + */ + private $redundantCategoryImageChecker; + + public function __construct( + Filesystem $filesystem, + ImageUploader $imageUploader, + RedundantCategoryImageChecker $redundantCategoryImageChecker + ) { + $this->filesystem = $filesystem; + $this->imageUploader = $imageUploader; + $this->redundantCategoryImageChecker = $redundantCategoryImageChecker; + } + + /** + * Removes Image file if it is not used anymore. + * + * @param CategoryResource $subject + * @param CategoryResource $result + * @param AbstractModel $category + * @return CategoryResource + * + * @throws \Magento\Framework\Exception\FileSystemException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + CategoryResource $subject, + CategoryResource $result, + AbstractModel $category + ): CategoryResource { + $originalImage = $category->getOrigData('image'); + if (null !== $originalImage + && $originalImage !== $category->getImage() + && $this->redundantCategoryImageChecker->execute($originalImage) + ) { + $basePath = $this->imageUploader->getBasePath(); + $baseImagePath = $this->imageUploader->getFilePath($basePath, $originalImage); + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface $mediaDirectory */ + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete($baseImagePath); + } + + return $result; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php new file mode 100644 index 0000000000000..ff4d2f93c912a --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Plugin\Model\ResourceModel; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Eav\Model\ResourceModel\ReadSnapshot; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Extend Eav ReadSnapshot by adding data from product or category attributes with global scope. + * Default ReadSnapshot returns only data for current scope where entity is editing, but attributes with global scope, + * e.g. price, is written only to default scope (store_id = 0) in case Catalog Price Scope set to "Global" + */ +class ReadSnapshotPlugin +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var EavConfig + */ + private $config; + + /** + * @param MetadataPool $metadataPool + * @param EavConfig $config + */ + public function __construct( + MetadataPool $metadataPool, + EavConfig $config + ) { + $this->metadataPool = $metadataPool; + $this->config = $config; + } + + /** + * @param ReadSnapshot $subject + * @param array $entityData + * @param string $entityType + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(ReadSnapshot $subject, array $entityData, $entityType) + { + if (!in_array($entityType, [ProductInterface::class, CategoryInterface::class], true)) { + return $entityData; + } + + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $globalAttributes = []; + $attributesMap = []; + $eavEntityType = $metadata->getEavEntityType(); + $attributes = null === $eavEntityType + ? [] + : $this->config->getEntityAttributes($eavEntityType, new \Magento\Framework\DataObject($entityData)); + + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute */ + foreach ($attributes as $attribute) { + if (!$attribute->isStatic() && $attribute->isScopeGlobal()) { + $globalAttributes[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); + $attributesMap[$attribute->getAttributeId()] = $attribute->getAttributeCode(); + } + } + + if ($globalAttributes) { + $selects = []; + foreach ($globalAttributes as $table => $attributeIds) { + $select = $connection->select() + ->from( + ['t' => $table], + ['value' => 't.value', 'attribute_id' => 't.attribute_id'] + ) + ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) + ->where('attribute_id' . ' in (?)', $attributeIds) + ->where('store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID); + $selects[] = $select; + } + $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( + $selects, + \Magento\Framework\DB\Select::SQL_UNION_ALL + ); + foreach ($connection->fetchAll($unionSelect) as $attributeValue) { + $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; + } + } + + return $entityData; + } +} diff --git a/app/code/Magento/Catalog/Plugin/ProductAuthorization.php b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php new file mode 100644 index 0000000000000..ce2fe19cf1aee --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Authorization; +use Magento\Framework\Exception\LocalizedException; + +/** + * Perform additional authorization for product operations. + */ +class ProductAuthorization +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving of a product. + * + * @param ProductRepositoryInterface $subject + * @param ProductInterface $product + * @param bool $saveOptions + * @throws LocalizedException + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + ProductRepositoryInterface $subject, + ProductInterface $product, + $saveOptions = false + ): array { + $this->authorization->authorizeSavingOf($product); + + return [$product, $saveOptions]; + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php index 54a13be864db7..77368517a3155 100644 --- a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php @@ -30,7 +30,7 @@ public function getValue() $this->value = false; foreach ($this->priceInfo->getPrices() as $price) { if ($price instanceof BasePriceProviderInterface && $price->getValue() !== false) { - $this->value = min($price->getValue(), $this->value ?: $price->getValue()); + $this->value = min($price->getValue(), $this->value !== false ? $this->value: $price->getValue()); } } } diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php new file mode 100644 index 0000000000000..212d4e7536701 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredOptions.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Price; + +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; + +/** + * Configured Options model. + */ +class ConfiguredOptions +{ + /** + * Get value of configured options. + * + * @param float $basePrice + * @param ItemInterface $item + * @return float + */ + public function getItemOptionsValue(float $basePrice, ItemInterface $item): float + { + $product = $item->getProduct(); + $value = 0.; + $optionIds = $item->getOptionByCode('option_ids'); + if ($optionIds) { + foreach (explode(',', $optionIds->getValue()) as $optionId) { + $option = $product->getOptionById($optionId); + if ($option) { + $itemOption = $item->getOptionByCode('option_' . $option->getId()); + /** @var $group \Magento\Catalog\Model\Product\Option\Type\DefaultType */ + $group = $option->groupFactory($option->getType()) + ->setOption($option) + ->setConfigurationItem($item) + ->setConfigurationItemOption($itemOption); + $value += $group->getOptionPrice($itemOption->getValue(), $basePrice); + } + } + } + return $value; + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php index 87d031d8d5b35..f8e89b95569a5 100644 --- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; +use Magento\Framework\App\ObjectManager; /** * Configured price model @@ -25,21 +26,29 @@ class ConfiguredPrice extends FinalPrice implements ConfiguredPriceInterface */ protected $item; + /** + * @var ConfiguredOptions + */ + private $configuredOptions; + /** * @param Product $saleableItem * @param float $quantity * @param CalculatorInterface $calculator * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency - * @param ItemInterface $item + * @param ItemInterface|null $item + * @param ConfiguredOptions|null $configuredOptions */ public function __construct( Product $saleableItem, $quantity, CalculatorInterface $calculator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - ItemInterface $item = null + ItemInterface $item = null, + ConfiguredOptions $configuredOptions = null ) { $this->item = $item; + $this->configuredOptions = $configuredOptions ?: ObjectManager::getInstance()->get(ConfiguredOptions::class); parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); } @@ -56,9 +65,10 @@ public function setItem(ItemInterface $item) /** * Get value of configured options * - * @return array + * @deprecated ConfiguredOptions::getItemOptionsValue is used instead + * @return float */ - protected function getOptionsValue() + protected function getOptionsValue(): float { $product = $this->item->getProduct(); $value = 0.; @@ -88,6 +98,9 @@ protected function getOptionsValue() */ public function getValue() { - return $this->item ? parent::getValue() + $this->getOptionsValue() : parent::getValue(); + $basePrice = parent::getValue(); + return $this->item + ? $basePrice + $this->configuredOptions->getItemOptionsValue($basePrice, $this->item) + : $basePrice; } } diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php index e6d35a0f5239a..155f8d55d9a4c 100644 --- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceInterface.php @@ -18,6 +18,11 @@ interface ConfiguredPriceInterface */ const CONFIGURED_PRICE_CODE = 'configured_price'; + /** + * Regular price type configured + */ + const CONFIGURED_REGULAR_PRICE_CODE = 'configured_regular_price'; + /** * @param ItemInterface $item * @return $this diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php new file mode 100644 index 0000000000000..607bcb411c1cd --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPriceSelection.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Price; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Configured price selection model + */ +class ConfiguredPriceSelection +{ + /** + * @var \Magento\Framework\Pricing\Adjustment\CalculatorInterface + */ + private $calculator; + + /** + * @param \Magento\Framework\Pricing\Adjustment\CalculatorInterface $calculator + */ + public function __construct( + \Magento\Framework\Pricing\Adjustment\CalculatorInterface $calculator + ) { + $this->calculator = $calculator; + } + + /** + * Get Selection pricing list. + * + * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface $price + * @return array + */ + public function getSelectionPriceList(\Magento\Catalog\Pricing\Price\ConfiguredPriceInterface $price): array + { + $selectionPriceList = []; + foreach ($price->getOptions() as $option) { + $selectionPriceList = array_merge( + $selectionPriceList, + $this->createSelectionPriceList($option, $price->getProduct()) + ); + } + return $selectionPriceList; + } + + /** + * Create Selection Price List + * + * @param ExtensibleDataInterface $option + * @param Product $product + * @return array + */ + private function createSelectionPriceList(ExtensibleDataInterface $option, Product $product): array + { + return $this->calculator->createSelectionPriceList($option, $product); + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php new file mode 100644 index 0000000000000..75b4d13d1c8d8 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Price; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Framework\Pricing\Adjustment\CalculatorInterface; + +/** + * Configured regular price model + */ +class ConfiguredRegularPrice extends RegularPrice implements ConfiguredPriceInterface +{ + /** + * Price type configured + */ + const PRICE_CODE = self::CONFIGURED_REGULAR_PRICE_CODE; + + /** + * @var null|ItemInterface + */ + private $item; + + /** + * @var ConfiguredOptions + */ + private $configuredOptions; + + /** + * @param Product $saleableItem + * @param float $quantity + * @param CalculatorInterface $calculator + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param ConfiguredOptions $configuredOptions + * @param ItemInterface|null $item + */ + public function __construct( + Product $saleableItem, + $quantity, + CalculatorInterface $calculator, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + ConfiguredOptions $configuredOptions, + ItemInterface $item = null + ) { + $this->item = $item; + $this->configuredOptions = $configuredOptions; + parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); + } + + /** + * @param ItemInterface $item + * @return $this + */ + public function setItem(ItemInterface $item) + { + $this->item = $item; + return $this; + } + + /** + * Price value of product with configured options + * + * @return bool|float + */ + public function getValue() + { + $basePrice = parent::getValue(); + return $this->item && $basePrice !== false + ? $basePrice + $this->configuredOptions->getItemOptionsValue($basePrice, $this->item) + : $basePrice; + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php index 5026286610118..43697be8ccf9d 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPrice.php @@ -35,32 +35,42 @@ class CustomOptionPrice extends AbstractPrice implements CustomOptionPriceInterf */ protected $excludeAdjustment = null; + /** + * @var CustomOptionPriceCalculator + */ + private $customOptionPriceCalculator; + /** * @param SaleableInterface $saleableItem * @param float $quantity * @param CalculatorInterface $calculator * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency - * @param array $excludeAdjustment + * @param array|null $excludeAdjustment + * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator */ public function __construct( SaleableInterface $saleableItem, $quantity, CalculatorInterface $calculator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - $excludeAdjustment = null + $excludeAdjustment = null, + CustomOptionPriceCalculator $customOptionPriceCalculator = null ) { parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->excludeAdjustment = $excludeAdjustment; + $this->customOptionPriceCalculator = $customOptionPriceCalculator + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); } /** * Get minimal and maximal option values * + * @param string $priceCode * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function getValue() + public function getValue($priceCode = \Magento\Catalog\Pricing\Price\BasePrice::PRICE_CODE) { $optionValues = []; $options = $this->product->getOptions(); @@ -85,7 +95,8 @@ public function getValue() } else { /** @var $optionValue \Magento\Catalog\Model\Product\Option\Value */ foreach ($optionItem->getValues() as $optionValue) { - $price = $optionValue->getPrice($optionValue->getPriceType() == Value::TYPE_PERCENT); + $price = + $this->customOptionPriceCalculator->getOptionPriceByPriceCode($optionValue, $priceCode); if ($min === null) { $min = $price; } elseif ($price < $min) { @@ -133,12 +144,13 @@ public function getCustomAmount($amount = null, $exclude = null, $context = []) * Return the minimal or maximal price for custom options * * @param bool $getMin + * @param string $priceCode * @return float */ - public function getCustomOptionRange($getMin) + public function getCustomOptionRange($getMin, $priceCode = \Magento\Catalog\Pricing\Price\BasePrice::PRICE_CODE) { $optionValue = 0.; - $options = $this->getValue(); + $options = $this->getValue($priceCode); foreach ($options as $option) { if ($getMin) { $optionValue += $option['min']; diff --git a/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php new file mode 100644 index 0000000000000..6e2fee656e368 --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/CustomOptionPriceCalculator.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Pricing\Price; + +use Magento\Catalog\Model\Product\Option\Value as ProductOptionValue; + +/** + * Calculates prices of custom options of the product. + */ +class CustomOptionPriceCalculator +{ + /** + * Calculates prices of custom option by code. + * + * Price is calculated depends on Price Code. + * Existing logic was taken from methods \Magento\Catalog\Model\Product\Option\Value::(getPrice|getRegularPrice) + * where $priceCode was hardcoded and changed to have dynamical approach. + * + * Examples of usage: + * \Magento\Catalog\Pricing\Price\CustomOptionPrice::getValue + * \Magento\Catalog\Model\Product\Option\Value::getPrice + * \Magento\Catalog\Model\Product\Option\Value::getRegularPrice + * + * @param ProductOptionValue $optionValue + * @param string $priceCode + * @return float|int + */ + public function getOptionPriceByPriceCode( + ProductOptionValue $optionValue, + string $priceCode = BasePrice::PRICE_CODE + ) { + if ($optionValue->getPriceType() === ProductOptionValue::TYPE_PERCENT) { + $basePrice = $optionValue->getOption()->getProduct()->getPriceInfo()->getPrice($priceCode)->getValue(); + $price = $basePrice * ($optionValue->getData(ProductOptionValue::KEY_PRICE) / 100); + return $price; + } + return $optionValue->getData(ProductOptionValue::KEY_PRICE); + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index 387ef9416ef68..a5e573caa381e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -29,8 +29,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +51,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { @@ -58,6 +62,6 @@ public function getAmount(SaleableInterface $saleableItem) return $value === null ? null - : $this->calculator->getAmount($value, $saleableItem); + : $this->calculator->getAmount($value, $saleableItem, 'tax'); } } diff --git a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php index 609255d852da3..2c4e332e71237 100644 --- a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php @@ -22,14 +22,14 @@ class RegularPrice extends AbstractPrice implements BasePriceProviderInterface /** * Get price value * - * @return float|bool + * @return float */ public function getValue() { if ($this->value === null) { $price = $this->product->getPrice(); $priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price); - $this->value = $priceInCurrentCurrency ? floatval($priceInCurrentCurrency) : false; + $this->value = $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : 0; } return $this->value; } diff --git a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php index b1bfc6ff4ad6f..77c48fdb1667e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php @@ -11,6 +11,7 @@ use Magento\Framework\Pricing\Price\AbstractPrice; use Magento\Framework\Pricing\Price\BasePriceProviderInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\WebsiteInterface; /** * Special price model @@ -46,6 +47,8 @@ public function __construct( } /** + * Retrieve special price. + * * @return bool|float */ public function getValue() @@ -96,19 +99,19 @@ public function getSpecialToDate() } /** - * @return bool + * @inheritdoc */ public function isScopeDateInInterval() { return $this->localeDate->isScopeDateInInterval( - $this->product->getStore(), + WebsiteInterface::ADMIN_CODE, $this->getSpecialFromDate(), $this->getSpecialToDate() ); } /** - * @return bool + * @inheritdoc */ public function isPercentageDiscount() { diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php index 893978a3b6b3b..f250927889c29 100644 --- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Pricing\Price; use Magento\Catalog\Model\Product; @@ -82,7 +80,7 @@ public function __construct( GroupManagementInterface $groupManagement, CustomerGroupRetrieverInterface $customerGroupRetriever = null ) { - $quantity = floatval($quantity) ? $quantity : 1; + $quantity = (float)$quantity ? $quantity : 1; parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->customerSession = $customerSession; $this->groupManagement = $groupManagement; diff --git a/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php index 0722f018ae4eb..d70000cfec761 100644 --- a/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/ConfiguredPriceBox.php @@ -7,12 +7,60 @@ namespace Magento\Catalog\Pricing\Render; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Catalog\Pricing\Price\ConfiguredPriceInterface; +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Framework\View\Element\Template\Context; /** * Class for configured_price rendering */ class ConfiguredPriceBox extends FinalPriceBox { + /** + * @var \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection + */ + private $configuredPriceSelection; + + /** + * @param Context $context + * @param SaleableInterface $saleableItem + * @param PriceInterface $price + * @param RendererPool $rendererPool + * @param array $data + * @param SalableResolverInterface|null $salableResolver + * @param MinimalPriceCalculatorInterface|null $minimalPriceCalculator + * @param \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection|null $configuredPriceSelection + */ + public function __construct( + Context $context, + SaleableInterface $saleableItem, + PriceInterface $price, + RendererPool $rendererPool, + array $data = [], + SalableResolverInterface $salableResolver = null, + MinimalPriceCalculatorInterface $minimalPriceCalculator = null, + \Magento\Catalog\Pricing\Price\ConfiguredPriceSelection $configuredPriceSelection = null + ) { + $this->configuredPriceSelection = $configuredPriceSelection + ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\Pricing\Price\ConfiguredPriceSelection::class); + parent::__construct( + $context, + $saleableItem, + $price, + $rendererPool, + $data, + $salableResolver, + $minimalPriceCalculator + ); + } + /** * Retrieve an item instance to the configured price model * @@ -34,4 +82,63 @@ protected function _prepareLayout() } return parent::_prepareLayout(); } + + /** + * {@inheritdoc} + */ + public function getPriceType($priceCode) + { + $price = $this->saleableItem->getPriceInfo()->getPrice($priceCode); + $item = $this->getData('item'); + if ($price instanceof \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface + && $item instanceof \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface) { + $price->setItem($item); + } + return $price; + } + + /** + * @return PriceInterface + */ + public function getConfiguredPrice() + { + /** @var \Magento\Bundle\Pricing\Price\ConfiguredPrice $configuredPrice */ + $configuredPrice = $this->getPrice(); + if (empty($this->configuredPriceSelection->getSelectionPriceList($configuredPrice))) { + // If there was no selection we must show minimal regular price + return $this->getSaleableItem()->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + } + + return $configuredPrice; + } + + /** + * @return PriceInterface + */ + public function getConfiguredRegularPrice() + { + /** @var \Magento\Bundle\Pricing\Price\ConfiguredPrice $configuredPrice */ + $configuredPrice = $this->getPriceType(ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE); + if (empty($this->configuredPriceSelection->getSelectionPriceList($configuredPrice))) { + // If there was no selection we must show minimal regular price + return $this->getSaleableItem()->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE); + } + + return $configuredPrice; + } + + /** + * Define if the special price should be shown + * + * @return bool + */ + public function hasSpecialPrice() + { + if ($this->price->getPriceCode() == ConfiguredPriceInterface::CONFIGURED_PRICE_CODE) { + $displayRegularPrice = $this->getConfiguredRegularPrice()->getAmount()->getValue(); + $displayFinalPrice = $this->getConfiguredPrice()->getAmount()->getValue(); + return $displayFinalPrice < $displayRegularPrice; + } + return parent::hasSpecialPrice(); + } } diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index f370c49cdfa20..e0a92ea0e0bea 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -115,7 +115,8 @@ protected function wrapResult($html) { return '<div class="price-box ' . $this->getData('css_classes') . '" ' . 'data-role="priceBox" ' . - 'data-product-id="' . $this->getSaleableItem()->getId() . '"' . + 'data-product-id="' . $this->getSaleableItem()->getId() . '" ' . + 'data-price-box="product-id-' . $this->getSaleableItem()->getId() . '"' . '>' . $html . '</div>'; } diff --git a/app/code/Magento/Catalog/Pricing/Render/PriceBox.php b/app/code/Magento/Catalog/Pricing/Render/PriceBox.php index 190168ed583fc..678b45ce97e7b 100644 --- a/app/code/Magento/Catalog/Pricing/Render/PriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/PriceBox.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Pricing\Render; use Magento\Catalog\Model\Product; @@ -71,7 +73,9 @@ public function jsonEncode($valueToEncode) * * @param int $length * @param string|null $chars + * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function getRandomString($length, $chars = null) { @@ -93,4 +97,21 @@ public function getCanDisplayQty(Product $product) } return true; } + + /** + * Format percent + * + * @param float $percent + * + * @return string + */ + public function formatPercent(float $percent): string + { + /*First rtrim - trim zeros. So, 10.00 -> 10.*/ + /*Second rtrim - trim dot. So, 10. -> 10*/ + return rtrim( + rtrim(number_format($percent, 2), '0'), + '.' + ); + } } diff --git a/app/code/Magento/Catalog/Setup/InstallData.php b/app/code/Magento/Catalog/Setup/InstallData.php index 5b1a10b098eb5..045ddd8a80c95 100644 --- a/app/code/Magento/Catalog/Setup/InstallData.php +++ b/app/code/Magento/Catalog/Setup/InstallData.php @@ -124,7 +124,6 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface // update attributes group and sort $attributes = [ 'custom_design' => ['group' => 'design', 'sort' => 10], - // 'custom_design_apply' => array('group' => 'design', 'sort' => 20), 'custom_design_from' => ['group' => 'design', 'sort' => 30], 'custom_design_to' => ['group' => 'design', 'sort' => 40], 'page_layout' => ['group' => 'design', 'sort' => 50], diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php index a96f58ecc046a..4df65437e3a77 100644 --- a/app/code/Magento/Catalog/Setup/InstallSchema.php +++ b/app/code/Magento/Catalog/Setup/InstallSchema.php @@ -674,7 +674,7 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, null, ['unsigned' => true, 'nullable' => false, 'default' => '0'], - 'Attriute Set ID' + 'Attribute Set ID' ) ->addColumn( 'parent_id', @@ -2077,6 +2077,13 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con ['unsigned' => true, 'nullable' => false, 'default' => '0'], 'Is Disabled' ) + ->addIndex( + $installer->getIdxName( + 'catalog_product_entity_media_gallery_value', + ['entity_id', 'value_id', 'store_id'] + ), + ['entity_id', 'value_id', 'store_id'] + ) ->addIndex( $installer->getIdxName('catalog_product_entity_media_gallery_value', ['store_id']), ['store_id'] diff --git a/app/code/Magento/Catalog/Setup/RecurringData.php b/app/code/Magento/Catalog/Setup/RecurringData.php new file mode 100644 index 0000000000000..1726df7c70ca1 --- /dev/null +++ b/app/code/Magento/Catalog/Setup/RecurringData.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Setup; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Framework\Setup\InstallDataInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * @inheritDoc + */ +class RecurringData implements InstallDataInterface +{ + + /** + * @var CategorySetupFactory + */ + private $categorySetupFactory; + + /** + * @param CategorySetupFactory $categorySetupFactory + */ + public function __construct(CategorySetupFactory $categorySetupFactory) + { + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritDoc + */ + public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $this->addCustomLayoutFileAttributes($setup); + } + + /** + * Add custom layout selector attributes. + * + * @param ModuleDataSetupInterface $setup + * @return void + */ + private function addCustomLayoutFileAttributes(ModuleDataSetupInterface $setup) + { + /** @var CategorySetup $eavSetup */ + $eavSetup = $this->categorySetupFactory->create(['setup' => $setup]); + $productAttr = $eavSetup->getAttribute(Product::ENTITY, 'custom_layout_update_file'); + if (!$productAttr) { + $eavSetup->addAttribute( + Product::ENTITY, + 'custom_layout_update_file', + [ + 'type' => 'varchar', + 'label' => 'Custom Layout Update', + 'input' => 'select', + 'source' => \Magento\Catalog\Model\Product\Attribute\Source\LayoutUpdate::class, + 'required' => false, + 'sort_order' => 51, + 'backend' => \Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate::class, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, + 'group' => 'Design', + 'is_used_in_grid' => false, + 'is_visible_in_grid' => false, + 'is_filterable_in_grid' => false + ] + ); + $eavSetup->updateAttribute( + Product::ENTITY, + 'custom_layout_update', + 'is_visible', + false + ); + } + + $categoryAttr = $eavSetup->getAttribute(Category::ENTITY, 'custom_layout_update_file'); + if (!$categoryAttr) { + $eavSetup->addAttribute( + Category::ENTITY, + 'custom_layout_update_file', + [ + 'type' => 'varchar', + 'label' => 'Custom Layout Update', + 'input' => 'select', + 'source' => \Magento\Catalog\Model\Category\Attribute\Source\LayoutUpdate::class, + 'required' => false, + 'sort_order' => 51, + 'backend' => \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::class, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, + 'group' => 'Custom Design', + 'is_used_in_grid' => false, + 'is_visible_in_grid' => false, + 'is_filterable_in_grid' => false + ] + ); + $eavSetup->updateAttribute( + Category::ENTITY, + 'custom_layout_update', + 'is_visible', + false + ); + } + } +} diff --git a/app/code/Magento/Catalog/Setup/UpgradeData.php b/app/code/Magento/Catalog/Setup/UpgradeData.php index a290d4870bd49..4e0be7396a166 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeData.php +++ b/app/code/Magento/Catalog/Setup/UpgradeData.php @@ -392,6 +392,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $this->upgradeWebsiteAttributes->upgrade($setup); } + if (version_compare($context->getVersion(), '2.2.5') < 0) { + $this->enableSegmentation($setup); + } + $setup->endSetup(); } @@ -436,4 +440,44 @@ private function changePriceAttributeDefaultScope($categorySetup) } } } + + /** + * @param ModuleDataSetupInterface $setup + * @return void + */ + private function enableSegmentation(ModuleDataSetupInterface $setup) + { + $catalogCategoryProductIndexColumns = array_keys( + $setup->getConnection()->describeTable($setup->getTable('catalog_category_product_index')) + ); + + $storeSelect = $setup->getConnection()->select()->from($setup->getTable('store'))->where('store_id > 0'); + foreach ($setup->getConnection()->fetchAll($storeSelect) as $store) { + $catalogCategoryProductIndexSelect = $setup->getConnection()->select() + ->from( + $setup->getTable('catalog_category_product_index') + )->where( + 'store_id = ?', + $store['store_id'] + ); + + $indexTable = $setup->getTable('catalog_category_product_index') . + '_' . + \Magento\Store\Model\Store::ENTITY . + $store['store_id']; + + $setup->getConnection()->query( + $setup->getConnection()->insertFromSelect( + $catalogCategoryProductIndexSelect, + $indexTable, + $catalogCategoryProductIndexColumns, + \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } + + $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index')); + $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index_replica')); + $setup->getConnection()->truncateTable($setup->getTable('catalog_category_product_index_tmp')); + } } diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php index 616bee43de00e..0483fd847df18 100755 --- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php +++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Setup; use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter; +use Magento\Catalog\Model\Product\Exception; use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; @@ -21,10 +22,13 @@ class UpgradeSchema implements UpgradeSchemaInterface /** * {@inheritdoc} * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); + if (version_compare($context->getVersion(), '2.0.1', '<')) { $this->addSupportVideoMediaAttributes($setup); $this->removeGroupPrice($setup); @@ -126,6 +130,23 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $this->fixCustomerGroupIdColumn($setup); } + if (version_compare($context->getVersion(), '2.2.4', '<')) { + $this->removeAttributeSetRelation($setup); + } + + if (version_compare($context->getVersion(), '2.2.5', '<')) { + $this->addGeneralIndexOnGalleryValueTable($setup); + } + + if (version_compare($context->getVersion(), '2.2.5', '<')) { + $this->enableSegmentation($setup); + } + + if (version_compare($context->getVersion(), '2.2.6', '<')) { + $this->addStoreIdFieldForWebsiteIndexTable($setup); + $this->removeIndexFromPriceIndexTable($setup); + } + $setup->endSetup(); } @@ -517,6 +538,7 @@ private function addSupportVideoMediaAttributes(SchemaSetupInterface $setup) ), 'value_id' ); + $this->addForeignKeys($setup); } @@ -699,4 +721,124 @@ private function addReplicaTable(SchemaSetupInterface $setup, $existingTable, $r ); $setup->getConnection()->query($sql); } + + /** + * Remove foreign key between catalog_product_entity and eav_attribute_set tables. + * Drop foreign key to delegate cascade on delete to plugin. + * @see \Magento\Catalog\Plugin\Model\AttributeSetRepository\RemoveProducts + * + * @param SchemaSetupInterface $setup + * @return void + */ + private function removeAttributeSetRelation(SchemaSetupInterface $setup) + { + $setup->getConnection()->dropForeignKey( + $setup->getTable('catalog_product_entity'), + $setup->getFkName('catalog_product_entity', 'attribute_set_id', 'eav_attribute_set', 'attribute_set_id') + ); + } + + /** + * Adds index for table catalog_product_entity_media_gallery_value + * It was added because it suits best for selecting media data for products + * + * @see \Magento\Catalog\Model\ResourceModel\Product\Gallery::createBatchBaseSelect + * @param SchemaSetupInterface $setup + * @return void + * @throws \Exception + */ + private function addGeneralIndexOnGalleryValueTable(SchemaSetupInterface $setup) + { + $existingKeys = $setup->getConnection()->getIndexList( + $setup->getTable(Gallery::GALLERY_VALUE_TABLE) + ); + + $newIndexName = $setup->getConnection()->getIndexName( + $setup->getTable(Gallery::GALLERY_VALUE_TABLE), + ['entity_id', 'value_id', 'store_id'] + ); + + if (!array_key_exists($newIndexName, $existingKeys)) { + $entityIdKeyName = $setup->getConnection()->getIndexName( + $setup->getTable(Gallery::GALLERY_VALUE_TABLE), + ['entity_id'] + ); + + if (array_key_exists($entityIdKeyName, $existingKeys)) { + $keyColumns = $existingKeys[$entityIdKeyName]['COLUMNS_LIST']; + $linkField = reset($keyColumns); + + $setup->getConnection()->addIndex( + $setup->getTable(Gallery::GALLERY_VALUE_TABLE), + $newIndexName, + [$linkField, 'value_id', 'store_id'] + ); + } + } + } + + /** + * @param SchemaSetupInterface $setup + * @return void + */ + private function enableSegmentation(SchemaSetupInterface $setup) + { + $storeSelect = $setup->getConnection()->select()->from($setup->getTable('store'))->where('store_id > 0'); + foreach ($setup->getConnection()->fetchAll($storeSelect) as $store) { + $indexTable = $setup->getTable('catalog_category_product_index') . + '_' . + \Magento\Store\Model\Store::ENTITY . + $store['store_id']; + + $setup->getConnection()->createTable( + $setup->getConnection()->createTableByDdl( + $setup->getTable('catalog_category_product_index'), + $indexTable + ) + ); + $setup->getConnection()->createTable( + $setup->getConnection()->createTableByDdl( + $setup->getTable('catalog_category_product_index'), + $indexTable . '_replica' + ) + ); + } + } + + /** + * @param SchemaSetupInterface $setup + */ + private function addStoreIdFieldForWebsiteIndexTable(SchemaSetupInterface $setup) + { + $setup->getConnection()->addColumn( + $setup->getTable('catalog_product_index_website'), + 'default_store_id', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + 'nullable' => false, + 'comment' => 'Default store id for website ' + ] + ); + } + + /** + * Table "catalog_product_index_price_tmp" used as template of "catalog_product_index_price" table + * for create temporary tables during indexation. Indexes are removed from performance perspective + * @param SchemaSetupInterface $setup + */ + private function removeIndexFromPriceIndexTable(SchemaSetupInterface $setup) + { + $setup->getConnection()->dropIndex( + $setup->getTable('catalog_product_index_price_tmp'), + $setup->getIdxName('catalog_product_index_price_tmp', ['customer_group_id']) + ); + $setup->getConnection()->dropIndex( + $setup->getTable('catalog_product_index_price_tmp'), + $setup->getIdxName('catalog_product_index_price_tmp', ['website_id']) + ); + $setup->getConnection()->dropIndex( + $setup->getTable('catalog_product_index_price_tmp'), + $setup->getIdxName('catalog_product_index_price_tmp', ['min_price']) + ); + } } diff --git a/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php b/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php index 3d300d9c849a9..05e4bb3817beb 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php +++ b/app/code/Magento/Catalog/Setup/UpgradeWebsiteAttributes.php @@ -148,6 +148,20 @@ private function processAttributeValues(ModuleDataSetupInterface $setup, array $ */ private function fetchAttributeValues(ModuleDataSetupInterface $setup, $tableName) { + $multipleStoresInWebsite = array_values( + array_reduce( + array_filter($this->getGroupedStoreViews($setup), function ($storeViews) { + return is_array($storeViews) && count($storeViews) > 1; + }), + 'array_merge', + [] + ) + ); + + if (count($multipleStoresInWebsite) < 1) { + return []; + } + $connection = $setup->getConnection(); $batchSelectIterator = $this->batchQueryGenerator->generate( 'value_id', @@ -158,27 +172,18 @@ private function fetchAttributeValues(ModuleDataSetupInterface $setup, $tableNam '*' ) ->join( - [ - 'cea' => $setup->getTable('catalog_eav_attribute'), - ], + ['cea' => $setup->getTable('catalog_eav_attribute')], 'cpei.attribute_id = cea.attribute_id', '' ) ->join( - [ - 'st' => $setup->getTable('store'), - ], + ['st' => $setup->getTable('store')], 'st.store_id = cpei.store_id', 'st.website_id' ) - ->where( - 'cea.is_global = ?', - self::ATTRIBUTE_WEBSITE - ) - ->where( - 'cpei.store_id <> ?', - self::GLOBAL_STORE_VIEW_ID - ) + ->where('cea.is_global = ?', self::ATTRIBUTE_WEBSITE) + ->where('cpei.store_id IN (?)', $multipleStoresInWebsite), + 1000 ); foreach ($batchSelectIterator as $select) { @@ -201,17 +206,15 @@ private function getGroupedStoreViews(ModuleDataSetupInterface $setup) ->select() ->from( $setup->getTable('store'), - '*' - ); + ['store_id', 'website_id'] + )->where('store_id <> ?', self::GLOBAL_STORE_VIEW_ID); - $storeViews = $connection->fetchAll($query); + $storeViews = $connection->fetchPairs($query); $this->groupedStoreViews = []; - foreach ($storeViews as $storeView) { - if ($storeView['store_id'] != 0) { - $this->groupedStoreViews[$storeView['website_id']][] = $storeView['store_id']; - } + foreach ($storeViews as $storeId => $websiteId) { + $this->groupedStoreViews[$websiteId][] = $storeId; } return $this->groupedStoreViews; diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml new file mode 100644 index 0000000000000..23ebf89bf7c95 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddSimpleProductToCart"> + <arguments> + <argument name="product" defaultValue="product"/> + </arguments> + <amOnPage stepKey="navigateProductPage" url="/{{product.name}}.html"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click stepKey="addToCart" selector="{{StorefrontProductPageSection.addToCartBtn}}"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> + </actionGroup> + + <!--Click Add to Cart button in storefront product page--> + <actionGroup name="addToCartFromStorefrontProductPage"> + <arguments> + <argument name="productName"/> + </arguments> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> + <waitForElementNotVisible selector="{{StorefrontProductPageSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> + <waitForElementNotVisible selector="{{StorefrontProductPageSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.addToCartButtonTitleIsAddToCart}}" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForProductAddedMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> + </actionGroup> + + <actionGroup name="StorefrontAddProductToCartQuantityActionGroup" extends="addToCartFromStorefrontProductPage"> + <arguments> + <argument name="quantity" type="string" defaultValue="1"/> + </arguments> + <waitForElementVisible selector="{{StorefrontProductPageSection.qtyInput}}" time="30" before="addToCart" stepKey="waitQuantityFieldVisible"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{quantity}}" after="waitQuantityFieldVisible" stepKey="fillQuantityField"/> + </actionGroup> + + <actionGroup name="AddSimpleProductToCartWithUrlKeyActionGroup" extends="AddSimpleProductToCart"> + <amOnPage url="{{StorefrontProductPage.url(product.custom_attributes[url_key])}}" stepKey="navigateProductPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml new file mode 100644 index 0000000000000..45fe419ad14fe --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAnchorCategoryActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAnchorCategoryActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <!--Open Category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Enable Anchor for category --> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml new file mode 100644 index 0000000000000..2594a963767f1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssignCategoryToProductAndSaveActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <!-- on edit Product page catalog/product/edit/id/{{product_id}}/ --> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> + <checkOption selector="{{AdminProductFormSection.selectCategory(categoryName)}}" stepKey="selectCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> + <waitForPageLoad stepKey="waitForApplyCategory"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSavingProduct"/> + <see userInput="You saved the product." selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml new file mode 100644 index 0000000000000..eb19ab11f2c76 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assign Image role in admin product page --> + <actionGroup name="AdminAssignImageRolesActionGroup"> + <arguments> + <argument name="image"/> + </arguments> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggleState('closed')}}" dependentSelector="{{AdminProductImagesSection.productImagesToggleState('open')}}" visible="false" stepKey="clickSectionImage"/> + <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> + <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> + <checkOption selector="{{AdminProductImagesSection.roleImage('Base')}}" stepKey="checkRoleBase"/> + <checkOption selector="{{AdminProductImagesSection.roleImage('Small')}}" stepKey="checkRoleSmall"/> + <checkOption selector="{{AdminProductImagesSection.roleImage('Thumbnail')}}" stepKey="checkRoleThumbnail"/> + <checkOption selector="{{AdminProductImagesSection.roleImage('Swatch')}}" stepKey="checkRoleSwatch"/> + <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml new file mode 100644 index 0000000000000..c8a525f367a8e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Create a new category--> + <actionGroup name="CreateCategory"> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterCategoryName"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> + </actionGroup> + + <!--Actions to delete category--> + <actionGroup name="DeleteCategory"> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="clickCategoryLink"/> + <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.message}}" stepKey="waitForConfirmationModal"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="Are you sure you want to delete this category?" stepKey="seeDeleteConfirmationMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad time="60" stepKey="waitForDeleteToFinish"/> + <see selector="You deleted the category." stepKey="seeDeleteSuccess"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="dontSeeCategoryInTree"/> + </actionGroup> + + <!--Actions to switch store view in category edit page--> + <actionGroup name="switchCategoryStoreView"> + <arguments> + <argument name="store"/> + <argument name="catName"/> + </arguments> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(catName)}}" stepKey="navigateToCreatedCategory"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <scrollToTopOfPage stepKey="scrollToToggle"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewDropdownToggle}}" stepKey="openStoreViewDropDown"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewOption(store)}}" stepKey="selectStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" stepKey="selectStoreViewAccept"/> + <waitForPageLoad stepKey="waitForStoreViewChangeLoad"/> + </actionGroup> + + <!-- Go to admin category page by id --> + <actionGroup name="goToAdminCategoryPageById"> + <arguments> + <argument name="id" type="string"/> + </arguments> + <amOnPage url="{{AdminCategoryEditPage.url(id)}}" stepKey="amOnAdminCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="{{id}}" stepKey="seeCategoryPageTitle"/> + </actionGroup> + + <!--Open tab "Products in Category" if she closed--> + <actionGroup name="OpenProductsInCategorySection"> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSection"/> + <conditionalClick selector="{{AdminCategoryProductsSection.sectionHeader}}" dependentSelector="{{AdminCategoryProductsSection.tabProductClosed}}" visible="true" stepKey="openProductsInCategory"/> + <waitForPageLoad time="60" stepKey="waitForPageLoad"/> + </actionGroup> + + <actionGroup name="SeeProductInProductCategoryGridForCurrentCategory"> + <arguments> + <argument name="product"/> + </arguments> + <see selector="{{AdminCategoryProductsGridSection.nameColumn}}" userInput="{{product.name}}" stepKey="seeProductNameInGrid"/> + <see selector="{{AdminCategoryProductsGridSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> + <see selector="{{AdminCategoryProductsGridSection.priceColumn}}" userInput="{{product.price}}" stepKey="seeProductPriceInGrid"/> + </actionGroup> + + <actionGroup name="AdminNavigateToCategoryInTree"> + <arguments> + <argument name="category"/> + </arguments> + <amOnPage url="{{AdminCategoryPage.page}}" stepKey="amOnCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> + <waitForPageLoad stepKey="waitForTreeToExpand"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(category.name)}}" stepKey="navigateToCreatedCategory" /> + <waitForPageLoad stepKey="waitForCategoryPageLoaded"/> + </actionGroup> + + <actionGroup name="ChangeSeoUrlKey"> + <arguments> + <argument name="value" type="string"/> + </arguments> + <conditionalClick selector="{{AdminCategorySEOSection.SectionHeader}}" dependentSelector="{{AdminCategorySEOSection.UrlKeyInput}}" visible="false" stepKey="openSeoSection"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{value}}" stepKey="enterURLKey"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="assertSuccessMessage"/> + </actionGroup> + + <actionGroup name="ChangeSeoUrlKeyForSubCategory" extends="ChangeSeoUrlKey"> + <arguments> + <argument name="value" type="string"/> + </arguments> + <uncheckOption selector="{{AdminCategorySEOSection.urlKeyDefaultValueCheckbox}}" before="enterURLKey" stepKey="uncheckDefaultValue"/> + </actionGroup> + + <!-- Save category form --> + <actionGroup name="saveCategoryForm"> + <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="assertSuccess"/> + </actionGroup> + + <actionGroup name="DeleteDefaultCategoryChildren"> + <annotations> + <description>Deletes all children categories of Default Root Category.</description> + </annotations> + + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategoryPage"/> + <executeInSelenium function="function ($webdriver) use ($I) { + $children = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a')); + while (!empty($children)) { + $I->click('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a'); + $I->waitForPageLoad(30); + $I->click('#delete'); + $I->waitForElementVisible('aside.confirm .modal-footer button.action-accept'); + $I->click('aside.confirm .modal-footer button.action-accept'); + $I->waitForPageLoad(30); + $I->waitForElementVisible('#messages div.message-success', 30); + $I->see('You deleted the category.', '#messages div.message-success'); + $children = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::xpath('//ul[contains(@class, \'x-tree-node-ct\')]/li[@class=\'x-tree-node\' and contains(., + \'{{DefaultCategory.name}}\')]/ul[contains(@class, \'x-tree-node-ct\')]/li//a')); + } + }" stepKey="deleteAllChildCategories"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml new file mode 100644 index 0000000000000..11b5aabf0cee9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Action to delete product attribute--> + <actionGroup name="DeleteProductAttribute"> + <arguments> + <argument name="productAttribute"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributesGridPage"/> + <waitForPageLoad time="30" stepKey="waitForProductAttributesGridPageLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersBeforeDelete"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" + userInput="{{productAttribute.default_label}}" stepKey="fillAttributeDefaultLabelInput"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="applyFilters"/> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad"/> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="deleteProductAttribute"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitingForWarningModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="navigateToProductAttribute"> + <arguments> + <argument name="attributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributeGridPageLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.filterByAttributeCode}}" userInput="{{attributeCode}}" stepKey="fillAttributeCodeFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductAttributeGridSection.attributeCode(attributeCode)}}" stepKey="navigateToAttributeEditPage" /> + <waitForPageLoad stepKey="waitForAttributeEditPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml new file mode 100644 index 0000000000000..d2301abc2085e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRootCategoryActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Create a new root category--> + <actionGroup name="AdminCreateRootCategoryActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="NewRootCategory"/> + </arguments> + <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddRootCategoryButton}}" stepKey="clickOnAddRootCategoryButton"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterNewRootCategoryName"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear" /> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml new file mode 100644 index 0000000000000..b7c455796aea1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateRecentlyProductsWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <fillField selector="{{AdminCatalogProductWidgetSection.productsToDisplay}}" userInput="{{widget.products_to_display}}" stepKey="fillNumberOfProductsToDisplay"/> + <selectOption selector="{{AdminCatalogProductWidgetSection.productAttributesToShow}}" parameterArray="['Name', 'Image', 'Price']" stepKey="selectAllProductAttributes"/> + <selectOption selector="{{AdminCatalogProductWidgetSection.productButtonsToShow}}" parameterArray="['Add to Cart', 'Add to Compare', 'Add to Wishlist']" stepKey="selectAllProductButtons"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductBySkuActionGroup.xml new file mode 100644 index 0000000000000..9b9b269c2601e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductBySkuActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Delete a product by filtering grid and using delete action--> + <actionGroup name="AdminDeleteProductBySkuActionGroup"> + <arguments> + <argument name="sku" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{sku}}" stepKey="seeProductSkuInGrid"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="record(s) have been deleted." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminGenerateProductConfigurationsByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminGenerateProductConfigurationsByAttributeCodeActionGroup.xml new file mode 100644 index 0000000000000..73b55ce3dd8eb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminGenerateProductConfigurationsByAttributeCodeActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGenerateProductConfigurationsByAttributeCodeActionGroup"> + <arguments> + <argument name="attributeCode" type="string" defaultValue="SomeString"/> + </arguments> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="99" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml new file mode 100644 index 0000000000000..e9b244d4cf223 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetByNameActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenAttributeSetByNameActionGroup"> + <arguments> + <argument name="attributeSetName" type="string" defaultValue="Default"/> + </arguments> + <click selector="{{AdminProductAttributeSetGridSection.attributeSetName(attributeSetName)}}" stepKey="chooseAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml new file mode 100644 index 0000000000000..c6f0c3332b1d5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenAttributeSetGridPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenAttributeSetGridPageActionGroup"> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetPage"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml new file mode 100644 index 0000000000000..ca1303f180ca4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenProductIndexPageActionGroup"> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndexPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml new file mode 100644 index 0000000000000..1a9c7a911d528 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -0,0 +1,357 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Navigate to create product page from product grid page--> + <actionGroup name="goToCreateProductPage"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForElementVisible selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="waitForAddProductDropdown" time="30"/> + <click selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="clickAddProductType"/> + <waitForPageLoad time="30" stepKey="waitForCreateProductPageLoad"/> + <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> + </actionGroup> + + <!--Fill main fields in create product form with name and sku --> + <actionGroup name="fillProductNameAndSkuInProductForm"> + <arguments> + <argument name="product"/> + </arguments> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> + </actionGroup> + + <!--Fill main fields in create product form--> + <actionGroup name="fillMainProductForm"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="selectStockStatus"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeight"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="fillProductWeight"/> + </actionGroup> + + <!--Fill main fields in create product form with no weight, useful for virtual and downloadable products --> + <actionGroup name="fillMainProductFormNoWeight"> + <arguments> + <argument name="product" defaultValue="DownloadableProduct"/> + </arguments> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="selectStockStatus"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has no weight" stepKey="selectWeight"/> + </actionGroup> + + <!--Save product and see success message--> + <actionGroup name="saveProductForm"> + <annotations> + <description>Clicks on the Save button. Validates that the Success Message is present and correct.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollTopPageProduct"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitProductSaveSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> + + <!-- Save product but do not expect a success message --> + <actionGroup name="SaveProductFormNoSuccessCheck" extends="saveProductForm"> + <remove keyForRemoval="waitProductSaveSuccessMessage"/> + <remove keyForRemoval="seeSaveConfirmation"/> + </actionGroup> + + <!--Upload image for product--> + <actionGroup name="addProductImage"> + <arguments> + <argument name="image" defaultValue="ProductImage"/> + </arguments> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesSection"/> + <waitForPageLoad time="30" stepKey="waitForPageRefresh"/> + <waitForElementVisible selector="{{AdminProductImagesSection.imageUploadButton}}" stepKey="seeImageSectionIsReady"/> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="{{image.file}}" stepKey="uploadFile"/> + <waitForElementNotVisible selector="{{AdminProductImagesSection.uploadProgressBar}}" stepKey="waitForUpload"/> + <waitForElementVisible selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="waitForThumbnail"/> + </actionGroup> + + <!--Add special price to product in Admin product page--> + <actionGroup name="AddSpecialPriceToProductActionGroup"> + <arguments> + <argument name="price" type="string" defaultValue="8"/> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollToTopOfPage stepKey="initScrollToTopOfThePage"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice"/> + <conditionalClick selector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" dependentSelector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" visible="true" stepKey="checkUseDefault"/> + <fillField userInput="{{price}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> + <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> + </actionGroup> + + <!--Set product to website--> + <actionGroup name="ProductSetWebsite"> + <arguments> + <argument name="website"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.website(website.name)}}" visible="false" stepKey="clickToOpenProductInWebsite"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <click selector="{{ProductInWebsitesSection.website(website.name)}}" stepKey="selectWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + </actionGroup> + + <actionGroup name="ProductSetAdvancedPricing"> + <arguments> + <argument name="website" type="string" defaultValue="All Websites [USD]"/> + <argument name="group" type="string" defaultValue="Retailer"/> + <argument name="quantity" type="string" defaultValue="1"/> + <argument name="price" type="string" defaultValue="Discount"/> + <argument name="amount" type="string" defaultValue="45"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfThePagePreventHeaderOverlap"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" stepKey="waitForSelectCustomerGroupNameAttribute"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{website}}" stepKey="selectProductWebsiteValue"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{group}}" stepKey="selectProductCustomGroupValue"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{quantity}}" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="{{price}}" stepKey="selectProductTierPriceValueType"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="{{amount}}" stepKey="selectProductTierPricePriceInput"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <waitForPageLoad stepKey="waitForProductSave"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad time="60" stepKey="waitForProductSave1"/> + <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> + + <!--Switch to New Store view--> + <actionGroup name="SwitchToTheNewStoreView"> + <arguments> + <argument name="storeViewName"/> + </arguments> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToUp"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="waitForElementBecomeVisible"/> + <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(storeViewName.name)}}" stepKey="chooseStoreView"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + + <!--Select Product In Websites--> + <actionGroup name="SelectProductInWebsitesActionGroup"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenProductInWebsite"/> + <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + </actionGroup> + + <!--Remove product image--> + <actionGroup name="RemoveProductImage"> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesSection"/> + <waitForPageLoad time="30" stepKey="waitForPageRefresh"/> + <click selector="{{AdminProductImagesSection.removeImageButton}}" stepKey="clickRemoveImage"/> + </actionGroup> + + <!-- Assert no product image in Admin Product page --> + <actionGroup name="AssertProductImageNotInAdminProductPage"> + <arguments> + <argument name="image" defaultValue="MagentoLogo" /> + </arguments> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dontSeeElement selector="{{AdminProductImagesSection.imageFile(image.filename)}}" stepKey="seeImage"/> + </actionGroup> + + <!--Check tier price with a discount percentage on product--> + <actionGroup name="AssertDiscountsPercentageOfProduct"> + <arguments> + <argument name="amount" type="string" defaultValue="45"/> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <grabValueFrom selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="grabProductTierPriceInput"/> + <assertEquals stepKey="assertProductTierPriceInput"> + <expectedResult type="string">{{amount}}</expectedResult> + <actualResult type="string">$grabProductTierPriceInput</actualResult> + </assertEquals> + </actionGroup> + + <actionGroup name="CreatedProductConnectToWebsite"> + <arguments> + <argument name="website"/> + <argument name="product"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductGridPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="openProduct"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openWebsitesList"/> + <click selector="{{ProductInWebsitesSection.website(website.name)}}" stepKey="selectWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> + + <!--Create simple product and assign to category in Admin--> + <actionGroup name="AdminCreateSimpleProductAndAssignToCategory"> + <arguments> + <argument name="category"/> + <argument name="simpleProduct"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="assertSaveMessageSuccess"/> + <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> + <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> + <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> + <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> + </actionGroup> + + <!--Create a Simple Product--> + <actionGroup name="CreateSimpleProductAndAddToWebsite"> + <arguments> + <argument name="product"/> + <argument name="website" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillProductSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillProductPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillProductQuantity"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> + <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForProductPageSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> + + <actionGroup name="AdminAssignProductToCategory" extends="AdminProductAssignCategory"> + <arguments> + <argument name="productId" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" before="searchAndSelectCategory" stepKey="amOnPage"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" after="searchAndSelectCategory" stepKey="clickOnSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" after="clickOnSaveButton" stepKey="waitForSaveProductMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." after="waitForSaveProductMessage" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="AdminChangeProductAttributeSet"> + <arguments> + <argument name="attributeSet"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttributeSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSet.attribute_set_name}}" stepKey="searchForAttributeSet"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeSetFilterResultByName(attributeSet.attribute_set_name)}}" stepKey="waitForNewAttributeSetIsShown"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResultByName(attributeSet.attribute_set_name)}}" stepKey="selectAttributeSet"/> + </actionGroup> + + <actionGroup name="AdminProductAssignCategory"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="searchAndSelectCategory"/> + </actionGroup> + + <!--Navigate to created product page directly via ID--> + <actionGroup name="goToProductPageViaID"> + <arguments> + <argument name="productId" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="goToProduct"/> + </actionGroup> + + <!-- This action group goes to the product index page, opens the drop down and clicks the specified product type for adding a product --> + <actionGroup name="GoToSpecifiedCreateProductPage"> + <arguments> + <argument type="string" name="productType" defaultValue="simple"/> + </arguments> + <comment userInput="actionGroup:GoToSpecifiedCreateProductPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addTypeProduct(productType)}}" stepKey="clickAddProduct"/> + <waitForPageLoad stepKey="waitForFormToLoad"/> + </actionGroup> + + <!-- Change any product data product description You should be on product page --> + <actionGroup name="AdminChangeProductDescriptionActionGroup"> + <arguments> + <argument name="description" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductContentSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeader}}" visible="true" stepKey="openDescriptionDropDown"/> + <fillField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="{{description}}" stepKey="fillLongDescription"/> + </actionGroup> + <!-- Change any product data product short description You should be on product page --> + <actionGroup name="AdminChangeProductShortDescriptionActionGroup" extends="AdminChangeProductDescriptionActionGroup"> + <remove keyForRemoval="fillLongDescription"/> + <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="{{description}}" stepKey="fillShortDescription"/> + </actionGroup> + + <!-- This action group simply navigates to the product catalog page --> + <actionGroup name="AdminGoToProductCatalogPage"> + <comment userInput="actionGroup:GoToProductCatalogPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> + + <!-- You are on product Edit Page --> + <!-- Assert checkbox available for website in Product In Websites --> + <actionGroup name="AdminAssertWebsiteIsAvailableInProductWebsites"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToProductInWebsitesSection"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.sectionHeaderOpened}}" visible="false" stepKey="expandProductWebsitesSection"/> + <seeElement selector="{{ProductInWebsitesSection.website(website)}}" stepKey="seeCheckboxForWebsite"/> + </actionGroup> + + <!-- You are on product Edit Page --> + <!-- Assert checkbox not available for website in Product In Websites --> + <actionGroup name="AdminAssertWebsiteIsNotAvailableInProductWebsites" extends="AdminAssertWebsiteIsAvailableInProductWebsites"> + <remove keyForRemoval="seeCheckboxForWebsite"/> + <dontSeeElement selector="{{ProductInWebsitesSection.website(website)}}" after="expandProductWebsitesSection" stepKey="dontSeeCheckboxForWebsite"/> + </actionGroup> + + <!-- You are on product Edit Page --> + <!-- Assert checkbox Is checked for website in Product In Websites --> + <actionGroup name="AdminAssertProductIsAssignedToWebsite" extends="AdminAssertWebsiteIsAvailableInProductWebsites"> + <remove keyForRemoval="seeCheckboxForWebsite"/> + <seeCheckboxIsChecked selector="{{ProductInWebsitesSection.website(website)}}" after="expandProductWebsitesSection" stepKey="seeCustomWebsiteIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml new file mode 100644 index 0000000000000..30aa39901cb57 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="navigateToEditProductAttribute"> + <arguments> + <argument name="attributeLabel" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" userInput="{{attributeLabel}}" stepKey="navigateToAttributeEditPage1" /> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="navigateToAttributeEditPage2" /> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="navigateToAttributeEditPage3" /> + </actionGroup> + <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> + <arguments> + <argument name="useForPromoRule" type="string" defaultValue="Yes"/> + </arguments> + <click selector="{{AdminEditAttributeStorefrontPropertiesSection.storeFrontPropertiesTab}}" stepKey="clickStoreFrontPropertiesTab"/> + <waitForElementVisible selector="{{AdminEditAttributeStorefrontPropertiesSection.useForPromoRuleConditions}}" stepKey="waitForUseForPromoRuleConditionsVisible"/> + <selectOption selector="{{AdminEditAttributeStorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="{{useForPromoRule}}" stepKey="changeOption"/> + <click selector="{{AttributePropertiesSection.save}}" stepKey="saveAttribute"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the product attribute." stepKey="successMessage"/> + </actionGroup> + <actionGroup name="navigateToProductAttributeByCode"> + <arguments> + <argument name="attributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributesPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.filterByAttributeCode}}" userInput="{{attributeCode}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.attributeCode(attributeCode)}}" stepKey="clickRowToEdit"/> + <waitForPageLoad stepKey="waitForColorAttributePageLoad"/> + </actionGroup> + <!--Save product attribute and see success message--> + <actionGroup name="SaveProductAttribute"> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveAttribute"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="deleteProductAttribute"> + <arguments> + <argument name="ProductAttribute"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributeGridPageLoad"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterAttributeCode}}" + userInput="{{ProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForAttributeEditPageLoad" /> + <click selector="{{AttributePropertiesSection.deleteAttribute}}" stepKey="deleteAttribute"/> + <waitForElement selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForDeleteConfirmation"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to do this?" stepKey="seeConfirmationMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDeleteAttribute"/> + <waitForPageLoad stepKey="waitForPageLoadAfterDeleteAttribute"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the product attribute." stepKey="seeDeleteSuccessMessage"/> + </actionGroup> + <actionGroup name="navigateToCreatedProductAttribute"> + <arguments> + <argument name="productAttribute"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributesGridPageLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <waitForPageLoad stepKey="waitForAttributesGridPageLoad1"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterAttributeCode}}" + userInput="{{productAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForAttributePageLoad" /> + </actionGroup> + <actionGroup name="StartCreateProductAttribute"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="attributeType" type="string" defaultValue="select"/> + </arguments> + <amOnPage url="{{AdminProductAttributeNewPage.url}}" stepKey="goToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForProductAttributeNewPageLoad"/> + <fillField selector="{{AttributePropertiesSection.defaultLabel}}" userInput="{{attributeCode}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.inputType}}" userInput="{{attributeType}}" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminNewAttributePanelSection.addOption}}" stepKey="waitForElementVisible"/> + </actionGroup> + <actionGroup name="AdminFillProductAttributePropertiesActionGroup" extends="StartCreateProductAttribute"> + <remove keyForRemoval="waitForElementVisible"/> + </actionGroup> + <actionGroup name="AddOptionToProductAttribute"> + <arguments> + <argument name="optionName" type="string"/> + <argument name="optionNumber" type="string"/> + </arguments> + <click selector="{{AdminNewAttributePanelSection.addOption}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminNewAttributePanelSection.optionAdminValue('optionNumber')}}" time="30" stepKey="waitForOptionRow"/> + <fillField selector="{{AdminNewAttributePanelSection.optionAdminValue('optionNumber')}}" userInput="{{optionName}}" stepKey="fillAdminLabel"/> + </actionGroup> + <actionGroup name="SetScopeToProductAttribute"> + <arguments> + <argument name="scope" type="string" defaultValue="1"/> + </arguments> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.scope}}" userInput="{{scope}}" stepKey="selectGlobalScope"/> + </actionGroup> + <actionGroup name="SetUseInLayeredNavigationToProductAttribute"> + <arguments> + <argument name="useInLayeredNavigation" type="string" defaultValue="1"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{StorefrontPropertiesSection.storefrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{useInLayeredNavigation}}" stepKey="selectUseInLayeredNavigation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml new file mode 100644 index 0000000000000..9817e24bed963 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssignAttributeToGroup"> + <arguments> + <argument name="group" type="string"/> + <argument name="attribute"/> + </arguments> + <conditionalClick selector="{{AdminProductAttributeSetEditSection.attributeGroupExtender(group)}}" dependentSelector="{{AdminProductAttributeSetEditSection.attributeGroupCollapsed(group)}}" visible="true" stepKey="extendGroup"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <dragAndDrop selector1="{{AdminProductAttributeSetEditSection.unassignedAttribute(attribute)}}" selector2="{{AdminProductAttributeSetEditSection.lineItemAttributeGroup(group, '1')}}" stepKey="dragAndDropToGroupProductDetails"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <see userInput="{{attribute}}" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + </actionGroup> + <actionGroup name="UnassignAttributeFromGroup"> + <arguments> + <argument name="group" type="string"/> + <argument name="attribute" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductAttributeSetEditSection.attributeGroupExtender(group)}}" dependentSelector="{{AdminProductAttributeSetEditSection.attributeGroupCollapsed(group)}}" visible="true" stepKey="extendGroup"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <dragAndDrop selector1="{{AdminProductAttributeSetEditSection.assignedAttribute(attribute)}}" selector2="{{AdminProductAttributeSetEditSection.lineItemUnassignedAttribute('1')}}" stepKey="dragAndDropToUnassigned"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <see userInput="{{attribute}}" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> + </actionGroup> + <actionGroup name="SaveAttributeSet"> + <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickSave"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + </actionGroup> + <actionGroup name="CreateAttributeSet"> + <arguments> + <argument name="attributeSetName" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <click selector="{{AdminProductAttributeSetGridSection.addAttributeSetBtn}}" stepKey="clickAddAttributeSet"/> + <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{attributeSetName}}" stepKey="fillName"/> + <selectOption selector="{{AdminProductAttributeSetSection.basedOn}}" userInput="Default" stepKey="changeOption"/> + <click selector="{{AdminProductAttributeSetSection.save}}" stepKey="clickSaveAttributeSet"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + </actionGroup> + <actionGroup name="AssignProductToAttributeSet"> + <arguments> + <argument name="attributeSetName" type="string"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSetName}}" stepKey="searchForAttrSet"/> + <waitForAjaxLoad stepKey="waitForLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml new file mode 100644 index 0000000000000..640dd9ae6d264 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCheckUnsupportedFileActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductCheckUnsupportedFileActionGroup"> + <arguments> + <argument name="filename" type="string"/> + </arguments> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="{{filename}}" stepKey="attachImage"/> + <waitForPageLoad stepKey="waitForUploadImage"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="We don't recognize or support this file extension type." stepKey="seeErrorMessage"/> + <click selector="{{AdminProductImagesSection.modalOkBtn}}" stepKey="closeModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml new file mode 100644 index 0000000000000..d10c10827f4cd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Filter the product grid by new from date filter--> + <actionGroup name="filterProductGridBySetNewFromDate"> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.newFromDateFilter}}" userInput="05/16/2018" stepKey="fillSetAsNewProductFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + </actionGroup> + + <!--Filter the product grid by the SKU field--> + <actionGroup name="filterProductGridBySku"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + </actionGroup> + + <!--Filter the product grid by the Name field--> + <actionGroup name="filterProductGridByName"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + </actionGroup> + + <!--Delete a product by filtering grid and using delete action--> + <actionGroup name="deleteProductUsingProductGrid"> + <arguments> + <argument name="product"/> + <argument name="productCount" type="string" defaultValue="1"/> + </arguments> + <!--TODO use other action group for filtering grid when MQE-539 is implemented --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of {{productCount}} record(s) have been deleted." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + </actionGroup> + + <actionGroup name="DeleteProductByName" extends="deleteProductUsingProductGrid"> + <arguments> + <argument name="product" type="string"/> + </arguments> + <remove keyForRemoval="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product}}" stepKey="fillProductSkuFilter" after="openProductFilters"/> + <remove keyForRemoval="seeProductSkuInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{product}}" stepKey="seeProductNameInGrid" after="clickApplyFilters"/> + </actionGroup> + + <!--Disabled a product by filtering grid and using change status action--> + <actionGroup name="ChangeStatusProductUsingProductGridActionGroup"> + <arguments> + <argument name="product"/> + <argument name="status" defaultValue="Enable" type="string" /> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickChangeStatusAction"/> + <click selector="{{AdminProductGridSection.changeStatus('status')}}" stepKey="clickChangeStatusDisabled"/> + <waitForPageLoad stepKey="waitForStatusToBeChanged"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) have been updated." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + </actionGroup> + + <!-- Sort products by ID descending --> + <actionGroup name="sortProductsByIdDescending"> + <conditionalClick selector="{{AdminProductGridTableHeaderSection.id('ascend')}}" dependentSelector="{{AdminProductGridTableHeaderSection.id('descend')}}" visible="false" stepKey="sortById"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + + <!--Filter and select the the product --> + <actionGroup name="filterAndSelectProduct"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{productSku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku(productSku)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForProductTitle"/> + </actionGroup> + + <actionGroup name="DeleteAllProducts"> + <conditionalClick selector="{{AdminProductGridSection.multicheckDropdown}}" dependentSelector="{{AdminDataGridTableSection.firstRow}}" visible="true" stepKey="openMulticheckDropdown"/> + <conditionalClick selector="{{AdminProductGridSection.multicheckOption('Select All')}}" dependentSelector="{{AdminDataGridTableSection.firstRow}}" visible="true" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModalPopUp"/> + <grabTextFrom selector="{{AdminConfirmationModalSection.message}}" stepKey="grabConfirmationMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <executeInSelenium function="function () use ($I, $grabConfirmationMessage) { + if ($grabConfirmationMessage !== 'You haven\'t selected any items!') { + $I->waitForElementVisible('#messages div.message-success'); + $I->see('record(s) have been deleted.', '#messages div.message-success'); + } + }" stepKey="waitSuccessMessage"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml new file mode 100644 index 0000000000000..8d3b9df6c2e15 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminResetProductGridToDefaultViewActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Reset the product grid to the default view--> + <actionGroup name="AdminResetProductGridToDefaultViewActionGroup"> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> + <waitForElementVisible selector="{{AdminProductGridFilterSection.viewBookmark('Default View')}}" time="10" stepKey="waitForViewBookmarkElementVisible"/> + <click selector="{{AdminProductGridFilterSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> + <see selector="{{AdminProductGridFilterSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml new file mode 100644 index 0000000000000..8da2b2226735a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUnassignCategoryOnProductAndSaveActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <!-- on edit Product page catalog/product/edit/id/{{product_id}}/ --> + <click selector="{{AdminProductFormSection.unselectCategories(categoryName)}}" stepKey="clearCategory"/> + <waitForPageLoad stepKey="waitForDelete"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSavingProduct"/> + <see userInput="You saved the product." selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml new file mode 100644 index 0000000000000..b419597455152 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey"> + <arguments> + <argument name="product"/> + </arguments> + <!-- Go to storefront product page, assert product name and sku --> + <amOnPage url="{{product.custom_attributes[url_key]}}.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInTitle userInput="{{product.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + </actionGroup> + <actionGroup name="AssertProductNameAndSkuInStorefrontProductPage"> + <arguments> + <argument name="product"/> + </arguments> + <!-- Go to storefront product page, assert product name and sku --> + <amOnPage url="{{product.urlKey}}.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInTitle userInput="{{product.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml new file mode 100644 index 0000000000000..85537fea47e50 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInAdditionalInformationTabActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckAttributeInAdditionalInformationTabActionGroup"> + <arguments> + <argument name="attributeLabel" type="string"/> + <argument name="attributeValue" type="string"/> + </arguments> + <click selector="{{StorefrontProductAdditionalInformationSection.additionalInfoTab}}" stepKey="clickTab"/> + <see userInput="{{attributeLabel}}" selector=" {{StorefrontProductAdditionalInformationSection.attributeLabel}}" stepKey="seeAttributeLabel"/> + <see userInput="{{attributeValue}}" selector="{{StorefrontProductAdditionalInformationSection.attributeValue}}" stepKey="seeAttributeValue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml new file mode 100644 index 0000000000000..5806bb48d783c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInAdditionalInformationTabActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckAttributeNotInAdditionalInformationTabActionGroup"> + <arguments> + <argument name="attributeLabel" type="string"/> + </arguments> + <conditionalClick selector="{{StorefrontProductAdditionalInformationSection.additionalInfoTab}}" dependentSelector="{{StorefrontProductAdditionalInformationSection.additionalInfoTab}}" visible="true" stepKey="clickTab"/> + <dontSee userInput="{{attributeLabel}}" selector="{{StorefrontProductAdditionalInformationSection.additionalInfoContainer}}" stepKey="dontSeeAttributeLabel"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml new file mode 100644 index 0000000000000..0a0e4a0fa0ca1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckItemInLayeredNavigationActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckItemInLayeredNavigationActionGroup"> + <arguments> + <argument name="itemType"/> + <argument name="itemName"/> + </arguments> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle('itemType')}}" dependentSelector="{{StorefrontCategorySidebarSection.filterOptions}}" visible="false" stepKey="expandFilterOptions"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml new file mode 100644 index 0000000000000..e9e7f1c01e028 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeSetActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Create a new attribute set --> + <actionGroup name="CreateAttributeSetActionGroup"> + <arguments> + <argument name="nameLabel" type="string"/> + <argument name="basedOn" defaultValue="Default" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.addAttributeSetBtn}}" stepKey="clickAddAttributeSet"/> + <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{nameLabel}}" stepKey="fillName"/> + <selectOption selector="{{AdminProductAttributeSetSection.basedOn}}" userInput="{{basedOn}}" stepKey="selectDefaultSet"/> + <click selector="{{AdminProductAttributeSetSection.save}}" stepKey="clickSave"/> + <see userInput="You saved the attribute set." selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml new file mode 100644 index 0000000000000..9cc2fa03fc70a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewGroupInAttributeSetActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateNewGroupInAttributeSetActionGroup"> + <arguments> + <argument name="groupName" type="string" defaultValue="TestGroupName"/> + </arguments> + <click selector="{{AdminProductAttributeSetSection.addNewGroupBtn}}" stepKey="clickAddNewGroup"/> + <waitForElementVisible selector="{{AdminPromptModalSection.promptField}}" stepKey="waitModalWindow"/> + <fillField selector="{{AdminPromptModalSection.promptField}}" userInput="{{groupName}}" stepKey="fillNewGroupName"/> + <click selector="{{AdminPromptModalSection.modalOk}}" stepKey="clickOkInModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml new file mode 100644 index 0000000000000..e679d59fde791 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddProductCustomOption"> + <arguments> + <argument name="option"/> + </arguments> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionType(option.type_label)}}" stepKey="selectTypeFile"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku}}" userInput="{{option.title}}" stepKey="fillSku"/> + </actionGroup> + <!--Add a custom option of type "file" to a product--> + <actionGroup name="AddProductCustomOptionFile" extends="AddProductCustomOption"> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" after="fillSku" stepKey="fillCompatibleExtensions"/> + </actionGroup> + <actionGroup name="ImportProductCustomizableOptions"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsImportModalSection.selectProductTitle}}" stepKey="waitForTitleVisible"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickResetFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickFilterButton"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsImportModalSection.nameFilter}}" stepKey="waitForNameField"/> + <fillField selector="{{AdminProductCustomizableOptionsImportModalSection.nameFilter}}" userInput="{{productName}}" stepKey="fillProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <checkOption selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="checkProductCheckbox"/> + <click selector="{{AdminProductCustomizableOptionsImportModalSection.importButton}}" stepKey="clickImport"/> + </actionGroup> + <actionGroup name="CheckCustomizableOptionImport"> + <arguments> + <argument name="option"/> + <argument name="optionIndex" type="string"/> + </arguments> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionTitleInputByIndex(optionIndex)}}" stepKey="grabOptionTitle"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionPriceByIndex(optionIndex)}}" stepKey="grabOptionPrice"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionSkuByIndex(optionIndex)}}" stepKey="grabOptionSku"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionTitle" stepKey="assertOptionTitle"/> + <assertEquals expected="{{option.price}}" expectedType="string" actual="$grabOptionPrice" stepKey="assertOptionPrice"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionSku" stepKey="assertOptionSku"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml new file mode 100644 index 0000000000000..050c7b930a085 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Delete attribute set --> + <actionGroup name="DeleteAttributeSetActionGroup"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{AdminProductAttributeSetGridSection.attributeSetNameFilter}}" userInput="{{name}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.applyFilterButton}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.attributeSetRowByIndex('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml new file mode 100644 index 0000000000000..929649f2971e7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/MoveCategoryActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="MoveCategoryActionGroup"> + <arguments> + <argument name="childCategory"/> + <argument name="parentCategory"/> + </arguments> + <click stepKey="expandAllCategoriesTree" selector="{{AdminCategorySidebarTreeSection.expandAll}}"/> + <waitForAjaxLoad stepKey="waitForCategoriesExpand"/> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree('childCategory')}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('parentCategory')}}" stepKey="moveCategory"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.message}}" stepKey="waitForWarningMessageVisible"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForCategoryPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml new file mode 100644 index 0000000000000..f27547a330512 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenEditProductOnBackendActionGroup"> + <arguments> + <argument name="product" defaultValue="product"/> + </arguments> + <click stepKey="clickOnProductRow" selector="{{AdminProductGridSection.firstRow}}"/> + <waitForPageLoad time="30" stepKey="waitForProductPageLoad"/> + <seeInField stepKey="seeProductSkuOnEditProductPage" selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..e74cee080f7e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductFromCategoryPageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenProductFromCategoryPageActionGroup"> + <arguments> + <argument name="category"/> + <argument name="product"/> + </arguments> + <!-- Go to storefront category page --> + <amOnPage url="{{StorefrontCategoryPage.url(category.custom_attributes[url_path])}}" stepKey="navigateToCategoryPage"/> + <!-- Go to storefront product page --> + <click selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo(product.name)}}" stepKey="openProductPage"/> + <waitForAjaxLoad stepKey="waitForImageLoader"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageActionGroup.xml new file mode 100644 index 0000000000000..1e219594ca91c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenStorefrontProductPageActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenStoreFrontProductPageActionGroup"> + <arguments> + <argument name="productUrlKey" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(productUrlKey)}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> + + <actionGroup name="StorefrontOpenProductPageOnSecondStore"> + <annotations> + <description>Goes to the Storefront Product page for the provided store code and Product URL.</description> + </annotations> + <arguments> + <argument name="storeCode" type="string"/> + <argument name="productUrl" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontStoreViewProductPage.url(storeCode,productUrl)}}" stepKey="openProductPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml new file mode 100644 index 0000000000000..15c1a8079d4ac --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductsOnAdminActionGroup.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SaveProductOnProductPageOnAdmin"> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + </actionGroup> + + <actionGroup name="DeleteProductOnProductsGridPageByName"> + <arguments> + <argument name="product"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clearFilters"/> + <click selector="{{AdminProductGridSection.checkbox(product.name)}}" stepKey="tickCheckbox"/> + <click selector="{{AdminProductGridActionSection.actionsSelectBox}}" stepKey="openActionsSelectBox"/> + <click selector="{{AdminProductGridActionSection.deleteOptionInActionsSelectBox}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="clickOkInConfirmation"/> + </actionGroup> + + <actionGroup name="DeleteAllProductsOnProductsGridPageFilteredByName"> + <arguments> + <argument name="product"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickOnFiltersButton"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clearFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillNameFieldInFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="applyFilters"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="clickMulticheckDropDown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllFilteredProducts"/> + <click selector="{{AdminProductGridActionSection.actionsSelectBox}}" stepKey="openActionsSelectBox"/> + <click selector="{{AdminProductGridActionSection.deleteOptionInActionsSelectBox}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="clickOkInConfirmation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml new file mode 100644 index 0000000000000..a55576621bd57 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAndMultiSelectActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="searchAndMultiSelectActionGroup"> + <arguments> + <argument name="dropDownSelector"/> + <argument name="options" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{dropDownSelector}} .action-select.admin__action-multiselect" stepKey="waitForDropdown"/> + <click selector="{{dropDownSelector}} .action-select.admin__action-multiselect" stepKey="clickDropdown"/> + <selectMultipleOptions filterSelector="{{dropDownSelector}} .admin__action-multiselect-search-wrap>input" optionSelector="{{dropDownSelector}} .admin__action-multiselect-label>span" stepKey="selectSpecifiedOptions"> + <array>[{{options}}]</array> + </selectMultipleOptions> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml new file mode 100644 index 0000000000000..6cb939a7af410 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SearchForProductOnBackendActionGroup"> + <arguments> + <argument name="product" defaultValue="product"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="60" stepKey="waitForProductsPageToLoad"/> + <click stepKey="openFiltersSectionOnProductsPage" selector="{{AdminProductFiltersSection.FiltersButton}}"/> + <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <fillField stepKey="fillSkuFieldOnFiltersSection" userInput="{{product.sku}}" selector="{{AdminProductFiltersSection.SkuInput}}"/> + <click stepKey="clickApplyFiltersButton" selector="{{AdminProductFiltersSection.Apply}}"/> + </actionGroup> + <actionGroup name="SearchForProductOnBackendByNameActionGroup" extends="SearchForProductOnBackendActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <remove keyForRemoval="fillSkuFieldOnFiltersSection"/> + <fillField userInput="{{productName}}" selector="{{AdminProductFiltersSection.NameInput}}" after="cleanFiltersIfTheySet" stepKey="fillNameFieldOnFiltersSection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml new file mode 100644 index 0000000000000..91afd2eb5d4c7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddToCartCustomOptionsProductPageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Click Add to Cart button in storefront product page--> + <actionGroup name="StorefrontAddToCartCustomOptionsProductPageActionGroup"> + <arguments> + <argument name="productName"/> + </arguments> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> + <see selector="{{StorefrontProductPageSection.successMsg}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml new file mode 100644 index 0000000000000..c25b73bab21ae --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Check the product in recently viewed widget --> + <actionGroup name="StorefrontAssertProductInRecentlyViewedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" stepKey="waitWidgetRecentlyViewedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyViewedWidget"/> + </actionGroup> + + <!-- Check the product in recently compared widget --> + <actionGroup name="StorefrontAssertProductInRecentlyComparedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" stepKey="waitWidgetRecentlyComparedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyComparedWidget"/> + </actionGroup> + + <!-- Check the product in recently ordered widget --> + <actionGroup name="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" stepKey="waitWidgetRecentlyOrderedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyOrderedWidget"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml new file mode 100644 index 0000000000000..399a26abb8c86 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodOptionPresentInCartActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert shipping method name and price are present in cart --> + <actionGroup name="StorefrontAssertShippingMethodOptionPresentInCartActionGroup"> + <arguments> + <argument name="methodName" type="string"/> + <argument name="price" type="string"/> + </arguments> + <see selector="{{StorefrontCheckoutCartSummarySection.methodName}}" userInput="{{methodName}}" stepKey="seeShippingName"/> + <see selector="{{StorefrontCheckoutCartSummarySection.shippingPrice}}" userInput="{{price}}" stepKey="seeShippingPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml new file mode 100644 index 0000000000000..00821dd833e4a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Check the category page --> + <actionGroup name="StorefrontCheckCategoryActionGroup"> + <arguments> + <argument name="category"/> + <argument name="productCount" type="string"/> + </arguments> + <seeInCurrentUrl url="/{{category.custom_attributes[url_key]}}.html" stepKey="checkUrl"/> + <seeInTitle userInput="{{category.name}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{category.name}}" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="assertCategoryName"/> + <see userInput="{{productCount}}" selector="{{StorefrontCategoryMainSection.productCount}} span" stepKey="assertProductCount"/> + </actionGroup> + <!--Check category is empty--> + <actionGroup name="StorefrontCheckEmptyCategoryActionGroup" extends="StorefrontCheckCategoryActionGroup"> + <remove keyForRemoval="assertProductCount"/> + <amOnPage url="{{StorefrontCategoryPage.url(category.name)}}" before="checkUrl" stepKey="goToCategoryStorefront"/> + <see selector="{{StorefrontCategoryMainSection.categoryEmptyMessage}}" userInput="We can't find products matching the selection." stepKey="seeCategoryEmpty"/> + </actionGroup> + + <!-- Check simple product on the category page --> + <actionGroup name="StorefrontCheckCategorySimpleProduct"> + <arguments> + <argument name="product"/> + </arguments> + <seeElement selector="{{StorefrontCategoryProductSection.productTitleByName(product.name)}}" stepKey="assertProductName"/> + <see userInput="${{product.price}}.00" selector="{{StorefrontCategoryProductSection.ProductPriceByName(product.name)}}" stepKey="AssertProductPrice"/> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> + <!-- @TODO: This causes a parsing error after updating the magento-testing-framework to version 2.4.4 --> + <!--<executeInSelenium function="function($webdriver) use ($I) { + $productName = '//main//li[.//a[contains(text(), \'' . {{product.name}} . '\' )]]//div[@data-container=\'product-grid\']'; + $I->assertEquals('2', $webdriver->findElement(\Facebook\WebDriver\WebDriverBy::xpath($productName))->getCSSValue('z-index')); + }" stepKey="assertProductContainerIsOpened"/>--> + <seeElement selector="{{StorefrontCategoryProductSection.productAddToCartByName(product.name)}}" stepKey="assertAddToCart" /> + </actionGroup> + + <actionGroup name="StorefrontCheckAddToCartButtonAbsence" extends="StorefrontCheckCategorySimpleProduct"> + <dontSeeElement selector="{{StorefrontCategoryProductSection.productAddToCartByName(product.name)}}" stepKey="assertAddToCart"/> + </actionGroup> + + <actionGroup name="StorefrontSwitchCategoryViewToListMode"> + <click selector="{{StorefrontCategoryMainSection.modeListButton}}" stepKey="switchCategoryViewToListMode"/> + <seeElement selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="assertCategoryTitle"/> + </actionGroup> + + <!-- Go to storefront category product page by given parameters --> + <actionGroup name="GoToStorefrontCategoryPageByParameters"> + <arguments> + <argument name="category" type="string"/> + <argument name="mode" type="string"/> + <argument name="sortBy" type="string" defaultValue="position"/> + <argument name="sort" type="string" defaultValue="asc"/> + </arguments> + <!-- Go to storefront category page --> + <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_mode={{mode}}&product_list_order={{sortBy}}&product_list_dir={{sort}}" stepKey="onCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitCategoryPageLoaded"/> + </actionGroup> + + <actionGroup name="VerifyCategoryPageParameters"> + <arguments> + <argument name="categoryName" type="string"/> + <argument name="mode" type="string"/> + <argument name="numOfProductsPerPage" type="string"/> + <argument name="sortBy" type="string" defaultValue="position"/> + </arguments> + <seeInTitle userInput="{{categoryName}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{categoryName}}" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="assertCategoryName"/> + <see userInput="{{mode}}" selector="{{StorefrontCategoryPagerSection.modeGridIsActive}}" stepKey="assertViewMode"/> + <see userInput="{{numOfProductsPerPage}}" selector="{{StorefrontCategoryPagerSection.perPageSelected}}" stepKey="assertNumberOfProductsPerPage"/> + <see userInput="{{sortBy}}" selector="{{StorefrontCategoryPagerSection.sortedBy}}" stepKey="assertSortedBy"/> + </actionGroup> + + <actionGroup name="StorefrontGoToSubCategoryPage"> + <arguments> + <argument name="parentCategory"/> + <argument name="subCategory"/> + <argument name="urlPath" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(parentCategory.name)}}" stepKey="moveMouseOnMainCategory"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="waitForSubCategoryVisible"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="goToCategory"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInCurrentUrl url="{{urlPath}}.html" stepKey="checkUrl"/> + <seeInTitle userInput="{{subCategory.name}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{subCategory.name}}" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="assertCategoryName"/> + </actionGroup> + <!-- Assert product store view image in storefront category page --> + <actionGroup name="AssertStorefrontActiveImageCategoryActionGroup"> + <arguments> + <argument name="category"/> + <argument name="product"/> + <argument name="image" type="string"/> + </arguments> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(category.name)}}" stepKey="openCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByNameAndSrc(product.name, image)}}" stepKey="seeImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml new file mode 100644 index 0000000000000..e560e7facc7fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryCheckActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Actions to check if a category exists on StoreFront--> + <actionGroup name="StorefrontCategoryCheckActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + <amOnPage url="/{{categoryEntity.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <see selector="{{StorefrontCategoryMainSection.categoryTitle}}" userInput="{{categoryEntity.name_lwr}}" stepKey="assertCategoryOnStorefront"/> + <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeCategoryNameInTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml new file mode 100644 index 0000000000000..5c975998ab92e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- You must already be on the category page --> + <actionGroup name="StorefrontCheckProductPriceInCategoryActionGroup" extends="StorefrontCheckCategorySimpleProduct"> + <remove keyForRemoval="AssertProductPrice"/> + <see userInput="{{product.price}}" selector="{{StorefrontCategoryProductSection.ProductPriceByName(product.name)}}" stepKey="AssertProductPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml new file mode 100644 index 0000000000000..47c8abada5222 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add Product to Compare from the category page and check message --> + <actionGroup name="StorefrontAddCategoryProductToCompareActionGroup"> + <arguments> + <argument name="productVar"/> + </arguments> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(productVar.name)}}" stepKey="moveMouseOverProduct" /> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCompareByName(productVar.name)}}" stepKey="clickAddProductToCompare"/> + <waitForElement selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForAddCategoryProductToCompareSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added product {{productVar.name}} to the comparison list." stepKey="assertAddCategoryProductToCompareSuccessMessage"/> + </actionGroup> + + <!-- Add Product to Compare from the product page and check message --> + <actionGroup name="StorefrontAddProductToCompareActionGroup"> + <arguments> + <argument name="productVar"/> + </arguments> + <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickAddToCompare" /> + <waitForElement selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForAddProductToCompareSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added product {{productVar.name}} to the comparison list." stepKey="assertAddProductToCompareSuccessMessage"/> + </actionGroup> + + <!-- Check the product in compare sidebar --> + <actionGroup name="StorefrontCheckCompareSidebarProductActionGroup"> + <arguments> + <argument name="productVar"/> + </arguments> + <waitForElement selector="{{StorefrontComparisonSidebarSection.productTitleByName(productVar.name)}}" stepKey="waitForProduct"/> + </actionGroup> + + <!-- Open and check comparison page --> + <actionGroup name="StorefrontOpenAndCheckComparisionActionGroup"> + <click selector="{{StorefrontComparisonSidebarSection.compare}}" stepKey="clickCompare"/> + <waitForLoadingMaskToDisappear stepKey="waitForComparePageloaded" /> + <seeInCurrentUrl url="{{StorefrontProductComparePage.url}}" stepKey="checkUrl"/> + <seeInTitle userInput="Products Comparison List" stepKey="assertPageNameInTitle"/> + <see userInput="Compare Products" selector="{{StorefrontProductCompareMainSection.pageName}}" stepKey="assertPageName"/> + </actionGroup> + + <!-- Check the simple product in comparison page --> + <actionGroup name="StorefrontCheckCompareSimpleProductActionGroup"> + <arguments> + <argument name="productVar"/> + </arguments> + <seeElement selector="{{StorefrontProductCompareMainSection.productLinkByName(productVar.name)}}" stepKey="assertProductName"/> + <see userInput="${{productVar.price}}.00" selector="{{StorefrontProductCompareMainSection.productPriceByName(productVar.name)}}" stepKey="assertProductPrice1"/> + <see userInput="{{productVar.sku}}" selector="{{StorefrontProductCompareMainSection.productAttributeByCodeAndProductName('SKU', productVar.name)}}" stepKey="assertProductPrice2"/> + <seeElement selector="{{StorefrontProductCompareMainSection.productAddToCartByName(productVar.name)}}" stepKey="assertProductAddToCart"/> + </actionGroup> + + <!-- Clear the compare list --> + <actionGroup name="StorefrontClearCompareActionGroup"> + <waitForElementVisible selector="{{StorefrontComparisonSidebarSection.clearAll}}" time="30" stepKey="waitForClearAll"/> + <click selector="{{StorefrontComparisonSidebarSection.clearAll}}" stepKey="clickClearAll"/> + <waitForElementVisible selector="{{StorefrontModalConfirmationSection.okButton}}" stepKey="waitForClearOk"/> + <click selector="{{StorefrontModalConfirmationSection.okButton}}" stepKey="clickClearOk"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitAssertMessageCleared"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="You cleared the comparison list." stepKey="assertMessageCleared"/> + <waitForElementVisible selector="{{StorefrontComparisonSidebarSection.noItemsMessage}}" stepKey="assertNoItems"/> + </actionGroup> + + <actionGroup name="RemoveProductFromComparisonList"> + <arguments> + <argument name="product"/> + </arguments> + <click selector="{{StorefrontProductCompareMainSection.removeProduct(product.name)}}" stepKey="clickRemoveProductButton"/> + <waitForElementVisible selector="{{StorefrontModalConfirmationSection.okButton}}" stepKey="waitForConfirmationPopup"/> + <click selector="{{StorefrontModalConfirmationSection.okButton}}" stepKey="confirmProductRemove"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForProductRemoveSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="You removed product {{product.name}} from the comparison list." stepKey="assertProductRemoveSuccessMessage"/> + </actionGroup> + + <actionGroup name="StorefrontCheckCompareOutOfStockProductActionGroup" extends="StorefrontCheckCompareSimpleProductActionGroup"> + <remove keyForRemoval="assertProductAddToCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..1a8da822855ea --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontGoToCategoryPageActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontend"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="toCategory"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + </actionGroup> + <actionGroup name="StorefrontGoToSubCategoryPageActionGroup" extends="StorefrontGoToCategoryPageActionGroup"> + <arguments> + <argument name="subCategoryName" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="toCategory"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategoryName)}}" stepKey="openSubCategory" after="toCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..6cd2eef63c5af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontNavigateCategoryPageActionGroup"> + <annotations> + <description>Navigates storefront category page by url key</description> + </annotations> + <arguments> + <argument name="categoryUrlKey"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url(categoryUrlKey)}}" stepKey="navigateStorefrontCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitCategoryPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml new file mode 100644 index 0000000000000..692d1f4266b98 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenHomePageActionGroup.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenHomePageActionGroup"> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml new file mode 100644 index 0000000000000..7166daa51fd5b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Check the simple product on the product page --> + <actionGroup name="StorefrontCheckSimpleProduct"> + <arguments> + <argument name="product"/> + </arguments> + <seeInCurrentUrl url="/{{product.custom_attributes[url_key]}}.html" stepKey="checkUrl"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + <see userInput="${{product.price}}.00" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> + <seeElement selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="assertAddToCart" /> + <see userInput="{{product.custom_attributes[description]}}" selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="assertProductDescription"/> + <see userInput="{{product.custom_attributes[short_description]}}" selector="{{StorefrontProductInfoMainSection.productShortDescription}}" stepKey="assertProductShortDescription"/> + </actionGroup> + + <!-- Assert option image and price in storefront product page --> + <actionGroup name="AssertOptionImageAndPriceInStorefrontProductActionGroup"> + <arguments> + <argument name="label" type="string"/> + <argument name="image" type="string"/> + <argument name="price" type="string"/> + </arguments> + <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(image)}}" stepKey="seeImage"/> + <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeProductPrice"/> + </actionGroup> + <!-- Assert image Store View in storefront product page --> + <actionGroup name="AssertStorefrontActiveImageProductActionGroup"> + <arguments> + <argument name="image" type="string"/> + </arguments> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(image)}}" stepKey="seeActiveImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml new file mode 100644 index 0000000000000..3d187fb315bff --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitcherActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SwitchToVersion4ActionGroup"> + <amOnPage url="{{ConfigurationStoresPage.url}}" stepKey="navigateToWYSIWYGConfigPage1"/> + <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> + <waitForElementVisible selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="waitForCheckbox" /> + <uncheckOption selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="uncheckUseSystemValue"/> + <waitForElementVisible selector="{{ContentManagementSection.Switcher}}" stepKey="waitForSwitcherDropdown" /> + <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 4" stepKey="switchToVersion4" /> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> + <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml new file mode 100644 index 0000000000000..5f50b2790a164 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesInMagentoAdminActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckVisibilityOfProductOnProductsGridPageByName"> + <arguments> + <argument name="product"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductFiltersSection.FiltersButton}}" stepKey="openFiltersSectionOnProductsPage"/> + <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <see userInput="{{product.name}}" selector="{{AdminProductGridSection.productNameInNameColumn}}" stepKey="seeBundleProductInProductNameColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml new file mode 100644 index 0000000000000..667697d691a60 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyEntitiesOnStorefrontActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CheckVisibilityOfProductOnCategoryPageByName"> + <arguments> + <argument name="product"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url($$createPreReqCategory.name$$)}}" stepKey="amOnStorefrontTestCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="{{product.name}}" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="seeProductNameOnPage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml new file mode 100644 index 0000000000000..67e71cc3b9e92 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogAttributeSet" type="CatalogAttributeSet"> + <data key="attribute_set_name" unique="suffix">test_set_</data> + <data key="attributeGroupId">7</data> + <data key="skeletonId">4</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml new file mode 100644 index 0000000000000..cb2bacfd2f2da --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- Catalog > Price --> + <entity name="GlobalCatalogPriceScopeConfigData"> + <!-- Default configuration --> + <data key="path">catalog/price/scope</data> + <data key="scope_id">0</data> + <data key="label">Global</data> + <data key="value">0</data> + </entity> + <entity name="WebsiteCatalogPriceScopeConfigData"> + <data key="path">catalog/price/scope</data> + <data key="scope_id">0</data> + <data key="label">Website</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryData.xml new file mode 100644 index 0000000000000..39eb6f88d1292 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryData.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogInventoryOptionsShowOutOfStockEnable"> + <data key="path">cataloginventory/options/show_out_of_stock</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="CatalogInventoryOptionsShowOutOfStockDisable"> + <!-- Magento default value --> + <data key="path">cataloginventory/options/show_out_of_stock</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml new file mode 100644 index 0000000000000..8d460fb7cbf1d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogPriceScopeWebsite" type="catalog_price_config_state"> + <requiredEntity type="scope">ScopeWebsite</requiredEntity> + <requiredEntity type="default_product_price">DefaultProductPrice</requiredEntity> + </entity> + <entity name="ScopeWebsite" type="scope"> + <data key="value">1</data> + </entity> + <entity name="DefaultProductPrice" type="default_product_price"> + <data key="value">0</data> + </entity> + <entity name="DefaultConfigCatalogPrice" type="catalog_price_config_state"> + <requiredEntity type="scope">ScopeGlobal</requiredEntity> + <requiredEntity type="default_product_price">DefaultProductPrice</requiredEntity> + </entity> + <entity name="ScopeGlobal" type="scope"> + <data key="value">0</data> + </entity> + <entity name="DefaultProductPrice" type="default_product_price"> + <data key="value"/> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml new file mode 100644 index 0000000000000..52ade613e5e03 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">EnableCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="EnableCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">1</data> + </entity> + + <entity name="DisableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">DefaultCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="DefaultCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">0</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml new file mode 100644 index 0000000000000..d36877025e970 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="RememberPaginationCatalogStorefrontConfig" type="catalog_storefront_config"> + <requiredEntity type="grid_per_page_values">GridPerPageValues</requiredEntity> + <requiredEntity type="remember_pagination">RememberCategoryPagination</requiredEntity> + </entity> + + <entity name="GridPerPageValues" type="grid_per_page_values"> + <data key="value">9,12,20,24</data> + </entity> + + <entity name="RememberCategoryPagination" type="remember_pagination"> + <data key="value">1</data> + </entity> + + <entity name="DefaultGridPerPageValues" type="grid_per_page_values"> + <data key="value">9,15,30</data> + </entity> + + <entity name="DefaultRememberCategoryPagination" type="remember_pagination"> + <data key="value">1</data> + </entity> + + <entity name="DefaultCatalogStorefrontConfiguration" type="default_catalog_storefront_config"> + <requiredEntity type="catalogStorefrontFlagZero">DefaultCatalogStorefrontFlagZero</requiredEntity> + <data key="grid_per_page_values">DefaultGridPerPageValues</data> + <data key="remember_pagination">DefaultRememberCategoryPagination</data> + </entity> + + <entity name="DefaultCatalogStorefrontFlagZero" type="catalogStorefrontFlagZero"> + <data key="value">0</data> + </entity> + + <entity name="DefaultListAllowAll" type="list_allow_all"> + <data key="value">0</data> + </entity> + + <entity name="DefaultFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">0</data> + </entity> + + <entity name="UseFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">UseFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">UseFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="UseFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">1</data> + </entity> + + <entity name="UseFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">1</data> + </entity> + + <entity name="DefaultFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">DefaultFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">DefaultFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="DefaultFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml new file mode 100644 index 0000000000000..ff1e04a22c85e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultCategory" type="category"> + <data key="name" unique="suffix">simpleCategory</data> + <data key="name_lwr" unique="suffix">simplecategory</data> + <data key="is_active">true</data> + </entity> + <entity name="ApiCategory" type="category"> + <data key="name" unique="suffix">ApiCategory</data> + <data key="is_active">true</data> + </entity> + <entity name="SimpleSubCategory" type="category"> + <data key="name" unique="suffix">SimpleSubCategory</data> + <data key="name_lwr" unique="suffix">simplesubcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + </entity> + <entity name="NewRootCategory" type="category"> + <data key="name" unique="suffix">NewRootCategory</data> + <data key="name_lwr" unique="suffix">newrootcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <data key="parent_id">1</data> + </entity> + <entity name="SubCategoryWithParent" type="category"> + <data key="name" unique="suffix">subCategory</data> + <data key="name_lwr" unique="suffix">subCategory</data> + <data key="is_active">true</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="DefaultRootCategoryGetter" type="category"> + <var key="category" entityKey="category" entityType="category"/> + </entity> + <entity name="GearCategory" type="category"> + <data key="name" unique="suffix">Gear</data> + <data key="url_key" unique="suffix">gear</data> + </entity> + <entity name="BagsCategory" type="category"> + <data key="name" unique="suffix">Bags</data> + <data key="url_key" unique="suffix">bags</data> + </entity> + <entity name="SubCategoryNonAnchor" extends="SubCategoryWithParent"> + <requiredEntity type="custom_attribute">CustomAttributeCategoryNonAnchor</requiredEntity> + </entity> + <entity name="DefaultCategory" type="category"> + <data key="name">Default Category</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryProductLinkData.xml new file mode 100644 index 0000000000000..80e78e86853a9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryProductLinkData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultCategoryProductLink" type="CategoryProductLink"> + <var key="sku" entityKey="sku" entityType="product"/> + <var key="category_id" entityKey="id" entityType="category"/> + </entity> + <entity name="CustomCategoryProductLink" type="CategoryProductLink"> + <var key="sku" entityKey="sku" entityType="product2"/> + <var key="category_id" entityKey="id" entityType="category"/> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml new file mode 100644 index 0000000000000..1b349518c31e0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomAttributeCategoryUrlKey" type="custom_attribute"> + <data key="attribute_code">url_key</data> + <data key="value" unique="suffix">category</data> + </entity> + <entity name="CustomAttributeProductUrlKey" type="custom_attribute"> + <data key="attribute_code">url_key</data> + <data key="value" unique="suffix">product</data> + </entity> + <entity name="CustomAttributeCategoryIds" type="custom_attribute_array"> + <data key="attribute_code">category_ids</data> + <var key="value" entityType="category" entityKey="id"/> + </entity> + <entity name="CustomAttributeProductAttribute" type="custom_attribute"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <var key="value" entityKey="value" entityType="ProductAttributeOption"/> + </entity> + <entity name="ApiProductDescription" type="custom_attribute"> + <data key="attribute_code">description</data> + <data key="value" unique="suffix">API Product Description</data> + </entity> + <entity name="ApiProductShortDescription" type="custom_attribute"> + <data key="attribute_code">short_description</data> + <data key="value" unique="suffix">API Product Short Description</data> + </entity> + <entity name="ApiProductNewsFromDate" type="custom_attribute"> + <data key="attribute_code">news_from_date</data> + <data key="value">2018-05-17 00:00:00</data> + </entity> + <entity name="CustomAttributeCategoryNonAnchor" type="custom_attribute"> + <data key="attribute_code">is_anchor</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml new file mode 100644 index 0000000000000..ec8a355523d63 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAttributeFrontendLabel" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">attribute</data> + <data key="default_label" unique="suffix">attribute</data> + </entity> + <entity name="ProductAttributeFrontendLabelThree" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">attributeThree</data> + <data key="default_label" unique="suffix">attributeThree</data> + </entity> + <entity name="ColorAttributeFrontandLabel" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label">color</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml new file mode 100644 index 0000000000000..36eab0b602544 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MagentoLogoImageContent" type="image_content"> + <data key="base64_encoded_data">iVBORw0KGgoAAAANSUhEUgAAAP8AAAEsCAYAAAAM1WX/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACY7SURBVHhe7Z0LlBxXnd6HXWyWhx9g8HS3pJFGPVU1klmC2RB28frBIbY00y2NpBnN9GMkG4JJcOJsgCVrszZg56wJuxj2BTjLY8FsADsb20kwmEc4WYgNxPYuiwX2yg9ZxrKsefS7R5IlufP/qm+N76hbo5qa7p6qru875zs6M5quvvd/76/q3lt1/9VDhU/7Ll79ikrauL66y8pVUsZ1ezf1nan+i6KoblV+bP22uUlrT+1dG2vlycEa/pWfH5kej4+oP6Eoqpt0aGx9vJqxvnF892Dt2JUbavmMNW/8fEx+X81aXy9MxNerj1AUFWQ9eGHPy4sp4wOHJ60ZXOWLWauW08CH8TN+r0YBM8W0+f47B3vOUIegKCpoyo/HL6tkzAcA9eFdgw3Qn2z8/5z8Hf6+krEeyE/EL1WHoigqCHo+2X9+MW18dm5y8MSLV25sCvrpfEI+V520TuA4h4b6etWhKYryqwTWXTJ3fxpXbyzone5qfyrjc+WsjALeLSeBrPl0MWNOqq+gKMpPmh3rf2Mpbdxbu2pj7ejuDZ6hP9k4Do6HEUQpY34zl+r/TfWVFEWtpH52ee+rZF7/kbmsVQb4hZPgbZVxXBx/btIqldLmjQeSkVeqIlAU1Wnl0gNJAf/nGOJXlzHEd2scH99jLwhmzX8sTgwkVFEoiuqEcjvW9pWzxleO7hqsHT/pnn2njO/F95cz5pefk/KoolEU1S5VM+Y1law1hatvqck9+04Z34vvr48CrEMolyoiRVGtVG5n/0Vylf07zLvd3LPvlFEOlAflkqnA/5kdX/92VWSKopajZ8Zir53LGrdWJ61jL161oW0Lest1fUFwQ60i5ZxLG7fuT/Sdq6pAUdRSVU4ZqXLGenK59+w7ZZTP2SxUzlpPlNPGhKoKRVFuND22zpQh9N+euHJD7YXdK7Ogt1yj3Ch/JWv97fSOPktVjaKoZsK++krGuK6cNUu4emKzTTOwgmJns5CcyIqVtPkH9zJvAEU1amZi3eXVjPkQYMHmGr8P8d0a9XA2C1Wz5oOFsXX/UlWZosKt6a1rotWs9XmsmGOY3AygbjHqd3hysFbNWH+1X+qtQkBR4VMpZbynOmkewFWxlO2eq/2pjPqhnvboJms+i/qrUFBUOFRI9f+WdP7v4tbYkRZuwgmKUV/UG/WXqcB3Cjvjb1Ghoaju1L6RtedUM8YtMsw/2s5NOEGxs1moOmkdKWeMjyM+KlQU1T3KTxjb57LWL+3V7wDcs++UEQfEoz4VsH6RT63fpkJGUcFWPXGm+V+PyzD35MSZ9EI7iUQrGetriJsKIUUFS0icmU8ZH0BCTFzVmiXOpBca8XGeDTictaaLqYH/gDiqkFKU/1VKxy+Zy1h24sxuumffKSNe9mYhiV81a92fG4tfokJLUf7UoZ19vcWM8ZnqMhJn0guNONqJRFPGXyIxqQo1RflHFSTOnLT24WoVhE04QTHiaG8Werf9bMBTchLYrUJOUSurfCr+5lLG+taLV25oaeJMeqER13oi0Q21Ytr4Vn7nwD9TTUBRndWeS899dTltfFSu9hXes++cnWcD5iTuiP/By3tfpZqEotqvysRAopOJM+mFRrz1RKKVlDGsmoai2qN64kzzS0d3r1ziTHqh0Q5HdtmJRP86t7N/jWoqimqdyinj31Wz1iFcbVYycSa90GgHJ5GotM/zTCRKtUzFifjvyBDfTpyJKwyh96fRLmgftBMSiRbT639bNSFFLU3PXBF7bSVdT5yJ3WfNOhztPzuJRKXdXqhkjE8ykSi1JBVSRqqStR7nPftgGu3lPBuAdkR7qqalqOY6OBrfKJ3l7iAnzqQXGu2IRUEZBdw1s3XNBtXUFFXXnrHBM2SIf305Y5ZxtecmnO4x2lFLJFpCO+8ZHDxDNT0VZs2MDlxezVh24sxqiBb0AASemMO/zf6/24x2RfvW7woMPjjDRKLh1dT2NZG5tHkbEkqeCNEmHEAA6Oek3vm0+Rh2z4XtsWS0t51INGv+F2zGUl2CCoNKE8bV1UkrNIkzYdSxrJJnYgNSLjWQrSUjr5wZj++qZq2n8Xv8f1hi8VIiUetXTCQaAk3vjL9FzvahTJyJ9/xVZIg/lzH//Int/W9QIbF1UH6uZI2/kJOA/XfNPt+NRvs7iUQlLt/Jj8cvVCGhukUzm/vPKmaMj8sV70iYNuGgc9svzJA6y0nv/unR+KUqJE01PR6/rDpp3m9fEUO0/uFsFqpI/yimjFv2Sn9RIaGCrPzEeiTO/AU6dFgSZ6KO8yvcGTNXThkfuGN7z6+pkCyqj17c8+tyovygjBJy+HxY7nygjkwk2iWa2rbOqGSsO5AQMmyJM+uJMDfgzTjfOLTFWyLMqR39A3LiuBPHCWf87ESi30AcVEgovwsJH4sTxgfDljhTv3LJVfuX+fTADhWSZamUGhiVk8Cj9nFDOHJCP5KpwAd+ICMiFRLKj8qND1wcxsSZzpy1nDXx8otbprec8xoVkpZoZvPZZ1UzxsflpBLONZN32W8Wuh/9S4WE8ouwWl1MG39RnbSOvyids1lDdqPROZ3VagHzu+1+7RVeM1bJGN8L590S3CIdPI5+hv6mQkKtpJDQ8XDIEmeijs596nLGPFDp8H1qGQVcXZXvxfeH6jkJNa2qZq19chLYpcJBdVpI4FhKG6FMnGm/6lqGo5W0eRte8a1C0lHNjK2OyRTjr7CHvttfLa4b/cxJJCr9716ZCrxJhYRqtw4kI68sp4yPze2y7E04YZp/Os+mlzLmQ4Xx+BUqJCuqQmr9JinPw/YVMSQjL9hZZ5nLWmUkEkW/VCGh2qFieiCJhI2h62hq5bmaNYtytf+DvZt6zlQh8YX2buo7s5IxrkP57BOylLdZPbrN9glZTQWQ0LWYGUiokFCtUn5i7dpqxrr9qFz5wpQ4E50L+9ExrC6nzXtmR+MbVUh8qdlUfGM5Y/4PJx9CWE7OMPol+mc5bX0lP7J2rQoJtRzJvPLasCXORB21xaXHZVg5ocIRCMm0DJmQnkAmnXAtwjojNOsQEr6qcFBLVXE8/rZKxrITZ2JxKyxXEWcuiRx0hbRxq8wlz1MhCZRQ7lLa+BTqgfo0q2s3Gv3Ufsmo1BmJRGfHmEjUtZBwsZoxPilXjtB1Gjv7rP1AidU1nWYW2Y8n5SQu9QrfSdx+/uKFatb8k30ja89RIaGaqTAxkLaHizJ0CuNw8fCkdbCcMq+pqXh0i27o6XlZQYbBc1nr+bBO36RfM5FoM81MGIMSnLvCmDgTC0Uv7FZvnMms7VMh6Uqphdsvo75hWriFnYXbSsb879M7DEuFJLy6F7eI0sb1SKyIs2OYNuE4t4jkavgPsyF711xR6iv1/hnqH5Zbtqijs1mojESiGeM69H8VknBpdjx+hcyFHrQBCNtc0O70VjXMb5nFW47lJHBTNWvNIR5helirvlkIdwXMB5FAVoWk+4XEmdWM+QUs/oT1sVC56n0zlzJ+U4Uk1Do0PvAmice9oX1MW0Y+1bT1eXChQtKdKqSN91YnQ7ghRG3Ckavc0/kJ40oVDkpTfnzgKonPfsQpnIlEzWcLE8bVKhzdI2zCkcp9J6yJM6VT42r/508m+89XIaGa6HmJTyVj/aVMicKbSHTSvC8/Gn+zCklw9eTYurPLKeMW6fyHcc8+TPM650EPmdfdPzUWv0SFhHKh6Yn4pQJBaJOyVLODh8tp84/AjwpJsITEmQL9L9GAoUycmbVmkTiT6Z+8CXGrMJFosBKJHhpbH0fiyOOhTpxpfu35HX39KiTUMnRoIr5eLiJfC3MiUan/1wsSBxUS/8lOnClXusMhT5xZmjC2q5BQLRQSkoY+kWjafD84UyHxh/Lj8cukYew5Whg34eBlD9WMccujLU6cSS3U9BbzNSqR6NGwrSFpzwbcn59Y/OUrHRFWZ4tp47Nzk4MnXgzZyy6d1VkZkn13Zqz/rSokVAdUSPe/VaaW9USiu0J290g4q05aJ8op4zPgT4Wks0ICQ+n49Rc8hmgYNp84M2s9V8p04X3ZAKmUNt4ro4CDaI/QJRJ9tz0KeLqYMSdVONqv2bH+NyJxIYZdYU2cicSV0ylzRRJnUgs1M2bEymnj8+F9YtTO6fjNXKq/fU+M/uzy3lfJvP4jc1mrHNb5VhmJMydC9Cx2gFRIx6+Q9rETiYbx2YC5SatUSps3tjyRaA6JMzPmzxHYMCXOdFZaZY5VkCH+dX5LnEkt1A8uXv0KGZVeLyAU0G5ov2bt2m0Gj/OJRLPmPxYnWpBINLdjbV85a3wlbIkz4fnEmRnjLuQbUCGhAiC0l0wF7kb7hTE/hJ1INGN++TnhV4VkaapmzGsqWWsKZ5PQJs6cMMdVOKgAqpA2JtCOaM8wZoYSfg+BYxWO0yu3s/8iOWuEMnFm/X1s1jEZOt5aGIu9VoWECrCeHYm+TqZsn0a7on3DtFbl7C+xE4mOr3+7CkmjnpHOPpc1bq0HaUP4gvQupFiy/m7RIFGBVS5tXlS1s0GHNJGocD0nFzUkyFUhqQt54csZ68nwDY/qQ/w5GR4VJox/i0STKiRUF+qOnp5fU4lE7fdAhOkRdGc6W85aT8y/ByI3MXDHcRkahHJhROosQ/yvYGHTDgYVCiGRqEwFbg9lIlGpL3gH9z3Ht6+pFXbG6/8pV8KT/7ibjDOgljjzZ8VxY0j1ByqEQuJU9AP0h1DcwlZ8g3dw3zM7FKnZ3rq6lhuXkwD+oAtPAvbcB42ctSphTpxJLdSesXNfXcZbnyetStc+vKaYBt/g3GG+Z3Y4WrPtnAS29dXyKUN9oMmBAmaczecTZ05a/2t2vP8C1e4UNS88ti6jgO5KJAp+wbHwDK7nGVfMvwS/Y/Wfue1ra/m0Wf9wswP73PoiR3XS2ldMx3epdqaoU6qYMnZ3xYY1cCv8gmOda92N8DvGHyditdxo/0sHO/kLfOwT9tbHwVolY3zmudH1r1dtS1GnFfpLJWt9Zk76T+A2CylOwS34bQa941PDD+OD8JZVtRwWBXFgH58EcJaef7AhYz4wvSN+mWpPilqypsf63yH96MeBeOBNsQlOwes8u824Vl4cfsfqQLmta2r58QHfnQTQKM4mHDlj54oZ44NMnEm1QrchPV3a+H3pV3n0L989G+CwKFyCTzfQO3YHv2N10Ny2tbWcsyjYrEAd9nzizKz5tUPbfJzokAqs7MS0WevreC7AN4lEhT9wCB51Pt16afA7xpdgPWDHuhVbFMTZ10mcKY3CxJlUR2QnEp1c4USi4A2LecLf6eb1i9kb/I7xpUk5CYytf6lQJxe0Da4/r2xDf1iu9n/ExJlUJ7V389lnlTPGx/FyjY4+G6D4Am/gziv0jpcHP4wCwFva/5AQzrK4B2tvVMia353eGX+Lag+K6rimR/t/q+IkEm3nswGKKfshHZeLeW68fPgd2wWSf0fW1PITrV0URFDnE2dmzAOllPEeFf/QSaY3g7WxwTPUj77Q3k19Z6Jc6sfQqZoxrm7LC2gdhoQncGXz1QLoHbcOfseqcDn7ScHWrAfgXuuRXYO1ctr6/MzY6piKeah0aGdfbylrfTmXMp6ckWGn+rUvlHvnWWdLuZ4qZ80voZzq16HSzLboKrkwfQH9tCXPBoAbezGvbwFXrXTr4XeMwi5jURBnTydxZiljPlwYj1+h4hw6VVLWu8oZ61e1q9+IWD7+zGjsN9R/+UJIGjmbMp9C+UpSzkp64Cr1X6FTJR2/opQ2/x791lMiUXDSgsU8N24f/I5R+OSqJS0KOvfs5yatfCVtXI/EjCq2odLUePzCatb8NuaUWOuA82njsZZnaF2msEkqlzL3Yt7rrMlUs9a9U93wqmkP2if9Vfrth5eUSFRxUV/MU/P6Zjy10O2HH0ZFxDkXi4J64sywziORJr2aMf9T5aRXmwcBfpTTuRtTxt2YtHmz38rbKc2OxjeeNpGoYgFcgA+HlaYctdidgd+xU7GRNbXchLNzsD400l52+bjMnUKbOLM0Ed9azVh77FicdB85KPDDepvKKOCRXCq+Rf156FRIGSmJweN4y878ZiGn7wsH9cW8zkHvuLPwO1aVxI4j6TS1Oek0MkQ6WkoZn0TCRRWzUCk/tmadDPH/BleIUz1BFiT4daM+yJojo5mv5kfWrlUfC5Wkzc5DYlj0c/R39PvFdtx1wisDv2OpdHnLqlppbN3B2XEjlIkzsQehOGG+v5I1T/tq86DCj/q8tPfCnC6nzN9DPj318VAJiUQr0t/R71cKescrC7/4ha2x2vRQ5CcqNqGSDPl+V672/xdQuNk1FlT4HaN+zq5LGQb/KLfTvEgdIlSaHer9Kfp9Mx466RWH/6gEYWY48pCKSyj0mAwBKxnzzypZ6wTSpDcDpZmDDr9u5NAvS/0rafNPwzTVq4lnhyMPo98346GTJvwdVmFiIF2dtJ7A1X6pmWK6CX7U28m0JPPgx2dTRkodrqtF+DWHBX7kDpzLmPcgRxwW9ZYCveNugt8x4uDc3pXR0D2zqfhGddiuFOHX3O3wA9RCeuBGmeOWcZUruHng4xTuRvgdIy6IT3XSLEm8bnjIZ3VslQi/5m6Gf3Zi/WYZ0tqPerYiL3w3ww8jPtp7FR6eHVu/SX1F14jwa+5G+PFq5HLG/GKrX23e7fDrRtywSaYkcczt7F+jvirwIvyauw3+UnrgfXLVeg5Xr5Zu7xSHCX7Ebf5dipPWgVJq4F+rrwu0CL/mboFfhqi/Xc2a/9tO7LCrPYkdwgS/Y8QR8bQ3C2Ws7xfH429TXxtIEX7NQYd/38jac0pp84/l6nSk3Smdwgi/4/nUbRLnUsr8xJNj685WXx8oEX7NQYa/PGGNVTuYzDHM8MOIr7ZZ6JcyFRhVRQiMCL/mIMI/vcOwyhnzv9lpnNvc4XWHHX7d2CyE+JfT5p3TY5apiuJ7EX7NQYL/wQt7Xl7JmB+Sq878Cxyadcx2mfAvtLNZSNojV0kbv4/2UUXyrQi/5qDAXxg33ilX+/9nrz57Sc/UAhP+RqMdnHRv5Yz103LKfIcqli9F+DX7Hf79W9dEK1nzs/ZLG5ewCacdJvyLG5ukqpPWCbyc9entayKqeL4S4dfsZ/grafNflbPms7iqtPqevRcT/sWN9nGeDShlzGdlivZuVUTfiPBr9iP8eBmInjhzpaF3TPjdGe2FWKlnA76FRKiqqCsuwq/ZT/Dnd7/pHJnXNyTO9IsJ/9LsPBsg7TlXTps3o31VkVdMhF+zn+DP7ej/cO09F9SqbXpCb7km/Es32hHtiXadGe2/XhV5xUT4NfsJ/unNvTfXRvtquZ2dffGoWxP+JVq1H3Lh10bX1KR9b1JFXjERfs1+gn92KPrR2og0ylCkltu6ppYbb+07B5drwu/Sqs3Qfrmt9Vz4aFfpZx9RRV4xEX7NvoN/26p62VRmVbwrLZdy3jHQpKN10IT/NEb7APoJo5YbWfiOO7Qr4V9owq9pAfyOnZPAdm/vHGylCf8iRrvgHXfSTnq7OSb8jSb8mprC7xidCS8eHe1/qbOd3AHbbMLfxKod0C6LvdiS8Dea8GtaFH4YHQvesqq+KKiGmQ0dsk0m/JoRd4k/2gHtMd82zdpNTPgbTfg1nRZ+x05H27pa5pedWxQk/GIVa8Qd8T8d9I4Jf6MJvybX8DtWnQ6LgnlnUbBZh22RQw8/4itxRrz1+Lsx4W804de0ZPgdOyeBHe1dFAwt/IgnFvMkvnq8l2LC32jCr8kz/I7RKZOxWm6sPYuCoYNfxQ/xRFy9QO+Y8Dea8GtaNvwwOii8ZXUttzNe78AtOgmEBn4VM8QPcZyPabN4uzThbzTh19QS+B2rDpsbWWM/dNKKk0DXw+9APzFgx60V0Dsm/I0m/JpaCr9j1Xlz29Yue1Gwq+FHXOzFvLUL4tYqE/5GE35NbYHfsXMSsBcFVWdvBsEi7kr4EQeJx3IW89yY8Dea8GtqK/yO0bmTq+ydZvOd/2QgTuGugl/VG3FAPNoFvWPC32jCr6kj8MPo6DAWBcfdLwp2Bfyqrqh3qxbz3JjwN5rwa+oY/I5Vx8fiVn5+UbAJMMqBht95FNrecdfaxTw3JvyNJvyaOg6/YwVBbjsWBU/9kFBg4Ud9pF6on17fTprwN5rwa1ox+B0DikV2DgYOflX+3Oi6RXfcdcKEv9GEX9OKww8PwQIJFgXxkJAGUWDgd6DHQzrOYh7q1ay+HTLhbzTh1+QL+B3bwIixc1AtCh69cqO/4Uf5pJz2Yt4Sdtx1woS/0YRfk6/gd+wAtK2vdnQSw2nznwCbKrIvdOjSc1+dy1iPH8ladjn9BL1jwt9owq/Jl/A7FpiObJWpwLa+R2pjg2eoIvtCezf1nZnbtmbPYSmf36B3TPgbTfg1+Rp+sR2rod4HVXF9pdmh3of80KFPZcLfaMKvKRDw+yRWuvzUoU9lwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt9owq+J8HsT4Xcvwq+Z8Ls34fduwt/onmNbV9UKiagEpvkftNuE370Jv3cT/rrBOXgH9z0zQ9HPVZKx2gsrVBjC796E37sJf93gHLyDe7tAUphNpUT04drIqloZ/9HkQ+0y4Xdvwu/dYYYfPINr8F1KxB4C7/XSKO27ePUrSsnYHxYT0fKL8kd59aFmB2ulCb97E37vDiP84DcvQ3zwLBf3Umk48uEfCOf1kjTRbKL3AjlL3I3hwZEt7S8g4Xdvwu/dYYQf/ILjciJ218Et52+sl8CFColoRkYB+xA0+dc+izT7guWa8Ls34ffusMAPTsGr4vap2WQ0Xf/mJepAMnKeTAU+XU5GXzjepsISfvcm/N4dBvhz4uMjsVpJeBVuP/WY8Fv/1mUol4xcVEzEfoS5Q7XFC4KE370Jv3d3M/zgsSpDfPBZTER+ODsUeXv921qkG3p6XiYjgGtl/jCFQGIhoRUnAcLv3oTfu7sRfvAHDlE34fJQcTh6LTitf1MbdHBL77ricOyrWExoRQUIv3sTfu/uRvidRflSInp7ftMb1ta/oQMqJqJbxHvss84ypgKE370Jv3d3C/zgzL5nL/UR/h4pJqPJ+pE7rL2bzz6rOBS9uZKIVU7IfAMLDs0KvJgJv3sTfu/uBvjBF+b1MsSvCPgf23Ppua+uH3UFld8ce3N+OHLfiZFY7bAMQ5YyCiD87k34vTvo8IMrrOTLHP9bU8Jb/Wg+Un4odlUlGd2vhiSuTgKE370Jv3cHEX7wA45Q9nIiuj+XiO2uH8Wnmtq0JiKjgNuweQA7h5pVSjfhd2/C791BhB/8gKPcUPRzh4bO660fIQDKD/W+o5yM/gRBt3cRNakcTPjdm/B7d1DgByfgxd5kl4j+OD98/mX1TwZMezf1nSlzlA9JJXIIfrNnAwi/exN+7w4C/M49e5k6z+aHYx/cMzZ4Rv1TAdb0pqgpo4A7m20WIvzuTfi92+/wH1XPzQgnd0wNRY36X3eR8oneUTmrPYaGKCXrowDC796E37v9CD/6f0nds5fR8aP5ZGRH/a+6VPsTfeeWEtE/LiaiR/FsAEYDhN+dCb93+w1+9Hv0f2HhSCER/cTPhYv6X4RAM1esfmslEf1+bXR1TaD7hfr1iorwexPhX5okVo+i30v//9705t5/rn4dLqHTlJOR908PR+6rXX3hy+u/XTkRfm8i/O5VGxs8Q2J1nwzzf0/9Ktya3nLOa2oX9/y6+nHFRPi9ifC7F/o5+rv6kfKLCL83EX4q8CL83kT4qcCL8HsT4acCL8LvTYSfCrwIvzcRfirwIvzeRPipwIvwexPhpwIvwu9NhJ8KvAi/NxF+KvAi/N5E+KnAi/B7E+GnAi/C702Enwq8CL83EX4q8CL83kT4qcDL7/Aj+8v0cPQnqri+kpTvpyjfyWX2iwk/taj8Dj/e0VZIRA9ODUcuVkX2hXKbey8pSrlQvmbl9oMJP7Wo/A6/k9O9lIgeKyVif3rgksh5qugrouc29b5eyvFnUp7ji72bwQ8m/NSi8jv8MABzXuEk0D1ZHI5kVfE7qmIyMinf/xTK4fbVbCtpwk8tqunN0Zv8Dr9uvAPhmMyzy8noPQcv771AVaOten7oDW8sJ6L/89hI4zsY/Gy7XYd6P6aqQVELJVf+j9XGVmNe7fsrmWPntc1yFS6XEpEb9l28+hWqOi3VA2+L/UZpOHKjjDQq+D4vr2NfCaMd0Z5oV175qVNqZnh1TK6iX6jKFc3PK9cnGx0ci232VCAZ/fupzb1Dqkot0dRw77Ac9x9wfHxPUE6MMNqxIu1ZGI58YXrrmqiqEkU11+xwZFM5EXsoqJ29KmWWofkXf7U5ukpVyZOevSK6Wo7zpSCfDNGOU0ORzapKFHV64SWjpUTshqIMpwM5zMUoIBE9WBiOvU9VaUnKDcWukc8/j+MEaRqUF6O90G6lZOQP0Y6qShS1NB1M9F5QTMTuxsLa4QAtcAHWOSmvWg/4/kxi9VtVlRbVzPCqf1FJRr+PV0nh80Ea9WABEiMUmaLcM7vp/I2qShS1POGWmlxNAnNryzFGLOqdcEcLw9E/2XNF7LWqSgv0jPy+JP+Pv8PfB2mkg/ZQ7fJkLhHNqCpRVOt0IBk5T4aSn5J58LHjI8EaBeCtyACkkog+NpvoHVNVslUY7t0pV/t/sqcK6u3JzY7jN+MEhXYoJ6MvlJKxTz/7zujrVJUoqj2Sq8vvlhORH9bkConFtSAOjWeGIt+Y2Ry5fHYoegd+DtI9e8QbcX9RwC9KO+SSkYtU01BU+1W7oedlxWT02nIiNhXERTEAjxVx/Iufg1B2lNFZzETci0PRf3+DtINqEorqrA5tfv364nDsq1gM9POW1mbOC0jNfu9X40SFOMsJ4PbnE2/oV01AUSsrmQpsKSVij9TnzcGaCvjZiKNzz76SjP08NxzZqkJOUf4RXr0sV9Oby4loNUgr5n414odblTLEr8jU5KZHf4evtqZ8rvzm2JuLyci3j0vHxTCVo4ClGfFC3BC/4nDk24inCi1FBUP5odhVMgrYjyFrkJ4NWCkjPs49e7naP50f7r1ShZKigqepTa+L5Iain0Oyi2NyJWvW6em6ER+Z35/IS7yelripEFJUsJXfsuod5WT0x3g2wO9ZbzppxAHxQFxklPRAfvj8y1TIKKp7hE0m+eHof5ROnsNCVtButbXaqL+9oJeMzs4moh/aMzZ4hgoVRXWnnt0UNYvJ6J32k3UBezagVcYzEbDM8e+YGooaKjQUFQ4VEr1jlWT00fqzAd2/IIj6OXsM5Gr/aH5z76gKBUWFT/sTfeeWEtFPBG033VKNejm7C4vJ2H9GvVUIKCrcOhjgffSLGfVAfVCvaiL6Pbd5BSgqdJpN9P4bGRo/h6FxkDYLnWyU29mEI/U5UBqOvFdVkaKoUwm592R4/EVcMbEo2AwuvxvlRvmREPWZ4dUxVTWKotxodiiyWU4CD9v3wAPwbADK52zCKSViDyERqqoKRVFLFfLwF4ajNxZ9nEgU0DuJM+VkVSokIjf8oE3vD6Co0Gk20XuBXFXvxiOwfsu6g/LYj+YmYncd3MLEmRTVFqlEovswtF7JzUL4XmcTjvz7VIGJMymq/aonEo19WubVx4+vwIIgph524sxE7JiU41OPSXlU0SiK6oRyw5GLi4nYjzDX7kQiURy/njgTV/vID0tMnElRK6cbenpeVtYSiWKzTKtPAjgejovj43uKw9Fr8b2qCBRFraScRKJYfGt1IlF7A5Ict5SI3l5g4kyK8qeKiegW8R77Kr2MqQA+59yzl+M9UkxGk+orKIryq/ZuPvus4lD05koiVvGyWcjZhCND/IoM8W96dAsTZ1JUoITEl/nhyH0nRur57t2MAuqJM/Eij8i3p5g4k6KCLSQSrSRPnUgUP+P39QW96P5iIrZbfZSiqKBratOaiFzNb7MTiW5dmEgUP+P3SDR6aOi8XvURiqK6SfmhXiQS/Qnu1QN4/CtX+x8zcSZFhUC4R19I9l5THYk9kRuOvE/9mgqVenr+P+OvTjWo+kMRAAAAAElFTkSuQmCC</data> + <data key="type">image/png</data> + <data key="name" unique="prefix">magento-logo.png</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml new file mode 100644 index 0000000000000..dfe6bfdf681e1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="productAttributeWithTwoOptions" type="ProductAttribute"> + <data key="name" unique="suffix">ProductAttributeWithTwoOptions</data> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">textarea</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="backend_type">text</data> + <data key="is_wysiwyg_enabled">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">testattribute</data> + <data key="frontend_input">select</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="ProductAttributeMultiselectTwoOptions" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">multiselect</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="ProductAttributeText" extends="productAttributeWithTwoOptions"> + <data key="frontend_input">text</data> + </entity> + <entity name="productVisualSwatchAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">swatch_visual</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml new file mode 100644 index 0000000000000..aa7c10122d930 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAttributeMediaGalleryEntryMagentoLogo" type="media_gallery_entries"> + <data key="media_type">image</data> + <data key="label" unique="suffix">Magento Logo</data> + <data key="position">1</data> + <array key="types"> + <item>image</item> + <item>small_image</item> + <item>thumbnail</item> + </array> + <data key="disabled">false</data> + <requiredEntity type="image_content">MagentoLogoImageContent</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml new file mode 100644 index 0000000000000..8ecae212b7c2d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="productAttributeOption1" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option1</data> + <data key="is_default">false</data> + <data key="sort_order">0</data> + <requiredEntity type="StoreLabel">Option1Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option1Store1</requiredEntity> + </entity> + <entity name="productAttributeOption2" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option2</data> + <data key="is_default">true</data> + <data key="sort_order">1</data> + <requiredEntity type="StoreLabel">Option2Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option2Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOptionGetter" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + </entity> + <entity name="ProductAttributeOption3" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option3</data> + <data key="is_default">false</data> + <data key="sort_order">2</data> + <requiredEntity type="StoreLabel">Option3Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option3Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption4" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option4</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option4Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option4Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption5" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option5</data> + <data key="is_default">false</data> + <data key="sort_order">4</data> + <requiredEntity type="StoreLabel">Option5Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option5Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption6" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option6</data> + <data key="is_default">false</data> + <data key="sort_order">5</data> + <requiredEntity type="StoreLabel">Option6Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option6Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption7" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">Green</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option7Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option8Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption8" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">Red</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option9Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option10Store1</requiredEntity> + </entity> + <entity name="ProductAttributeAdminOption1" extends="productAttributeOption1"> + <requiredEntity type="StoreLabel">AdminOption1Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option1Store1</requiredEntity> + </entity> + <entity name="ProductAttributeAdminOption2" extends="productAttributeOption2"> + <requiredEntity type="StoreLabel">AdminOption2Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option2Store1</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml new file mode 100644 index 0000000000000..1ee1d08e192d6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AddToDefaultSet" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">4</data> + <data key="attributeGroupId">7</data> + <data key="sortOrder">0</data> + <data key="label">Default</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml new file mode 100644 index 0000000000000..8d588a192ed90 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="colorProductAttribute" type="product_attribute"> + <data key="default_label" unique="suffix">Color</data> + <data key="attribute_quantity">1</data> + <data key="input_type">Dropdown</data> + </entity> + <entity name="colorProductAttribute1" type="product_attribute"> + <data key="name" unique="suffix">White</data> + <data key="price">1.00</data> + </entity> + <entity name="colorProductAttribute2" type="product_attribute"> + <data key="name" unique="suffix">Red</data> + <data key="price">2.00</data> + </entity> + <entity name="colorProductAttribute3" type="product_attribute"> + <data key="name" unique="suffix">Blue</data> + <data key="price">3.00</data> + </entity> + <entity name="ColorProductAttribute4" type="product_attribute"> + <data key="name" unique="suffix">Green</data> + <data key="price">4.00</data> + </entity> + <entity name="ColorProductAttribute5" type="product_attribute"> + <data key="name" unique="suffix">Black</data> + <data key="price">5.00</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml new file mode 100644 index 0000000000000..343c42a7e62f5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -0,0 +1,357 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultProduct" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimpleProduct" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimpleOne" type="product2"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="ApiSimpleTwo" type="product2"> + <data key="sku" unique="suffix">api-simple-product-two</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product Two</data> + <data key="price">234.00</data> + <data key="urlKey" unique="suffix">api-simple-product-two</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="ApiSimpleProductUpdateDescription" type="product2"> + <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> + </entity> + <entity name="ApiSimpleProductUpdateName" type="product"> + <data key="name" unique="suffix">Updated Api Simple Product</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + </entity> + <entity name="SimpleProduct" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">200.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProduct2" type="product"> + <data key="sku" unique="suffix">SimpleProduct2</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct2</data> + <data key="price">300.00</data> + <data key="quantity">200</data> + <data key="visibility">4</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProduct3" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">123.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">1000</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> + <entity name="SimpleOne" type="product2"> + <data key="sku" unique="suffix">SimpleOne</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">1.23</data> + <data key="visibility">4</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attributes">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="SimpleTwo" type="product2"> + <data key="sku" unique="suffix">SimpleTwo</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">1.23</data> + <data key="visibility">4</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductUrlKey</requiredEntity> + </entity> + <entity name="SimpleOption" type="product2"> + <data key="sku" unique="suffix">SimpleOne</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProductOption</data> + <data key="price">10.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="SetProductVisibilityHidden" type="product2"> + <data key="visibility">1</data> + </entity> + <entity name="ProductImage" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento-logo.png</data> + <data key="fileName">magento-logo</data> + </entity> + <entity name="MagentoLogo" type="image"> + <data key="title" unique="suffix">MagentoLogo</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento-logo.png</data> + <data key="filename">magento-logo</data> + <data key="file_extension">png</data> + </entity> + <entity name="TestImageNew" type="image"> + <data key="title" unique="suffix">magento-again</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento-again.jpg</data> + <data key="filename">magento-again</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="productWithDescription" type="product"> + <data key="sku" unique="suffix">testProductWithDescriptionSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductWithDescriptionName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testproductwithdescriptionurlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> + <entity name="productWithOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <data key="file">magento.jpg</data> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionArea</requiredEntity> + <requiredEntity type="product_option">ProductOptionFile</requiredEntity> + <requiredEntity type="product_option">ProductOptionDropDown</requiredEntity> + <requiredEntity type="product_option">ProductOptionRadiobutton</requiredEntity> + <requiredEntity type="product_option">ProductOptionCheckbox</requiredEntity> + <requiredEntity type="product_option">ProductOptionMultiSelect</requiredEntity> + <requiredEntity type="product_option">ProductOptionDate</requiredEntity> + <requiredEntity type="product_option">ProductOptionDateTime</requiredEntity> + <requiredEntity type="product_option">ProductOptionTime</requiredEntity> + </entity> + <entity name="VirtualProduct" type="product"> + <data key="sku" unique="suffix">virtualproduct</data> + <data key="type_id">virtual</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="price">99.99</data> + <data key="quantity">250</data> + <data key="weight">0</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> + <entity name="SimpleProductWithNewFromDate" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">125.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">1000</data> + <data key="urlKey" unique="suffix">simpleproduct</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductNewsFromDate</requiredEntity> + </entity> + <entity name="SimpleProductWithImage" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">125.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">1000</data> + <data key="urlKey" unique="suffix">simple-product-with-image</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="media_gallery_entries">ProductAttributeMediaGalleryEntryMagentoLogo</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiProductWithDescription" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> + <entity name="ApiSimpleOneHidden" type="product2"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">1</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="ApiSimpleTwoHidden" type="product2"> + <data key="sku" unique="suffix">api-simple-product-two</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">1</data> + <data key="name" unique="suffix">Api Simple Product Two</data> + <data key="price">234.00</data> + <data key="urlKey" unique="suffix">api-simple-product-two</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="ProductWithOptions2" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> + </entity> + <entity name="ProductWithFileOption" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionFile</requiredEntity> + </entity> + <entity name="SimpleProductWithCustomAttributeSet" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <var key="attribute_set_id" entityKey="attribute_set_id" entityType="CatalogAttributeSet"/> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="weight">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimpleSingleQty" extends="ApiSimpleOne"> + <data key="quantity">1</data> + <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> + </entity> + <entity name="GetProduct2" type="product2"> + <var key="sku" entityKey="sku" entityType="product2"/> + </entity> + <entity name="ApiSimpleWithQty100" extends="ApiSimpleOne"> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="ProductWithFieldOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionField2</requiredEntity> + </entity> + <entity name="defaultSimpleProduct" type="product"> + <data key="name" unique="suffix">Testp</data> + <data key="sku" unique="suffix">testsku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="price">560.00</data> + <data key="urlKey" unique="suffix">testurl-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="Magento3" type="image"> + <data key="title" unique="suffix">Magento3</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento3.jpg</data> + <data key="filename">magento3</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="OutOfStockProduct" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">OutOfStockProduct</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">0</data> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProductUpdatePrice11" type="product2"> + <data key="price">11.00</data> + </entity> + <entity name="SimpleProductUpdatePrice14" type="product2"> + <data key="price">14.00</data> + </entity> + <entity name="SimpleProductUpdatePrice16" type="product2"> + <data key="price">16.00</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml new file mode 100644 index 0000000000000..5b2dc5e691a2b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EavStockItem" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_1000</requiredEntity> + </entity> + <entity name="EavStock1" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_1</requiredEntity> + </entity> + <entity name="EavStock100" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_100</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml new file mode 100644 index 0000000000000..9f74a86802e0f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductGridPagerData"> + <data key="pageSize">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml new file mode 100644 index 0000000000000..000bb2095002c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="RelatedProductLink" type="product_link"> + <var key="sku" entityKey="sku" entityType="product2"/> + <var key="linked_product_sku" entityKey="sku" entityType="product"/> + <data key="link_type">related</data> + <data key="linked_product_type">simple</data> + <data key="position">1</data> + <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml new file mode 100644 index 0000000000000..bd4f807880ab8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="OneRelatedProductLink" type="product_links"> + <requiredEntity type="product_link">RelatedProductLink</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml new file mode 100644 index 0000000000000..82ce0d076f115 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductOptionField" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionField</data> + <data key="sku">OptionField</data> + <data key="type">field</data> + <data key="type_label">Field</data> + <data key="is_require">true</data> + <data key="sort_order">1</data> + <data key="price">10</data> + <data key="price_type">fixed</data> + <data key="max_characters">0</data> + </entity> + <entity name="ProductOptionField2" type="product_option" extends="ProductOptionField"> + <data key="title">OptionField2</data> + <data key="sku">OptionField2</data> + <data key="price">20</data> + </entity> + <entity name="ProductOptionArea" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionArea</data> + <data key="type">area</data> + <data key="is_require">true</data> + <data key="sort_order">2</data> + <data key="price">10</data> + <data key="price_type">percent</data> + <data key="max_characters">0</data> + </entity> + <entity name="ProductOptionFile" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionFile</data> + <data key="type">file</data> + <data key="type_label">File</data> + <data key="is_require">true</data> + <data key="sort_order">3</data> + <data key="price">9.99</data> + <data key="price_type">fixed</data> + <data key="file_extension">png, jpg, gif</data> + <data key="image_size_x">0</data> + <data key="image_size_y">0</data> + </entity> + <entity name="ProductOptionDropDown" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionDropDown</data> + <data key="type">drop_down</data> + <data key="sort_order">4</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueDropdown1</requiredEntity> + <requiredEntity type="product_option_value">ProductOptionValueDropdown2</requiredEntity> + </entity> + <entity name="ProductOptionDropDownWithLongValuesTitle" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionDropDownWithLongTitles</data> + <data key="type">drop_down</data> + <data key="sort_order">4</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle1</requiredEntity> + <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle2</requiredEntity> + </entity> + <entity name="ProductOptionRadiobutton" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionRadioButtons</data> + <data key="type">radio</data> + <data key="sort_order">5</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueRadioButtons1</requiredEntity> + <requiredEntity type="product_option_value">ProductOptionValueRadioButtons2</requiredEntity> + </entity> + <entity name="ProductOptionCheckbox" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionCheckbox</data> + <data key="type">checkbox</data> + <data key="sort_order">6</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueCheckbox</requiredEntity> + </entity> + <entity name="ProductOptionMultiSelect" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionMultiSelect</data> + <data key="type">multiple</data> + <data key="sort_order">7</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueMultiSelect1</requiredEntity> + <requiredEntity type="product_option_value">ProductOptionValueMultiSelect2</requiredEntity> + </entity> + <entity name="ProductOptionDate" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionDate</data> + <data key="type">date</data> + <data key="sort_order">8</data> + <data key="is_require">true</data> + <data key="price">1234</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionDateTime" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionDateTime</data> + <data key="type">date_time</data> + <data key="sort_order">9</data> + <data key="is_require">true</data> + <data key="price">0.00</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionTime" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionTime</data> + <data key="type">time</data> + <data key="sort_order">10</data> + <data key="is_require">true</data> + <data key="price">0.00</data> + <data key="price_type">percent</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml new file mode 100644 index 0000000000000..82063a771d2a3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductOptionValueDropdown1" type="product_option_value"> + <data key="title">OptionValueDropDown1</data> + <data key="sort_order">1</data> + <data key="price">0.01</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueDropdown2" type="product_option_value"> + <data key="title">OptionValueDropDown2</data> + <data key="sort_order">2</data> + <data key="price">0.01</data> + <data key="price_type">percent</data> + </entity> + <entity name="ProductOptionValueRadioButtons1" type="product_option_value"> + <data key="title">OptionValueRadioButtons1</data> + <data key="sort_order">1</data> + <data key="price">99.99</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueRadioButtons2" type="product_option_value"> + <data key="title">OptionValueRadioButtons2</data> + <data key="sort_order">2</data> + <data key="price">99.99</data> + <data key="price_type">percent</data> + </entity> + <entity name="ProductOptionValueCheckbox" type="product_option_value"> + <data key="title">OptionValueCheckbox</data> + <data key="sort_order">1</data> + <data key="price">123</data> + <data key="price_type">percent</data> + </entity> + <entity name="ProductOptionValueMultiSelect1" type="product_option_value"> + <data key="title">OptionValueMultiSelect1</data> + <data key="sort_order">1</data> + <data key="price">1</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueMultiSelect2" type="product_option_value"> + <data key="title">OptionValueMultiSelect2</data> + <data key="sort_order">2</data> + <data key="price">2</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueDropdownLongTitle1" type="product_option_value"> + <data key="title">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111</data> + <data key="sort_order">1</data> + <data key="price">10</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueDropdownLongTitle2" type="product_option_value"> + <data key="title">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222</data> + <data key="sort_order">2</data> + <data key="price">20</data> + <data key="price_type">percent</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml new file mode 100644 index 0000000000000..a071c068b575d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Qty_1000" type="stock_item"> + <data key="qty">1000</data> + <data key="is_in_stock">true</data> + </entity> + <entity name="Qty_1" type="stock_item"> + <data key="qty">1</data> + <data key="is_in_stock">true</data> + </entity> + <entity name="Qty_100" type="stock_item"> + <data key="qty">100</data> + <data key="is_in_stock">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml new file mode 100644 index 0000000000000..3af9b2c54a4f0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Option1Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option1</data> + </entity> + <entity name="Option1Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option1</data> + </entity> + <entity name="Option2Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option2</data> + </entity> + <entity name="Option2Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option2</data> + </entity> + <entity name="Option3Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option3</data> + </entity> + <entity name="Option3Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option3</data> + </entity> + <entity name="Option4Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option4</data> + </entity> + <entity name="Option4Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option4</data> + </entity> + <entity name="Option5Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option5</data> + </entity> + <entity name="Option5Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option5</data> + </entity> + <entity name="Option6Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option6</data> + </entity> + <entity name="Option6Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option6</data> + </entity> + <entity name="Option7Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">Green</data> + </entity> + <entity name="Option8Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">Green</data> + </entity> + <entity name="Option9Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">Red</data> + </entity> + <entity name="Option10Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">Red</data> + </entity> + <entity name="AdminOption1Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">admin_option_1</data> + </entity> + <entity name="AdminOption2Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">admin_option_2</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml new file mode 100644 index 0000000000000..ae1b5afe4008a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="TestDataTierPrice" type="data"> + <data key="goldenPrice1">$676.50</data> + <data key="goldenPrice2">$615.00</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml new file mode 100644 index 0000000000000..d2f8339892c07 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductLinkWidget" extends="ProductsListWidget"> + <data key="type">Catalog Product Link</data> + <data key="template">Product Link Block Template</data> + </entity> + <entity name="RecentlyComparedProductsWidget" type="widget"> + <data key="type">Recently Compared Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Compared Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + <data key="products_to_display">5</data> + </entity> + <entity name="RecentlyViewedProductsWidget" type="widget"> + <data key="type">Recently Viewed Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Viewed Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + <data key="products_to_display">5</data> + </entity> + <entity name="CatalogCategoryLinkWidget" type="widget"> + <data key="type">Catalog Category Link</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Test Widget</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="sort_order">0</data> + <data key="display_on">All Pages</data> + <data key="container">Main Content Area</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/LICENSE.txt b/app/code/Magento/Catalog/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Catalog/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 <insert your license name here>" 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/app/code/Magento/Catalog/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Catalog/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Catalog/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 <insert your license name here>" 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/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml new file mode 100644 index 0000000000000..00a5f8fef18f1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="AddCatalogAttributeToAttributeSet" dataType="CatalogAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets" method="POST"> + <contentType>application/json</contentType> + <object key="attributeSet" dataType="CatalogAttributeSet"> + <field key="attribute_set_name">string</field> + <field key="sort_order">integer</field> + </object> + <field key="skeletonId">integer</field> + </operation> + <operation name="DeleteCatalogAttributeFromAttributeSet" dataType="CatalogAttributeSet" type="delete" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetCatalogAttributesFromDefaultSet" dataType="CatalogAttributeSet" type="get" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml new file mode 100644 index 0000000000000..4de036565eee3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogStorefrontConfiguration" dataType="catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="catalog_storefront_config"> + <object key="frontend" dataType="catalog_storefront_config"> + <object key="fields" dataType="catalog_storefront_config"> + <object key="list_mode" dataType="list_mode"> + <field key="value">string</field> + </object> + <object key="grid_per_page_values" dataType="grid_per_page_values"> + <field key="value">string</field> + </object> + <object key="grid_per_page" dataType="grid_per_page"> + <field key="value">string</field> + </object> + <object key="list_per_page_values" dataType="list_per_page_values"> + <field key="value">string</field> + </object> + <object key="list_per_page" dataType="list_per_page"> + <field key="value">string</field> + </object> + <object key="default_sort_by" dataType="default_sort_by"> + <field key="value">string</field> + </object> + <object key="list_allow_all" dataType="list_allow_all"> + <field key="value">integer</field> + </object> + <object key="remember_pagination" dataType="remember_pagination"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_category" dataType="flat_catalog_category"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_product" dataType="flat_catalog_product"> + <field key="value">integer</field> + </object> + <object key="swatches_per_product" dataType="swatches_per_product"> + <field key="value">string</field> + </object> + <object key="show_swatches_in_product_list" dataType="show_swatches_in_product_list"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DefaultCatalogStorefrontConfiguration" dataType="default_catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="default_catalog_storefront_config"> + <object key="frontend" dataType="default_catalog_storefront_config"> + <object key="fields" dataType="default_catalog_storefront_config"> + <object key="list_mode" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="grid_per_page_values" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="grid_per_page" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_per_page_values" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_per_page" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="default_sort_by" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="remember_pagination" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="flat_catalog_category" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="swatches_per_product" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="show_swatches_in_product_list" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_allow_all" dataType="list_allow_all"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_product" dataType="flat_catalog_product"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml new file mode 100644 index 0000000000000..7c57827356242 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogPriceConfigState" dataType="catalog_price_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="catalog_price_config_state"> + <object key="price" dataType="catalog_price_config_state"> + <object key="fields" dataType="catalog_price_config_state"> + <object key="scope" dataType="scope"> + <field key="value">string</field> + </object> + <object key="default_product_price" dataType="default_product_price"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml new file mode 100644 index 0000000000000..c51eee83c8369 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogRecentlyProductsConfiguration" dataType="catalog_recently_products" type="create" + auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="catalog_recently_products"> + <object key="recently_products" dataType="catalog_recently_products"> + <object key="fields" dataType="catalog_recently_products"> + <object key="synchronize_with_backend" dataType="synchronize_with_backend"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml new file mode 100644 index 0000000000000..c22743f6a0642 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/category-meta.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCategory" dataType="category" type="create" auth="adminOauth" url="/V1/categories" method="POST"> + <contentType>application/json</contentType> + <object key="category" dataType="category"> + <field key="parent_id">integer</field> + <field key="name">string</field> + <field key="is_active">boolean</field> + <field key="position">integer</field> + <field key="level">integer</field> + <field key="children">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="path">string</field> + <field key="include_in_menu">boolean</field> + <array key="available_sort_by"> + <value>string</value> + </array> + <field key="extension_attributes">empty_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </object> + </operation> + + <operation name="UpdateCategory" dataType="category" type="update" auth="adminOauth" url="/V1/categories/{id}" method="PUT"> + <contentType>application/json</contentType> + <object key="category" dataType="category"> + <field key="id">integer</field> + <field key="parent_id">integer</field> + <field key="name">string</field> + <field key="is_active">boolean</field> + <field key="position">integer</field> + <field key="level">integer</field> + <field key="children">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="path">string</field> + <array key="available_sort_by"> + <value>string</value> + </array> + <field key="include_in_menu">boolean</field> + <field key="extension_attributes">empty_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </object> + </operation> + + <operation name="DeleteCategory" dataType="category" type="delete" auth="adminOauth" url="/V1/categories/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + + <operation name="GetCategory" dataType="category" type="get" auth="adminOauth" url="/V1/categories" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/category_product_link-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/category_product_link-meta.xml new file mode 100644 index 0000000000000..d9ff69d506e66 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/category_product_link-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCatalogCategoryProductLink" dataType="CategoryProductLink" + type="create" + auth="adminOauth" + url="/V1/categories/{categoryId}/products" + method="POST"> + <contentType>application/json</contentType> + <object key="productLink" dataType="CategoryProductLink"> + <field key="sku">string</field> + <field key="position">integer</field> + <field key="category_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml new file mode 100644 index 0000000000000..aed9b7a979836 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/custom_attribute-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomAttribute" dataType="custom_attribute" type="create"> + <field key="attribute_code">string</field> + <field key="value">string</field> + </operation> + <operation name="CreateCustomAttributeArray" dataType="custom_attribute_array" type="create"> + <field key="attribute_code">string</field> + <array key="value"> + <value>string</value> + </array> + </operation> + <operation name="UpdateCustomAttribute" dataType="custom_attribute" type="update"> + <field key="attribute_code">string</field> + <field key="value">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml new file mode 100644 index 0000000000000..d8410593cb5b4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/empty_extension_attribute-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateEmptyExtensionAttribute" dataType="empty_extension_attribute" type="create"> + </operation> + <operation name="UpdateEmptyExtensionAttribute" dataType="empty_extension_attribute" type="update"> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml new file mode 100644 index 0000000000000..d0bcbd3e5db97 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/frontend_label-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateFrontendLabel" dataType="FrontendLabel" type="create"> + <field key="store_id">integer</field> + <field key="label">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml new file mode 100644 index 0000000000000..23be8408d4a1e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProduct" dataType="product" type="create" auth="adminOauth" url="/V1/products" method="POST"> + <contentType>application/json</contentType> + <object dataType="product" key="product"> + <field key="sku">string</field> + <field key="name">string</field> + <field key="attribute_set_id">integer</field> + <field key="price">number</field> + <field key="status">integer</field> + <field key="visibility">integer</field> + <field key="type_id">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="weight">integer</field> + <field key="extension_attributes">product_extension_attribute</field> + <array key="product_links"> + <value>product_link</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute_array</value> + </array> + <array key="options"> + <value>product_option</value> + </array> + <array key="media_gallery_entries"> + <value>media_gallery_entries</value> + </array> + </object> + </operation> + <operation name="UpdateProduct" dataType="product" type="update" auth="adminOauth" url="/V1/products/{sku}" method="PUT"> + <contentType>application/json</contentType> + <object dataType="product" key="product"> + <field key="id">integer</field> + <field key="sku">string</field> + <field key="name">string</field> + <field key="attribute_set_id">integer</field> + <field key="price">number</field> + <field key="status">integer</field> + <field key="visibility">integer</field> + <field key="type_id">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="weight">integer</field> + <field key="extension_attributes">product_extension_attribute</field> + <array key="product_links"> + <value>product_link</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute_array</value> + </array> + <array key="options"> + <value>product_option</value> + </array> + </object> + <field key="saveOptions">boolean</field> + </operation> + <operation name="deleteProduct" dataType="product" type="delete" auth="adminOauth" url="/V1/products/{sku}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="CreateProduct2" dataType="product2" type="create" auth="adminOauth" url="/V1/products" method="POST"> + <contentType>application/json</contentType> + <object dataType="product2" key="product"> + <field key="sku">string</field> + <field key="name">string</field> + <field key="attribute_set_id">integer</field> + <field key="price">number</field> + <field key="status">integer</field> + <field key="visibility">integer</field> + <field key="type_id">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="weight">integer</field> + <field key="extension_attributes">product_extension_attribute</field> + <array key="product_links"> + <value>product_link</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + <array key="options"> + <value>product_option</value> + </array> + </object> + </operation> + <operation name="UpdateProduct2" dataType="product2" type="update" auth="adminOauth" url="/V1/products/{sku}" method="PUT"> + <contentType>application/json</contentType> + <object dataType="product2" key="product"> + <field key="id">integer</field> + <field key="sku">string</field> + <field key="name">string</field> + <field key="attribute_set_id">integer</field> + <field key="price">number</field> + <field key="status">integer</field> + <field key="visibility">integer</field> + <field key="type_id">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="weight">integer</field> + <field key="extension_attributes">product_extension_attribute</field> + <array key="product_links"> + <value>product_link</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + <array key="options"> + <value>product_option</value> + </array> + </object> + <field key="saveOptions">boolean</field> + </operation> + <operation name="deleteProduct2" dataType="product2" type="delete" auth="adminOauth" url="/V1/products/{sku}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetProduct2" dataType="product2" type="get" auth="adminOauth" url="/V1/products/{sku}" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml new file mode 100644 index 0000000000000..93396352ba506 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute-meta.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductAttribute" dataType="ProductAttribute" type="create" auth="adminOauth" url="/V1/products/attributes" method="POST"> + <contentType>application/json</contentType> + <object dataType="ProductAttribute" key="attribute"> + <field key="attribute_code">string</field> + <field key="default_value">string</field> + <field key="frontend_input">string</field> + <field key="entity_type_id">string</field> + <field key="backend_type">string</field> + <field key="backend_model">string</field> + <field key="source_model">string</field> + <field key="frontend_class">string</field> + <field key="default_frontend_label">string</field> + <field key="note">string</field> + <field key="scope">string</field> + <field key="is_unique">boolean</field> + <field key="is_searchable">boolean</field> + <field key="is_visible_in_advanced_search">boolean</field> + <field key="is_comparable">boolean</field> + <field key="is_used_for_promo_rules">boolean</field> + <field key="is_visible_on_front">boolean</field> + <field key="used_in_product_listing">boolean</field> + <field key="is_wysiwyg_enabled">boolean</field> + <field key="is_html_allowed_on_front">boolean</field> + <field key="used_for_sort_by">boolean</field> + <field key="is_filterable">boolean</field> + <field key="is_filterable_in_search">boolean</field> + <field key="is_used_in_grid">boolean</field> + <field key="is_visible_in_grid">boolean</field> + <field key="is_filterable_in_grid">boolean</field> + <field key="is_visible">boolean</field> + <field key="is_required">boolean</field> + <field key="is_user_defined">boolean</field> + <field key="extension_attributes">empty_extension_attribute-meta</field> + <array key="apply_to"> + <value>string</value> + </array> + <array key="options"> + <value>ProductAttributeOption</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute_array</value> + </array> + <array key="validation_rules"> + <value>validation_rule</value> + </array> + <array key="frontend_labels"> + <value>FrontendLabel</value> + </array> + </object> + </operation> + <operation name="UpdateProductAttribute" dataType="ProductAttribute" type="update" auth="adminOauth" url="/V1/products/attributes/{attribute_code}" method="PUT"> + <contentType>application/json</contentType> + <object dataType="ProductAttribute" key="attribute"> + <field key="attribute_code">string</field> + <field key="attribute_id">string</field> + <field key="default_value">string</field> + <field key="frontend_input">string</field> + <field key="entity_type_id">string</field> + <field key="backend_type">string</field> + <field key="backend_model">string</field> + <field key="source_model">string</field> + <field key="frontend_class">string</field> + <field key="default_frontend_label">string</field> + <field key="note">string</field> + <field key="scope">string</field> + <field key="is_unique">boolean</field> + <field key="is_searchable">boolean</field> + <field key="is_visible_in_advanced_search">boolean</field> + <field key="is_comparable">boolean</field> + <field key="is_used_for_promo_rules">boolean</field> + <field key="is_visible_on_front">boolean</field> + <field key="used_in_product_listing">boolean</field> + <field key="is_wysiwyg_enabled">boolean</field> + <field key="is_html_allowed_on_front">boolean</field> + <field key="used_for_sort_by">boolean</field> + <field key="is_filterable">boolean</field> + <field key="is_filterable_in_search">boolean</field> + <field key="is_used_in_grid">boolean</field> + <field key="is_visible_in_grid">boolean</field> + <field key="is_filterable_in_grid">boolean</field> + <field key="is_visible">boolean</field> + <field key="is_required">boolean</field> + <field key="is_user_defined">boolean</field> + <field key="extension_attributes">empty_extension_attribute-meta</field> + <array key="apply_to"> + <value>string</value> + </array> + <array key="options"> + <value>ProductAttributeOption</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute_array</value> + </array> + <array key="validation_rules"> + <value>validation_rule</value> + </array> + <array key="frontend_labels"> + <value>FrontendLabel</value> + </array> + </object> + </operation> + <operation name="DeleteProductAttribute" dataType="ProductAttribute" type="delete" auth="adminOauth" url="/V1/products/attributes/{attribute_code}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml new file mode 100644 index 0000000000000..50690655cf919 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_media_gallery_entry-meta.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductAttributeMediaGalleryEntry" dataType="media_gallery_entries" type="create"> + <field key="id">integer</field> + <field key="media_type" required="true">string</field> + <field key="label" required="true">string</field> + <field key="position" required="true">integer</field> + <field key="disabled" required="true">boolean</field> + <array key="types"> + <value>string</value> + </array> + <field key="file">string</field> + <field key="content">image_content</field> + <field key="extension_attributes">empty_extension_attribute</field> + </operation> + <operation name="CreateImageContent" dataType="image_content" type="create"> + <field key="base64_Encoded_data">string</field> + <field key="type" required="true">string</field> + <field key="name" required="true">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml new file mode 100644 index 0000000000000..176afa8d58d7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_option-meta.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductAttributeOption" dataType="ProductAttributeOption" type="create" auth="adminOauth" url="/V1/products/attributes/{attribute_code}/options" method="POST"> + <contentType>application/json</contentType> + <object dataType="ProductAttributeOption" key="option"> + <field key="label">string</field> + <field key="value">string</field> + <field key="sort_order">integer</field> + <field key="is_default">boolean</field> + <array key="store_labels"> + <value>StoreLabel</value> + </array> + </object> + </operation> + <operation name="DeleteProductAttributeOption" dataType="ProductAttributeOption" type="delete" auth="adminOauth" url="/V1/products/attributes/{attribute_code}/options/{option_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetProductAttributeOption" dataType="ProductAttributeOption" type="get" auth="adminOauth" url="/V1/products/attributes/{attribute_code}/options/" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml new file mode 100644 index 0000000000000..eef82b07aaf4f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_attribute_set-meta.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="AddProductAttributeToAttributeSet" dataType="ProductAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets/attributes" method="POST"> + <contentType>application/json</contentType> + <field key="attributeSetId">integer</field> + <field key="attributeGroupId">integer</field> + <field key="attributeCode">string</field> + <field key="sortOrder">integer</field> + </operation> + <operation name="DeleteProductAttributeFromAttributeSet" dataType="ProductAttributeSet" type="delete" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}/attributes/{attribute_code}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetProductAttributesFromDefaultSet" dataType="ProductAttributesFromDefaultSet" type="get" auth="adminOauth" url="/V1/products/attribute-sets/4/attributes" method="GET"> + <contentType>application/json</contentType> + </operation> + <operation name="GetDefaultProductAttributeSetInfo" dataType="DefaultProductAttributeSetInfo" type="get" auth="adminOauth" url="/V1/products/attribute-sets/4" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml new file mode 100644 index 0000000000000..8d0d1e66c81e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_extension_attribute-meta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductExtensionAttribute" dataType="product_extension_attribute" type="create"> + <field key="stock_item">stock_item</field> + </operation> + <operation name="UpdateProductExtensionAttribute" dataType="product_extension_attribute" type="update"> + <field key="stock_item">stock_item</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml new file mode 100644 index 0000000000000..09544d05a9b56 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductLink" dataType="product_link" type="create"> + <field key="sku">string</field> + <field key="link_type">string</field> + <field key="linked_product_sku">string</field> + <field key="linked_product_type">string</field> + <field key="position">integer</field> + <object key="extension_attributes" dataType="product_link_extension_attribute"> + <field key="qty">integer</field> + </object> + </operation> + <operation name="UpdateProductLink" dataType="product_link" type="update"> + <field key="sku">string</field> + <field key="link_type">string</field> + <field key="linked_product_sku">string</field> + <field key="linked_product_type">string</field> + <field key="position">integer</field> + <object key="extension_attributes" dataType="product_link_extension_attribute"> + <field key="qty">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml new file mode 100644 index 0000000000000..07ea02f5b7aee --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link_extension_attribute-meta.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductLinkExtensionAttribute" dataType="product_link_extension_attribute" type="create"> + <contentType>application/json</contentType> + <field key="qty">integer</field> + </operation> + <operation name="UpdateProductLinkExtensionAttribute" dataType="product_link_extension_attribute" type="update"> + <contentType>application/json</contentType> + <field key="qty">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml new file mode 100644 index 0000000000000..3a8999b523f13 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductLinks" dataType="product_links" type="create" auth="adminOauth" url="/V1/products/{sku}/links" method="POST"> + <contentType>application/json</contentType> + <array key="items"> + <value>product_link</value> + </array> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml new file mode 100644 index 0000000000000..adc5a33507af6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option-meta.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductOption" dataType="product_option" type="create"> + <field key="product_sku">string</field> + <field key="option_id">integer</field> + <field key="title">string</field> + <field key="type">string</field> + <field key="sort_order">integer</field> + <field key="is_require">boolean</field> + <field key="price">number</field> + <field key="price_type">string</field> + <field key="sku">string</field> + <field key="file_extension">string</field> + <field key="max_characters">integer</field> + <field key="max_size_x">integer</field> + <field key="max_size_y">integer</field> + <array key="values"> + <value>product_option_value</value> + </array> + </operation> + <operation name="UpdateProductOption" dataType="product_option" type="update"> + <field key="product_sku">string</field> + <field key="option_id">integer</field> + <field key="title">string</field> + <field key="type">string</field> + <field key="sort_order">integer</field> + <field key="is_require">boolean</field> + <field key="price">number</field> + <field key="price_type">string</field> + <field key="sku">string</field> + <field key="file_extension">string</field> + <field key="max_characters">integer</field> + <field key="max_size_x">integer</field> + <field key="max_size_y">integer</field> + <array key="values"> + <value>product_option_value</value> + </array> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml new file mode 100644 index 0000000000000..f4273f5796830 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_option_value-meta.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductOptionValue" dataType="product_option_value" type="create"> + <field key="title">string</field> + <field key="sort_order">integer</field> + <field key="price">number</field> + <field key="price_type">string</field> + <field key="sku">string</field> + <field key="option_type_id">integer</field> + </operation> + <operation name="UpdateProductOptionValue" dataType="product_option_value" type="update"> + <field key="title">string</field> + <field key="sort_order">integer</field> + <field key="price">number</field> + <field key="price_type">string</field> + <field key="sku">string</field> + <field key="option_type_id">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml new file mode 100644 index 0000000000000..e7e79d69055c6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/stock_item-meta.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateStockItem" dataType="stock_item" type="create"> + <field key="qty">integer</field> + <field key="is_in_stock">boolean</field> + </operation> + <operation name="UpdateStockItem" dataType="stock_item" type="update"> + <field key="qty">integer</field> + <field key="is_in_stock">boolean</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml new file mode 100644 index 0000000000000..abb9b003dc59e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/store_label-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateStoreLabel" dataType="StoreLabel" type="create"> + <field key="store_id">integer</field> + <field key="label">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml new file mode 100644 index 0000000000000..c568e52b2ab3c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/validation_rule-meta.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateValidationRule" dataType="validation_rule" type="create"> + <field key="key">string</field> + <field key="value">string</field> + </operation> + <operation name="UpdateValidationRule" dataType="validation_rule" type="update"> + <field key="key">string</field> + <field key="value">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml new file mode 100644 index 0000000000000..e5d3704bafcd1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCatalogProductPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCatalogProductPage" url="/catalog/product/" area="admin" module="Magento_Catalog"> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml new file mode 100644 index 0000000000000..c031578e2a208 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCategoryEditPage" url="catalog/category/edit/id/{{categoryId}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminCategoryMainActionsSection"/> + <section name="AdminCategoryProductsSection"/> + <section name="AdminCategorySidebarActionSection"/> + <section name="AdminCategorySidebarTreeSection"/> + <section name="AdminCategoryBasicFieldSection"/> + <section name="AdminCategorySEOSection"/> + <section name="AdminCategoryModalSection"/> + <section name="AdminCategoryContentSection"/> + <section name="AdminCategoryDisplaySettingsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml new file mode 100644 index 0000000000000..54e9194ca450d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> + <section name="AdminCategorySidebarActionSection"/> + <section name="AdminCategorySidebarTreeSection"/> + <section name="AdminCategoryBasicFieldSection"/> + <section name="AdminCategorySEOSection"/> + <section name="AdminCategoryModalSection"/> + <section name="AdminCategoryProductsGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml new file mode 100644 index 0000000000000..035b6739c5aaf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryProductAttributeEditPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCategoryProductAttributeEditPage" url="catalog/product_attribute/edit/" area="admin" module="Magento_Catalog"> + <section name="AdminProductAttributeEditSection"/> + <section name="AdminConfirmationModalSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml new file mode 100644 index 0000000000000..dd5d5aef08a7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> + <section name="AdminNewWidgetSelectProductPopupSection"/> + <section name="AdminCatalogProductWidgetSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml new file mode 100644 index 0000000000000..04070ba17356d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductAttributeEditPage" url="catalog/product_attribute/edit/" area="admin" module="Magento_Catalog"> + <section name="AdminProductAttributeEditSection"/> + <section name="AdminConfirmationModalSection"/> + <section name="AdminEditAttributeStorefrontPropertiesSection"/> + <section name="AdminMainActionsSection"/> + <section name="AdminMessagesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml new file mode 100644 index 0000000000000..685781dabab85 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeFormPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="ProductAttributePage" url="catalog/product_attribute/new/" area="admin" module="Magento_Catalog"> + <section name="AttributePropertiesSection"/> + <section name="StorefrontPropertiesSection"/> + <section name="AdvancedAttributePropertiesSection"/> + <section name="AdminAttributeOptionsSection"/> + <section name="AttributeManageSwatchSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml new file mode 100644 index 0000000000000..3a9bd1647a476 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductAttributeGridPage" url="catalog/product_attribute" area="admin" module="Catalog"> + <section name="AdminProductAttributeGridSection"/> + <section name="AdminDataGridHeaderSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml new file mode 100644 index 0000000000000..9116e9ae7446f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductAttributeNewPage" url="catalog/product_attribute/new/" area="admin" module="Magento_Catalog"> + <section name="AttributePropertiesSection"/> + <section name="StorefrontPropertiesSection"/> + <section name="AdvancedAttributePropertiesSection"/> + <section name="AdminAttributeOptionsSection"/> + <section name="AttributeManageSwatchSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml new file mode 100644 index 0000000000000..1e5ef870ba8fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductAttributeSetEditPage" url="catalog/product_set/edit/id/{{var1}}" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductAttributeSetEditSection"/> + <section name="AdminProductAttributeSetActionSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml new file mode 100644 index 0000000000000..ace73a45c69a2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeSetGridPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductAttributeSetGridPage" url="catalog/product_set/" area="admin" module="Magento_Catalog"> + <section name="AdminProductAttributeSetGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..68c13dc29b365 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductFormSection"/> + <section name="AdminProductFormActionSection"/> + <section name="AdminProductSEOSection"/> + <section name="AdminProductImagesSection"/> + <section name="AdminProductMessagesSection"/> + <section name="AdminMessagesSection"/> + <section name="AdminProductCustomizableOptionsSection" /> + <section name="AdminAddProductsToOptionPanelSection" /> + <section name="AdminProductFormAdvancedPricingSection"/> + <section name="AdminProductContentSection"/> + <section name="AdminProductCustomizableOptionsImportModalSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml new file mode 100644 index 0000000000000..4b240c4dc9421 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductEditPage" url="catalog/product/edit/id/{{productId}}" parameterized="true" area="admin" module="Magento_Catalog"> + <!-- This page object only exists for the url. Use the AdminProductCreatePage for selectors. --> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml new file mode 100644 index 0000000000000..03915ba68b642 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductIndexPage.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductIndexPage" url="catalog/product/index" area="admin" module="Magento_Catalog"> + <section name="AdminProductGridActionSection" /> + <section name="AdminProductGridFilterSection" /> + <section name="AdminProductGridSection" /> + <section name="AdminProductGridTableHeaderSection" /> + <section name="AdminMessagesSection" /> + <section name="AdminProductFiltersSection" /> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml new file mode 100644 index 0000000000000..ee60c809aec10 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductPage" url="catalog/product/view" area="admin" module="Magento_Catalog"> + <section name="StorefrontProductInfoMainSection" /> + <section name="StorefrontProductInfoDetailsSection" /> + <section name="StorefrontProductImageSection" /> + <section name="StorefrontMessagesSection" /> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml new file mode 100644 index 0000000000000..5d74ec979ce14 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductUpdateAttributesPage" url="catalog/product_action_attribute/edit/" area="admin" module="Magento_Catalog"> + <section name="AdminUpdateAttributesHeaderSection"/> + <section name="AdminUpdateAttributesWebsiteSection"/> + <section name="AdminUpdateAttributesAttributesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml new file mode 100644 index 0000000000000..0ada59c623451 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCategoryPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + <section name="StorefrontCategoryMainSection"/> + <section name="WYSIWYGToolbarSection"/> + <section name="StorefrontCategoryPagerSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml new file mode 100644 index 0000000000000..f0599a021d4c4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductComparePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontProductComparePage" url="catalog/product_compare/index" module="Magento_Catalog" area="storefront"> + <section name="StorefrontProductCompareMainSection" /> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml new file mode 100644 index 0000000000000..fdfee62f6dc0b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + <section name="StorefrontProductPageSection"/> + <section name="StorefrontProductAdditionalInformationSection"/> + <section name="StorefrontProductMediaSection"/> + <section name="StorefrontProductInfoMainSection"/> + <section name="StorefrontProductRelatedProductsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml new file mode 100644 index 0000000000000..046323bb368da --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <!-- It is created to open product page with store code setting--> + <page name="StorefrontStoreViewProductPage" url="/{{storeCode}}/{{productUrlKey}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/README.md b/app/code/Magento/Catalog/Test/Mftf/README.md new file mode 100644 index 0000000000000..e7a95609c394b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Functional Tests + +The Functional Test Module for **Magento Catalog** module. diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml new file mode 100644 index 0000000000000..75fda66ac5cb1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminAddProductsToOptionPanelSection"> + <element name="addSelectedProducts" type="button" selector=".product_form_product_form_bundle-items_modal button.action-primary" timeout="30"/> + <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml new file mode 100644 index 0000000000000..5ec0aec009508 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogProductWidgetSection"> + <element name="productsToDisplay" type="input" selector="input[name='parameters[page_size]']"/> + <element name="productAttributesToShow" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> + <element name="productButtonsToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml new file mode 100644 index 0000000000000..a32ed228c8570 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCategoryBasicFieldSection"> + <element name="IncludeInMenu" type="checkbox" selector="input[name='include_in_menu']"/> + <element name="includeInMenuLabel" type="text" selector="input[name='include_in_menu']+label"/> + <element name="includeInMenuUseDefault" type="checkbox" selector="input[name='use_default[include_in_menu]']"/> + <element name="EnableCategory" type="checkbox" selector="input[name='is_active']"/> + <element name="enableCategoryLabel" type="text" selector="input[name='is_active']+label"/> + <element name="enableUseDefault" type="checkbox" selector="input[name='use_default[is_active]']"/> + <element name="CategoryNameInput" type="input" selector="input[name='name']"/> + <element name="categoryNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> + <element name="requiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> + <element name="requiredFieldIndicatorColor" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('color');"/> + <element name="panelFieldControl" type="input" selector='//aside//div[@data-index="{{arg1}}"]/descendant::*[@name="{{arg2}}"]' parameterized="true"/> + </section> + <section name="CatalogWYSIWYGSection"> + <element name="ShowHideBtn" type="button" selector="#togglecategory_form_description"/> + <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by"/> + <element name="Style" type="button" selector=".mce-txt" /> + <element name="Bold" type="button" selector=".mce-i-bold" /> + <element name="Italic" type="button" selector=".mce-i-italic" /> + <element name="Underline" type="button" selector=".mce-i-underline" /> + <element name="AlignLeft" type="button" selector=".mce-i-alignleft" /> + <element name="AlignCenter" type="button" selector=".mce-i-aligncenter" /> + <element name="AlignRight" type="button" selector=".mce-i-alignright" /> + <element name="Bullet" type="button" selector=".mce-i-bullist" /> + <element name="Numlist" type="button" selector=".mce-i-numlist" /> + <element name="InsertLink" type="button" selector=".mce-i-link" /> + <element name="InsertImage" type="button" selector=".mce-i-image" /> + <element name="InsertTable" type="button" selector=".mce-i-table" /> + <element name="SpecialCharacter" type="button" selector=".mce-i-charmap"/> + <element name="InsertImageIcon" type="button" selector=".mce-i-image"/> + <element name="Browse" type="button" selector=".mce-i-browse"/> + <element name="BrowseUploadImage" type="file" selector=".fileupload" /> + <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> + <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> + <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> + <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> + <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> + <element name="UploadImage" type="file" selector=".fileupload" /> + <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> + <element name="InsertFile" type="text" selector="#insert_files"/> + <element name="CreateFolder" type="button" selector="#new_folder" /> + <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> + <element name="CancelBtn" type="button" selector="#cancel" /> + <element name="FolderName" type="button" selector="input[data-role='promptField']" /> + <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> + <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> + <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> + <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml new file mode 100644 index 0000000000000..faa320cd114de --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCategoryContentSection"> + <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> + <element name="addCMSBlock" type="select" selector="[name='landing_page']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml new file mode 100644 index 0000000000000..d545feca3e711 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCategoryDisplaySettingsSection"> + <element name="settingsHeader" type="button" selector="[data-index='display_settings'] strong.admin__collapsible-title" timeout="30"/> + <element name="displayMode" type="button" selector="[name='display_mode']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml new file mode 100644 index 0000000000000..14489d155bcc3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryMainActionsSection"> + <element name="SaveButton" type="button" selector=".page-actions-inner #save" timeout="30"/> + <element name="DeleteButton" type="button" selector=".page-actions-inner #delete" timeout="30"/> + <element name="categoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> + <element name="categoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> + <element name="categoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml new file mode 100644 index 0000000000000..1214cfd2eb224 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryMessagesSection"> + <element name="SuccessMessage" type="text" selector=".message-success"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml new file mode 100644 index 0000000000000..03b9d76778555 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryModalSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryModalSection"> + <element name="message" type="text" selector="aside.confirm div.modal-content"/> + <element name="title" type="text" selector="aside.confirm .modal-header .modal-title"/> + <element name="ok" type="button" selector="aside.confirm .modal-footer .action-primary"/> + <element name="cancel" type="button" selector="aside.confirm .modal-footer .action-secondary"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml new file mode 100644 index 0000000000000..61a4c497d5b3a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryProductsGridSection"> + <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml new file mode 100644 index 0000000000000..ef339668b2c98 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryProductsSection"> + <element name="sectionHeader" type="button" selector="div[data-index='assign_products']" timeout="30"/> + <element name="tabProductClosed" type="block" selector="div[data-index='assign_products'] [data-state-collapsible='closed']"/> + <element name="addProducts" type="button" selector="#catalog_category_add_product_tabs" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml new file mode 100644 index 0000000000000..93ba3581ff81f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySEOSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategorySEOSection"> + <element name="SectionHeader" type="button" selector="div[data-index='search_engine_optimization']" timeout="30"/> + <element name="UrlKeyInput" type="input" selector="input[name='url_key']"/> + <element name="urlKeyDefaultValueCheckbox" type="button" selector="input[name='use_default[url_key]']"/> + <element name="urlKeyRedirectCheckbox" type="button" selector="[data-index='url_key_create_redirect'] input[type='checkbox']"/> + <element name="MetaTitleInput" type="input" selector="input[name='meta_title']"/> + <element name="MetaKeywordsInput" type="textarea" selector="textarea[name='meta_keywords']"/> + <element name="MetaDescriptionInput" type="textarea" selector="textarea[name='meta_description']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml new file mode 100644 index 0000000000000..e53a9989d661c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarActionSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategorySidebarActionSection"> + <element name="AddRootCategoryButton" type="button" selector="#add_root_category_button" timeout="30"/> + <element name="AddSubcategoryButton" type="button" selector="#add_subcategory_button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml new file mode 100644 index 0000000000000..e35fe56156f4f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCategorySidebarTreeSection"> + <element name="collapseAll" type="button" selector=".tree-actions a:first-child"/> + <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> + <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> + <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> + <element name="categoryInTreeUnderRoot" type="text" selector="//div[@class='x-tree-root-node']/li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> + <element name="category" type="button" selector="//span[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="expandRootCategory" type="button" selector="//div[@class='x-tree-root-node']/li/div/a/span[contains(., '{{categoryName}}')]/ancestor::div/img[contains(@class, 'x-tree-elbow-end-plus')]" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml new file mode 100644 index 0000000000000..7dd78bcddb5f5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryWarningMessagesPopupSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryWarningMessagesPopupSection"> + <element name="warningMessage" type="text" selector=".modal-inner-wrap .modal-content .message.message-notice"/> + <element name="cancelButton" type="button" selector=".modal-inner-wrap .action-secondary"/> + <element name="okButton" type="button" selector=".modal-inner-wrap .action-primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml new file mode 100644 index 0000000000000..8f7425b5a19e0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AttributePropertiesSection"> + <element name="AdvancedProperties" type="button" selector="#advanced_fieldset-wrapper"/> + <element name="Save" type="button" selector="#save"/> + <element name="defaultLabel" type="input" selector="#attribute_label"/> + <element name="inputType" type="select" selector="#frontend_input"/> + <element name="deleteAttribute" type="button" selector="#delete" timeout="30"/> + </section> + <section name="StorefrontPropertiesSection"> + <element name="pageTitle" type="text" selector="//span[text()='Storefront Properties']" /> + <element name="storeFrontPropertiesTab" selector="#product_attribute_tabs_front" type="button"/> + <element name="enableWYSIWYG" type="select" selector="#enabled"/> + <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + </section> + <section name="AdvancedAttributePropertiesSection"> + <element name="advancedAttributePropertiesSectionToggle" + type="button" selector="#advanced_fieldset-wrapper"/> + <element name="attributeCode" type="text" selector="#attribute_code"/> + <element name="scope" type="select" selector="#is_global"/> + <element name="addToColumnOptions" type="select" selector="#is_used_in_grid"/> + <element name="useInFilterOptions" type="select" selector="#is_filterable_in_grid"/> + <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> + </section> + <section name="AdminAttributeOptionsSection"> + <element name="addOption" type="button" selector="#add_new_option_button"/> + <element name="nthOptionAdminLabel" type="input" + selector="(//*[@id='manage-options-panel']//tr[{{var}}]//input[contains(@name, 'option[value]')])[1]" parameterized="true"/> + </section> + <section name="StorefrontPropertiesSection"> + <element name="storefrontPropertiesTab" selector="#product_attribute_tabs_front" type="button" timeout="30"/> + <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + </section> + <section name="AttributeManageSwatchSection"> + <element name="swatchField" type="input" selector="input[name='swatchtext[value][option_{{option_index}}][{{index}}]'][placeholder='Swatch']" parameterized="true"/> + <element name="descriptionField" type="input" selector="input[name='optiontext[value][option_{{option_index}}][{{index}}]'][placeholder='Description']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml new file mode 100644 index 0000000000000..e430998f1e2be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminEditAttributeStorefrontPropertiesSection"> + <element name="storeFrontPropertiesTab" selector="#product_attribute_tabs_front" type="button"/> + <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml new file mode 100644 index 0000000000000..8bc9c03642c15 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewWidgetSection"> + <element name="selectProduct" type="button" selector=".btn-chooser" timeout="30"/> + <element name="selectCategory" type="button" selector="button[title='Select Category...']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml new file mode 100644 index 0000000000000..0da67849f85c6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewWidgetSelectProductPopupSection"> + <element name="filterBySku" type="input" selector=".data-grid-filters input[name='chooser_sku']"/> + <element name="firstRow" type="select" selector=".even>td" timeout="20"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml new file mode 100644 index 0000000000000..798832f707bf0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeEditSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeEditSection"> + <element name="deleteAttribute" type="button" selector="#delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml new file mode 100644 index 0000000000000..f27c27fbd20f4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeGridSection"> + <element name="attributeCode" type="text" selector="//td[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="createNewAttributeBtn" type="button" selector="#add"/> + <element name="gridFilterFrontEndLabel" type="input" selector="#attributeGrid_filter_frontend_label"/> + <element name="gridFilterAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> + <element name="search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> + <element name="firstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]" timeout="30"/> + <element name="attributeCodeFilterInput" type="input" selector=".admin__data-grid-filters input[name='attribute_code']"/> + <element name="filterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml new file mode 100644 index 0000000000000..ad035503bff9f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGroupSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeGroupSection"> + <element name="folderName" type="text" selector="//span[text()='{{var1}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml new file mode 100644 index 0000000000000..bf06119b75e4e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetActionSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeSetActionSection"> + <element name="save" type="button" selector="button[title='Save']" timeout="30"/> + <element name="reset" type="button" selector="button[title='Reset']" timeout="30"/> + <element name="back" type="button" selector="button[title='Back']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml new file mode 100644 index 0000000000000..3334cd2dc1805 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetEditSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeSetEditSection"> + <!-- Groups Column --> + <element name="groupTree" type="block" selector="#tree-div1"/> + <element name="attributeGroup" type="text" selector="//*[@id='tree-div1']//span[text()='{{groupName}}']" parameterized="true"/> + <element name="attributeGroupExtender" type="button" selector="//*[@id='tree-div1']//span[text()='{{groupName}}']" parameterized="true"/> + <element name="attributeGroupCollapsed" type="button" selector="//*[@id='tree-div1']//span[text()='{{groupName}}']/parent::*/parent::*[contains(@class, 'collapsed')]" parameterized="true"/> + <element name="assignedAttribute" type="text" selector="//*[@id='tree-div1']//span[text()='{{attributeName}}']" parameterized="true"/> + <element name="lineItemYthAttributeGroup" type="text" selector="//*[@id='tree-div1']/ul/div/li[{{y}}]//li[{{x}}]" parameterized="true"/> + <element name="lineItemAttributeGroup" type="text" selector="//*[@id='tree-div1']//span[text()='{{groupName}}']/parent::*/parent::*/parent::*//li[{{x}}]//a/span" parameterized="true"/> + <!-- Unassigned Attributes Column --> + <element name="unassignedAttributesTree" type="block" selector="#tree-div2"/> + <element name="unassignedAttribute" type="text" selector="//*[@id='tree-div2']//span[text()='{{attributeName}}']" parameterized="true"/> + <element name="lineItemUnassignedAttribute" type="text" selector="//*[@id='tree-div2']//li[{{x}}]//a/span" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml new file mode 100644 index 0000000000000..661ed4998ffc7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeSetGridSection"> + <element name="attributeSetName" type="text" selector="//td[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="attributeSetNameFilter" type="input" selector="#setGrid_filter_set_name"/> + <element name="applyFilterButton" type="button" selector="#setGrid [data-action='grid-filter-apply']" timeout="30"/> + <element name="attributeSetRowByIndex" type="block" selector="#setGrid_table tbody tr:nth-of-type({{var1}})" parameterized="true"/> + <element name="addAttributeSetBtn" type="button" selector="button.add-set" timeout="30"/> + <element name="deleteOptionByName" type="button" selector="//*[contains(@value, '{{arg}}')]/../following-sibling::td[contains(@class, 'delete')]/button" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml new file mode 100644 index 0000000000000..a7d8d59cd9043 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeSetSection"> + <element name="save" type="button" selector="button[title='Save']" timeout="30"/> + <element name="deleteBtn" type="button" selector="button[title='Delete']" timeout="30"/> + <element name="attribute" type="button" selector="//span[text()='{{var1}}']" parameterized="true"/> + <element name="addNewGroupBtn" type="button" selector="button.add" timeout="30"/> + <element name="name" type="input" selector="#attribute_set_name"/> + <element name="basedOn" type="select" selector="#skeleton_set"/> + </section> + <section name="AdminModifyAttributesSection"> + <!-- Parameter is the attribute name --> + <element name="dropDownAttributeByName" type="select" selector="//*[text()='{{attributeName}}']/../../..//select" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml new file mode 100644 index 0000000000000..102a21cdca9c0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeUnassignedSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributeUnassignedSection"> + <element name="productAttributeName" type="text" selector="//span[text()='{{var1}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml new file mode 100644 index 0000000000000..56fb556436f7d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductContentSection"> + <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> + <element name="descriptionTextArea" type="textarea" selector="div[data-index='content'] #product_form_description"/> + <element name="shortDescriptionTextArea" type="textarea" selector="div[data-index='content'] #product_form_short_description"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml new file mode 100644 index 0000000000000..0b1b5c966422a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductCustomizableOptionsImportModalSection"> + <element name="selectProductTitle" type="text" selector="//aside[contains(@class, '_show')]//h1[normalize-space(text())='Select Product']"/> + <element name="nameFilter" type="input" selector="//aside[contains(@class, '_show')]//input[@name='name']"/> + <element name="importButton" type="button" selector="//aside[contains(@class, '_show')]//button[contains(@class, 'action-primary') and normalize-space(.)='Import']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml new file mode 100644 index 0000000000000..ca4e17fe92d6b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductCustomizableOptionsSection"> + <element name="checkIfCustomizableOptionsTabOpen" type="text" selector="//span[text()='Customizable Options']/parent::strong/parent::*[@data-state-collapsible='closed']"/> + <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']" timeout="30"/> + <element name="useDefaultOptionTitle" type="text" selector="[data-index='options'] tr.data-row [data-index='title'] [name^='options_use_default']"/> + <element name="useDefaultOptionValueTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/> + <element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/> + <element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Title']/parent::label/parent::div/parent::div//input[@class='admin__control-text']" parameterized="true"/> + <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div/parent::div//div[@data-role='selected-option']" parameterized="true"/> + <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> + <element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/> + <element name="fillOptionValueTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='Title']/parent::label/parent::div/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> + <element name="fillOptionValuePrice" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='Price']/parent::label/parent::div//div[@class='admin__control-addon']/input" parameterized="true"/> + <element name="clickSelectPriceType" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody//tr[@data-repeat-index='{{var2}}']//span[text()='Price Type']/parent::label/parent::div/parent::div//select" parameterized="true"/> + <!-- Elements that make it easier to select the most recently added element --> + <element name="lastOptionTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, '_required')]//input" /> + <element name="lastOptionTypeParent" type="block" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__action-multiselect-text')]" /> + <!-- var 1 represents the option type that you want to select, i.e "radio buttons" --> + <element name="optionType" type="block" selector="//*[@data-index='custom_options']//label[text()='{{var1}}'][ancestor::*[contains(@class, '_active')]]" parameterized="true" /> + <element name="optionPrice" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='price']"/> + <element name="optionPriceType" type="select" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) select[name*='price_type']"/> + <element name="optionFileExtensions" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='file_extension']"/> + <element name="optionSku" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='sku']"/> + <element name="optionTitleInputByIndex" type="input" selector="input[name='product[options][{{index}}][title]']" parameterized="true"/> + <element name="importOptions" type="button" selector="[data-index='custom_options'] [data-index='button_import']" timeout="30"/> + <element name="optionPriceByIndex" type="input" selector="input[name='product[options][{{index}}][price]']" parameterized="true"/> + <element name="optionSkuByIndex" type="input" selector="input[name='product[options][{{index}}][sku]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml new file mode 100644 index 0000000000000..8e13f9c38f805 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFiltersSection"> + <element name="FiltersButton" type="button" selector="#container > div > div.admin__data-grid-header > div:nth-child(1) > div.data-grid-filters-actions-wrap > div > button"/> + <element name="clearFiltersButton" type="button" selector="//div[@class='admin__data-grid-header']//button[@class='action-tertiary action-clear']" timeout="10"/> + <element name="NameInput" type="input" selector="input[name=name]"/> + <element name="SkuInput" type="input" selector="input[name=sku]"/> + <element name="Apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="allCheckbox" type="checkbox" selector="div[data-role='grid-wrapper'] label[data-bind='attr: {for: ko.uid}']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml new file mode 100644 index 0000000000000..3eab31e32cc24 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductFormActionSection"> + <element name="saveButton" type="button" selector="#save-button" timeout="30"/> + <element name="saveArrow" type="button" selector="button[data-ui-id='save-button-dropdown']" timeout="30"/> + <element name="saveAndClose" type="button" selector="span[id='save_and_close']" timeout="30"/> + <element name="changeStoreButton" type="button" selector="#store-change-button"/> + <element name="selectStoreView" type="button" selector="//ul[@data-role='stores-list']/li/a[normalize-space(.)='{{var1}}']" timeout="10" parameterized="true"/> + <element name="saveAndNew" type="button" selector="#save_and_new" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml new file mode 100644 index 0000000000000..9714c1f6eb483 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductFormAdvancedPricingSection"> + <element name="customerGroupPriceAddButton" type="button" selector="[data-action='add_new_row']" timeout="30"/> + <element name="customerGroupPriceDeleteButton" type="button" selector="[data-action='remove_row']" timeout="30"/> + <element name="advancedPricingCloseButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-close" timeout="30"/> + <element name="productTierPriceWebsiteSelect" type="select" selector="[name='product[tier_price][{{var1}}][website_id]']" parameterized="true"/> + <element name="productTierPriceCustGroupSelect" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]']" parameterized="true"/> + <element name="productTierPriceQtyInput" type="input" selector="[name='product[tier_price][{{var1}}][price_qty]']" parameterized="true"/> + <element name="productTierPriceValueTypeSelect" type="select" selector="[name='product[tier_price][{{var1}}][value_type]']" parameterized="true"/> + <element name="productTierPriceFixedPriceInput" type="input" selector="[name='product[tier_price][{{var1}}][price]']" parameterized="true"/> + <element name="productTierPricePercentageValuePriceInput" type="input" selector="[name='product[tier_price][{{var1}}][percentage_value]']" parameterized="true"/> + <element name="productTierPricePercentageError" type="text" selector="div[data-index='percentage_value'] label.admin__field-error" /> + <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> + <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="5"/> + <element name="useDefaultPrice" type="checkbox" selector="input[name='use_default[special_price]']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml new file mode 100644 index 0000000000000..594a1e4171a4e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormChangeStoreSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductFormChangeStoreSection"> + <element name="storeSelector" type="button" selector="//a[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="acceptButton" type="button" selector="button[class='action-primary action-accept']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml new file mode 100644 index 0000000000000..d9c74087578a5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormSection"> + <element name="attributeSet" type="select" selector="div[data-index='attribute_set_id'] .admin__field-control"/> + <element name="attributeSetFilter" type="input" selector="div[data-index='attribute_set_id'] .admin__field-control input"/> + <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> + <element name="productName" type="input" selector=".admin__field[data-index=name] input"/> + <element name="productNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> + <element name="productPriceUseDefault" type="checkbox" selector=".admin__field[data-index=price] [name='use_default[price]']"/> + <element name="productSku" type="input" selector=".admin__field[data-index=sku] input"/> + <element name="enableProductAttributeLabel" type="text" selector="//span[text()='Enable Product']/parent::label"/> + <element name="enableProductAttributeLabelWrapper" type="text" selector="//span[text()='Enable Product']/parent::label/parent::div"/> + <element name="productStatus" type="checkbox" selector="input[name='product[status]']"/> + <element name="productStatusUseDefault" type="checkbox" selector="input[name='use_default[status]']"/> + <element name="productPrice" type="input" selector=".admin__field[data-index=price] input"/> + <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']"/> + <element name="productTaxClass" type="select" selector="select[name='product[tax_class_id]']"/> + <element name="productTaxClassUseDefault" type="checkbox" selector="input[name='use_default[tax_class_id]']"/> + <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']"/> + <element name="productQuantity" type="input" selector=".admin__field[data-index=qty] input"/> + <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']"/> + <element name="productWeight" type="input" selector=".admin__field[data-index=weight] input"/> + <element name="productWeightSelect" type="select" selector="select[name='product[product_has_weight]']"/> + <element name="contentTab" type="button" selector="//strong[@class='admin__collapsible-title']/span[text()='Content']"/> + <element name="validationErrorLabel" type="text" selector="//label[@class='admin__field-error']"/> + <element name="visibility" type="select" selector="//select[@name='product[visibility]']"/> + <element name="visibilityUseDefault" type="checkbox" selector="//input[@name='use_default[visibility]']"/> + <element name="divByDataIndex" type="input" selector="div[data-index='{{var}}']" parameterized="true"/> + <element name="attributeSetSearchCount" type="text" selector="div[data-index='attribute_set_id'] .admin__action-multiselect-search-count"/> + <element name="attributeLabelByText" type="text" selector="//*[@class='admin__field']//span[text()='{{attributeLabel}}']" parameterized="true"/> + <element name="addAttributeBtn" type="button" selector="#addAttribute"/> + <element name="attributeSetFilterResultByName" type="text" selector="//label/span[text() = '{{var}}']" timeout="30" parameterized="true"/> + <element name="attributeSetDropDown" type="select" selector="div[data-index='attribute_set_id'] .action-select.admin__action-multiselect"/> + <element name="requiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> + <element name="requiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> + <element name="customAttributeDropdownField" type="select" selector="select[name='product[{{attributeCode}}]']" parameterized="true"/> + <element name="customAttributeInputField" type="select" selector="input[name='product[{{attributeCode}}]']" parameterized="true"/> + <element name="customAttributeSelectOptions" type="select" selector="select[name='product[{{attributeCode}}]'] option" parameterized="true"/> + <element name="currentCategory" type="text" selector=".admin__action-multiselect-crumb > span"/> + <element name="searchCategory" type="input" selector=".action-menu._active input.admin__action-multiselect-search"/> + <element name="selectCategory" type="input" selector="//label[contains(., '{{categoryName}}')]" parameterized="true"/> + <element name="done" type="button" selector=".admin__action-multiselect-actions-wrap button" timeout="30"/> + <element name="save" type="button" selector="#save-button"/> + <element name="unselectCategories" type="button" selector="//span[@class='admin__action-multiselect-crumb']/span[contains(.,'{{category}}')]/../button" parameterized="true" timeout="30"/> + <element name="enableProductLabel" type="checkbox" selector=".admin__actions-switch > input[name='product[status]']+label"/> + </section> + <section name="ProductInWebsitesSection"> + <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> + <element name="sectionHeaderOpened" type="button" selector="[data-index='websites']._show" timeout="30"/> + <element name="website" type="checkbox" selector="//label[contains(text(), '{{var1}}')]/parent::div//input[@type='checkbox']" parameterized="true"/> + <element name="isWebsiteDisabled" type="checkbox" selector="//label[contains(text(), '{{websiteName}}')]/parent::div//input[@type='checkbox' and @disabled]" parameterized="true"/> + </section> + <section name="ProductWYSIWYGSection"> + <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> + <element name="v436" type ="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']" /> + <element name="v3" type ="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']" /> + <element name="TinymceDescription3" type ="button" selector="//span[text()='Description']" /> + <element name="Tinymce3MSG" type="button" selector=".admin__field-error"/> + <element name="SaveConfig" type ="button" selector="#save" /> + <element name="v4" type="button" selector="#category_form_description_v4"/> + <element name="WYSIWYGBtn" type="button" selector=".//button[@class='action-default scalable action-wysiwyg']"/> + </section> + <section name="ProductAttributeWYSIWYGSection"> + <element name="TextArea" type ="text" selector="//div[@data-index='{{var1}}']//textarea" parameterized="true"/> + <element name="showHideBtn" type="button" selector="//button[contains(@id,'{{var1}}')]" parameterized="true"/> + <element name="InsertImageBtn" type="button" selector="//div[contains(@id, '{{var1}}')]//span[text()='Insert Image...']" parameterized="true"/> + <element name="Style" type="button" selector="//div[contains(@id, '{{var1}}')]//span[text()='Paragraph']" parameterized="true"/> + <element name="Bold" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-bold']" parameterized="true"/> + <element name="Italic" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-bold']" parameterized="true"/> + <element name="Underline" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-underline']" parameterized="true"/> + <element name="AlignLeft" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-alignleft']" parameterized="true"/> + <element name="AlignCenter" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-aligncenter']" parameterized="true"/> + <element name="AlignRight" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-alignright']" parameterized="true"/> + <element name="Numlist" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-bullist']" parameterized="true"/> + <element name="Bullet" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-numlist']" parameterized="true"/> + <element name="InsertLink" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-link']" parameterized="true"/> + <element name="InsertImageIcon" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-image']" parameterized="true"/> + <element name="InsertTable" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-table']" parameterized="true"/> + <element name="SpecialCharacter" type="button" selector="//div[contains(@id, '{{var1}}')]//i[@class='mce-ico mce-i-charmap']" parameterized="true"/> + <element name="TinyMCE4" type="text" selector="//div[contains(@id, '{{var1}}')]//div[@class='mce-branding-powered-by']" parameterized="true"/> + </section> + <section name="ProductDescriptionWYSIWYGToolbarSection"> + <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_description']//div[@class='mce-branding-powered-by']" /> + <element name="showHideBtn" type="button" selector="#toggleproduct_form_description"/> + <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_description > .scalable.action-add-image.plugin" /> + <element name="Style" type="button" selector="//div[@id='editorproduct_form_description']//span[text()='Paragraph']" /> + <element name="Bold" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-bold']" /> + <element name="Italic" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-italic']" /> + <element name="Underline" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-underline']" /> + <element name="AlignLeft" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-alignleft']" /> + <element name="AlignCenter" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-aligncenter']" /> + <element name="AlignRight" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-alignright']" /> + <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-bullist']" /> + <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-numlist']" /> + <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-link']" /> + <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-image']" /> + <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-table']" /> + <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-charmap']" /> + <element name="Browse" type="button" selector=".mce-i-browse"/> + <element name="BrowseUploadImage" type="file" selector=".fileupload" /> + <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> + <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> + <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> + <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> + <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> + <element name="UploadImage" type="file" selector=".fileupload" /> + <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> + <element name="InsertFile" type="text" selector="#insert_files"/> + <element name="CreateFolder" type="button" selector="#new_folder" /> + <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> + <element name="CancelBtn" type="button" selector="#cancel" /> + <element name="FolderName" type="button" selector="input[data-role='promptField']" /> + <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> + <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> + <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> + <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> + </section> + <section name="ProductShortDescriptionWYSIWYGToolbarSection"> + <element name="TinyMCE4" type ="button" selector="//div[@id='editorproduct_form_short_description']//div[@class='mce-branding-powered-by']" /> + <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_short_description > .scalable.action-add-image.plugin" /> + <element name="showHideBtn" type="button" selector="#toggleproduct_form_short_description"/> + <element name="Style" type="button" selector="//div[@id='editorproduct_form_short_description']//span[text()='Paragraph']" /> + <element name="Bold" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-bold']" /> + <element name="Italic" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-italic']" /> + <element name="Underline" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-underline']" /> + <element name="AlignLeft" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-alignleft']" /> + <element name="AlignCenter" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-aligncenter']" /> + <element name="AlignRight" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-alignright']" /> + <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-bullist']" /> + <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-numlist']" /> + <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-link']" /> + <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-image']" /> + <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-table']" /> + <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-charmap']"/> + <element name="Browse" type="button" selector=".mce-i-browse"/> + <element name="BrowseUploadImage" type="file" selector=".fileupload" /> + <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> + <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> + <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> + <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> + <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> + <element name="UploadImage" type="file" selector=".fileupload" /> + <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> + <element name="InsertFile" type="text" selector="#insert_files"/> + <element name="CreateFolder" type="button" selector="#new_folder" /> + <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> + <element name="CancelBtn" type="button" selector="#cancel" /> + <element name="FolderName" type="button" selector="input[data-role='promptField']" /> + <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> + <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> + <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> + <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> + </section> + <section name="AdminProductFormConfigurationsSection"> + <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> + <element name="currentVariationsRows" type="button" selector=".data-row"/> + <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> + <element name="currentVariationsSkuCells" type="textarea" selector=".admin__control-fields[data-index='sku_container']"/> + <element name="currentVariationsPriceCells" type="textarea" selector=".admin__control-fields[data-index='price_container']"/> + <element name="currentVariationsQuantityCells" type="textarea" selector=".admin__control-fields[data-index='quantity_container']"/> + <element name="currentVariationsAttributesCells" type="textarea" selector=".admin__control-fields[data-index='attributes']"/> + </section> + <section name="AdminCreateProductConfigurationsPanel"> + <element name="next" type="button" selector=".steps-wizard-navigation .action-next-step" timeout="30"/> + <element name="createNewAttribute" type="button" selector=".select-attributes-actions button[title='Create New Attribute']" timeout="30"/> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> + <element name="attributeCode" type="input" selector=".admin__control-text[name='attribute_code']"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> + + <element name="selectAll" type="button" selector=".action-select-all"/> + <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> + <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> + <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> + + <element name="applyUniquePricesByAttributeToEachSku" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']"/> + <element name="selectAttribute" type="select" selector="#select-each-price" timeout="30"/> + <element name="attribute1" type="input" selector="#apply-single-price-input-0"/> + <element name="attribute2" type="input" selector="#apply-single-price-input-1"/> + <element name="attribute3" type="input" selector="#apply-single-price-input-2"/> + + <element name="applySingleQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-single-inventory-radio']" timeout="30"/> + <element name="quantity" type="input" selector="#apply-single-inventory-input"/> + <element name="applySinglePriceToAllSkus" type="radio" selector=".admin__field-label[for='apply-single-price-radio']"/> + <element name="singlePrice" type="input" selector="#apply-single-price-input"/> + <element name="attributeByName" type="input" selector="//label[text()='{{var}}']/preceding-sibling::input" parameterized="true"/> + <element name="checkboxByName" type="input" selector="//div[text()='{{var}}']//ancestor::tr//input" parameterized="true"/> + </section> + <section name="AdminNewAttributePanel"> + <element name="saveAttribute" type="button" selector="#save" timeout="30"/> + <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> + <element name="defaultLabel" type="input" selector="#attribute_label"/> + </section> + <section name="AdminChooseAffectedAttributeSetPopup"> + <element name="confirm" type="button" selector="button[data-index='confirm_button']" timeout="30"/> + <element name="closePopUp" type="button" selector=".modal-popup._show [data-role='closeBtn']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml new file mode 100644 index 0000000000000..9f51c6b8e21b4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridActionSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductGridActionSection"> + <element name="addProductToggle" type="button" selector=".action-toggle.primary.add" timeout="30"/> + <element name="addSimpleProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-simple']" timeout="30"/> + <element name="addConfigurableProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-configurable']" timeout="30"/> + <element name="addBundleProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-bundle']" timeout="30"/> + <element name="addTypeProduct" type="button" selector=".item[data-ui-id='products-list-add-new-product-button-item-{{type}}']" parameterized="true"/> + <element name="productName" type="text" selector="//div[text()='{{var1}}']" parameterized="true"/> + <element name="actionsSelectBox" type="select" selector="//div[contains(@class,'col-xs-2')]//button[contains(@title,'Select Items')]"/> + <element name="deleteOptionInActionsSelectBox" type="select" selector="//div[contains(@class,'col-xs-2')]//span[contains(text(),'Delete')]"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml new file mode 100644 index 0000000000000..d8567df81b6b3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridConfirmActionSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductGridConfirmActionSection"> + <element name="title" type="text" selector=".modal-popup.confirm h1.modal-title"/> + <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> + <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> + <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml new file mode 100644 index 0000000000000..a02da2df51702 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductGridFilterSection"> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> + <element name="clearAll" type="button" selector=".admin__data-grid-header .admin__data-grid-filters-current._show .action-clear" timeout="30"/> + <element name="columnsDropdown" type="button" selector=".admin__data-grid-action-columns button.admin__action-dropdown"/> + <element name="viewColumnOption" type="checkbox" selector="//div[contains(@class, '_active')]//div[contains(@class, 'admin__data-grid-action-columns-menu')]//div[@class='admin__field-option']//label[text()='{{col}}']/preceding-sibling::input" parameterized="true"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="newFromDateFilter" type="input" selector="input.admin__control-text[name='news_from_date[from]']"/> + <element name="skuFilter" type="input" selector="input.admin__control-text[name='sku']"/> + <element name="nameFilter" type="input" selector="input.admin__control-text[name='name']"/> + <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="name" type="input" selector="input.admin__control-text[name='name']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml new file mode 100644 index 0000000000000..e6d9cae3e9442 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductGridSection"> + <element name="productRowBySku" type="block" selector="//div[@id='container']//tr//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> + <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> + <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> + <element name="productGridElement1" type="input" selector="#addselector" /> + <element name="productGridElement2" type="text" selector="#addselector" /> + <element name="productGridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> + <element name="multicheckDropdown" type="button" selector="div[data-role='grid-wrapper'] th.data-grid-multicheck-cell button.action-multicheck-toggle"/> + <element name="multicheckOption" type="button" selector="//div[@data-role='grid-wrapper']//th[contains(@class, data-grid-multicheck-cell)]//li//span[text() = '{{label}}']" parameterized="true"/> + <element name="bulkActionDropdown" type="button" selector="div.admin__data-grid-header-row.row div.action-select-wrap button.action-select"/> + <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> + <element name="productGridXRowYColumnButton" type="input" selector="table.data-grid tr.data-row:nth-child({{row}}) td:nth-child({{column}})" parameterized="true" timeout="30"/> + <element name="productNameInNameColumn" type="input" selector="//td[4]/div[@class='data-grid-cell-content']"/> + <element name="checkbox" type="checkbox" selector="//div[contains(text(),'{{product}}')]/ancestor::tr[contains(@class, 'data-row')]//input[@class='admin__control-checkbox']" parameterized="true" /> + <element name="firstRowCheckbox" type="checkbox" selector="tr.data-row:nth-of-type(1) input.admin__control-checkbox"/> + <element name="bulkActionOption" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-select-wrap')]//ul/li/span[text() = '{{label}}']" parameterized="true"/> + <element name="productGridNameProduct" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="adminImgGridThumbnail" type="text" selector="img.admin__control-thumbnail[src*='/{{var1}}']" parameterized="true"/> + <element name="selectRowBasedOnName" type="input" selector="//td/div[text()='{{var1}}']" parameterized="true"/> + <element name="changeStatus" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-menu-item')]//ul/li/span[text() = '{{status}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml new file mode 100644 index 0000000000000..7341a6ded7a09 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridTableHeaderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductGridTableHeaderSection"> + <element name="id" type="button" selector=".//*[@class='sticky-header']/following-sibling::*//th[@class='data-grid-th _sortable _draggable _{{order}}']/span[text()='ID']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml new file mode 100644 index 0000000000000..390cebcc57c65 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductImagesSection"> + <element name="productImagesToggle" type="button" selector="div[data-index=gallery] .admin__collapsible-title"/> + <element name="imageFileUpload" type="input" selector="#fileupload"/> + <element name="imageUploadButton" type="button" selector="div.image div.fileinput-button"/> + <element name="imageFile" type="text" selector="//*[@id='media_gallery_content']//img[contains(@src, '{{url}}')]" parameterized="true"/> + <element name="removeImageButton" type="button" selector=".action-remove"/> + <element name="modalOkBtn" type="button" selector="button.action-primary.action-accept"/> + <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> + <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> + <element name="altText" type="textarea" selector=".image-panel textarea[data-role='image-description']"/> + <element name="roleImage" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = '{{roleImage}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml new file mode 100644 index 0000000000000..5f2e6bd6cf721 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message.message-error.error"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml new file mode 100644 index 0000000000000..90c3856933be9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductSEOSection"> + <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> + <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> + <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml new file mode 100644 index 0000000000000..681d3bd91e817 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUpdateAttributesHeaderSection"> + <element name="saveButton" type="button" selector="button[data-ui-id='page-actions-toolbar-save-button']" timeout="30"/> + </section> + <section name="AdminUpdateAttributesWebsiteSection"> + <element name="website" type="button" selector="#attributes_update_tabs_websites"/> + <element name="addProductToWebsite" type="checkbox" selector="#add-products-to-website-content .website-checkbox"/> + </section> + <section name="AdminUpdateAttributesAttributesSection"> + <element name="formByStoreId" type="block" selector="//form[contains(@action,'store/{{store_id}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml new file mode 100644 index 0000000000000..9df1b6a383972 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AttributePropertiesSection"> + <element name="defaultLabel" type="input" selector="#attribute_label"/> + <element name="inputType" type="select" selector="#frontend_input"/> + <element name="valueRequired" type="select" selector="#is_required"/> + <element name="advancedProperties" type="button" selector="#advanced_fieldset-wrapper"/> + <element name="defaultValue" type="input" selector="#default_value_text"/> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="saveAndEdit" type="button" selector="#save_and_edit_button"/> + <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> + <element name="scope" type="select" selector="#is_global"/> + <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/CategoryDisplaySettingsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/CategoryDisplaySettingsSection.xml new file mode 100644 index 0000000000000..ea47c41816239 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/CategoryDisplaySettingsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CategoryDisplaySettingsSection"> + <element name="DisplaySettingTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Display Settings']"/> + <element name="anchor" type="checkbox" selector="input[name='is_anchor']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml new file mode 100644 index 0000000000000..42b03cd14701b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryMainSection"> + <element name="modeListButton" type="button" selector="#mode-list" timeout="10"/> + <element name="categoryTitle" type="text" selector="#page-title-heading span"/> + <element name="ProductItemInfo" type="button" selector=".product-item-info"/> + <element name="specifiedProductItemInfo" type="button" selector="//a[@class='product-item-link'][contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="AddToCartBtn" type="button" selector="button.action.tocart.primary"/> + <element name="SuccessMsg" type="button" selector="div.message-success"/> + <element name="productCount" type="text" selector="#toolbar-amount"/> + <element name="CatalogDescription" type="text" selector="//div[@class='category-description']//p"/> + <element name="mediaDescription" type="text" selector="img[alt='{{var1}}']" parameterized="true"/> + <element name="productsList" type="text" selector="//ol[@class='products list items product-items']"/> + <element name="categoryPageProductImagePlaceholderSmall" type="text" selector=".products-grid img[src*='placeholder/small_image.jpg']"/> + <element name="categoryPageProductImage" type="text" selector=".products-grid img[src*='/{{var1}}']" parameterized="true"/> + <element name="categoryPageProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> + <element name="categoryEmptyMessage" type="text" selector=".column.main .message.info.empty"/> + <element name="productName" type="text" selector=".product-item-name"/> + <element name="productLinkByHref" type="text" selector="a.product-item-link[href$='{{var1}}.html']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml new file mode 100644 index 0000000000000..64470ec1f40e4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryPagerSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryPagerSection"> + <element name="perPage" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[3]//*[@id='limiter']"/> + <element name="perPageSelected" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[3]//*[@id='limiter']/option[@selected='selected']"/> + <element name="sortedBy" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@id='sorter']"/> + <element name="modeGridIsActive" type="text" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@class='modes']/strong[@class='modes-mode active mode-grid']/span"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml new file mode 100644 index 0000000000000..e82f972eefcc5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryProductSection"> + <element name="ProductTitleByNumber" type="button" selector="//main//li[{{var1}}]//a[@class='product-item-link']" parameterized="true"/> + <element name="ProductPriceByNumber" type="text" selector="//main//li[{{var1}}]//span[@class='price']" parameterized="true"/> + <element name="ProductInfoByNumber" type="text" selector="//main//li[{{var1}}]//div[@class='product-item-info']" parameterized="true"/> + <element name="ProductAddToCompareByNumber" type="text" selector="//main//li[{{var1}}]//a[contains(@class, 'tocompare')]" parameterized="true"/> + <element name="productAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocart')]" parameterized="true" timeout="30"/> + <element name="productTitleByName" type="button" selector="//main//li//a[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="ProductPriceByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> + <element name="ProductImageByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[@class='product-image-photo']" parameterized="true"/> + <element name="ProductInfoByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//div[@class='product-item-info']" parameterized="true"/> + <element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> + <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> + <element name="productPriceFinal" type="text" selector="//span[@data-price-type='finalPrice']//span[@class='price'][contains(.,'{{var1}}')]" parameterized="true"/> + <element name="productPriceOld" type="text" selector="//span[@data-price-type='oldPrice']//span[@class='price'][contains(., '{{var1}}')]" parameterized="true"/> + <element name="productPriceLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="productPriceLinkAfterLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]/following::span[contains(text(), '{{var2}}')]" parameterized="true"/> + <element name="productNameInGrid" type="text" selector="//div[contains(@class,'products-grid')]//strong[contains(@class,'product-item-name')]//a[normalize-space(text())='{{productName}}']" parameterized="true"/> + <element name="productStockUnavailableByName" type="text" selector="//a[contains(@class, 'product-item-link') and normalize-space(text())='{{productName}}']/ancestor::div[contains(@class, 'product-item-details')]//span[contains(text(),'Out of stock')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml new file mode 100644 index 0000000000000..daa93dc3e6c38 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCategorySidebarSection"> + <element name="filterOptionsTitle" type="text" selector="//div[@class='filter-options-title' and contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="filterOptions" type="text" selector=".filter-options-content .items"/> + <element name="filterOption" type="text" selector=".filter-options-content .item"/> + <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> + <element name="filterByName" type="text" selector="//*[contains(text(), '{{arg}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml new file mode 100644 index 0000000000000..a54d46c490f91 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontComparisonSidebarSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontComparisonSidebarSection"> + <element name="compare" type="button" selector="div.block-compare a.compare"/> + <element name="clearAll" type="button" selector="div.block-compare a.clear"/> + <element name="productTitleByName" type="button" selector="//main//ol[@id='compare-items']//a[@class='product-item-link'][text()='{{var1}}']" parameterized="true"/> + <element name="noItemsMessage" type="text" selector="div.block-compare div.empty"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml new file mode 100644 index 0000000000000..9db7de6ccc063 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontFooterSection"> + <element name="SwitchStoreButton" type="button" selector="#switcher-store-trigger"/> + <element name="StoreLink" type="button" selector="//ul[@class='dropdown switcher-dropdown']//a[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml new file mode 100644 index 0000000000000..6b0130eefc39b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontHeaderSection"> + <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml new file mode 100644 index 0000000000000..1b97d0c0795a4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontMessagesSection"> + <element name="test" type="input" selector=".test"/> + <element name="success" type="text" selector="div.message-success.success.message"/> + <element name="error" type="text" selector="div.message-error.error.message"/> + <element name="notice" type="text" selector="div.message-notice"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml new file mode 100644 index 0000000000000..ff2e5f2f36015 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontMiniCartSection"> + <element name="quantity" type="button" selector="span.counter-number"/> + <element name="show" type="button" selector="a.showcart"/> + <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml new file mode 100644 index 0000000000000..7a8fa7bfc3c6a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontNavigationSection"> + <element name="topCategory" type="button" selector="//a[contains(@class,'level-top')]/span[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="subCategory" type="button" selector="//ul[contains(@class,'submenu')]//span[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="breadcrumbs" type="textarea" selector=".items"/> + <element name="categoryBreadcrumbs" type="textarea" selector=".breadcrumbs li"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml new file mode 100644 index 0000000000000..a7b72bbaa78aa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductRelatedProductsSection"> + <element name="relatedProductName" type="button" selector="//*[@class='block related']//a[contains(text(), '{{productName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml new file mode 100644 index 0000000000000..65d6b7c5f61cb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontProductActionSection"> + <element name="quantity" type="input" selector="#qty"/> + <element name="addToCart" type="button" selector="#product-addtocart-button"/> + <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> + <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> + <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml new file mode 100644 index 0000000000000..12c71e6da6004 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductAdditionalInformationSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductAdditionalInformationSection"> + <element name="additionalInfoTab" type="button" selector="#tab-label-additional-title"/> + <element name="additionalInfoContainer" type="block" selector="#additional"/> + <element name="attributeLabel" type="text" selector="#product-attribute-specs-table .col.label"/> + <element name="attributeValue" type="text" selector="#product-attribute-specs-table .col.data"/> + <!-- The tab transform to an accordion when window resize --> + <element name="moreInformationSectionToggleState" type="button" selector="#tab-label-additional[aria-expanded='{{expanded}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml new file mode 100644 index 0000000000000..d426864281d5f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductCompareMainSection"> + <element name="pageName" type="text" selector="#maincontent h1 span"/> + <element name="productLinkByName" type="button" selector="//*[@id='product-comparison']//tr//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="productPriceByName" type="text" selector="//*[@id='product-comparison']//td[.//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> + <element name="productImageByName" type="text" selector="//*[@id='product-comparison']//td[.//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]]//img[@class='product-image-photo']" parameterized="true"/> + <element name="productAttributeByCodeAndProductName" type="text" selector="//*[@id='product-comparison']//tr[.//th[./span[contains(text(), '{{var1}}')]]]//td[count(//*[@id='product-comparison']//tr//td[.//strong[@class='product-item-name']/a[contains(text(), '{{var2}}')]]/preceding-sibling::td)+1]/div" parameterized="true"/> + <element name="productAttributeByName" type="text" selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'{{name}}')]" parameterized="true"/> + <element name="removeProduct" type="button" selector="//table[@id='product-comparison']//thead//td[count(//table[@id='product-comparison']//strong[contains(@class, 'product-item-name') and contains(.,'{{productName}}')]/ancestor::td/preceding-sibling::td) + 1]//a[contains(@class, 'delete')]" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml new file mode 100644 index 0000000000000..77458e03060d1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoDetailsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoDetailsSection"> + <element name="productNameForReview" type="text" selector=".legend.review-legend>strong" /> + <element name="detailsTab" type="button" selector="#tab-label-description-title" /> + <!-- The tab transform to an accordion when window resize --> + <element name="detailsSectionToggleState" type="button" selector="#tab-label-description[aria-expanded='{{expanded}}']" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..ae0cb3f970108 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="stock" type="input" selector=".stock.available"/> + <element name="productName" type="text" selector=".base"/> + <element name="productSku" type="text" selector=".product.attribute.sku>.value"/> + <element name="productPrice" type="text" selector=".price"/> + <element name="specialPrice" type="text" selector=".special-price"/> + <element name="specialPriceValue" type="text" selector=".special-price .price"/> + <element name="qty" type="input" selector="#qty"/> + <element name="productStockStatus" type="text" selector=".stock[title=Availability]>span"/> + <element name="productDescription" type="text" selector="#description .value"/> + <element name="productImageSrc" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[contains(@src, '{{src}}')]" parameterized="true"/> + <element name="productOptionFieldInput" type="input" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@type='text']" parameterized="true"/> + <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> + <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> + <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> + <element name="productOptionRadioButtonsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@price='{{var2}}']" parameterized="true"/> + <element name="productOptionDataMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> + <element name="productOptionDataDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> + <element name="productOptionDataYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> + <element name="productOptionDateAndTimeMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> + <element name="productOptionDateAndTimeDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> + <element name="productOptionDateAndTimeYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> + <element name="productOptionDateAndTimeHour" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='hour']" parameterized="true"/> + <element name="productOptionDateAndTimeMinute" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='minute']" parameterized="true"/> + <element name="productOptionTimeHour" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='hour']" parameterized="true"/> + <element name="productOptionTimeMinute" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='minute']" parameterized="true"/> + + <!-- Only one of Upload/Url Inputs are available for File and Sample depending on the value of the corresponding TypeSelector --> + <element name="addLinkFileUploadFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@type='file']" parameterized="true" /> + + <element name="productShortDescription" type="text" selector="//div[@class='product attribute overview']//div[@class='value']"/> + <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> + <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> + <element name="productAttributeOptionsPrice" type="text" selector="//label[contains(.,'{{var1}}')]//span[@data-price-amount='{{var2}}']" parameterized="true"/> + <element name="productAttributeOptionsDropDown" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[@price='{{var2}}']" parameterized="true"/> + <element name="productAttributeOptionsRadioButtons" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//span[@data-price-amount='{{var2}}']" parameterized="true"/> + <element name="productAttributeOptionsCheckbox" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//span[@data-price-amount='{{var2}}']" parameterized="true"/> + <element name="productAttributeOptionsMultiselect" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[@price='{{var2}}']" parameterized="true"/> + <element name="productAttributeOptionsData" type="text" selector="//span[contains(.,'{{var1}}')]/../span[@class='price-notice']//span[@data-price-amount='{{var2}}']" parameterized="true"/> + <element name="mediaDescription" type="text" selector=".product.attribute.description>div>p>img"/> + <element name="mediaShortDescription" type="text" selector=".product.attribute.overview>div>p>img"/> + <element name="productAddToCompare" type="button" selector="a.action.tocompare"/> + <element name="productOptionDropDownTitle" type="text" selector="//label[contains(.,'{{var1}}')]" parameterized="true"/> + <element name="productOptionDropDownOptionTitle" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[contains(.,'{{var2}}')]" parameterized="true"/> + + <!-- Tier price selectors --> + <element name="productTierPriceByForTextLabel" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}][contains(text(),'Buy {{var2}} for')]" parameterized="true"/> + <element name="productTierPriceAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(text(), '{{var2}}')]" parameterized="true"/> + <element name="productTierPriceSavePercentageAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(@class, 'percent')][contains(text(), '{{var2}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml new file mode 100644 index 0000000000000..c37e9c75d57ff --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductMediaSection"> + <element name="imageFile" type="text" selector=".product.media img[src*='{{filename}}']" parameterized="true"/> + <element name="productImageActive" type="text" selector=".product.media div[data-active=true] > img[src*='{{filename}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml new file mode 100644 index 0000000000000..61eabb6cb34fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductPageSection"> + <element name="qtyInput" type="button" selector="input.input-text.qty"/> + <element name="addToCartBtn" type="button" selector="button.action.tocart.primary" timeout="30"/> + <element name="successMsg" type="button" selector="div.message-success"/> + <element name="addToWishlist" type="button" selector="a.action.towishlist" timeout="30"/> + <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> + <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> + <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> + <element name="alertMessage" type="text" selector=".page.messages [role=alert]"/> + <element name="messagesBlock" type="text" selector=".page.messages"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml new file mode 100644 index 0000000000000..ca0c32f142852 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontWidgetsSection"> + <element name="widgetRecentlyViewedProductsGrid" type="block" selector=".block.widget.block-viewed-products-grid"/> + <element name="widgetRecentlyComparedProductsGrid" type="block" selector=".block.widget.block-compared-products-grid"/> + <element name="widgetRecentlyOrderedProductsGrid" type="block" selector=".block.block-reorder"/> + <element name="widgetCategoryLinkByName" type="text" selector="//div[contains(@class, 'block-category-link')]/a/span[contains(., '{{categoryName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml new file mode 100644 index 0000000000000..3594940626bdc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AddOutOfStockProductToCompareListTest"> + <annotations> + <features value="Catalog"/> + <title value="Add out of stock product to compare list"/> + <description value="Add out of stock product to compare list"/> + <stories value="Add product to compare list"/> + <severity value="MAJOR"/> + <testCaseId value="MC-18542"/> + <useCaseId value="MAGETWO-98521"/> + <group value="catalog"/> + </annotations> + <before> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockTrue"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="OutOfStockProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!--Open product page--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPage"/> + <!--'Add to compare' link is not available--> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="doNotSeeAddToCompareLink"/> + + <!--Turn on 'out on stock' config--> + <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockEnable.path}} {{CatalogInventoryOptionsShowOutOfStockEnable.value}}" stepKey="setConfigShowOutOfStockTrue"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Add product to comparison list --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPageAgain"/> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addProductToCompare"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + + <!--See product in the comparison list--> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage"/> + <actionGroup ref="StorefrontCheckCompareOutOfStockProductActionGroup" stepKey="checkSimpleProductOnCompareListPage"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + + <!--Go to Category page and delete product from comparison list--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="onCategoryPage"/> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearCompareList"/> + + <!--Add product to compare list from Category page--> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addProductToCompareFromCategory"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + + <!--Check that product displays on add to compare widget--> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="seeProductNameOnCompareWidget"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + + <!--See product on the compare page--> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePageAgain"/> + <actionGroup ref="StorefrontCheckCompareOutOfStockProductActionGroup" stepKey="checkSimpleProductOnCompareListPageAgain"> + <argument name="productVar" value="$$createProduct$$"/> + </actionGroup> + + <actionGroup ref="RemoveProductFromComparisonList" stepKey="removeProductFromComparisonList"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml new file mode 100644 index 0000000000000..5742f44e1fedf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml @@ -0,0 +1,272 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminApplyTierPriceToProductTest"> + <annotations> + <features value="Apply tier price to a product"/> + <title value="You should be able to apply tier price to a product."/> + <description value="You should be able to apply tier price to a product."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76312"/> + <group value="product"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createSimpleUSCustomer"> + <field key="group_id">1</field> + </createData> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleUSCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Case: Group Price--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton1"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton1"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput1"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="10" stepKey="selectProductTierPricePriceInput"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct1"/> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin1"> + <argument name="customer" value="$$createSimpleUSCustomer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('90')}}" stepKey="assertProductFinalPriceIs90_1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_1"/> + <amOnPage url="customer/account/logout/" stepKey="logoutCustomer1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad3"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('90')}}" stepKey="assertProductFinalPriceIs90_2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_2"/> + <!--Case: Tier Price for General Customer Group--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> + <waitForPageLoad time="30" stepKey="waitForProductPageToLoad1"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton2"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForcustomerGroupPriceAddButton2"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" time="30" stepKey="waitForSelectCustomerGroupNameAttribute1"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="General" stepKey="selectCustomerGroupGeneral"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton2"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct2"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage3"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad4"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('100')}}" stepKey="assertProductFinalPriceIs100_1"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_3"/> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin2"> + <argument name="customer" value="$$createSimpleUSCustomer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage4"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('90')}}" stepKey="assertProductFinalPriceIs90_3"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_4"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_3"/> + <!--Case: Tier Price applied if Product quantity meets Tier Price Condition--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex3"/> + <waitForPageLoad time="30" stepKey="waitForProductPageToLoad2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct3"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage3"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton3"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForcustomerGroupPriceAddButton3"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" stepKey="waitForSelectCustomerGroupNameAttribute2"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="ALL GROUPS" stepKey="selectCustomerGroupAllGroups"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="15" stepKey="fillProductTierPriceQtyInput15"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickToLoseFocusOnRequiredInputElement"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty20PriceDiscountAnd18percent2"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('1')}}" userInput="20" stepKey="fillProductTierPriceQtyInput20"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('1')}}" userInput="Discount" stepKey="selectProductTierPriceValueType2"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('1')}}" userInput="18" stepKey="selectProductTierPricePriceInput18"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton3"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct3"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage5"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad6"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('100')}}" stepKey="assertProductFinalPriceIs100_2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('As low as')}}" stepKey="assertAsLowAsPriceLabel_1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLinkAfterLabel('As low as', '82')}}" stepKey="assertPriceAfterAsLowAsLabel_1"/> + <amOnPage url="customer/account/logout/" stepKey="logoutCustomer2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad7"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage6"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('100')}}" stepKey="assertProductFinalPriceIs100_3"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('As low as')}}" stepKey="assertAsLowAsPriceLabel_2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLinkAfterLabel('As low as', '82')}}" stepKey="assertPriceAfterAsLowAsLabel_2"/> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="goToProductPage1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', '15')}}" stepKey="assertProductTierPriceByForTextLabelForFirstRow1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('2', '20')}}" stepKey="assertProductTierPriceByForTextLabelForSecondRow1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('1', '90')}}" stepKey="assertProductTierPriceAmountForFirstRow1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('2', '82')}}" stepKey="assertProductTierPriceAmountForSecondRow1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('1', '10')}}" stepKey="assertProductTierPriceSavePercentageAmountForFirstRow1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('2', '18')}}" stepKey="assertProductTierPriceSavePercentageAmountForSecondRow1"/> + <fillField userInput="10" selector="{{StorefrontProductInfoMainSection.qty}}" stepKey="fillProductQuantity1"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontViewAndEditCartFromMiniCartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <seeInField userInput="10" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="seeInQtyField10"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField1"/> + <assertEquals message="Shopping cart should contain subtotal $1,000" stepKey="assertSubtotalField1"> + <expectedResult type="string">$1,000.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField1</actualResult> + </assertEquals> + <fillField userInput="15" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="fillProductQuantity2"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCartButton1"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField2"/> + <assertEquals message="Shopping cart should contain subtotal $1,350" stepKey="assertSubtotalField2"> + <expectedResult type="string">$1,350.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField2</actualResult> + </assertEquals> + <fillField userInput="20" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="fillProductQuantity3"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCartButton2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField3"/> + <assertEquals message="Shopping cart should contain subtotal $1,640" stepKey="assertSubtotalField3"> + <expectedResult type="string">$1,640.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField3</actualResult> + </assertEquals> + <!--Tier Price is changed in Shopping Cart and is changed on Product page if Tier Price parameters are changed in Admin--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex4"/> + <waitForPageLoad time="30" stepKey="waitForProductPageToLoa4"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct4"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage4"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton4"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForcustomerGroupPriceAddButton4"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('1')}}" userInput="25" stepKey="selectProductTierPricePercentageValue2"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton4"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct4"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage1"/> + <waitForPageLoad time="30" stepKey="waitForShoppingCartPagePageLoad1"/> + <seeInField userInput="20" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="seeInQtyField20"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField4"/> + <assertEquals message="Shopping cart should contain subtotal $1,500" stepKey="assertSubtotalField4"> + <expectedResult type="string">$1,500.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField4</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.subtotal}}" stepKey="grabTextFromCheckoutCartSummarySectionSubtotal1"/> + <assertEquals message="Shopping cart summary section should contain subtotal $1,500" stepKey="assertSubtotalFieldFromCheckoutCartSummarySection1"> + <expectedResult type="string">$1,500.00</expectedResult> + <actualResult type="variable">grabTextFromCheckoutCartSummarySectionSubtotal1</actualResult> + </assertEquals> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart1"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="waitForminiCartSubtotalField1"/> + <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabTextFromMiniCartSubtotalField"/> + <assertEquals message="Mini shopping cart should contain subtotal $1,500" stepKey="assertSubtotalFieldFromMiniShoppingCart1"> + <expectedResult type="string">$1,500.00</expectedResult> + <actualResult type="variable">grabTextFromMiniCartSubtotalField</actualResult> + </assertEquals> + <amOnPage url="{{AdminSalesConfigPage.url('#sales_msrp-link')}}" stepKey="navigateToAdminSalesConfigPageMAPTab1"/> + <waitForPageLoad time="30" stepKey="waitForAdminSalesConfigPageLoad1"/> + <uncheckOption selector="{{AdminSalesConfigSection.enableMAPUseSystemValue}}" stepKey="uncheckMAPUseSystemValue"/> + <selectOption selector="{{AdminSalesConfigSection.enableMAPSelect}}" userInput="Yes" stepKey="setEnableMAPYes"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigSuccessMessage1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="flushCache1"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage2"/> + <waitForPageLoad time="30" stepKey="waitForShoppingCartPagePageLoad2"/> + <seeInField userInput="20" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="seeInQtyField20_2"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField5"/> + <assertEquals message="Shopping cart should contain subtotal $1,500" stepKey="assertSubtotalField5"> + <expectedResult type="string">$1,500.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField5</actualResult> + </assertEquals> + <amOnPage url="{{AdminSalesConfigPage.url('#sales_msrp-link')}}" stepKey="navigateToAdminSalesConfigPageMAPTab2"/> + <waitForPageLoad time="30" stepKey="waitForAdminSalesConfigPageLoad2"/> + <selectOption selector="{{AdminSalesConfigSection.enableMAPSelect}}" userInput="No" stepKey="setEnableMAPNo"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig2"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigSuccessMessage2"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="flushCache2"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage3"/> + <waitForPageLoad time="30" stepKey="waitForShoppingCartPagePageLoad3"/> + <seeInField userInput="20" selector="{{CheckoutCartProductSection.productQuantityByName($$createSimpleProduct.name$$)}}" stepKey="seeInQtyField20_3"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField6"/> + <assertEquals message="Shopping cart should contain subtotal $1,500" stepKey="assertSubtotalField6"> + <expectedResult type="string">$1,500.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField6</actualResult> + </assertEquals> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="goToProductPage2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', '15')}}" stepKey="assertProductTierPriceByForTextLabelForFirstRow2"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('2', '20')}}" stepKey="assertProductTierPriceByForTextLabelForSecondRow2"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('1', '90')}}" stepKey="assertProductTierPriceAmountForFirstRow2"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('2', '75')}}" stepKey="assertProductTierPriceAmountForSecondRow2"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('1', '10')}}" stepKey="assertProductTierPriceSavePercentageAmountForFirstRow2"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('2', '25')}}" stepKey="assertProductTierPriceSavePercentageAmountForSecondRow2"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex5"/> + <waitForPageLoad time="30" stepKey="waitForProductPageToLoad3"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct5"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage5"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton5"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="waitForcustomerGroupPriceDeleteButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteFirstRowOfCustomerGroupPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteSecondRowOfCustomerGroupPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton5"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct5"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage6"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton6"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForcustomerGroupPriceAddButton5"/> + <dontSeeElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" stepKey="dontSeeQtyInputOfFirstRow"/> + <dontSeeElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('1')}}" stepKey="dontSeeQtyInputOfSecondRow"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="closeAdvancedPricingPopup"/> + <waitForElementVisible selector="{{AdminProductFormSection.productPrice}}" stepKey="waitForAdminProductFormSectionProductPriceInput"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="200" stepKey="fillProductPrice200"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage4"/> + <waitForPageLoad time="30" stepKey="waitForShoppingCartPagePageLoad4"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField7"/> + <assertEquals message="Shopping cart should contain subtotal $4,000" stepKey="assertSubtotalField7"> + <expectedResult type="string">$4,000.00</expectedResult> + <actualResult type="variable">grabTextFromSubtotalField7</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.subtotal}}" stepKey="grabTextFromCheckoutCartSummarySectionSubtotal2"/> + <assertEquals message="Shopping cart summary section should contain subtotal $4,000" stepKey="assertSubtotalFieldFromCheckoutCartSummarySection2"> + <expectedResult type="string">$4,000.00</expectedResult> + <actualResult type="variable">grabTextFromCheckoutCartSummarySectionSubtotal2</actualResult> + </assertEquals> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart2"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="waitForminiCartSubtotalField2"/> + <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabTextFromMiniCartSubtotalField2"/> + <assertEquals message="Mini shopping cart should contain subtotal $4,000" stepKey="assertSubtotalFieldFromMiniShoppingCart2"> + <expectedResult type="string">$4,000.00</expectedResult> + <actualResult type="variable">grabTextFromMiniCartSubtotalField2</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml new file mode 100644 index 0000000000000..4b75e0db0c9ab --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminAssignProductAttributeToAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Add/Update attribute set"/> + <title value="Admin should be able to assign attributes to an attribute set"/> + <description value="Admin should be able to assign attributes to an attribute set"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76299"/> + <group value="attributeSet"/> + </annotations> + <before> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="attribute"/> + + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to default attribute set edit page --> + <amOnPage url="{{AdminProductAttributeSetEditPage.url(AddToDefaultSet.attributeSetId)}}" stepKey="onAttributeSetEdit"/> + <!-- Assert created attribute in unassigned section --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> + <!-- Assign attribute to a group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!-- Assert attribute in a group --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <!-- Save attribute set --> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + <!-- Go to create new product page --> + <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!-- Assert attribute can be used in product creation --> + <seeElement selector="{{AdminProductFormSection.attributeLabelByText($$attribute.attribute[frontend_labels][0][label]$$)}}" stepKey="seeLabel"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml new file mode 100644 index 0000000000000..01967fa1e0851 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeProductAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Update product"/> + <title value="Attributes from the selected attribute set should be shown"/> + <description value="Attributes from the selected attribute set should be shown"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-15414"/> + <useCaseId value="MAGETWO-98380"/> + <group value="catalog"/> + </annotations> + <before> + <!--Create category product, attribute, attribute set--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ProductAttributeText" stepKey="createProductAttribute"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Assign attribute to attribute set--> + <amOnPage url="{{AdminProductAttributeSetEditPage.url($$createAttributeSet.attribute_set_id$$)}}" stepKey="openAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + </before> + <after> + <!--Delete created entities--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + </after> + <!--Open created product--> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="openProductEditPage"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsLoaded"/> + <dontSeeElement selector="{{AdminProductFormSection.customAttributeInputField($$createProductAttribute.attribute_code$$)}}" stepKey="dontSeeCreatedAttribute"/> + <!--Change product attribute set--> + <actionGroup ref="AdminChangeProductAttributeSet" stepKey="changeProductAttributeSet"> + <argument name="attributeSet" value="$$createAttributeSet$$"/> + </actionGroup> + <!--Check new attribute is visible on product edit page--> + <seeElement selector="{{AdminProductFormSection.customAttributeInputField($$createProductAttribute.attribute_code$$)}}" stepKey="seeAttributeInForm"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml new file mode 100644 index 0000000000000..1ba30ae80818a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingAttributeValueOnProductEditPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create/configure Dropdown product attribute"/> + <title value="Checking attribute values on a product edit page"/> + <description value="Checking attribute values on a product edit page"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15746"/> + <useCaseId value="MAGETWO-74037"/> + <group value="catalog"/> + </annotations> + <before> + <!--Create Dropdown product attribute--> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createDropdownProductAttribute"/> + <!--Add options to attribute--> + <createData entity="ProductAttributeAdminOption1" stepKey="createFirstOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="ProductAttributeAdminOption2" stepKey="createSecondOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <!--Add attribute to Default Attribute Set--> + <createData entity="AddToDefaultSet" stepKey="attributeSet"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create Simple product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete product attribute--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDropdownProductAttribute" stepKey="deleteDropdownProductAttribute"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Go to Product edit page--> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <scrollToTopOfPage stepKey="scrollToShowAttribute"/> + + <!-- Grab attribute dropdown options --> + <grabMultiple selector="{{AdminProductFormSection.customAttributeSelectOptions($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" stepKey="clickOnAttributeDropdown" /> + + <!--Check attribute dropdown options--> + <assertContains stepKey="seeFirstAdminOption"> + <actualResult type="variable">clickOnAttributeDropdown</actualResult> + <expectedResult type="string">admin_option_1</expectedResult> + </assertContains> + <assertContains stepKey="seeSecondAdminOption"> + <actualResult type="variable">clickOnAttributeDropdown</actualResult> + <expectedResult type="string">admin_option_2</expectedResult> + </assertContains> + + <assertNotContains stepKey="dontSeeFirstStoreOption"> + <actualResult type="variable">clickOnAttributeDropdown</actualResult> + <expectedResult type="string">option1</expectedResult> + </assertNotContains> + <assertNotContains stepKey="dontSeeSecondStoreOption"> + <actualResult type="variable">clickOnAttributeDropdown</actualResult> + <expectedResult type="string">option2</expectedResult> + </assertNotContains> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml new file mode 100644 index 0000000000000..6f477a5325e64 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryTest"> + <annotations> + <features value="Category Creation"/> + <stories value="Create a Category via the Admin"/> + <title value="You should be able to create a Category in the admin back-end."/> + <description value="You should be able to create a Category in the admin back-end."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-72102"/> + <group value="category"/> + </annotations> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + + <!-- Literal URL below, need to refactor line + StorefrontCategoryPage when support for variable URL is implemented--> + <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> + <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertTitle"/> + <see selector="{{StorefrontCategoryMainSection.categoryTitle}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="assertInfo1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml new file mode 100644 index 0000000000000..294766794b92c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateProductCustomAttributeSet"> + <annotations> + <features value="Catalog"/> + <stories value="Add/Update attribute set"/> + <title value="Admin should be able to create a simple product using a custom attribute set"/> + <description value="Admin should be able to create a simple product using a custom attribute set"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-79284"/> + <group value="catalog"/> + <group value="attributeSet"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create the new attribute set --> + <actionGroup ref="CreateAttributeSetActionGroup" stepKey="createAttributeSet"> + <argument name="nameLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <!-- Move attributes--> + <dragAndDrop selector1="{{AdminProductAttributeSetSection.attribute('meta_keyword')}}" selector2="{{AdminProductAttributeSetSection.attribute('manufacturer')}}" stepKey="unassign1"/> + <actionGroup ref="CreateNewGroupInAttributeSetActionGroup" stepKey="createNewGroup"/> + <dragAndDrop selector1="{{AdminProductAttributeSetSection.attribute('manufacturer')}}" selector2="{{AdminProductAttributeSetSection.attribute('TestGroupName')}}" stepKey="assignManufacturer"/> + <click selector="{{AdminProductAttributeSetSection.save}}" stepKey="clickSave2"/> + </before> + + <after> + <!-- Delete the new attribute set --> + <actionGroup ref="DeleteAttributeSetActionGroup" stepKey="deleteAttributeSet"> + <argument name="name" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!-- Go to new product page and see a default attribute --> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToNewProductPage"/> + <waitForPageLoad stepKey="wait2"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="expandSEOSection"/> + <seeElementInDOM selector="{{AdminProductFormSection.divByDataIndex('meta_keyword')}}" stepKey="seeMetaKeyword"/> + <dontSeeElementInDOM selector="{{AdminProductFormSection.divByDataIndex('testgroupname')}}" stepKey="dontSeeTestGroupName"/> + + <!-- Switch from default attribute set to new attribute set --> + <!-- A scrollToTopOfPage is needed to hide the floating header --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="searchForAttrSet"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeSetSearchCount}}" stepKey="waitLoadOption"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + + <!-- See new attribute set --> + <seeElementInDOM selector="{{AdminProductFormSection.divByDataIndex('testgroupname')}}" stepKey="seeTestGroupName"/> + <dontSeeElementInDOM selector="{{AdminProductFormSection.divByDataIndex('meta_keyword')}}" stepKey="dontSeeMetaKeyword"/> + + <!-- Finish filling the new product page --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillSimpleProductMain"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + + <!-- Check the storefront --> + <amOnPage url="{{_defaultProduct.name}}.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="seeProductNameInTitlte"/> + <see userInput="{{_defaultProduct.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{_defaultProduct.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + <see userInput="${{_defaultProduct.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDropdownAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDropdownAttributeTest.xml new file mode 100644 index 0000000000000..9bc17d19c4285 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDropdownAttributeTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateProductDropdownAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create/configure Dropdown product attribute"/> + <title value="Admin should be able to create dropdown product attribute"/> + <description value="Admin should be able to create dropdown product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95868"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productAttributeWithDropdownTwoOptions"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Set attribute properties --> + <fillField selector="{{AttributePropertiesSection.defaultLabel}}" + userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.inputType}}" + userInput="{{productAttributeWithDropdownTwoOptions.frontend_input}}" stepKey="fillInputType"/> + + <!-- Set advanced attribute properties --> + <click selector="{{AdvancedAttributePropertiesSection.advancedAttributePropertiesSectionToggle}}" + stepKey="showAdvancedAttributePropertiesSection"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.attributeCode}}" + stepKey="waitForSlideOut"/> + <fillField selector="{{AdvancedAttributePropertiesSection.attributeCode}}" + userInput="{{productAttributeWithDropdownTwoOptions.attribute_code}}" + stepKey="fillAttributeCode"/> + + <!-- Add new attribute options --> + <click selector="{{AdminAttributeOptionsSection.addOption}}" stepKey="clickAddOption1"/> + <fillField selector="{{AdminAttributeOptionsSection.nthOptionAdminLabel('1')}}" + userInput="Fish and Chips" stepKey="fillAdminValue1"/> + + <click selector="{{AdminAttributeOptionsSection.addOption}}" stepKey="clickAddOption2"/> + <fillField selector="{{AdminAttributeOptionsSection.nthOptionAdminLabel('2')}}" + userInput="Fish & Chips" stepKey="fillAdminValue2"/> + + <!-- Save the new product attribute --> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave1"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" + stepKey="waitForSuccessMessage"/> + + <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> + <argument name="productAttribute" value="productAttributeWithDropdownTwoOptions"/> + </actionGroup> + <!-- Check attribute data --> + <grabValueFrom selector="{{AdminAttributeOptionsSection.nthOptionAdminLabel('2')}}" + stepKey="secondOptionAdminLabel"/> + <assertEquals actual="$secondOptionAdminLabel" expected="'Fish & Chips'" + stepKey="assertSecondOption"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml new file mode 100644 index 0000000000000..35b663da4f5ad --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateRootCategoryAndSubcategoriesTest"> + <annotations> + <features value="Create Root Category and Subcategory"/> + <title value="You should be able to create Root Category and Subcategory."/> + <description value="You should be able to create Root Category and Subcategory."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76259"/> + <group value="category"/> + </annotations> + <!--Delete all created data during the test execution and assign Default Root Category to Store--> + <after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin2"/> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="Main Website Store"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.storeInFirstRow}}" stepKey="clickOnstoreInFirstRow"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad1" /> + <selectOption userInput="Default Category" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectOptionDefaultCategory"/> + <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkOnModalDialog2"/> + <actionGroup ref="DeleteCategory" stepKey="deleteCreatedNewRootCategory"> + <argument name="categoryEntity" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout2"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="amOnAdminCategoryPage"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Create new root category--> + <actionGroup ref="AdminCreateRootCategoryActionGroup" stepKey="createNewRootCategory"> + <argument name="categoryEntity" value="NewRootCategory"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <!--Create subcategory--> + <actionGroup ref="CreateCategory" stepKey="createSubcategory1"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage3"/> + <!--Create another subcategory--> + <actionGroup ref="CreateCategory" stepKey="createSubcategory2"> + <argument name="categoryEntity" value="SubCategoryWithParent"/> + </actionGroup> + <!--Assign new created root category to store--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="Main Website Store"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterSearchButton"/> + <click selector="{{AdminStoresGridSection.storeInFirstRow}}" stepKey="clickOnstoreInFirstRow"/> + <waitForPageLoad stepKey="waitForPageAdminStoresGroupEditLoad" /> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectOptionCreatedNewRootCategory"/> + <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkOnModalDialog1"/> + <actionGroup ref="logout" stepKey="logout1"/> + <!--Go to storefront and verify created subcategory on frontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad2"/> + <actionGroup ref="StorefrontCategoryCheckActionGroup" stepKey="checkCreatedSubcategory1OnFrontend"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <actionGroup ref="StorefrontCategoryCheckActionGroup" stepKey="checkCreatedSubcategory2OnFrontend"> + <argument name="categoryEntity" value="SubCategoryWithParent"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml new file mode 100644 index 0000000000000..740daefdff313 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateSimpleProductTest"> + <annotations> + <features value="Product Creation"/> + <stories value="Create a Simple Product via Admin"/> + <title value="You should be able to create a Simple Product in the admin back-end."/> + <description value="You should be able to create a Simple Product in the admin back-end."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createPreReqCategory.name$$]" stepKey="searchAndSelectCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + <seeInField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> + <seeInField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> + <seeInField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> + <seeInField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> + + <!-- Go to storefront category page, assert product visibility --> + <amOnPage url="{{StorefrontCategoryPage.url($$createPreReqCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <see userInput="{{_defaultProduct.name}}" stepKey="assertProductPresent"/> + <see userInput="{{_defaultProduct.price}}" stepKey="assertProductPricePresent"/> + + <!-- Go to storefront product page, assert product visibility --> + <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{_defaultProduct.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{_defaultProduct.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> + <see userInput="{{_defaultProduct.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml new file mode 100644 index 0000000000000..539d87b4a1064 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFilterProductGridByNameByStoreViewTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter products"/> + <title value="Product grid filtering by store view level attribute"/> + <description value="Verify that products grid can be filtered on all store view level by attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98755"/> + <useCaseId value="MAGETWO-97405"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="scopeName" value="_defaultStore.name"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="filterProductGridByName" stepKey="filterGridByName"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.firstRow}}" userInput="{{SimpleProduct3.name}}" stepKey="seeProductNameInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml new file mode 100644 index 0000000000000..96403d5dc887a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFilteringCategoryProductsUsingScopeSelectorTest"> + <annotations> + <features value="Catalog"/> + <title value="Filtering Category Products using scope selector"/> + <description value="Filtering Category Products using scope selector"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-78408"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website, Sore and Store View--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + + <!--Create Simple Product and Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct0"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Set filter to product name and product0 not assigned to any website--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct0.name$$')}}" + stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsitesSection"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenWebsiteSection"/> + <waitForPageLoad stepKey="waitForToOpenedWebsiteSection"/> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage"/> + + <!-- Set filter to product name and product2 in website 2 only --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct2.name$$')}}" + stepKey="clickOpenProductForEdit1"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen1"/> + + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct1"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage1"/> + + <!-- Set filter to product name and product12 assigned to both websites 1 and 2 --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad1"/> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct12.name$$')}}" + stepKey="clickOpenProductForEdit2"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen2"/> + + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites1"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage2"/> + </before> + <after> + <deleteData createDataKey="createProduct0" stepKey="deleteProduct"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct3"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + <!--Clear products filter--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsFilters"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Step 1-2: Open Category page and Set scope selector to All Store Views--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="clickCategoryName"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection"/> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory"/> + <assertRegExp expected="/\(4\)$/" expectedType="string" actual="$grabTextFromCategory" actualType="variable" + message="wrongCountProductOnAllStoreViews" stepKey="checkCountProducts"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="seeProductName"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName3"/> + + <!-- Step 3: Set scope selector to Website1( Storeview for the Website 1) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewDropdownToggle}}" + stepKey="clickStoresList"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewOption('Default Store View')}}" + stepKey="clickStoreView"/> + <waitForElementVisible selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="waitForPopup1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" stepKey="clickActionAccept"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad2"/> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory1"/> + <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory1" actualType="variable" + message="wrongCountProductOnWebsite1" stepKey="checkCountProducts1"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName4"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName5"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="dontSeeProductName1"/> + + <!-- Step 4: Set scope selector to Website2 ( StoreView for Website 2) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewDropdownToggle}}" + stepKey="clickStoresList1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad3"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewOption(SecondStoreUnique.name)}}" + stepKey="clickStoreView1"/> + <waitForElementVisible selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="waitForPopup2"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="clickActionAccept1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad4"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection2"/> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory2"/> + <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory2" actualType="variable" + message="wrongCountProductOnWebsite2" stepKey="checkCountProducts2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName6"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName7"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName2"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="dontSeeProductName3"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml new file mode 100644 index 0000000000000..128429e3344ba --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminGridPageNumberAfterSaveAndCloseActionTest"> + <annotations> + <features value="Catalog"/> + <title value="Checking Catalog grid page number after Save and Close action"/> + <description value="Checking Catalog grid page number after Save and Close action"/> + <stories value="Verify Catalog Grid"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17278"/> + <useCaseId value="MAGETWO-93755"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Clear product grid--> + <comment userInput="Clear product grid" stepKey="commentClearProductsGrid"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToProductsGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsFilters"/> + <actionGroup ref="DeleteAllProducts" stepKey="deleteProductsIfTheyExist"/> + <!--Create products--> + <createData entity="SimpleProduct3" stepKey="createProduct1"/> + <createData entity="SimpleProduct3" stepKey="createProduct2"/> + <!--Update product count per page--> + <actionGroup ref="AdminDataGridSelectCustomPageSize" stepKey="selectCustomPageSize"> + <argument name="pageSize" value="{{ProductGridPagerData.pageSize}}"/> + </actionGroup> + </before> + <after> + <!--Delete created data--> + <deleteData stepKey="deleteProduct1" createDataKey="createProduct1"/> + <deleteData stepKey="deleteProduct2" createDataKey="createProduct2"/> + <!--Revert products count per page --> + <conditionalClick selector="{{AdminDataGridPaginationSection.previousPage}}" dependentSelector="{{AdminDataGridPaginationSection.previousPage}}" visible="true" stepKey="clickPrevPageOrderGrid"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToProductsGridPage"/> + <actionGroup ref="AdminDataGridDeleteCustomPageSize" stepKey="deleteCustomAddedPageSize"> + <argument name="pageSize" value="{{ProductGridPagerData.pageSize}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToProductsGridPage"/> + <!--Go to the next page and edit the product--> + <comment userInput="Go to the next page and edit the product" stepKey="commentEdiProduct"/> + <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOrderGrid"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.currentPage}}" stepKey="waitCurrentPageNumberAppeares"/> + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="2" stepKey="seeOnSecondPageOrderGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndClose" stepKey="saveAndCloseProduct"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.currentPage}}" stepKey="waitCurrentPageNumberAppearesAfterProductEdit"/> + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="2" stepKey="seeOnSecondPageOrderGridAfterProductSaved"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml new file mode 100644 index 0000000000000..e03f5f71dbdcc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportCustomizableOptionToProductWithSKUTest"> + <annotations> + <features value="Catalog"/> + <title value="Import customizable options to a product with existing SKU"/> + <description value="Import customizable options to a product with existing SKU"/> + <stories value="Import customizable options"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15740"/> + <useCaseId value="MAGETWO-73157"/> + <skip> + <issueId value="MC-16313"/> + </skip> + <group value="catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <updateData createDataKey="createFirstProduct" entity="ProductWithFieldOptions" stepKey="updateProductCustomOptions" /> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductByName" stepKey="deleteSecondProduct"> + <argument name="product" value="$$createSecondProduct.name$$"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Change second product sku to first product sku--> + <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToSecondProductEditPage"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + + <!--Import customizable options and check--> + <actionGroup ref="ImportProductCustomizableOptions" stepKey="importProductCustomOptions"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkFirstOptionImport"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkSecondOptionImport"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + + <!--Save product and check sku changed message--> + <actionGroup ref="saveProductForm" stepKey="saveSecondProduct"/> + <waitForElementVisible selector="{{AdminMessagesSection.noticeMessage}}" stepKey="waitForSkuChangedMessage"/> + <see selector="{{AdminMessagesSection.noticeMessage}}" userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> + + <!-- Check that custom options are present on Admin product page in Customizable Option section after Product save --> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSectionAfterProductSave"/> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkFirstCustomOptionAfterProductSave"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkSecondCustomOptionAfterProductSave"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml new file mode 100644 index 0000000000000..0909856168fe2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminMoveAnchoredCategoryTest"> + <annotations> + <features value="Category Moving"/> + <title value="Move Anchored Category with Products"/> + <description value="You should be able to move a category via categories tree and made changes should be applied on frontend without forced cache cleaning"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76273"/> + <group value="category"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simpleSubCategoryOne"/> + <createData entity="SimpleSubCategory" stepKey="simpleSubCategoryTwo"/> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategoryWithParent"> + <requiredEntity createDataKey="simpleSubCategoryOne"/> + </createData> + <createData entity="_defaultProduct" stepKey="productOne"> + <requiredEntity createDataKey="simpleSubCategoryWithParent"/> + </createData> + <createData entity="_defaultProduct" stepKey="productTwo"> + <requiredEntity createDataKey="simpleSubCategoryOne"/> + </createData> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + <deleteData createDataKey="productOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="productTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="simpleSubCategoryWithParent" stepKey="deleteSubcategoryWithParent"/> + <deleteData createDataKey="simpleSubCategoryTwo" stepKey="deleteSubcategoryTwo"/> + </after> + <!--Move category one to category two--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategoryPage"/> + <waitForPageLoad stepKey="waitForAdminCategoryPageLoad1"/> + <actionGroup ref="MoveCategoryActionGroup" stepKey="moveSimpleSubCategoryOneToSimpleSubCategoryTwo"> + <argument name="childCategory" value="$$simpleSubCategoryOne.name$$"/> + <argument name="parentCategory" value="$$simpleSubCategoryTwo.name$$"/> + </actionGroup> + <!--Verify that navigation menu categories level is correct--> + <amOnPage url="/" stepKey="amOnStorefrontPage1"/> + <waitForPageLoad stepKey="waitForPageToLoadAfterHomePageOpened1"/> + <seeElement selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryTwo.name$$)}}" stepKey="verifyThatTopCategoryIsSubCategoryTwo"/> + <moveMouseOver selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryTwo.name$$)}}" stepKey="mouseOverSubCategoryTwo"/> + <waitForAjaxLoad stepKey="waitForAjaxOnMouseOverSubCategoryTwo"/> + <seeElement selector="{{StorefrontNavigationSection.subCategory($$simpleSubCategoryOne.name$$)}}" stepKey="verifyThatFirstLevelIsSubCategoryOne"/> + <moveMouseOver selector="{{StorefrontNavigationSection.subCategory($$simpleSubCategoryOne.name$$)}}" stepKey="mouseOverSubCategoryOne"/> + <waitForAjaxLoad stepKey="waitForAjaxOnMouseOverSubCategoryOne"/> + <seeElement selector="{{StorefrontNavigationSection.subCategory($$simpleSubCategoryWithParent.name$$)}}" stepKey="verifyThatSecondLevelIsSubCategoryWithParent1"/> + <!--Open category one via navigation menu. Verify that subcategory is shown in layered navigation--> + <click selector="{{StorefrontNavigationSection.subCategory($$simpleSubCategoryOne.name$$)}}" stepKey="openSimpleSubCategoryOneByNavigationMenu1"/> + <actionGroup ref="CheckItemInLayeredNavigationActionGroup" stepKey="verifySimpleSubCategoryWithParentInLayeredNavigation1"> + <argument name="itemType" value="Category"/> + <argument name="itemName" value="$$simpleSubCategoryWithParent.name$$"/> + </actionGroup> + <!--Open category one by direct URL. Verify simple product is visible on it. Open this product and perform assertions--> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openFirstProductFromSubCategoryOneCategoryPage1"> + <argument name="category" value="$$simpleSubCategoryOne$$"/> + <argument name="product" value="$$productOne$$"/> + </actionGroup> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbs1"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryTwo.name$$" stepKey="seeSubCategoryTwoInBreadcrumbsOnSubCategoryOne"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryOneInBreadcrumbsOnSubCategoryOne1"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productOne.name$$" stepKey="seeProductInBreadcrumbsOnSubCategoryOne1"/> + <!--Open category two by direct URL. Verify simple product is visible on it. Open this product and perform assertions--> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openFirstProductFromSubCategoryWithParentCategoryPage"> + <argument name="category" value="$$simpleSubCategoryWithParent$$"/> + <argument name="product" value="$$productOne$$"/> + </actionGroup> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbsOnSubCategoryWithParent"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryTwo.name$$" stepKey="seeSubCategoryTwoInBreadcrumbsOnSubCategoryWithParent"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryOneInBreadcrumbsOnSubCategoryWithParent"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryWithParentInBreadcrumbsOnSubCategoryWithParent"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productOne.name$$" stepKey="seeProductInBreadcrumbsOnSubCategoryWithParent"/> + <!--Move category one to the same level as category two--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategoryPage2"/> + <waitForPageLoad stepKey="waitForAdminCategoryPageLoad2"/> + <actionGroup ref="MoveCategoryActionGroup" stepKey="moveSimpleSubCategoryOneToDefaultCategory"> + <argument name="childCategory" value="$$simpleSubCategoryOne.name$$"/> + <argument name="parentCategory" value="Default Category"/> + </actionGroup> + <!--Verify that navigation menu categories level is correct--> + <amOnPage url="/" stepKey="amOnStorefrontPage2"/> + <waitForPageLoad stepKey="waitForPageToLoadAfterHomePageOpened2"/> + <seeElement selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryOne.name$$)}}" stepKey="verifyThatSubCategoryOneIsTopCategory"/> + <seeElement selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryTwo.name$$)}}" stepKey="verifyThatSubCategoryTwoIsTopCategory"/> + <moveMouseOver selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryOne.name$$)}}" stepKey="mouseOverTopSubCategoryOne"/> + <waitForAjaxLoad stepKey="waitForAjaxOnMouseOverTopSubCategoryOne"/> + <seeElement selector="{{StorefrontNavigationSection.subCategory($$simpleSubCategoryWithParent.name$$)}}" stepKey="verifyThatSecondLevelIsSubCategoryWithParent2"/> + <!--Open category one via navigation menu. Verify that subcategory is shown in layered navigation--> + <click selector="{{StorefrontNavigationSection.topCategory($$simpleSubCategoryOne.name$$)}}" stepKey="openSimpleSubCategoryOneByNavigationMenu2"/> + <actionGroup ref="CheckItemInLayeredNavigationActionGroup" stepKey="verifySimpleSubCategoryWithParentInLayeredNavigation2"> + <argument name="itemType" value="Category"/> + <argument name="itemName" value="$$simpleSubCategoryWithParent.name$$"/> + </actionGroup> + <!--Open category one by direct URL. Verify simple product is visible on it. Open this product and perform assertions--> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openFirstProductFromSubCategoryOneCategoryPage2"> + <argument name="category" value="$$simpleSubCategoryOne$$"/> + <argument name="product" value="$$productOne$$"/> + </actionGroup> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbs2"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryOneInBreadcrumbsOnSubCategoryOne2"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productOne.name$$" stepKey="seeProductInBreadcrumbsOnSubCategoryOne2"/> + <!--Open category subcategory by direct URL. Verify simple product is visible on it. Open this product and perform assertions--> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openFirstProductFromSubCategoryOneCategoryPage3"> + <argument name="category" value="$$simpleSubCategoryWithParent$$"/> + <argument name="product" value="$$productOne$$"/> + </actionGroup> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbs3"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryOneInBreadcrumbsOnSubCategoryOne3"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$simpleSubCategoryOne.name$$" stepKey="seeSubCategoryWithParentInBreadcrumbsOnSubCategoryWithParent3"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productOne.name$$" stepKey="seeProductInBreadcrumbsOnSubCategoryOne3"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml new file mode 100644 index 0000000000000..39fa355eea6ed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -0,0 +1,225 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveProductBetweenCategoriesTest"> + <annotations> + <stories value="Move Product"/> + <title value="Move Product between Categories (Cron is ON, 'Update by Schedule' Mode)"/> + <description value="Verifies correctness of showing data (products, categories) on Storefront after moving an anchored category in terms of products/categories association"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10141"/> + <group value="catalog"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + <createData entity="_defaultCategory" stepKey="createAnchoredCategory1"/> + <createData entity="_defaultCategory" stepKey="createSecondCategory"/> + + <!-- Switch "Category Product" and "Product Category" indexers to "Update by Schedule" mode --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> + <waitForPageLoad stepKey="waitForManagementPage"/> + + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> + <argument name="indexerValue" value="catalog_category_product"/> + </actionGroup> + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> + <argument name="indexerValue" value="catalog_product_category"/> + </actionGroup> + </before> + + <after> + <!-- Switch "Category Product" and "Product Category" indexers to "Update by Save" mode --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> + <waitForPageLoad stepKey="waitForManagementPage"/> + + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> + <argument name="indexerValue" value="catalog_category_product"/> + <argument name="action" value="Update on Save"/> + </actionGroup> + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> + <argument name="indexerValue" value="catalog_product_category"/> + <argument name="action" value="Update on Save"/> + </actionGroup> + + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createAnchoredCategory1" stepKey="deleteAnchoredCategory1"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create the anchored category <Cat1_anchored> --> + <actionGroup ref="AdminAnchorCategoryActionGroup" stepKey="anchorCategory"> + <argument name="categoryName" value="$$createAnchoredCategory1.name$$"/> + </actionGroup> + + <!-- Create subcategory <Sub1> of the anchored category --> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> + + <!-- Assign <product1> to the <Sub1> --> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct.id$$)}}" stepKey="goToProduct"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="activateDropDownCategory"/> + <fillField userInput="{{SimpleSubCategory.name}}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearch"/> + <waitForPageLoad stepKey="waitForSubCategory"/> + <click selector="{{AdminProductFormSection.selectCategory(SimpleSubCategory.name)}}" stepKey="selectSub1Category"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> + <waitForPageLoad stepKey="waitForApplyCategory"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSavingChanges"/> + + <!-- Enable `Use Categories Path for Product URLs` on Stores -> Configuration -> Catalog -> Catalog -> Search Engine Optimization --> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url('#catalog_seo-link')}}" stepKey="onConfigPage"/> + <uncheckOption selector="{{AdminCatalogSearchEngineConfigurationSection.systemValueUseCategoriesPath}}" stepKey="uncheckDefault"/> + <selectOption userInput="Yes" selector="{{AdminCatalogSearchEngineConfigurationSection.selectUseCategoriesPatForProductUrls}}" stepKey="selectYes"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the configuration." stepKey="seeMessage"/> + + <!-- Navigate to the Catalog > Products --> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="onCatalogProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!-- Click on <product1>: Product page opens--> + <actionGroup ref="filterProductGridByName" stepKey="filterProduct"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct($$simpleProduct.name$$)}}" stepKey="clickProduct1"/> + <waitForPageLoad stepKey="waitForProductLoad"/> + + <!-- Clear "Categories" field and assign the product to <Cat2> and save the product --> + <grabTextFrom selector="{{AdminProductFormSection.currentCategory}}" stepKey="grabNameSubCategory"/> + <click selector="{{AdminProductFormSection.unselectCategories(SimpleSubCategory.name)}}" stepKey="removeCategory"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> + <checkOption selector="{{AdminProductFormSection.selectCategory($$createSecondCategory.name$$)}}" stepKey="selectCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="pressButtonDone"/> + <waitForPageLoad stepKey="waitForApplyCategory2"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="pushButtonSave"/> + <waitForPageLoad stepKey="waitForSavingProduct"/> + + <!--Product is saved --> + <see userInput="You saved the product." selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron"/> + + <!-- Clear invalidated cache on System>Tools>Cache Management page --> + <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="onCachePage"/> + <waitForPageLoad stepKey="waitForCacheManagementPage"/> + + <checkOption selector="{{AdminCacheManagementSection.configurationCheckbox}}" stepKey="checkConfigCache"/> + <checkOption selector="{{AdminCacheManagementSection.pageCacheCheckbox}}" stepKey="checkPageCache"/> + + <selectOption userInput="Refresh" selector="{{AdminCacheManagementSection.massActionSelect}}" stepKey="selectRefresh"/> + <waitForElementVisible selector="{{AdminCacheManagementSection.massActionSubmit}}" stepKey="waitSubmitButton"/> + <click selector="{{AdminCacheManagementSection.massActionSubmit}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitForRefresh"/> + + <see userInput="2 cache type(s) refreshed." stepKey="seeCacheRefreshedMessage"/> + <actionGroup ref="logout" stepKey="logout"/> + + <!-- Open frontend --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontend"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + + <!-- Open <Cat2> from navigation menu --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategory.name$$)}}" stepKey="openCat2"/> + <waitForPageLoad stepKey="waitForCategory2Page"/> + + <!-- # <Cat 2> should open # <product1> should be present on the page --> + <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryName"/> + <see userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProduct"/> + + <!-- Open <product1> --> + <click selector="{{StorefrontCategoryMainSection.productLinkByHref($$simpleProduct.urlKey$$)}}" stepKey="openProduct"/> + <waitForPageLoad stepKey="waitForProductPageLoading"/> + + <!-- # Product page should open successfully # Breadcrumb for product should be like <Cat 2> --> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductName"/> + <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCategoryInBreadcrumbs"/> + + <!-- Open <Cat1_anchored> category --> + <click selector="{{StorefrontNavigationSection.topCategory($$createAnchoredCategory1.name$$)}}" stepKey="clickCat1"/> + <waitForPageLoad stepKey="waitForCategory1PageLoad"/> + + <!-- # Category should open successfully # <product1> should be absent on the page --> + <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategory1Name"/> + <see userInput="We can't find products matching the selection." stepKey="seeEmptyNotice"/> + <dontSee userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> + + <!-- Log in to the backend: Admin user is logged in--> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAdmin"/> + + <!-- Navigate to the Catalog > Products: Navigate to the Catalog>Products --> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductsPage"/> + + <!-- Click on <product1> --> + <actionGroup ref="filterAndSelectProduct" stepKey="openSimpleProduct"> + <argument name="productSku" value="$$simpleProduct.sku$$"/> + </actionGroup> + + <!-- Clear "Categories" field and assign the product to <Sub1> and save the product --> + <click selector="{{AdminProductFormSection.unselectCategories($$createSecondCategory.name$$)}}" stepKey="clearCategory"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="activateDropDown"/> + <fillField userInput="{$grabNameSubCategory}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearchField"/> + <waitForPageLoad stepKey="waitForSearchSubCategory"/> + <click selector="{{AdminProductFormSection.selectCategory({$grabNameSubCategory})}}" stepKey="selectSubCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickButtonDone"/> + <waitForPageLoad stepKey="waitForCategoryApply"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickButtonSave"/> + <waitForPageLoad stepKey="waitForSaveChanges"/> + + <!-- Product is saved successfully --> + <see userInput="You saved the product." selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSaveMessage"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron2"/> + + <!-- Open frontend --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="onFrontendPage"/> + <waitForPageLoad stepKey="waitForFrontPageLoad"/> + + <!-- Open <Cat2> from navigation menu --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategory.name$$)}}" stepKey="openSecondCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryPage"/> + + <!-- # <Cat 2> should open # <product1> should be absent on the page --> + <see userInput="$$createSecondCategory.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeSecondCategory1Name"/> + <dontSee userInput="$$simpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeSimpleProduct"/> + + <!-- Click on <Cat1_anchored> category --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createAnchoredCategory1.name$$)}}" stepKey="clickAnchoredCategory"/> + <waitForPageLoad stepKey="waitForAnchoredCategoryPage"/> + + <!-- # Category should open successfully # <product1> should be present on the page --> + <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="see1CategoryName"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductNameOnCategory1Page"/> + + <!-- Breadcrumb for product should be like <Cat1_anchored>/<product> (if you clicks from anchor category) --> + <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCat1inBreadcrumbs"/> + <dontSee userInput="{$grabNameSubCategory}" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="dontSeeSubCategoryInBreadCrumbs"/> + + <!-- <Cat1_anchored>/<Sub1>/<product> (if you clicks from Sub1 category) --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createAnchoredCategory1.name$$)}}" stepKey="hoverCategory1"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName({$grabNameSubCategory})}}" stepKey="clickSubCat"/> + <waitForPageLoad stepKey="waitForSubCategoryPageLoad"/> + + <see userInput="{$grabNameSubCategory}" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeSubCategoryName"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeProductNameOnSubCategoryPage"/> + + <see userInput="{$grabNameSubCategory}" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeSubCategoryInBreadcrumbs"/> + <see userInput="$$createAnchoredCategory1.name$$" selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="seeCat1InBreadcrumbs"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml new file mode 100644 index 0000000000000..2a761f192f29f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminMultipleWebsitesUseDefaultValuesTest"> + <annotations> + <title value="Use Default Value checkboxes should be checked for new website scope"/> + <description value="Use Default Value checkboxes for product attribute should be checked for new website scope"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92990"/> + <group value="catalog"/> + </annotations> + <after> + <deleteData url="V1/products/{{_defaultProduct.sku}}" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="Second Website"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStoreGroup"> + <argument name="website" value="Second Website"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStore"> + <argument name="storeGroup" value="secondStoreGroup"/> + <argument name="customStore" value="secondStore"/> + </actionGroup> + + <!--Create a Simple Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForProductsGridPageLoad"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <waitForPageLoad stepKey="waitForPageProductFormLoad"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillProductSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillProductPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillProductQuantity"/> + + <!-- Add product to second website and save the product --> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> + <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForProductSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + + <!-- switch to the second store view --> + <scrollToTopOfPage stepKey="scrollToPageTopToSeeStoreSwitcher"/> + <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> + <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> + <scrollToTopOfPage stepKey="scrollToPageTopToSeeStoreSwitcher2"/> + <waitForElementVisible selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="waitForStoreSwitcherToBeVisible"/> + <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + + <!-- Check if Use Default Value checkboxes are checked --> + <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="seeProductStatusCheckboxChecked"/> + <seeCheckboxIsChecked selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="seeProductNameCheckboxChecked"/> + <seeCheckboxIsChecked selector="{{AdminProductFormSection.productTaxClassUseDefault}}" stepKey="seeTaxClassCheckboxChecked"/> + <seeCheckboxIsChecked selector="{{AdminProductFormSection.visibilityUseDefault}}" stepKey="seeVisibilityCheckboxChecked"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml new file mode 100644 index 0000000000000..d920ed90c80c5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -0,0 +1,321 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductCategoryIndexerInUpdateOnScheduleModeTest"> + <annotations> + <stories value="Product Categories Indexer"/> + <title value="Product Categories Indexer in Update on Schedule mode"/> + <description value="Verifies that in Update on Schedule mode if displaying of category products on Storefront changes due to product properties change, + the changes are NOT applied immediately, but applied only after cron runs (twice)."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10023"/> + <group value="catalog"/> + <group value="indexer"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <!-- Create category A without products --> + <createData entity="_defaultCategory" stepKey="createCategoryA"/> + + <!-- Create product A1 not assigned to any category --> + <createData entity="SimpleProduct3" stepKey="createProductA1"/> + + <!-- Create anchor category B with subcategory C--> + <createData entity="_defaultCategory" stepKey="createCategoryB"/> + <createData entity="SubCategoryWithParent" stepKey="createCategoryC"> + <requiredEntity createDataKey="createCategoryB"/> + </createData> + + <!-- Assign product B1 to category B --> + <createData entity="ApiSimpleProduct" stepKey="createProductB1"> + <requiredEntity createDataKey="createCategoryB"/> + </createData> + + <!-- Assign product C1 to category C --> + <createData entity="ApiSimpleProduct" stepKey="createProductC1"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + + <!-- Assign product C2 to category C --> + <createData entity="ApiSimpleProduct" stepKey="createProductC2"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + + <!-- Switch indexers to "Update by Schedule" mode --> + <actionGroup ref="AdminSwitchAllIndexerToActionModeActionGroup" stepKey="onUpdateBySchedule"> + <argument name="action" value="Update by Schedule"/> + </actionGroup> + </before> + <after> + <!-- Switch indexers to "Update on Save" mode --> + <actionGroup ref="AdminSwitchAllIndexerToActionModeActionGroup" stepKey="onUpdateOnSave"> + <argument name="action" value="Update on Save"/> + </actionGroup> + <!-- Delete data --> + <deleteData createDataKey="createProductA1" stepKey="deleteProductA1"/> + <deleteData createDataKey="createProductB1" stepKey="deleteProductB1"/> + <deleteData createDataKey="createProductC1" stepKey="deleteProductC1"/> + <deleteData createDataKey="createProductC2" stepKey="deleteProductC2"/> + <deleteData createDataKey="createCategoryA" stepKey="deleteCategoryA"/> + <deleteData createDataKey="createCategoryC" stepKey="deleteCategoryC"/> + <deleteData createDataKey="createCategoryB" stepKey="deleteCategoryB"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Case: change product category from product page --> + <!-- 1. Open Admin > Catalog > Products > Product A1 --> + <amOnPage url="{{AdminProductEditPage.url($$createProductA1.id$$)}}" stepKey="goToProductA1"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- 2. Assign category A to product A1. Save product --> + <actionGroup ref="AdminAssignCategoryToProductAndSaveActionGroup" stepKey="assignProduct"> + <argument name="categoryName" value="$$createCategoryA.name$$"/> + </actionGroup> + + <!-- 3. Open category A on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryA"> + <argument name="categoryName" value="$$createCategoryA.name$$"/> + </actionGroup> + + <!-- The category is still empty --> + <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryA1Name"/> + <see userInput="We can't find products matching the selection." stepKey="seeEmptyNotice"/> + <dontSee userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProductA1"/> + + <!-- 4. Run cron to reindex --> + <wait time="60" stepKey="waitForChanges"/> + <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCLI command="cron:run" stepKey="runTwiceCron"/> + + <!-- 5. Open category A on Storefront again --> + <reloadPage stepKey="reloadCategoryA"/> + + <!-- Category A displays product A1 now --> + <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeTitleCategoryA1"/> + <see userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductA1"/> + + <!--6. Open Admin > Catalog > Products > Product A1. Unassign category A from product A1 --> + <amOnPage url="{{AdminProductEditPage.url($$createProductA1.id$$)}}" stepKey="OnPageProductA1"/> + <waitForPageLoad stepKey="waitForProductA1PageLoad"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryA"> + <argument name="categoryName" value="$$createCategoryA.name$$"/> + </actionGroup> + + <!-- 7. Open category A on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryA"> + <argument name="categoryName" value="$$createCategoryA.name$$"/> + </actionGroup> + + <!-- Category A still contains product A1 --> + <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryAOnPage"/> + <see userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductA1"/> + + <!-- 8. Run cron reindex --> + <wait time="60" stepKey="waitOneMinute"/> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runTwiceCron1"/> + + <!-- 9. Open category A on Storefront again --> + <reloadPage stepKey="refreshCategoryAPage"/> + + <!-- Category A is empty now --> + <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeOnPageCategoryAName"/> + <see userInput="We can't find products matching the selection." stepKey="seeOnPageEmptyNotice"/> + <dontSee userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProductA1OnPage"/> + + <!-- Case: change product status --> + <!-- 10. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays product B1, C1 and C2 --> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryBOnPage"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductB1"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductC2"/> + + <!-- 11. Open product C1 in Admin. Make it disabled (Enable Product = No)--> + <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="goToProductC1"/> + <waitForPageLoad stepKey="waitForProductC1PageLoad"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickOffEnableToggleAgain"/> + <!-- Saved successfully --> + <actionGroup ref="saveProductForm" stepKey="saveProductC1"/> + + <!-- 12. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="toCategoryBStorefront"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays product B1, C1 and C2 --> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="categoryBOnPage"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductB1"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC2"/> + + <!-- 13. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goToCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C still displays products C1 and C2 --> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="categoryCOnPage"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC1inCategoryC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC2InCategoryC2"/> + + <!-- 14. Run cron to reindex --> + <wait time="60" stepKey="waitMinute"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cron:run" stepKey="runTwiceCron2"/> + + <!-- 15. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="onPageCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays product B1 and C2 only--> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeTitleCategoryBOnPage"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryBPageProductB1"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryBPageProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryBPageProductC2"/> + + <!-- 16. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C displays only product C2 now --> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeTitleOnCategoryCPage"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryCPageProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCPageProductC2"/> + + <!-- 17. Repeat steps 10-16, but enable products instead. --> + <!-- 17.11 Open product C1 in Admin. Make it enabled --> + <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="goToEditProductC1"/> + <waitForPageLoad stepKey="waitForProductC1Page"/> + <scrollToTopOfPage stepKey="scrollTopOfProductPage"/> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickOnEnableToggleAgain"/> + + <!-- Saved successfully --> + <actionGroup ref="saveProductForm" stepKey="saveChangedProductC1"/> + + <!-- 17.12. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays product B1 and C2 --> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="titleCategoryBOnPage"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBPageProductB1"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryBPageProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBPageProductC2"/> + + <!-- 17.13. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openToCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C displays product C2 --> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="titleOnCategoryCPage"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryCPageProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryCPageProductC2"/> + + <!-- 17.14. Run cron to reindex --> + <wait time="60" stepKey="waitForOneMinute"/> + <magentoCLI command="cron:run" stepKey="runCron3"/> + <magentoCLI command="cron:run" stepKey="runTwiceCron3"/> + + <!-- 17.15. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openPageCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays products B1, C1, C2 again, but only after reindex. --> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="onPageSeeCategoryBTitle"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductB1"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryBProductC2"/> + + <!-- 17.16. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openOnStorefrontCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C displays products C1, C2 again, but only after reindex.--> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="onPageSeeCategoryCTitle"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryCProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="onPageSeeCategoryCProductC2"/> + + <!-- Case: change product visibility --> + <!-- 18. Repeat steps 10-17 but change product Visibility instead of product status --> + <!-- 18.11 Open product C1 in Admin. Make it enabled --> + <amOnPage url="{{AdminProductEditPage.url($$createProductC1.id$$)}}" stepKey="editProductC1"/> + <waitForPageLoad stepKey="waitProductC1Page"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="Search" + stepKey="changeVisibility"/> + + <!-- Saved successfully --> + <actionGroup ref="saveProductForm" stepKey="productC1Saved"/> + + <!-- 18.12. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goPageCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays products B1, C1, C2 again, but only after reindex. --> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryBTitle"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductB1"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryBProductC2"/> + + <!-- 18.13. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goPageCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C displays products C1, C2 again, but only after reindex.--> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeCategoryCTitle"/> + <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC2"/> + + <!-- 18.14. Run cron to reindex --> + <wait time="60" stepKey="waitExtraMinute"/> + <magentoCLI command="cron:run" stepKey="runCron4"/> + <magentoCLI command="cron:run" stepKey="runTwiceCron4"/> + + <!-- 18.15. Open category B on Storefront --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="navigateToPageCategoryB"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + </actionGroup> + + <!-- Category B displays product B1 and C2 only--> + <see userInput="$$createCategoryB.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeTitleCategoryB"/> + <see userInput="$$createProductB1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeTitleProductB1"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeCategoryBProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeTitleProductC2"/> + + <!-- 18.18. Open category C on Storefront --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="navigateToPageCategoryC"> + <argument name="categoryName" value="$$createCategoryB.name$$"/> + <argument name="subCategoryName" value="$$createCategoryC.name$$"/> + </actionGroup> + + <!-- Category C displays product C2 again, but only after reindex.--> + <see userInput="$$createCategoryC.name$$" selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="seeTitleCategoryC"/> + <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeOnCategoryCProductC1"/> + <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnPageTitleProductC2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml new file mode 100644 index 0000000000000..75d37d8baa931 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminProductGridFilteringByDateAttributeTest"> + <annotations> + <title value="Verify Set Product as new Filter input on Product Grid doesn't reset to currentDate"/> + <description value="Data input in the new from date filter field should not change"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92228"/> + <group value="product"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleProductWithNewFromDate" stepKey="createSimpleProductWithDate"/> + </before> + <after> + <deleteData createDataKey="createSimpleProductWithDate" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{AdminProductAttributeGridSection.resetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" userInput="Set Product as New from Date" stepKey="setAttributeLabel"/> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="searchForAttributeFromGrid"/> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="wait2"/> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="openAdvancedPropertiesTab"/> + <selectOption selector="#is_filterable_in_grid" userInput="Yes" stepKey="isFilterableInGrid"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductGridPageLoad"/> + <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="openColumnsdropDown1"/> + <checkOption selector="{{AdminProductGridFilterSection.viewColumnOption('Set Product as New from Date')}}" stepKey="showProductAsNewColumn"/> + <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="closeColumnsDropdown1"/> + <seeElement selector="{{AdminProductGridSection.columnHeader('Set Product as New from Date')}}" stepKey="seeNewFromDateColumn"/> + <waitForPageLoad stepKey="waitforFiltersToApply"/> + <actionGroup ref="filterProductGridBySetNewFromDate" stepKey="filterProductGridToCheckSetAsNewColumn"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnFirstRowProductGrid"/> + <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AdminFormSaveAndClose" stepKey="saveAndCloseProductForm"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="expandFilters"/> + <seeInField selector="{{AdminProductGridFilterSection.newFromDateFilter}}" userInput="05/16/2018" stepKey="checkForNewFromDate"/> + <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="openColumnsDropdown2"/> + <uncheckOption selector="{{AdminProductGridFilterSection.viewColumnOption('Set Product as New from Date')}}" stepKey="hideProductAsNewColumn"/> + <dontSeeElement selector="{{AdminProductGridSection.columnHeader('Set Product as New from Date')}}" stepKey="dontSeeNewFromDateColumn"/> + <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="closeColumnsDropdown2"/> + <click selector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearGridFilters"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml new file mode 100644 index 0000000000000..67243407344af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductImageAssignmentForMultipleStoresTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product image assignment for multiple stores"/> + <title value="Product image assignment for multiple stores"/> + <description value="Product image assignment for multiple stores"/> + <severity value="MAJOR"/> + <testCaseId value="MC-9368"/> + <group value="product"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <!-- Create Category and Simple Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + <!-- Login Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create Store View English --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createEnglishStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!--Create Store View France --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFrenchStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + </before> + <after> + <!-- Delete Category and Simple Product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Clear Filter Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductFilters"/> + <!-- Delete Store View English --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteEnglishStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!-- Delete Store View France --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFrenchStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <!-- Clear Filter Store --> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetFiltersOnStorePage"/> + <!-- Logout Admin --> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Search Product and Open Edit --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!-- Switch to the English store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchProductStoreViewToEnglish"> + <argument name="scopeName" value="customStoreEN.name"/> + </actionGroup> + <!-- Upload Image English --> + <actionGroup ref="addProductImage" stepKey="uploadImageForEnglishStoreView"/> + <actionGroup ref="saveProductForm" stepKey="saveProductImageForEnglishStoreView"/> + <!-- Switch to the French store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchProductStoreViewToFrench"> + <argument name="scopeName" value="customStoreFR.name"/> + </actionGroup> + <!-- Upload Image French --> + <actionGroup ref="addProductImage" stepKey="uploadImageForFrenchStoreView"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRoleForFrenchStoreView"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductImageForFrenchStoreView"/> + <!-- Switch to the All store view --> + <actionGroup ref="AdminSwitchToAllStoreViewActionGroup" stepKey="switchProductToAllStoreView"> + <argument name="scopeName" value="AllStoreViews.name"/> + </actionGroup> + <!-- Upload Image All Store View --> + <actionGroup ref="addProductImage" stepKey="uploadImageOnAllStoreView"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRoleForAllStoreView"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <!-- Change any product data product description --> + <actionGroup ref="AdminChangeProductDescriptionActionGroup" stepKey="changeProductDescription"> + <argument name="description" value="This is the long description"/> + </actionGroup> + <actionGroup ref="AdminChangeProductShortDescriptionActionGroup" stepKey="changeProductShortDescription"> + <argument name="description" value="This is the short description"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductAfterUploadImageOnAllStoreView"/> + <!-- Go to Product Page and see Default Store View--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToStorefrontProductPageOnDefaultStoreView"/> + <actionGroup ref="AssertStorefrontActiveImageProductActionGroup" stepKey="seeActiveImageOnDefaultStoreView"> + <argument name="image" value="{{TestImageNew.filename}}"/> + </actionGroup> + <!-- English Switch Store View and see English Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewToEnglish"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AssertStorefrontActiveImageCategoryActionGroup" stepKey="seeImageOnEnglishStoreView"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="image" value="{{ProductImage.filename}}"/> + </actionGroup> + <click selector="{{StorefrontCategoryProductSection.productTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad time="30" stepKey="waitForProductPage"/> + <actionGroup ref="AssertStorefrontActiveImageProductActionGroup" stepKey="seeActiveImagesInEnglishStoreView"> + <argument name="image" value="{{ProductImage.filename}}"/> + </actionGroup> + <!-- Switch France Store View and see France Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewToFrench"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="AssertStorefrontActiveImageCategoryActionGroup" stepKey="seeImageInFrenchStoreView"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="image" value="{{Magento3.filename}}"/> + </actionGroup> + <click selector="{{StorefrontCategoryProductSection.productTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPageAfterSwitchToFrenchStoreView"/> + <waitForPageLoad time="30" stepKey="waitForProductPage1"/> + <actionGroup ref="AssertStorefrontActiveImageProductActionGroup" stepKey="seeActiveImageToFrenchStoreView"> + <argument name="image" value="{{Magento3.filename}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml new file mode 100644 index 0000000000000..2ae817c732434 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminProductStatusAttributeDisabledByDefaultTest"> + <annotations> + <title value="Verify the default option value for product Status attribute is set correctly during product creation"/> + <description value="The default option value for product Status attribute is set correctly during product creation"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92838"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{AdminProductAttributeGridSection.resetFilter}}" stepKey="resetFiltersOnGrid1"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel1"/> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="searchForAttributeFromTheGrid1"/> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="clickOnAttributeRow1"/> + <waitForPageLoad stepKey="wait2"/> + <click selector="{{AdminNewAttributePanelSection.isDefault('1')}}" stepKey="resetOptionForStatusAttribute"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute1"/> + <waitForPageLoad stepKey="waitForSaveAttribute1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache1"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> + <waitForPageLoad stepKey="wait1"/> + <click selector="{{AdminProductAttributeGridSection.resetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel"/> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="wait2"/> + <click selector="{{AdminNewAttributePanelSection.isDefault('2')}}" stepKey="chooseDisabledOptionForStatus"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductGridPageToLoad"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickOnAddSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductEditToLoad"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="dontSeeCheckboxEnableProductIsChecked"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml new file mode 100644 index 0000000000000..197de39ead756 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminRemoveImageAffectsAllScopesTest"> + <annotations> + <features value="Catalog"/> + <stories value="MAGETWO-73449: Changes in default scope not effect product images in other scopes"/> + <title value="Effect of product images changes in default scope to other scopes"/> + <description value="Product image should be deleted from all scopes"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95344"/> + <group value="catalog"/> + </annotations> + <before> + <!-- login to admin, create default category and product --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create first custom website, store, store view --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{CustomWebSite.name}}"/> + <argument name="websiteCode" value="{{CustomWebSite.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createStore"> + <argument name="website" value="{{CustomWebSite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="storeGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!-- Create second custom website, store, store view --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{CustomWebSite.name}}"/> + </actionGroup> + + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <deleteData createDataKey="createCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteFirstProduct"/> + + <!-- Open product index page, clear filters and change gridview to default view --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + <actionGroup ref="resetAdminDataGridToDefaultView" stepKey="resetAdminDataGridToDefaultView"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Add image to product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProduct"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Add second image to product --> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!--"Product in Websites": select both Websites--> + <actionGroup ref="ProductSetWebsite" stepKey="productSetWebsite1"> + <argument name="website" value="CustomWebSite"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="productSetWebsite2"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + + <!--Open created product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!--Delete Image 1--> + <actionGroup ref="RemoveProductImage" stepKey="removeProductImage"/> + + <!--Click "Save" in the upper right corner--> + <actionGroup ref="saveProductForm" stepKey="saveProductFormAfterRemove"/> + + <!--Switch to "Store view 1"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectStoreView"> + <argument name="storeViewName" value="customStore"/> + </actionGroup> + + <!-- Assert product first image not in admin product form --> + <actionGroup ref="AssertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInAdminProductPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!--Switch to "Store view 2"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> + <argument name="storeViewName" value="SecondStoreUnique"/> + </actionGroup> + + <!-- Verify that Image 1 is deleted from the Second Store View list --> + <actionGroup ref="AssertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInSecondStoreViewPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml new file mode 100644 index 0000000000000..532ef76121857 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminRequiredFieldsHaveRequiredFieldIndicatorTest"> + <annotations> + <stories value="MAGETWO-73342: Clicking on area around the label of a toggle element results in the element's state being changed"/> + <title value="Required fields should have the required asterisk indicator "/> + <description value="Verify that Required fields should have the required indicator icon next to the field name"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97037"/> + <group value="catalog"/> + <group value="cms"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + + <!-- Verify that the Category Name field has the required field name indicator --> + <executeJS function="{{AdminCategoryBasicFieldSection.requiredFieldIndicator}}" stepKey="getRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="getRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator"/> + + <executeJS function="{{AdminCategoryBasicFieldSection.requiredFieldIndicatorColor}}" stepKey="getRequiredFieldIndicatorColor"/> + <assertEquals expected="rgb(226, 38, 38)" expectedType="string" actualType="variable" actual="getRequiredFieldIndicatorColor" stepKey="assertRequiredFieldIndicator1"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndexPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="addProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="addSimpleProduct"/> + + <!-- Verify that the Product Name and Sku fields have the required field name indicator --> + <executeJS function="{{AdminProductFormSection.requiredNameIndicator}}" stepKey="productNameRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="productNameRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator2"/> + <executeJS function="{{AdminProductFormSection.requiredSkuIndicator}}" stepKey="productSkuRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="productSkuRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator3"/> + + <!-- Verify that the CMS page have the required field name indicator next to Page Title --> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> + <click selector="{{CmsPagesPageActionsSection.addNewPage}}" stepKey="clickAddNewPage"/> + <executeJS function="{{CmsNewPagePageBasicFieldsSection.requiredFieldIndicator}}" stepKey="pageTitleRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="pageTitleRequiredFieldIndicator" stepKey="assertRequiredFieldIndicator4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml new file mode 100644 index 0000000000000..6a82df0a27385 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product attributes"/> + <title value="Saving configurable product with custom product attribute (images as swatches)"/> + <description value="Saving configurable product with custom product attribute (images as swatches)"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-17552"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Create a new product attribute: set Catalog Input Type for Store Owner: Visual Swatch --> + <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillAttributeProperties"> + <argument name="attributeCode" value="{{productVisualSwatchAttribute.attribute_code}}"/> + <argument name="attributeType" value="{{productVisualSwatchAttribute.frontend_input}}"/> + </actionGroup> + + <!-- Add a few Swatches and add images to Manage Swatch (Values of Your Attribute) + 1. Set swatch #1 using the color picker --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddFirstSwatch"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickFirstSwatch"> + <argument name="index" value="0"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.chooseColorRow('1')}}" stepKey="clickChooseFirstColor"/> + <actionGroup ref="setColorPickerValueByHex" stepKey="fillFirstHex"> + <argument name="nthColorPicker" value="1"/> + <argument name="hexColor" value="e74c3c"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="red" stepKey="fillFirstAdminField"/> + + <!-- 2. Set swatch #2 using the color picker --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSecondSwatch"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSecondSwatch"> + <argument name="index" value="1"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.chooseColorRow('2')}}" stepKey="clickChooseSecondColor"/> + <actionGroup ref="setColorPickerValueByHex" stepKey="fillSecondHex"> + <argument name="nthColorPicker" value="2"/> + <argument name="hexColor" value="3498db"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="blue" stepKey="fillSecondAdminField"/> + + <!-- Set Scope: Global in Advanced Attribute Properties --> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.scope}}" userInput="1" stepKey="selectGlobalScope"/> + + <!-- Click "Save Attribute" button --> + <click selector="{{AttributePropertiesSection.saveAndEdit}}" stepKey="clickSaveAndEdit"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + + <!-- Add created product attribute to the Default set --> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="productVisualSwatchAttribute.attribute_code"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Create configurable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openOpenProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateConfigurableProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + + <!-- Select Attribute Set --> + <actionGroup ref="AssignProductToAttributeSet" stepKey="selectAttributeSet"> + <argument name="attributeSetName" value="Default"/> + </actionGroup> + + <!-- Fill all the necessary information such as weight, name, SKU etc --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + + <!-- Click "Create Configurations" button, select created product attribute using the same Quantity for all products. Click "Generate products" button --> + <actionGroup ref="AdminGenerateProductConfigurationsByAttributeCodeActionGroup" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{productVisualSwatchAttribute.attribute_code}}"/> + </actionGroup> + + <!-- Using this action to concatenate 2 strings to have unique identifier for grid --> + <executeJS function="return '{{productVisualSwatchAttribute.attribute_code}}: red'" stepKey="attributeCodeRed"/> + <executeJS function="return '{{productVisualSwatchAttribute.attribute_code}}: blue'" stepKey="attributeCodeBlue"/> + + <!-- Add images for the products --> + <attachFile selector="{{AdminDataGridTableSection.rowTemplate({$attributeCodeRed})}}{{AdminProductFormConfigurationsSection.fileUploaderInput}}" userInput="{{MagentoLogo.file}}" stepKey="uploadImageForFirstProduct"/> + <attachFile selector="{{AdminDataGridTableSection.rowTemplate({$attributeCodeBlue})}}{{AdminProductFormConfigurationsSection.fileUploaderInput}}" userInput="{{ProductImage.file}}" stepKey="uploadImageForSecondProduct"/> + + <!-- Click "Save" button --> + <actionGroup ref="saveProductForm" stepKey="clickSaveButton"/> + + <!-- Delete all created product --> + <actionGroup ref="AdminDeleteProductBySkuActionGroup" stepKey="deleteCreatedProducts"> + <argument name="sku" value="{{BaseConfigurableProduct.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="productVisualSwatchAttribute"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml new file mode 100644 index 0000000000000..19b03b4bdc06d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductImagesTest"> + <annotations> + <features value="Catalog"/> + <stories value="Add/remove images and videos for all product types and category"/> + <title value="Admin should be able to add images of different types and sizes to Simple Product"/> + <description value="Admin should be able to add images of different types and sizes to Simple Product"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-76315"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="_defaultProduct" stepKey="secondProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- Go to the first product edit page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend1"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductOnBackend1"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + + <!-- Set url key --> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$firstProduct.name$$" stepKey="fillUrlKey"/> + + <click selector="{{AdminProductImagesSection.productImagesToggle}}" stepKey="expandImages"/> + + <!-- *.bmp is not allowed --> + <actionGroup ref="AdminProductCheckUnsupportedFileActionGroup" stepKey="attachBmp"> + <argument name="filename" value="bmp.bmp"/> + </actionGroup> + + <!-- *.ico is not allowed --> + <actionGroup ref="AdminProductCheckUnsupportedFileActionGroup" stepKey="attachIco"> + <argument name="filename" value="ico.ico"/> + </actionGroup> + + <!-- *.svg is not allowed --> + <actionGroup ref="AdminProductCheckUnsupportedFileActionGroup" stepKey="attachSvg"> + <argument name="filename" value="svg.svg"/> + </actionGroup> + + + <!-- 0kb size is not allowed --> + <actionGroup ref="AdminProductCheckUnsupportedFileActionGroup" stepKey="attachEmpty"> + <argument name="filename" value="empty.jpg"/> + </actionGroup> + + <!-- 1~ kb is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="small.jpg" stepKey="attachSmall"/> + <waitForPageLoad stepKey="waitForUploadSmall"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorSmall"/> + + <!-- 1~ mb is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="medium.jpg" stepKey="attachMedium"/> + <waitForPageLoad stepKey="waitForUploadMedium"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorMedium"/> + + <!-- 10~ mb is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="large.jpg" stepKey="attachLarge"/> + <waitForPageLoad stepKey="waitForUploadLarge"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorLarge"/> + + <!-- *.gif is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="gif.gif" stepKey="attachGif"/> + <waitForPageLoad stepKey="waitForUploadGif"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorGif"/> + + <!-- *.jpg is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="jpg.jpg" stepKey="attachJpg"/> + <waitForPageLoad stepKey="waitForUploadJpg"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorJpg"/> + + <!-- *.png is allowed --> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="png.png" stepKey="attachPng"/> + <waitForPageLoad stepKey="waitForUploadPng"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorPng"/> + + <!-- Save the first product and go to the storefront --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <amOnPage url="{{StorefrontProductPage.url($$firstProduct.name$$)}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront1"/> + + <!-- See all of the images that we uploaded --> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('small')}}" stepKey="seeSmall"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('medium')}}" stepKey="seeMedium"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('gif')}}" stepKey="seeGif"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('jpg')}}" stepKey="seeJpg"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('png')}}" stepKey="seePng"/> + + <!-- Go to the category page and see a placeholder image for the second product --> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.categoryPageProductImagePlaceholderSmall}}" stepKey="seePlaceholder"/> + + <!-- Go to the second product edit page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex2"/> + <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid2"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend2"> + <argument name="product" value="$$secondProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductOnBackend2"> + <argument name="product" value="$$secondProduct$$"/> + </actionGroup> + + <!-- Upload an image --> + <click selector="{{AdminProductImagesSection.productImagesToggle}}" stepKey="expandImages2"/> + <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="large.jpg" stepKey="attachLarge2"/> + <waitForPageLoad stepKey="waitForUploadLarge2"/> + <dontSeeElement selector="{{AdminConfirmationModalSection.message}}" stepKey="dontSeeErrorLarge2"/> + + <!-- Set url key --> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection2"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$secondProduct.name$$" stepKey="fillUrlKey2"/> + + <!-- Save the second product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + + <!-- Go to the admin grid and see the uploaded image --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> + <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid3"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend3"> + <argument name="product" value="$$secondProduct$$"/> + </actionGroup> + <seeElement selector="{{AdminProductGridSection.adminImgGridThumbnail('large')}}" stepKey="seeImgInGrid"/> + + <!-- Go to the category page and see the uploaded image --> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage2"/> + <waitForPageLoad stepKey="waitForStorefront2"/> + <seeElement selector="{{StorefrontCategoryMainSection.categoryPageProductImage('large')}}" stepKey="seeUploadedImg"/> + + <!-- Go to the product page and see the uploaded image --> + <amOnPage url="{{StorefrontProductPage.url($$secondProduct.name$$)}}" stepKey="goToStorefront2"/> + <waitForPageLoad stepKey="waitForStorefront3"/> + <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml new file mode 100644 index 0000000000000..bfa22e94ccbed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminUnassignProductAttributeFromAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Add/Update attribute set"/> + <title value="Assign/Unassign attributes to/from Attribute Set"/> + <description value="Assign/Unassign attributes to/from Attribute Set"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76299"/> + <group value="catalog"/> + <group value="attributeSet"/> + </annotations> + <before> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="attribute"/> + + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <createData entity="ApiProductWithDescription" stepKey="product"/> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Assert attribute presence in storefront product additional information --> + <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage1"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="CheckAttributeInAdditionalInformationTabActionGroup" stepKey="checkAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="$$attribute.attribute[frontend_labels][0][label]$$"/> + <argument name="attributeValue" value="$$option2.option[store_labels][0][label]$$"/> + </actionGroup> + <!-- Go to default attribute set edit page --> + <amOnPage url="{{AdminProductAttributeSetEditPage.url(AddToDefaultSet.attributeSetId)}}" stepKey="onAttributeSetEdit"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <!-- Assert created attribute in a group --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <!-- Unassign attribute from group --> + <actionGroup ref="UnassignAttributeFromGroup" stepKey="unAssignAttributeFromGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!-- Assert attribute in unassigned section --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> + <!-- Save attribute set --> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + <!-- Go to create new product page --> + <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad3"/> + <!-- Assert attribute not present in product creation --> + <dontSeeElement selector="{{AdminProductFormSection.attributeLabelByText($$attribute.attribute[frontend_labels][0][label]$$)}}" stepKey="dontSeeAttributeLabel"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + <!-- Assert removed attribute not presence in storefront product additional information --> + <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage2"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <actionGroup ref="CheckAttributeNotInAdditionalInformationTabActionGroup" stepKey="checkAttributeNotInMoreInformationTab"> + <argument name="attributeLabel" value="$$attribute.attribute[frontend_labels][0][label]$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml new file mode 100644 index 0000000000000..2e82f89b6fb87 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryStoreUrlKeyTest"> + <annotations> + <features value="SEO-friendly URL Key Update"/> + <stories value="Update SEO-friendly URL via the Admin"/> + <title value="SEO-friendly URL should update regardless of scope or redirect change."/> + <description value="SEO-friendly URL should update regardless of scope or redirect change."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-92916"/> + <group value="category"/> + </annotations> + <before> + <!-- Create category --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="CreateCategory" stepKey="createCategory"> + <argument name="categoryEntity" value="_defaultCategory"/> + </actionGroup> + </before> + <after> + <!-- Delete category and logout from admin account --> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="_defaultCategory"/> + </actionGroup> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!--Switch to "Default Store View" scope--> + <actionGroup ref="switchCategoryStoreView" stepKey="SwitchStoreView"> + <argument name="store" value="_defaultStore.name"/> + <argument name="catName" value="_defaultCategory.name"/> + </actionGroup> + <!--See "Use Default Value" checkboxes--> + <seeElement selector="{{AdminCategoryBasicFieldSection.enableUseDefault}}" stepKey="seeUseDefaultEnable"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.includeInMenuUseDefault}}" stepKey="seeUseDefaultMenu"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="seeUseDefaultName"/> + <!-- Update SEO key, uncheck "Create Redirect", confirm in frontend --> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <uncheckOption selector="{{AdminCategorySEOSection.urlKeyDefaultValueCheckbox}}" stepKey="uncheckUseDefaultUrlKey"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}-hattest" stepKey="enterURLKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.urlKeyRedirectCheckbox}}" stepKey="uncheckRedirect1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + <amOnPage url="" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForFrontendLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="clickCategory"/> + <see selector="{{StorefrontCategoryMainSection.categoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="assertCategoryOnStorefront"/> + <seeInTitle userInput="{{_defaultCategory.name}}" stepKey="seeCategoryNameInTitle"/> + <seeInCurrentUrl stepKey="verifyUrlKey" url="{{_defaultCategory.name_lwr}}-hattest.html"/> + <!-- Update SEO key to original, uncheck "Create Redirect", confirm in frontend --> + <!--Switch to "Default Store View" scope--> + <actionGroup ref="switchCategoryStoreView" stepKey="SwitchStoreView2"> + <argument name="store" value="_defaultStore.name"/> + <argument name="catName" value="_defaultCategory.name"/> + </actionGroup> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}" stepKey="enterOriginalURLKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.urlKeyRedirectCheckbox}}" stepKey="uncheckRedirect2"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterOriginalSeoKey"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterOriginalSeoKey"/> + <amOnPage url="" stepKey="goToStorefrontAfterOriginalSeoKey"/> + <waitForPageLoad stepKey="waitForFrontendLoadAfterOriginalSeoKey"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="clickCategoryAfterOriginalSeoKey"/> + <see selector="{{StorefrontCategoryMainSection.categoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="assertCategoryOnStorefront2"/> + <seeInTitle userInput="{{_defaultCategory.name}}" stepKey="seeCategoryNameInTitle2"/> + <seeInCurrentUrl url="{{_defaultCategory.name_lwr}}.html" stepKey="verifyUrlKeyAfterOriginalSeoKey"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml new file mode 100644 index 0000000000000..8555615cc8781 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateApplyingTierPriceWithEmptyDiscountValueTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminValidateApplyingTierPriceWithEmptyDiscountValueTest"> + <annotations> + <features value="Apply tier price"/> + <title value="Apply Tier Price with empty discount value"/> + <description value="Validate applying tier price to product"/> + <stories value="Apply Tier Price with empty discount value" /> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-96484"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupPrice"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType"/> + <clearField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="clearPercentageValueField"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <see selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageError}}" userInput="This is a required field." stepKey="assertPercentageError"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="10" stepKey="setPercentageValue"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <dontSee selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageError}}" userInput="This is a required field." stepKey="assertNoPercentageError"/> + <actionGroup ref="SaveProductOnProductPageOnAdmin" stepKey="saveProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml new file mode 100644 index 0000000000000..fd364682011c6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -0,0 +1,342 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="CheckTierPricingOfProductsTest"> + <annotations> + <features value="Shopping Cart"/> + <stories value="MAGETWO-88254 - [Magento Cloud] 'Tier Pricing' of Products changes to 'Price' (without discount) after Updated Items and Quantities in the Order of B2B Store View."/> + <title value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> + <description value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> + <testCaseId value="MAGETWO-95117"/> + <severity value="CRITICAL"/> + <group value="tierPrice"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <field key="price">123.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct2"> + <field key="price">123.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct3"> + <field key="price">123.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct4"> + <field key="price">123.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + + <!--Set Configuration--> + <createData entity="CatalogPriceScopeWebsite" stepKey="setPriceScopePerWebsite"/> + + <!--Set advanced pricing for all 4 products--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct1"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct1ForEdit"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="setWebsiteForProduct1"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct1"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct2ForEdit"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="setWebsiteForProduct2"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct2"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct3"> + <argument name="product" value="$$createProduct3$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct3ForEdit"> + <argument name="product" value="$$createProduct3$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="setWebsiteForProduct3"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct3"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct4"> + <argument name="product" value="$$createProduct4$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct4ForEdit"> + <argument name="product" value="$$createProduct4$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="setWebsiteForProduct4"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct4"> + <argument name="website" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <!--Flush cache--> + <magentoCLI command="cache:flush" stepKey="cleanCache"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductsIndexPageToLoad"/> + <actionGroup ref="resetAdminDataGridToDefaultView" stepKey="resetProductsGrid"/> + + <!--Edit customer info--> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerFrom"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="openCustomerAccountInformationSection"/> + <waitForPageLoad stepKey="waitCustomerAccountInformationSectionOpened"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="setCustomerGroup"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{SecondStoreUnique.name}}" stepKey="selectCustomerStore"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCustomer"/> + <waitForPageLoad stepKey="waitForCustomerPageLoadAfterSave"/> + <see userInput="You saved the customer." stepKey="checkCustomerIsSaved"/> + + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomersIndex"/> + <waitForPageLoad stepKey="waitForCustomersIndexPageToLoad"/> + <actionGroup ref="resetAdminDataGridToDefaultView" stepKey="resetCustomersGrid"/> + + <!--Create Cart Price Rule--> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="openCartPriceRulesGrid"/> + <waitForPageLoad stepKey="waitForCartPriceRulesGrid"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <waitForPageLoad stepKey="waitForNewCartPriceRulePageIsLoaded"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="ship" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{SecondWebsite.name}}" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="Retailer" stepKey="selectCustomerGroup"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ship" stepKey="setCode"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCustomer}}" userInput="0" stepKey="setUserPerCustomer"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="0" stepKey="setUserPerCoupon"/> + <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="0" stepKey="setPriority"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.freeShipping}}" userInput="For shipment with matching items" stepKey="selectFreeShippingType"/> + <click selector="{{AdminCartPriceRulesFormSection.saveAndContinue}}" stepKey="clickSaveAndContinueButton"/> + <waitForPageLoad stepKey="waitForCartPriceRuleSaved"/> + <see userInput="You saved the rule." stepKey="checkRuleSaved"/> + + <!--Create new order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="storeView" value="SecondStoreUnique"/> + </actionGroup> + + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> + <waitForPageLoad stepKey="waitForProductsOpened"/> + <!--TEST CASE #1--> + <!--Add 3 products to order with specified quantity--> + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct1.name$$)}}" stepKey="selectProduct1"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct1.name$$)}}" userInput="10" stepKey="addProductQuantity1"/> + + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct2.name$$)}}" stepKey="selectProduct2"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct2.name$$)}}" userInput="10" stepKey="addProductQuantity2"/> + + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct3.name$$)}}" stepKey="selectProduct3"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct3.name$$)}}" userInput="10" stepKey="addProductQuantity3"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addProductsToOrder"/> + <waitForLoadingMaskToDisappear stepKey="wait"/> + <!--Verify tier price values--> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct1.name$$)}}" stepKey="checkProductPrice1"/> + <assertEquals stepKey="verifyPrice1"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice1</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct2.name$$)}}" stepKey="checkProductPrice2"/> + <assertEquals stepKey="verifyPrice2"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice2</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct3.name$$)}}" stepKey="checkProductPrice3"/> + <assertEquals stepKey="verifyPrice3"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice3</actualResult> + </assertEquals> + + <!--Edit order and verify values--> + <waitForPageLoad stepKey="waitForPageLoaded2"/> + <click selector="{{AdminOrderFormItemsSection.customPrice($$createProduct1.name$$)}}" stepKey="clickOnCustomPrice"/> + <fillField selector="{{AdminOrderFormItemsSection.customQuantity($$createProduct1.name$$)}}" userInput="5" stepKey="clickOnQuantity"/> + <waitForLoadingMaskToDisappear stepKey="wait1"/> + <click selector="{{AdminOrderFormItemsSection.update}}" stepKey="clickToUpdate"/> + <waitForLoadingMaskToDisappear stepKey="wait2"/> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct1.name$$)}}" stepKey="checkProductPrice4"/> + <assertEquals stepKey="verifyPrice4"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice2}}</expectedResult> + <actualResult type="variable">$checkProductPrice4</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct2.name$$)}}" stepKey="checkProductPrice5"/> + <assertEquals stepKey="verifyPrice5"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice5</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct3.name$$)}}" stepKey="checkProductPrice6"/> + <assertEquals stepKey="verifyPrice6"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice3</actualResult> + </assertEquals> + + <!--Remove products from order--> + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct1.name$$)}}" userInput="Remove" stepKey="clickToRemove1"/> + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct2.name$$)}}" userInput="Remove" stepKey="clickToRemove2"/> + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct3.name$$)}}" userInput="Remove" stepKey="clickToRemove3"/> + <waitForLoadingMaskToDisappear stepKey="wait3"/> + <click selector="{{AdminOrderFormItemsSection.update}}" stepKey="clickToUpdate1"/> + <waitForPageLoad stepKey="waitProductsDeleted"/> + + <!--TEST CASE #2--> + <!--Add 3 products to order with specified quantity--> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct1"/> + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct1.name$$)}}" stepKey="selectProduct5"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct1.name$$)}}" userInput="10" stepKey="addProductQuantity5"/> + + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct2.name$$)}}" stepKey="selectProduct6"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct2.name$$)}}" userInput="10" stepKey="addProductQuantity6"/> + + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct3.name$$)}}" stepKey="selectProduct7"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct3.name$$)}}" userInput="10" stepKey="addProductQuantity7"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addProductsToOrder1"/> + <waitForLoadingMaskToDisappear stepKey="waitProductsAddedToOrder"/> + <!--Verify tier price values--> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct1.name$$)}}" stepKey="checkProductPrice7"/> + <assertEquals stepKey="verifyPrice7"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice7</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct2.name$$)}}" stepKey="checkProductPrice8"/> + <assertEquals stepKey="verifyPrice8"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice8</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct3.name$$)}}" stepKey="checkProductPrice9"/> + <assertEquals stepKey="verifyPrice9"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice9</actualResult> + </assertEquals> + + <!--Add one more product and verify values--> + <waitForPageLoad stepKey="waitForAllBlocksOnPageLoaded"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddcreateProduct2"/> + <waitForLoadingMaskToDisappear stepKey="wait8"/> + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct4.name$$)}}" stepKey="selectProduct8"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct4.name$$)}}" userInput="10" stepKey="addProductQuantity9"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addProductsToOrder2"/> + <waitForLoadingMaskToDisappear stepKey="wait9"/> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct4.name$$)}}" stepKey="checkProductPrice10"/> + <assertEquals stepKey="verifyPrice10"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice10</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct1.name$$)}}" stepKey="checkProductPrice11"/> + <assertEquals stepKey="verifyPrice11"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice11</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct2.name$$)}}" stepKey="checkProductPrice12"/> + <assertEquals stepKey="verifyPrice12"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice12</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct3.name$$)}}" stepKey="checkProductPrice13"/> + <assertEquals stepKey="verifyPrice13"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice13</actualResult> + </assertEquals> + + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct1.name$$)}}" userInput="Remove" stepKey="clickToRemove4"/> + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct2.name$$)}}" userInput="Remove" stepKey="clickToRemove5"/> + <selectOption selector="{{AdminOrderFormItemsSection.removeItems($$createProduct3.name$$)}}" userInput="Remove" stepKey="clickToRemove6"/> + <waitForLoadingMaskToDisappear stepKey="wait4"/> + <click selector="{{AdminOrderFormItemsSection.update}}" stepKey="clickToUpdate2"/> + <waitForLoadingMaskToDisappear stepKey="wait10"/> + + <!--TEST CASE #3--> + <waitForPageLoad stepKey="WaitProductsDeleted1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct4" /> + <click selector="{{AdminOrderFormItemsSection.selectProduct($$createProduct1.name$$)}}" stepKey="selectProduct9"/> + <fillField selector="{{AdminOrderFormItemsSection.setQuantity($$createProduct1.name$$)}}" userInput="10" stepKey="addProductQuantity10"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addProductsToOrder3"/> + <waitForLoadingMaskToDisappear stepKey="wait11"/> + <fillField selector="{{AdminOrderFormItemsSection.applyCoupon}}" userInput="ship" stepKey="addCouponCode"/> + <waitForLoadingMaskToDisappear stepKey="wait5"/> + <click selector="{{AdminOrderFormItemsSection.update}}" stepKey="clickToUpdate3"/> + <waitForLoadingMaskToDisappear stepKey="wait12"/> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct1.name$$)}}" stepKey="checkProductPrice14"/> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productPrice($$createProduct4.name$$)}}" stepKey="checkProductPrice15"/> + <assertEquals stepKey="verifyPrice14"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice14</actualResult> + </assertEquals> + <assertEquals stepKey="verifyPrice15"> + <expectedResult type="string">{{TestDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice15</actualResult> + </assertEquals> + + <after> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + <deleteData createDataKey="createProduct4" stepKey="deleteProduct4"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <createData entity="DefaultConfigCatalogPrice" stepKey="resetPriceScopeConfiguration"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="ship"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml new file mode 100644 index 0000000000000..f333db0e65aa8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="DeleteCategoriesTest"> + <annotations> + <features value="Delete categories"/> + <title value="Delete categories."/> + <description value="Delete Default Root Category and subcategories and vefify after products on storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76275"/> + <group value="testNotIsolated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategoryC"/> + <createData entity="productWithDescription" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSubCategory"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="productWithDescription" stepKey="createProduct2"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <createData entity="_defaultCategory" stepKey="createCategoryB"/> + <createData entity="productWithDescription" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategoryB"/> + </createData> + <createData entity="NewRootCategory" stepKey="createNewRootCategoryA"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage1"/> + <waitForPageLoad time="30" stepKey="waitForPageCategoryLoadAfterNavigate"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createNewRootCategoryA.name$$)}}" stepKey="openNewRootCategory"/> + <waitForPageLoad stepKey="waitForPageCategoryLoadAfterClickOnNewRootCategory"/> + <seeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="assertDeleteButtonIsPresent"/> + <!--Move categories from Default Category to NewRootCategory. --> + <actionGroup ref="MoveCategoryActionGroup" stepKey="MoveCategoryBToNewRootCategory"> + <argument name="childCategory" value="$$createCategoryC.name$$"/> + <argument name="parentCategory" value="$$createNewRootCategoryA.name$$"/> + </actionGroup> + <actionGroup ref="MoveCategoryActionGroup" stepKey="MoveCategoryCToNewRootCategory"> + <argument name="childCategory" value="$$createCategoryB.name$$"/> + <argument name="parentCategory" value="$$createNewRootCategoryA.name$$"/> + </actionGroup> + <!-- Change root category for Main Website Store. --> + <amOnPage stepKey="s1" url="{{AdminSystemStorePage.url}}"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="filterStoresGridByStore" stepKey="filterStoresGridByStore"> + <argument name="store" value="Main Website Store"/> + </actionGroup> + <click stepKey="s7" selector="{{AdminStoresGridSection.storeInFirstRow}}" /> + <waitForPageLoad stepKey="waitForPageAdminStoresGroupEditLoad" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{NewRootCategory.name}}" stepKey="setNewCategoryForStoreGroup"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreGroup"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModalSaveStoreGroup"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptModal" /> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForPageAdminStoresGridReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage"/> + + <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> + <!-- Perform cli reindex. --> + <!--<magentoCLI command="indexer:reindex" stepKey="magentoCli"/>--> + + <!-- Delete Default Root Category. --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPageAfterCLIReindexCommand"/> + <waitForPageLoad time="30" stepKey="waitForPageCategoryLoadAfterCLIReindexCommand"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="clickOnDefaultRootCategory"/> + <waitForPageLoad stepKey="waitForPageDefaultCategoryEditLoad" /> + <seeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="assertDeleteButtonIsPresent1"/> + <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="DeleteDefaultRootCategory"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.ok}}" stepKey="waitForModalDeleteDefaultRootCategory" /> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="acceptModal1"/> + <waitForElementVisible selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="waitForPageReloadAfterDeleteDefaultCategory"/> + <!-- Verify categories 1 and 3 their products. --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <waitForPageLoad stepKey="homeWaitForPageLoad"/> + + <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> + <!--<click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryC.name$$)}}" stepKey="browseClickCategoryC"/>--> + <!--<actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="browseAssertCategoryC">--> + <!--<argument name="category" value="$$createCategoryC$$"/>--> + <!--<argument name="productCount" value="2"/>--> + <!--</actionGroup>--> + <!--<actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct1">--> + <!--<argument name="product" value="$$createProduct1$$"/>--> + <!--</actionGroup>--> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryC.name$$)}}" stepKey="hoverCategory"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSubCategory.name$$)}}" stepKey="waitForSubcategory"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSubCategory.name$$)}}" stepKey="browseClickSubCategory"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="browseAssertSubcategory"> + <argument name="category" value="$$createSubCategory$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct2"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + + <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> + <!--<actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="browseAssertCategoryB">--> + <!--<argument name="category" value="$$createCategoryB$$"/>--> + <!--<argument name="productCount" value="1"/>--> + <!--</actionGroup>--> + <!--<actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct3">--> + <!--<argument name="product" value="$$createProduct3$$"/>--> + <!--</actionGroup>--> + + <!-- Delete Categories 1(with subcategory) and 3. --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPageAfterStoreFrontCategoryAssertions"/> + <waitForPageLoad time="30" stepKey="waitForCategoryPageLoadAfterStoreFrontCategoryAssertions"/> + <actionGroup ref="DeleteCategory" stepKey="deleteCategoryC"> + <argument name="categoryEntity" value="$$createCategoryC$$"/> + </actionGroup> + <actionGroup ref="DeleteCategory" stepKey="deleteCategoryB"> + <argument name="categoryEntity" value="$$createCategoryB$$"/> + </actionGroup> + <!-- Verify categories 1 and 3 are absent --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage1"/> + <waitForPageLoad stepKey="waitHomePageLoadAfterDeletingCategories"/> + <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryB.name$$)}}" stepKey="browseClickCategoryB"/> + <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryC.name$$)}}" stepKey="browseClickCategoryC"/> + <!-- Verify products 1-3 are available on storefront --> + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct1.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> + <waitForPageLoad stepKey="product1WaitForPageLoad"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct1Page"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct2.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> + <waitForPageLoad stepKey="product2WaitForPageLoad"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct2Page"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct3.custom_attributes[url_key]$$.html" stepKey="amOnProduct3Page"/> + <waitForPageLoad stepKey="product3WaitForPageLoad"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct3Page"> + <argument name="product" value="$$createProduct3$$"/> + </actionGroup> + <!-- Rename New Root Category to Default category --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPageAfterStoreFrontProductsAssertions"/> + <waitForPageLoad time="30" stepKey="waitForCategoryPageLoadAfterStoreFrontProductsAssertions"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createNewRootCategoryA.name$$')}}" stepKey="clickOnNewRootCategoryA"/> + <waitForPageLoad stepKey="waitForPageNewRootCategoryALoad" /> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="Default Category" stepKey="enterCategoryNameAsDefaultCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryDefaultCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml new file mode 100644 index 0000000000000..9575d7fea56fc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="SaveProductWithCustomOptionsAdditionalWebsiteTest"> + <annotations> + <features value="Save a product with Custom Options and assign to a different website"/> + <stories value="Purchase a product with Custom Options of different types"/> + <title value="You should be able to save a product with custom options assigned to a different website"/> + <description value="Custom Options should not be split when saving the product after assigning to a different website"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92749"/> + <group value="product"/> + </annotations> + + <after> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="addnewWebsite"> + <argument name="newWebsiteName" value="Second Website"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStoreGroup"> + <argument name="website" value="Second Website"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStore"> + <argument name="storeGroup" value="secondStoreGroup"/> + <argument name="customStore" value="secondStore"/> + </actionGroup> + + <!--Create a Simple Product with Custom Options --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad time="30" stepKey="waitForProductsGridPageLoad"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + + <!--Click Customizable Options--> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForPageLoad stepKey="waitAfterAddOption"/> + <fillField selector="input[name='product[options][0][title]']" userInput="Radio Option" stepKey="fillOptionTitle"/> + <click selector=".admin__dynamic-rows[data-index='options'] .action-select" stepKey="openOptionTypeDropDown"/> + <click selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li:nth-of-type(3) li:nth-of-type(2)" stepKey="selectRadioButtonType"/> + + <!--Add Option Values --> + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '0')}}" userInput="option 1" stepKey="fillOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '1')}}" userInput="option 2" stepKey="fillOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '1')}}" userInput="6" stepKey="fillOptionValuePrice2"/> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '2')}}" userInput="option 3" stepKey="fillOptionValueTitle3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '2')}}" userInput="7" stepKey="fillOptionValuePrice3"/> + + <!--Save the product with custom options --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitProductPageSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeProductSavedMessage"/> + + <!-- Add this product to second website --> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> + <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForProductPagetoSaveAgain"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> + + <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> + <scrollTo stepKey="scrollToCustomizableOptions" selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" /> + <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml new file mode 100644 index 0000000000000..835a6e0e54eed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -0,0 +1,336 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCatalogNavigationMenuUIDesktopTest"> + <annotations> + <features value="Catalog"/> + <stories value="Storefront Catalog Navigation Menu UI"/> + <title value="Storefront Catalog Navigation Menu UI, desktop"/> + <description value="Verify UI of Navigation Menu functionality on Storefront"/> + <testCaseId value="MC-8107"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="theme"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="DeleteDefaultCategoryChildren" stepKey="deleteRootCategoryChildren"/> + </before> + <after> + <actionGroup ref="DeleteDefaultCategoryChildren" stepKey="deleteRootCategoryChildren"/> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToDefault"> + <argument name="theme" value="{{MagentoLumaTheme.name}}"/> + </actionGroup> + <!-- Admin log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Content > Themes. Change theme to Blank --> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToBlank"> + <argument name="theme" value="{{MagentoBlankTheme.name}}"/> + </actionGroup> + + <!-- Open storefront --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> + + <!-- Assert no category - no menu --> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.navigationMenu}}" stepKey="dontSeeMenu"/> + + <!-- Assert single row - no hover state --> + <createData entity="ApiCategory" stepKey="createFirstCategoryBlank"> + <field key="name">Category A</field> + </createData> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForBlankSingleRowAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryBlank.name$$)}}" stepKey="hoverFirstCategoryBlank"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}" stepKey="assertNoHoverState"/> + + <!-- Create categories --> + <createData entity="ApiCategory" stepKey="createSecondCategoryBlank"> + <field key="name">TEST</field> + </createData> + <createData entity="ApiCategory" stepKey="createThirdCategoryBlank"> + <field key="name">_test2</field> + </createData> + <createData entity="ApiCategory" stepKey="createFourthCategoryBlank"> + <field key="name">test 3</field> + </createData> + <createData entity="ApiCategory" stepKey="createFifthCategoryBlank"> + <field key="name">Category with several products</field> + </createData> + <createData entity="ApiCategory" stepKey="createSixthCategoryBlank"> + <field key="name">test 5</field> + </createData> + <createData entity="ApiCategory" stepKey="createSeventhCategoryBlank"> + <field key="name">test 8</field> + </createData> + <createData entity="ApiCategory" stepKey="createEighthCategoryBlank"> + <field key="name">This is a very very very very very looong title</field> + </createData> + <createData entity="ApiCategory" stepKey="createNinthCategoryBlank"> + <field key="name">test 6</field> + </createData> + <createData entity="ApiCategory" stepKey="createTenthCategoryBlank"> + <field key="name">test 7</field> + </createData> + <createData entity="ApiCategory" stepKey="createEleventhCategoryBlank"> + <field key="name">test 4</field> + </createData> + <createData entity="ApiCategory" stepKey="createTwelfthCategoryBlank"> + <field key="name">Category with image</field> + </createData> + <createData entity="ApiCategory" stepKey="createThirteenthCategoryBlank"> + <field key="name">test 0</field> + </createData> + <createData entity="ApiCategory" stepKey="createCategoryWithoutChildrenBlank"> + <field key="name">Category with description & custom title</field> + </createData> + <createData entity="ApiCategory" stepKey="createCategoryWithChildrenBlank"> + <field key="name">Category with children</field> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelOneBlank"> + <field key="name">level 1 test category very very very long name</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelOneBlank"> + <field key="name">level 1 test category name</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createThirdCategoryLevelOneBlank"> + <field key="name">level 1 with children</field> + <requiredEntity createDataKey="createCategoryWithChildrenBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelTwoBlank"> + <field key="name">level 2 with children</field> + <requiredEntity createDataKey="createThirdCategoryLevelOneBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelThreeBlank"> + <field key="name">level 3 test</field> + <requiredEntity createDataKey="createCategoryLevelTwoBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelFourBlank"> + <field key="name">level 4</field> + <requiredEntity createDataKey="createCategoryLevelThreeBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelFourBlank"> + <field key="name">level 4 test</field> + <requiredEntity createDataKey="createCategoryLevelThreeBlank"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelFiveBlank"> + <field key="name">level 5</field> + <requiredEntity createDataKey="createSecondCategoryLevelFourBlank"/> + </createData> + + <!-- Several rows. Hover on category without children --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForBlankSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithoutChildrenBlank.name$$)}}" stepKey="hoverCategoryWithoutChildren"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createCategoryWithoutChildrenBlank.name$$, 'level0')}}" stepKey="dontSeeChildrenInCategory"/> + + <!-- Nested level 1. No hover state --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenBlank.name$$)}}" stepKey="hoverCategoryWithChildrenTopLevel"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkNoHoverState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemByLevel('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.white}}"/> + </actionGroup> + + <!-- Nested level 1. Hover state on 1st item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryLevelOneBlank.name$$)}}" stepKey="hoverCategoryLevelOneFirstItem"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverFirstItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Nested level 1 & 2. Hover state on the last item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneBlank.name$$)}}" stepKey="hoverCategoryLevelOneLastItem"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverLastItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Submenu appears rightward --> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level0')}}" stepKey="assertTopLevelMenuLeftDirection"/> + + <!-- Nested level 1 & 5 --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelTwoBlank.name$$)}}" stepKey="hoverCategoryLevelTwo"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuLeftDirection('level1')}}" stepKey="seeLevelOneMenuLeftDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelThreeBlank.name$$)}}" stepKey="hoverCategoryLevelThree"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuLeftDirection('level2')}}" stepKey="seeLevelTwoMenuRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategoryLevelFourBlank.name$$)}}" stepKey="hoverCategoryLevelFour"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level3')}}" stepKey="seeLevelThreeMenuRightDirection"/> + + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubcategoryHighlighted"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level3')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Delete all creation for Blank theme --> + <deleteData createDataKey="createFirstCategoryBlank" stepKey="deleteFirstCategoryBlank"/> + <deleteData createDataKey="createSecondCategoryBlank" stepKey="deleteSecondCategoryBlank"/> + <deleteData createDataKey="createThirdCategoryBlank" stepKey="deleteThirdCategoryBlank"/> + <deleteData createDataKey="createFourthCategoryBlank" stepKey="deleteFourthCategoryBlank"/> + <deleteData createDataKey="createFifthCategoryBlank" stepKey="deleteFifthCategoryBlank"/> + <deleteData createDataKey="createSixthCategoryBlank" stepKey="deleteSixthCategoryBlank"/> + <deleteData createDataKey="createSeventhCategoryBlank" stepKey="deleteSeventhCategoryBlank"/> + <deleteData createDataKey="createEighthCategoryBlank" stepKey="deleteEighthCategoryBlank"/> + <deleteData createDataKey="createNinthCategoryBlank" stepKey="deleteNinthCategoryBlank"/> + <deleteData createDataKey="createTenthCategoryBlank" stepKey="deleteTenthCategoryBlank"/> + <deleteData createDataKey="createEleventhCategoryBlank" stepKey="deleteEleventhCategoryBlank"/> + <deleteData createDataKey="createTwelfthCategoryBlank" stepKey="deleteTwelfthCategoryBlank"/> + <deleteData createDataKey="createThirteenthCategoryBlank" stepKey="deleteThirteenthCategoryBlank"/> + <deleteData createDataKey="createCategoryWithChildrenBlank" stepKey="deleteCategoryWithChildrenBlank"/> + <deleteData createDataKey="createCategoryWithoutChildrenBlank" stepKey="deleteCategoryWithoutChildrenBlank"/> + + <!-- Go to Content > Themes. Change theme to Luma --> + <actionGroup ref="AdminChangeStorefrontThemeActionGroup" stepKey="changeThemeToLuma"> + <argument name="theme" value="{{MagentoLumaTheme.name}}"/> + </actionGroup> + + <!-- Open storefront --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefront"/> + + <!-- Assert no category - no menu --> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.navigationMenu}}" stepKey="dontSeeMenuOnStorefront"/> + + <!-- Create categories --> + <createData entity="ApiCategory" stepKey="createFirstCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createSecondCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createThirdCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createFourthCategoryLuma"/> + + <!-- Single row. No hover state --> + <reloadPage stepKey="reload"/> + <waitForPageLoad stepKey="waitForLumaSingleRowAppear"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFirstCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFirstCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createSecondCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInSecondCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createThirdCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateThirdCategory"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFourthCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFourthCategory"/> + + <!-- Create categories for testing Luma theme --> + <createData entity="ApiCategory" stepKey="createFifthCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createCategoryWithChildrenLuma"/> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createThirdCategoryLevelOneLuma"> + <requiredEntity createDataKey="createCategoryWithChildrenLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createFirstCategoryLevelTwoLuma"> + <requiredEntity createDataKey="createThirdCategoryLevelOneLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createSecondCategoryLevelTwoLuma"> + <requiredEntity createDataKey="createThirdCategoryLevelOneLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelThreeLuma"> + <requiredEntity createDataKey="createSecondCategoryLevelTwoLuma"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryLevelFourLuma"> + <requiredEntity createDataKey="createCategoryLevelThreeLuma"/> + </createData> + <createData entity="ApiCategory" stepKey="createSixthCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createSeventhCategoryLuma"/> + <createData entity="ApiCategory" stepKey="createEighthCategoryLuma"/> + + <!-- Several rows. Hover on Category without children --> + <reloadPage stepKey="refresh"/> + <waitForPageLoad stepKey="waitForLumaSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFifthCategoryLuma.name$$)}}" stepKey="hoverOnCategoryWithoutChildren"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFifthCategoryLuma.name$$, 'level0')}}" stepKey="dontSeeSubcategoriesInCategory"/> + + <!-- Nested level 1. No hover state --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="hoverOnCategoryWithChildren"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkNoHighlightedInSubmenuAfterHover"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemByLevel('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.white}}"/> + </actionGroup> + + <!-- Nested level 1. Hover state on first item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnFirstItemLevelOne"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverOnFirstItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Nested levels 1 & 2. Hover state on last item --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnLastItemLevelOne"/> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkHighlightedAfterHoverOnLastItem"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Submenu appears rightward --> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level0')}}" stepKey="seeTopLevelRightDirection"/> + + <!-- Nested levels 1 & 5 --> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSecondCategoryLevelTwoLuma.name$$)}}" stepKey="hoverThirdCategoryLevelTwo"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level1')}}" stepKey="seeFirstLevelRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelThreeLuma.name$$)}}" stepKey="hoverOnCategoryLevelThree"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level2')}}" stepKey="seeSecondLevelRightDirection"/> + + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryLevelFourLuma.name$$)}}" stepKey="hoverOnCategoryLevelFour"/> + <seeElement selector="{{StorefrontNavigationMenuSection.submenuRightDirection('level3')}}" stepKey="seeThirdLevelRightDirection"/> + + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubcategoryHighlightedAfterHover"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemLevelHover('level3')}}"/> + <argument name="property" value="background-color"/> + <argument name="color" value="{{NavigationMenuColor.gray}}"/> + </actionGroup> + + <!-- Selected 1st level category --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="openTopLevelCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageLoaded"/> + + <!-- Assert category active state --> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkCategoryActiveState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.itemActiveState}}"/> + <argument name="property" value="border-color"/> + <argument name="color" value="{{NavigationMenuColor.orange}}"/> + </actionGroup> + + <!-- Selected subcategory. Assert active state --> + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="openSubcategory"> + <argument name="categoryName" value="$$createCategoryWithChildrenLuma.name$$"/> + <argument name="subCategoryName" value="$$createThirdCategoryLevelOneLuma.name$$"/> + </actionGroup> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithChildrenLuma.name$$)}}" stepKey="hoverOnCategory"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createThirdCategoryLevelOneLuma.name$$)}}" stepKey="hoverOnSubcategory"/> + + <!-- Assert subcategory active state --> + <actionGroup ref="StorefrontCheckElementColorActionGroup" stepKey="checkSubitemActiveState"> + <argument name="selector" value="{{StorefrontNavigationMenuSection.subItemActiveState}}"/> + <argument name="property" value="border-color"/> + <argument name="color" value="{{NavigationMenuColor.orange}}"/> + </actionGroup> + + <!-- Delete created category --> + <deleteData createDataKey="createFirstCategoryLuma" stepKey="deleteFirstCategoryLuma"/> + <deleteData createDataKey="createSecondCategoryLuma" stepKey="deleteSecondCategoryLuma"/> + <deleteData createDataKey="createThirdCategoryLuma" stepKey="deleteThirdCategoryLuma"/> + <deleteData createDataKey="createFourthCategoryLuma" stepKey="deleteFourthCategoryLuma"/> + <deleteData createDataKey="createFifthCategoryLuma" stepKey="deleteFifthCategoryLuma"/> + <deleteData createDataKey="createSixthCategoryLuma" stepKey="deleteSixthCategoryLuma"/> + <deleteData createDataKey="createSeventhCategoryLuma" stepKey="deleteSeventhCategoryLuma"/> + <deleteData createDataKey="createEighthCategoryLuma" stepKey="deleteEighthCategoryLuma"/> + <deleteData createDataKey="createCategoryWithChildrenLuma" stepKey="deleteCategoryWithChildrenLuma"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml new file mode 100644 index 0000000000000..fe7858313c848 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductAvailableAfterEnablingSubCategoriesTest"> + <annotations> + <features value="Catalog"/> + <stories value="Show category products on storefront"/> + <title value="Check that parent categories are showing products after enabling subcategories"/> + <description value="Check that parent categories are showing products after enabling subcategories"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13914"/> + <useCaseId value="MAGETWO-96489"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SubCategoryWithParent" stepKey="createSubCategory"> + <requiredEntity createDataKey="createCategory"/> + <field key="is_active">false</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Check anchor category is empty--> + <actionGroup ref="StorefrontCheckEmptyCategoryActionGroup" stepKey="checkEmptyAnchorCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="0"/> + </actionGroup> + <!--Enable subcategory--> + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="openCreatedSubCategory"> + <argument name="category" value="$$createSubCategory$$"/> + </actionGroup> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + <actionGroup ref="saveCategoryForm" stepKey="saveCategory"/> + <!--Check created product in anchor category on storefront--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="seeCreatedProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml new file mode 100644 index 0000000000000..325a6028e4464 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontProductNameWithDoubleQuote"> + <annotations> + <title value="Product with double quote in name"/> + <description value="Product with a double quote in the name should appear correctly on the storefront"/> + <severity value="CRITICAL"/> + <group value="product"/> + <testCaseId value="MAGETWO-92864"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProductWithImage" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">Double Quote"</field> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Check product in category listing--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByNameAndSrc($$createProduct.name$$, $$createProduct.product[media_gallery_entries][0][content][name]$$)}}" stepKey="seeCorrectImageCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.productTitleByName($$createProduct.name$$)}}" userInput="$$createProduct.name$$" stepKey="seeCorrectNameCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="$$createProduct.price$$" stepKey="seeCorrectPriceCategoryPage"/> + <!--Open product display page--> + <click selector="{{StorefrontCategoryProductSection.productTitleByName($$createProduct.name$$)}}" stepKey="clickProductToGoProductPage"/> + <waitForPageLoad stepKey="waitForProductDisplayPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeCorrectName"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="seeCorrectSku"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createProduct.price$$" stepKey="seeCorrectPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productImageSrc($$createProduct.product[media_gallery_entries][0][content][name]$$)}}" stepKey="seeCorrectImage"/> + <see selector="{{StorefrontProductInfoMainSection.stock}}" userInput="In Stock" stepKey="seeInStock"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$createCategory.name$$" stepKey="seeCorrectBreadCrumbCategory"/> + <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$createProduct.name$$" stepKey="seeCorrectBreadCrumbProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml new file mode 100644 index 0000000000000..c2b502fd43f34 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontProductsCompareWithEmptyAttributeTest"> + <annotations> + <title value="Product attribute is not visible on product compare page if it is empty"/> + <description value="Product attribute should not be visible on the product compare page if it is empty for all products that are being compared, not even displayed as N/A"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92271"/> + <group value="productCompare"/> + </annotations> + <before> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createProductAttribute"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <waitForPageLoad stepKey="waitLoginAsAdmin"/> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> + <click selector="{{AdminProductAttributeSetGridSection.attributeSetName('Default')}}" stepKey="chooseDefaultAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + <dragAndDrop selector1="{{AdminProductAttributeUnassignedSection.productAttributeName('$$createProductAttribute.attribute_code$$')}}" selector2="{{AdminProductAttributeGroupSection.folderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> + <click selector="{{AdminProductAttributeSetSection.save}}" stepKey="saveAttributeSet"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> + <seeElement selector=".message-success" stepKey="assertSuccess"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + <waitForPageLoad stepKey="waitClearCache"/> + <amOnPage url="{{StorefrontProductPage.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="goProductPageOnStorefront1"/> + <!-- View Simple Product 1 --> + <comment userInput="View simple product 1" stepKey="commentViewSimpleProduct1" after="goProductPageOnStorefront1"/> + <click selector="{{StorefrontCategoryProductSection.productTitleByName($$createSimpleProduct1.name$$)}}" stepKey="browseClickCategorySimpleProduct1View" after="commentViewSimpleProduct1"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct1Viewloaded" after="browseClickCategorySimpleProduct1View"/> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="compareAddSimpleProduct1ToCompare"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitAddSimpleProduct1AddedToCompare"/> + <amOnPage url="{{StorefrontProductPage.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="goProductPageOnStorefront2"/> + <!-- View Simple Product 2 --> + <comment userInput="View simple product 2" stepKey="commentViewSimpleProduct2" after="goProductPageOnStorefront2"/> + <click selector="{{StorefrontCategoryProductSection.productTitleByName($$createSimpleProduct2.name$$)}}" stepKey="browseClickCategorySimpleProduct2View" after="commentViewSimpleProduct2"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct2Viewloaded" after="browseClickCategorySimpleProduct2View"/> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="compareAddSimpleProduct2ToCompare"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitAddSimpleProduct2AddedToCompare"/> + <amOnPage url="{{StorefrontProductPage.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="goProductPageOnStorefront3"/> + <!-- Check products in comparison sidebar --> + <!-- Check simple product 1 in comparison sidebar --> + <comment userInput="Check simple product 1 in comparison sidebar" stepKey="commentCheckSimpleProduct1InComparisonSidebar" after="goProductPageOnStorefront3"/> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct1InSidebar" after="commentCheckSimpleProduct1InComparisonSidebar"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- Check simple product2 in comparison sidebar --> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct2InSidebar" after="compareSimpleProduct1InSidebar"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- Check products on comparison page --> + <!-- Check simple product 1 on comparison page --> + <comment userInput="Check simple product 1 on comparison page" stepKey="commentCheckSimpleProduct1OnComparisonPage"/> + <actionGroup ref="StorefrontOpenAndCheckComparisionActionGroup" stepKey="compareOpenComparePage" after="commentCheckSimpleProduct1OnComparisonPage"/> + <actionGroup ref="StorefrontCheckCompareSimpleProductActionGroup" stepKey="compareAssertSimpleProduct1InComparison" after="compareOpenComparePage"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <seeElement selector="{{StorefrontProductCompareMainSection.productAttributeByName('SKU')}}" stepKey="seeCompareAttributeSku"/> + <dontSeeElement selector="{{StorefrontProductCompareMainSection.productAttributeByName('$$createProductAttribute.attribute_code$$')}}" stepKey="dontSeeCompareTestAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..e7054839ef4d0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -0,0 +1,308 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> + <annotations> + <features value="Purchase a product with Custom Options on different Store Views"/> + <title value="You should be able to sell products with different variants of your own."/> + <description value="You should be able to sell products with different variants of your own."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77831"/> + <group value="product"/> + </annotations> + + <before> + <!-- Create Customer --> + + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!--Create Simple Product --> + + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <!--Create storeView 1--> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + + <!--Create storeView 2--> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Delete Store View EN --> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + + <!-- Delete Store View FR --> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearWebsitesGridFilters"/> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + </after> + + <!-- Open Product Grid, Filter product and open --> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Update Product with Option Value DropDown 1--> + + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> + <waitForPageLoad time="10" stepKey="waitForPageLoad3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> + + <!-- Switcher to Store FR--> + + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> + <argument name="scopeName" value="customStoreFR.name"/> + </actionGroup> + + <!-- Open tab Customizable Options --> + + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> + + <!-- Update Option Customizable Options and Option Value 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="uncheckUseDefaultOptionTitle"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('Custom Options 1')}}" userInput="FR Custom Options 1" stepKey="fillOptionTitle2"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionValueTitleByIndex('0')}}" stepKey="uncheckUseDefaultOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '0')}}" userInput="FR option1" stepKey="fillOptionValueTitle3"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionValueTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionValueTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + + <!-- Login Customer Storefront --> + + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Go to Product Page --> + + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> + + <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown1"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <!-- See Custom options are displayed as option1 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="option1" stepKey="seeProductOptionValueDropdown1Input1"/> + + <!-- See Custom options are displayed as option2 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> + + <!-- Place Order --> + + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Open Order --> + + <actionGroup ref="filterOrderGridById" stepKey="openOrdersGrid"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> + + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option1" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option2" stepKey="seeAdminOrderProductOptionValueDropdown2"/> + + <!-- Switch to FR Store View Storefront --> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> + + <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown1"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem1"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive1"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCar1t"/> + + <!-- See Custom options are displayed as option1 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions2"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="FR option1" stepKey="seeProductFrOptionValueDropdown1Input2"/> + + <!-- See Custom options are displayed as option2 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> + + <!-- Place Order --> + + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder2"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!-- Open Product Grid, Filter product and open --> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad16"/> + + <!-- Switcher to Store FR--> + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView1"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage1"/> + + <!-- Open tab Customizable Options --> + + <waitForPageLoad time="30" stepKey="waitForPageLoad17"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> + + <!-- Update Option Customizable Options and Option Value 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad18"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="checkUseDefaultOptionTitle"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionValueTitleByIndex('0')}}" stepKey="checkUseDefaultOptionValueTitle1"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad19"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionValueTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> + + <!--Go to Product Page--> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page2"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..823e000bb9c27 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductWithCustomOptionsTest"> + <annotations> + <features value="Purchase a product with Custom Options of different types"/> + <stories value="Purchase a product with Custom Options of different types"/> + <title value="You should be able to sell products with different variants of your own."/> + <description value="You should be able to sell products with different variants of your own."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Create Simple Product with Custom Options--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">17</field> + </createData> + <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> + <!-- Logout customer before in case of it logged in from previous test --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete product and category --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> + <!-- Logout customer --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + </after> + + <!-- Login Customer Storefront --> + + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginCustomerOnStorefront"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed prices for user parameters --> + + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct3Page"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDown.title, ProductOptionValueDropdown1.price)}}" stepKey="checkDropDownProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="checkButtonsProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="checkCheckboxProductOptiozn"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> + + <!--Generate year--> + <generateDate date="Now" format="Y" stepKey="year"/> + <generateDate date="Now" format="y" stepKey="shortYear"/> + + <!-- Adding items to the checkout --> + + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> + <selectOption userInput="{{ProductOptionValueDropdown1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDown.title)}}" stepKey="seeProductOptionDropDown"/> + <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="seeProductOptionRadioButtons"/> + <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="seeProductOptionCheckbox"/> + <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> + <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> + <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionTimeMinute(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeMinute"/> + + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickCart"/> + <click selector="{{StorefrontMiniCartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="s33"/> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> + + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Login to Admin and open Order --> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <actionGroup ref="filterOrderGridById" stepKey="filterByOrderId"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> + + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> + + <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> + + <click selector="{{OrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> + <actionGroup ref="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup" stepKey="selectBillingMethod"/> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="trySubmitOrder"/> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown11"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> + + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> + + <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> + + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnOrderPage"/> + + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($$createProduct.name$$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml new file mode 100644 index 0000000000000..df237e65440a8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle"> + <annotations> + <group value="Catalog"/> + <title value="Admin should be able to see the full title of the selected custom option value in the order"/> + <description value="Admin should be able to see the full title of the selected custom option value in the order"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93340"/> + <group value="product"/> + </annotations> + <before> + <!--Create Simple Product with Custom Options--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">17</field> + </createData> + <updateData createDataKey="createProduct" entity="ProductWithOptions2" stepKey="updateProductWithOptions"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + <!-- Login Customer Storefront --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Checking the correctness of displayed prices for user parameters --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDownWithLongValuesTitle.title, ProductOptionValueDropdownLongTitle1.price)}}" stepKey="checkDropDownProductOption"/> + <!-- Adding items to the checkout --> + <selectOption userInput="{{ProductOptionValueDropdownLongTitle1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDownWithLongValuesTitle.title)}}" stepKey="seeProductOptionDropDown"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + <waitForPageLoad stepKey="waitForCartToLoad"/> + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!-- Place Order --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!-- Login to Admin and open Order --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> + <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + <dontSee selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="dontSeeAdminOrderProductOptionValueDropdown1"/> + <grabTextFrom selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="productOptionValueText"/> + <assertEquals stepKey="checkProductOptionValue"> + <actualResult type="variable">productOptionValueText</actualResult> + <expectedResult type="string">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj111 ...</expectedResult> + </assertEquals> + <moveMouseOver selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="hoverProduct"/> + <grabTextFrom selector="{{AdminOrderItemsOrderedSection.productNameOptions}} span:nth-child(2)" stepKey="productOptionValueTruncatedText"/> + <assertEquals stepKey="checkProductOptionTruncatedValue"> + <actualResult type="variable">productOptionValueTruncatedText</actualResult> + <expectedResult type="string">11Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111</expectedResult> + </assertEquals> + <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptions}} span:nth-child(2)" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml new file mode 100644 index 0000000000000..483f3471b8c58 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRememberCategoryPaginationTest"> + <annotations> + <title value="Verify that Number of Products per page retained when visiting a different category"/> + <stories value="MAGETWO-73687: Number of Products displayed per page not retained when visiting a different category"/> + <description value="Verify that Number of Products per page retained when visiting a different category"/> + <features value="Catalog"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96307"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="_defaultCategory" stepKey="createCategory1"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory1"/> + </createData> + + <createData entity="RememberPaginationCatalogStorefrontConfig" stepKey="setRememberPaginationCatalogStorefrontConfig"/> + </before> + + <after> + <createData entity="DefaultCatalogStorefrontConfiguration" stepKey="setDefaultCatalogStorefrontConfiguration"/> + + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> + </after> + + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + </actionGroup> + + <selectOption selector="{{StorefrontCategoryPagerSection.perPage}}" userInput="12" stepKey="setPerPage" /> + <waitForPageLoad time="30" stepKey="waitForPageLoad"/> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.custom_attributes[url_key]$$"/> + </actionGroup> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategoryPageParameters"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategory1Page"> + <argument name="categoryUrlKey" value="$$createCategory1.custom_attributes[url_key]$$"/> + </actionGroup> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory1PageParameters"> + <argument name="categoryName" value="$$createCategory1.name$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml new file mode 100644 index 0000000000000..b9308edbb387f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <description value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <stories value="Verify product special price"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13788"/> + <useCaseId value="MAGETWO-95452"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!--Set timezone for default config--> + <amOnPage url="{{AdminConfigurationGeneralSectionPage.url('#general_locale-link')}}" stepKey="openLocaleSection"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Central European Standard Time (Europe/Paris)" stepKey="setTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + <!--Set timezone for Main Website--> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchStoreViewActionGroup"> + <argument name="scopeName" value="_defaultWebsite.name"/> + </actionGroup> + <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Greenwich Mean Time (Africa/Abidjan)" stepKey="setTimezone1"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig1"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!--Reset timezone--> + <amOnPage url="{{AdminConfigurationGeneralSectionPage.url('#general_locale-link')}}" stepKey="openLocaleSectionReset"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="{{_ENV.DEFAULT_TIMEZONE}}" stepKey="resetTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset"/> + + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup"> + <argument name="scopeName" value="_defaultWebsite.name"/> + </actionGroup> + <checkOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="checkUseDefault"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset1"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Set special price to created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="setSpecialPriceToCreatedProduct"> + <argument name="price" value="15"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Login to storefront from customer and check price--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="logInFromCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Go to the product page and check special price--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" userInput='$15.00' stepKey="assertSpecialPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml new file mode 100644 index 0000000000000..84a0f98d3a66e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -0,0 +1,227 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyCategoryProductAndProductCategoryPartialReindexTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Categories Indexer"/> + <title value="Verify Category Product and Product Category partial reindex"/> + <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8724"/> + <group value="catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!-- Change "Category Products" and "Product Categories" indexers to "Update by Schedule" mode --> + <magentoCLI command="indexer:set-mode" arguments="schedule catalog_category_product catalog_product_category" stepKey="setIndexerMode"/> + + <!-- Create categories K, L, M, N with different nesting in the tree and Anchor = Yes/No--> + <!-- Category K is an anchor category --> + <createData entity="_defaultCategory" stepKey="categoryK"/> + <!-- Category L is a non-anchor subcategory of category K --> + <createData entity="SubCategoryNonAnchor" stepKey="categoryL"> + <requiredEntity createDataKey="categoryK"/> + </createData> + <!-- Category M is a subcategory of category L --> + <createData entity="SubCategoryWithParent" stepKey="categoryM"> + <requiredEntity createDataKey="categoryL"/> + </createData> + <!-- Category N is a subcategory of category K --> + <createData entity="SubCategoryWithParent" stepKey="categoryN"> + <requiredEntity createDataKey="categoryK"/> + </createData> + + <!-- Create different Products with different settings, assign to categories: --> + <!-- Product A in 0 categories, i.e. not assigned to any category --> + <createData entity="SimpleProduct3" stepKey="productA"/> + <!-- Product B in 1 category M --> + <createData entity="SimpleProduct2" stepKey="productB"> + <requiredEntity createDataKey="categoryM"/> + </createData> + <!-- Product C in 2 categories M and N --> + <createData entity="SimpleProduct3" stepKey="productC"/> + + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryNAndMToProductC"> + <argument name="productId" value="$$productC.id$$"/> + <argument name="categoryName" value="$$categoryN.name$$, $$categoryM.name$$"/> + </actionGroup> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <!-- Change "Category Products" and "Product Categories" indexers to "Update on Save" mode --> + <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Delete data --> + <deleteData createDataKey="productA" stepKey="deleteProductA"/> + <deleteData createDataKey="productB" stepKey="deleteProductB"/> + <deleteData createDataKey="productC" stepKey="deleteProductC"/> + <deleteData createDataKey="categoryN" stepKey="deleteCategoryN"/> + <deleteData createDataKey="categoryM" stepKey="deleteCategoryM"/> + <deleteData createDataKey="categoryL" stepKey="deleteCategoryL"/> + <deleteData createDataKey="categoryK" stepKey="deleteCategoryK"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open categories K, L, M, N on Storefront --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.categoryEmptyMessage}}" stepKey="seeMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProducts"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onCategoryM"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryN"/> + + <!-- Open Products A, B, C to edit. Assign/unassign categories to/from them. Save changes --> + <!-- Assign category K to Product A --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryK"> + <argument name="productId" value="$$productA.id$$"/> + <argument name="categoryName" value="$$categoryK.name$$"/> + </actionGroup> + + <!-- Unassign category M from Product B --> + <amOnPage url="{{AdminProductEditPage.url($$productB.id$$)}}" stepKey="amOnEditCategoryPageB"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryM"> + <argument name="categoryName" value="$$categoryM.name$$"/> + </actionGroup> + + <!-- Assign category L to Product C --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryNAndM"> + <argument name="productId" value="$$productC.id$$"/> + <argument name="categoryName" value="$$categoryL.name$$"/> + </actionGroup> + + <!-- "One or more indexers are invalid. Make sure your Magento cron job is running." global warning message appears --> + <click selector="{{AdminSystemMessagesSection.systemMessagesDropdown}}" stepKey="openMessageSection"/> + <see userInput="One or more indexers are invalid. Make sure your Magento cron job is running." selector="{{AdminMessagesSection.warningMessage}}" stepKey="seeWarningMessage"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are not applied yet --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.categoryEmptyMessage}}" stepKey="seeEmptyMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryM"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCLI command="cron:run" stepKey="runCronAgain"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryK"/> + <see userInput="$$productA.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryKWithProductC"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryL"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLWithProductC"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryM"/> + <waitForPageLoad stepKey="waitForStorefrontCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMAndProductC"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="storefrontCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCAndCategoryN"/> + + <!-- Open categories K, L, N to edit. Assign/unassign Products to/from them. Save changes --> + + <!-- Remove Product A assignment for category K --> + <amOnPage url="{{AdminProductEditPage.url($$productA.id$$)}}" stepKey="amOnEditProductPageA"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryK"> + <argument name="categoryName" value="$$categoryK.name$$"/> + </actionGroup> + + <!-- Remove Product C assignment for category L --> + <amOnPage url="{{AdminProductEditPage.url($$productC.id$$)}}" stepKey="amOnEditProductPageC"/> + <actionGroup ref="AdminUnassignCategoryOnProductAndSaveActionGroup" stepKey="unassignCategoryL"> + <argument name="categoryName" value="$$categoryL.name$$"/> + </actionGroup> + + <!-- Add Product B assignment for category N --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignCategoryN"> + <argument name="productId" value="$$productB.id$$"/> + <argument name="categoryName" value="$$categoryN.name$$"/> + </actionGroup> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are not applied yet --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryK"/> + <see userInput="$$productA.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAWithCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryL"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLAndProductC"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMWithProductC"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="firstCronRun"/> + <magentoCLI command="cron:run" stepKey="secondCronRun"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> + + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryK"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productBOnCategoryK"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.categoryEmptyMessage}}" stepKey="noProductsMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductsOnCategoryL"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryL.custom_attributes[url_key]$$/$$categoryM.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryM"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMPageAndProductC"/> + + <!-- Category N contains only Products B and C --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onFrontendCategoryN"/> + <see userInput="$$productB.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBAndCategoryN"/> + <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryN"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml new file mode 100644 index 0000000000000..dfac039d6ab43 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="VerifyChildCategoriesShouldNotIncludeInMenuTest"> + <annotations> + <features value="Test child categories should not include in menu"/> + <title value="Test child categories should not include in menu."/> + <description value="Test child categories should not include in menu."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-75843"/> + <group value="category"/> + </annotations> + <after> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage2"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage1"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <!--Create new category under Default Category--> + <actionGroup ref="CreateCategory" stepKey="createSubcategory1"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <!--Create another subcategory under created category--> + <actionGroup ref="CreateCategory" stepKey="createSubcategory2"> + <argument name="categoryEntity" value="SubCategoryWithParent"/> + </actionGroup> + <!--Go to storefront and verify visibility of categories--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSimpleSubCategoryOnStorefront1"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront1"/> + <!--Set Include in menu to No on created category under Default Category --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage2"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory1"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="setNoToIncludeInMenuSelect"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton1"/> + <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="seeCheckboxEnableCategoryIsChecked"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="dontSeeCheckboxIncludeInMenuIsChecked"/> + <!--Go to storefront and verify visibility of categories--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage2"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront1"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront2"/> + <!--Set Enable category to No and Include in menu to Yes on created category under Default Category --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage3"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory2"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="SetNoToEnableCategorySelect"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="SetYesToIncludeInMenuSelect"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton2"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontSeeCheckboxEnableCategoryIsChecked"/> + <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="seeCheckboxIncludeInMenuIsChecked"/> + <!--Go to storefront and verify visibility of categories--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage3"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront2"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront3"/> + <!--Set Enable category to No and Include in menu to No on created category under Default Category --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage4"/> + <waitForPageLoad stepKey="waitForPageLoad7"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickOnCreatedSimpleSubCategory3"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="setNoToIncludeInMenuSelect2"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton3"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontSeeCheckboxEnableCategoryIsChecked2"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="dontSeeCheckboxIncludeInMenuIsChecked2"/> + <!--Go to storefront and verify visibility of categories--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage4"/> + <waitForPageLoad stepKey="waitForPageLoad8"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSimpleSubCategoryOnStorefront3"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SubCategoryWithParent.name)}}" stepKey="dontSeeSubCategoryWithParentOnStorefront4"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Category/AbstractCategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Category/AbstractCategoryTest.php index dafbc7d5fb285..1c96adccf1f5e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Category/AbstractCategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Category/AbstractCategoryTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Category; class AbstractCategoryTest extends \PHPUnit\Framework\TestCase @@ -51,8 +49,7 @@ protected function setUp() $this->contextMock = $this->createMock(\Magento\Backend\Block\Template\Context::class); - $this->requestMock = $this->getMockBuilder( - \Magento\Framework\App\RequestInterface::class) + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -60,13 +57,11 @@ protected function setUp() ->method('getRequest') ->will($this->returnValue($this->requestMock)); - $this->urlBuilderMock = $this->getMockBuilder( - \Magento\Framework\UrlInterface::class) + $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManagerMock = $this->getMockBuilder( - \Magento\Store\Model\StoreManagerInterface::class) + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php index b45df0380dcc6..5d8db5d5ba589 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Edit/Tab/AlertsTest.php @@ -55,6 +55,9 @@ public function testCanShowTab($priceAllow, $stockAllow, $canShowTab) $this->assertEquals($canShowTab, $this->alerts->canShowTab()); } + /** + * @return array + */ public function canShowTabDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php index 5e899263519da..1fc105686011f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/CategoryTest.php @@ -50,6 +50,9 @@ public function testIsAllowed($isAllowed) } } + /** + * @return array + */ public function isAllowedDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 804eef25ebdd9..4a20dfd98ab3c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -6,9 +6,14 @@ namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; -use Magento\Framework\Filesystem; +use Magento\Catalog\Model\Entity\Attribute; +use Magento\Catalog\Model\Product; use Magento\Framework\Phrase; +use Magento\MediaStorage\Helper\File\Storage\Database; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ContentTest extends \PHPUnit\Framework\TestCase { /** @@ -46,6 +51,11 @@ class ContentTest extends \PHPUnit\Framework\TestCase */ protected $imageHelper; + /** + * @var \Magento\MediaStorage\Helper\File\Storage\Database|\PHPUnit_Framework_MockObject_MockObject + */ + protected $databaseMock; + /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -67,13 +77,18 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); + $this->databaseMock = $this->getMockBuilder(Database::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->content = $this->objectManager->getObject( \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content::class, [ 'mediaConfig' => $this->mediaConfigMock, 'jsonEncoder' => $this->jsonEncoderMock, - 'filesystem' => $this->fileSystemMock + 'filesystem' => $this->fileSystemMock, + 'fileStorageDatabase' => $this->databaseMock ] ); } @@ -139,6 +154,13 @@ public function testGetImagesJson() $this->readMock->expects($this->any())->method('stat')->willReturnMap($sizeMap); $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); + $this->readMock->expects($this->any()) + ->method('isFile') + ->will($this->returnValue(true)); + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(false)); + $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); } @@ -217,6 +239,203 @@ public function testGetImagesJsonWithException() $this->imageHelper->expects($this->any())->method('getDefaultPlaceholderUrl')->willReturn($placeholderUrl); $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); + $this->readMock->expects($this->any()) + ->method('isFile') + ->will($this->returnValue(true)); + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(false)); + $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); } + + /** + * Test GetImageTypes() will return value for given attribute from data persistor. + * + * @return void + */ + public function testGetImageTypesFromDataPersistor() + { + $attributeCode = 'thumbnail'; + $value = 'testImageValue'; + $scopeLabel = 'testScopeLabel'; + $label = 'testLabel'; + $name = 'testName'; + $expectedTypes = [ + $attributeCode => [ + 'code' => $attributeCode, + 'value' => $value, + 'label' => $label, + 'name' => $name, + ], + ]; + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $product->expects($this->once()) + ->method('getData') + ->with($this->identicalTo($attributeCode)) + ->willReturn(null); + $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); + $product->expects($this->once()) + ->method('getMediaAttributes') + ->willReturn([$mediaAttribute]); + $this->galleryMock->expects($this->exactly(2)) + ->method('getDataObject') + ->willReturn($product); + $this->galleryMock->expects($this->once()) + ->method('getImageValue') + ->with($this->identicalTo($attributeCode)) + ->willReturn($value); + $this->galleryMock->expects($this->once()) + ->method('getScopeLabel') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($scopeLabel); + $this->galleryMock->expects($this->once()) + ->method('getAttributeFieldName') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($name); + $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); + } + + /** + * Test GetImageTypes() will return value for given attribute from product. + * + * @return void + */ + public function testGetImageTypesFromProduct() + { + $attributeCode = 'thumbnail'; + $value = 'testImageValue'; + $scopeLabel = 'testScopeLabel'; + $label = 'testLabel'; + $name = 'testName'; + $expectedTypes = [ + $attributeCode => [ + 'code' => $attributeCode, + 'value' => $value, + 'label' => $label, + 'name' => $name, + ], + ]; + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $product->expects($this->once()) + ->method('getData') + ->with($this->identicalTo($attributeCode)) + ->willReturn($value); + $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); + $product->expects($this->once()) + ->method('getMediaAttributes') + ->willReturn([$mediaAttribute]); + $this->galleryMock->expects($this->exactly(2)) + ->method('getDataObject') + ->willReturn($product); + $this->galleryMock->expects($this->never()) + ->method('getImageValue'); + $this->galleryMock->expects($this->once()) + ->method('getScopeLabel') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($scopeLabel); + $this->galleryMock->expects($this->once()) + ->method('getAttributeFieldName') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($name); + $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); + } + + /** + * Perform assertions. + * + * @param string $attributeCode + * @param string $scopeLabel + * @param array $expectedTypes + * @return void + */ + private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) + { + $this->content->setElement($this->galleryMock); + $result = $this->content->getImageTypes(); + $scope = $result[$attributeCode]['scope']; + $this->assertSame($scopeLabel, $scope->getText()); + unset($result[$attributeCode]['scope']); + $this->assertSame($expectedTypes, $result); + } + + /** + * Get media attribute mock. + * + * @param string $label + * @param string $attributeCode + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getMediaAttribute(string $label, string $attributeCode) + { + $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) + ->disableOriginalConstructor() + ->getMock(); + $frontend->expects($this->once()) + ->method('getLabel') + ->willReturn($label); + $mediaAttribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaAttribute->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $mediaAttribute->expects($this->once()) + ->method('getFrontend') + ->willReturn($frontend); + + return $mediaAttribute; + } + + /** + * Test GetImagesJson() calls MediaStorage functions to obtain image from DB prior to stat call + * + * @return void + */ + public function testGetImagesJsonMediaStorageMode() + { + $images = [ + 'images' => [ + [ + 'value_id' => '0', + 'file' => 'file_1.jpg', + 'media_type' => 'image', + 'position' => '0' + ] + ] + ]; + + $mediaPath = [ + ['file_1.jpg', 'catalog/product/image_1.jpg'] + ]; + + $this->content->setElement($this->galleryMock); + + $this->galleryMock->expects($this->once()) + ->method('getImages') + ->willReturn($images); + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->willReturn($this->readMock); + $this->mediaConfigMock->expects($this->any()) + ->method('getMediaPath') + ->willReturnMap($mediaPath); + + $this->readMock->expects($this->any()) + ->method('isFile') + ->will($this->returnValue(false)); + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(true)); + + $this->databaseMock->expects($this->once()) + ->method('saveFileToFilesystem') + ->with('catalog/product/image_1.jpg'); + + $this->content->getImagesJson(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php index 06e2368f3080e..1e04680676eb2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\Request\DataPersistorInterface; + class GalleryTest extends \PHPUnit\Framework\TestCase { /** @@ -32,18 +34,27 @@ class GalleryTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** + * @var DataPersistorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataPersistorMock; + public function setUp() { $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData']); $this->formMock = $this->createMock(\Magento\Framework\Data\Form::class); - + $this->dataPersistorMock = $this->getMockBuilder(DataPersistorInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->gallery = $this->objectManager->getObject( \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery::class, [ 'registry' => $this->registryMock, - 'form' => $this->formMock + 'form' => $this->formMock, + 'dataPersistor' => $this->dataPersistorMock ] ); } @@ -70,6 +81,68 @@ public function testGetImages() $this->assertSame($mediaGallery, $this->gallery->getImages()); } + /** + * Test getImages() will try get data from data persistor, if it's absent in registry. + * + * @return void + */ + public function testGetImagesWithDataPersistor() + { + $product = [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + [ + 'value_id' => '1', + 'file' => 'image_1.jpg', + 'media_type' => 'image', + ], + [ + 'value_id' => '2', + 'file' => 'image_2.jpg', + 'media_type' => 'image', + ], + ], + ], + ], + ]; + $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); + $this->productMock->expects($this->once())->method('getData')->willReturn(null); + $this->dataPersistorMock->expects($this->once()) + ->method('get') + ->with($this->identicalTo('catalog_product')) + ->willReturn($product); + + $this->assertSame($product['product']['media_gallery'], $this->gallery->getImages()); + } + + /** + * Test get image value from data persistor in case it's absent in product from registry. + * + * @return void + */ + public function testGetImageValue() + { + $product = [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + 'value_id' => '1', + 'file' => 'image_1.jpg', + 'media_type' => 'image', + ], + ], + 'small' => 'testSmallImage', + 'thumbnail' => 'testThumbnail' + ] + ]; + $this->dataPersistorMock->expects($this->once()) + ->method('get') + ->with($this->identicalTo('catalog_product')) + ->willReturn($product); + $this->assertSame($product['product']['small'], $this->gallery->getImageValue('small')); + } + public function testGetDataObject() { $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php index 55402eb1f6fd2..3f388d00eaf9f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php @@ -16,6 +16,11 @@ class PriceBoxTagsTest extends \PHPUnit\Framework\TestCase */ private $priceCurrencyInterface; + /** + * @var \Magento\Directory\Model\Currency | \PHPUnit_Framework_MockObject_MockObject + */ + private $currency; + /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface | \PHPUnit_Framework_MockObject_MockObject */ @@ -46,6 +51,9 @@ protected function setUp() $this->priceCurrencyInterface = $this->getMockBuilder( \Magento\Framework\Pricing\PriceCurrencyInterface::class )->getMock(); + $this->currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) + ->disableOriginalConstructor() + ->getMock(); $this->timezoneInterface = $this->getMockBuilder( \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class )->getMock(); @@ -82,7 +90,7 @@ protected function setUp() public function testAfterGetCacheKey() { $date = date('Ymd'); - $currencySymbol = '$'; + $currencyCode = 'USD'; $result = 'result_string'; $billingAddress = ['billing_address']; $shippingAddress = ['shipping_address']; @@ -95,7 +103,7 @@ public function testAfterGetCacheKey() '-', [ $result, - $currencySymbol, + $currencyCode, $date, $scopeId, $customerGroupId, @@ -104,7 +112,8 @@ public function testAfterGetCacheKey() ); $priceBox = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class) ->disableOriginalConstructor()->getMock(); - $this->priceCurrencyInterface->expects($this->once())->method('getCurrencySymbol')->willReturn($currencySymbol); + $this->priceCurrencyInterface->expects($this->once())->method('getCurrency')->willReturn($this->currency); + $this->currency->expects($this->once())->method('getCode')->willReturn($currencyCode); $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMock(); $this->scopeResolverInterface->expects($this->any())->method('getScope')->willReturn($scope); $scope->expects($this->any())->method('getId')->willReturn($scopeId); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php index 8932d77a81247..0cff8b2d0f207 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Rss/LinkTest.php @@ -72,6 +72,9 @@ public function testIsRssAllowed($isAllowed) $this->assertEquals($isAllowed, $this->link->isRssAllowed()); } + /** + * @return array + */ public function isRssAllowedDataProvider() { return [ @@ -98,6 +101,9 @@ public function testIsTopCategory($isTop, $categoryLevel) $this->assertEquals($isTop, $this->link->isTopCategory()); } + /** + * @return array + */ public function isTopCategoryDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/AbstractProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/AbstractProductTest.php index 11e7879612bd1..d20af905a7a1f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/AbstractProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/AbstractProductTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Block\Product; /** @@ -44,7 +42,10 @@ class AbstractProductTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->productContextMock = $this->createPartialMock(\Magento\Catalog\Block\Product\Context::class, ['getLayout', 'getStockRegistry', 'getImageBuilder']); + $this->productContextMock = $this->createPartialMock( + \Magento\Catalog\Block\Product\Context::class, + ['getLayout', 'getStockRegistry', 'getImageBuilder'] + ); $arrayUtilsMock = $this->createMock(\Magento\Framework\Stdlib\ArrayUtils::class); $this->layoutMock = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getBlock']); $this->stockRegistryMock = $this->getMockForAbstractClass( @@ -116,11 +117,12 @@ public function testGetProductPriceHtml() $priceRenderBlock->expects($this->once()) ->method('render') - ->will($this->returnValue($expectedPriceHtml)); + ->willReturn($expectedPriceHtml); - $this->assertEquals($expectedPriceHtml, $this->block->getProductPriceHtml( - $product, 'price_code', 'zone_code' - )); + $this->assertEquals( + $expectedPriceHtml, + $this->block->getProductPriceHtml($product, 'price_code', 'zone_code') + ); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php index e0b5d6ef3992a..8612858960c8c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageBuilderTest.php @@ -5,49 +5,43 @@ */ namespace Magento\Catalog\Test\Unit\Block\Product; +use Magento\Catalog\Block\Product\ImageBuilder; +use Magento\Catalog\Block\Product\ImageFactory; +use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Product; + class ImageBuilderTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Block\Product\ImageBuilder + * @var ImageBuilder */ - protected $model; + private $model; /** * @var \Magento\Catalog\Helper\ImageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $helperFactory; + private $helperFactory; /** - * @var \Magento\Catalog\Block\Product\ImageFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ImageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $imageFactory; + private $imageFactory; protected function setUp() { - $this->helperFactory = $this->getMockBuilder(\Magento\Catalog\Helper\ImageFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->imageFactory = $this->getMockBuilder(\Magento\Catalog\Block\Product\ImageFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->model = new \Magento\Catalog\Block\Product\ImageBuilder( - $this->helperFactory, - $this->imageFactory - ); + $this->helperFactory = $this->createPartialMock(\Magento\Catalog\Helper\ImageFactory::class, ['create']); + + $this->imageFactory = $this->createPartialMock(ImageFactory::class, ['create']); + + $this->model = new ImageBuilder($this->helperFactory, $this->imageFactory); } public function testSetProduct() { - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); + $productMock = $this->createMock(Product::class); $this->assertInstanceOf( - \Magento\Catalog\Block\Product\ImageBuilder::class, + ImageBuilder::class, $this->model->setProduct($productMock) ); } @@ -57,7 +51,7 @@ public function testSetImageId() $imageId = 'test_image_id'; $this->assertInstanceOf( - \Magento\Catalog\Block\Product\ImageBuilder::class, + ImageBuilder::class, $this->model->setImageId($imageId) ); } @@ -68,7 +62,7 @@ public function testSetAttributes() 'name' => 'value', ]; $this->assertInstanceOf( - \Magento\Catalog\Block\Product\ImageBuilder::class, + ImageBuilder::class, $this->model->setAttributes($attributes) ); } @@ -81,13 +75,9 @@ public function testCreate($data, $expected) { $imageId = 'test_image_id'; - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); + $productMock = $this->createMock(Product::class); - $helperMock = $this->getMockBuilder(\Magento\Catalog\Helper\Image::class) - ->disableOriginalConstructor() - ->getMock(); + $helperMock = $this->createMock(Image::class); $helperMock->expects($this->once()) ->method('init') ->with($productMock, $imageId) @@ -116,9 +106,7 @@ public function testCreate($data, $expected) ->method('create') ->willReturn($helperMock); - $imageMock = $this->getMockBuilder(\Magento\Catalog\Block\Product\Image::class) - ->disableOriginalConstructor() - ->getMock(); + $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class); $this->imageFactory->expects($this->once()) ->method('create') @@ -131,63 +119,231 @@ public function testCreate($data, $expected) $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create()); } + /** + * Check if custom attributes will be overridden when builder used few times + * @param array $data + * @dataProvider createMultipleCallsDataProvider + */ + public function testCreateMultipleCalls($data) + { + list ($firstCall, $secondCall) = array_values($data); + + $imageId = 'test_image_id'; + + $productMock = $this->createMock(Product::class); + + $helperMock = $this->createMock(Image::class); + $helperMock->expects($this->exactly(2)) + ->method('init') + ->with($productMock, $imageId) + ->willReturnSelf(); + + $helperMock->expects($this->exactly(2)) + ->method('getFrame') + ->willReturnOnConsecutiveCalls($firstCall['data']['frame'], $secondCall['data']['frame']); + $helperMock->expects($this->exactly(2)) + ->method('getUrl') + ->willReturnOnConsecutiveCalls($firstCall['data']['url'], $secondCall['data']['url']); + $helperMock->expects($this->exactly(4)) + ->method('getWidth') + ->willReturnOnConsecutiveCalls( + $firstCall['data']['width'], + $firstCall['data']['width'], + $secondCall['data']['width'], + $secondCall['data']['width'] + ); + $helperMock->expects($this->exactly(4)) + ->method('getHeight') + ->willReturnOnConsecutiveCalls( + $firstCall['data']['height'], + $firstCall['data']['height'], + $secondCall['data']['height'], + $secondCall['data']['height'] + ); + $helperMock->expects($this->exactly(2)) + ->method('getLabel') + ->willReturnOnConsecutiveCalls($firstCall['data']['label'], $secondCall['data']['label']); + $helperMock->expects($this->exactly(2)) + ->method('getResizedImageInfo') + ->willReturnOnConsecutiveCalls($firstCall['data']['imagesize'], $secondCall['data']['imagesize']); + $this->helperFactory->expects($this->exactly(2)) + ->method('create') + ->willReturn($helperMock); + + $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class); + + $this->imageFactory->expects($this->at(0)) + ->method('create') + ->with($firstCall['expected']) + ->willReturn($imageMock); + + $this->imageFactory->expects($this->at(1)) + ->method('create') + ->with($secondCall['expected']) + ->willReturn($imageMock); + + $this->model->setProduct($productMock); + $this->model->setImageId($imageId); + $this->model->setAttributes($firstCall['data']['custom_attributes']); + + $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create()); + + $this->model->setProduct($productMock); + $this->model->setImageId($imageId); + $this->model->setAttributes($secondCall['data']['custom_attributes']); + $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create()); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + return [ + $this->getTestDataWithoutAttributes(), + $this->getTestDataWithAttributes(), + ]; + } + /** * @return array */ - public function createDataProvider() + public function createMultipleCallsDataProvider(): array { return [ [ + [ + 'without_attributes' => $this->getTestDataWithoutAttributes(), + 'with_attributes' => $this->getTestDataWithAttributes(), + ], + ], + [ + [ + 'with_attributes' => $this->getTestDataWithAttributes(), + 'without_attributes' => $this->getTestDataWithoutAttributes(), + ], + ], + ]; + } + + /** + * @return array + */ + private function getTestDataWithoutAttributes(): array + { + return [ + 'data' => [ + 'frame' => 0, + 'url' => 'test_url_1', + 'width' => 100, + 'height' => 100, + 'label' => 'test_label', + 'custom_attributes' => [], + 'imagesize' => [100, 100], + ], + 'expected' => [ 'data' => [ - 'frame' => 0, - 'url' => 'test_url_1', + 'template' => 'Magento_Catalog::product/image_with_borders.phtml', + 'image_url' => 'test_url_1', 'width' => 100, 'height' => 100, 'label' => 'test_label', - 'custom_attributes' => [], - 'imagesize' => [100, 100], + 'ratio' => 1, + 'custom_attributes' => '', + 'resized_image_width' => 100, + 'resized_image_height' => 100, + 'product_id' => null ], - 'expected' => [ - 'data' => [ - 'template' => 'Magento_Catalog::product/image_with_borders.phtml', - 'image_url' => 'test_url_1', - 'width' => 100, - 'height' => 100, - 'label' => 'test_label', - 'ratio' => 1, - 'custom_attributes' => '', - 'resized_image_width' => 100, - 'resized_image_height' => 100, - ], + ], + ]; + } + + /** + * @return array + */ + private function getTestDataWithAttributes(): array + { + return [ + 'data' => [ + 'frame' => 1, + 'url' => 'test_url_2', + 'width' => 100, + 'height' => 50, + 'label' => 'test_label_2', + 'custom_attributes' => [ + 'name_1' => 'value_1', + 'name_2' => 'value_2', ], + 'imagesize' => [120, 70], ], - [ + 'expected' => [ 'data' => [ - 'frame' => 1, - 'url' => 'test_url_2', + 'template' => 'Magento_Catalog::product/image.phtml', + 'image_url' => 'test_url_2', 'width' => 100, 'height' => 50, 'label' => 'test_label_2', - 'custom_attributes' => [ - 'name_1' => 'value_1', - 'name_2' => 'value_2', - ], - 'imagesize' => [120, 70], - ], - 'expected' => [ - 'data' => [ - 'template' => 'Magento_Catalog::product/image.phtml', - 'image_url' => 'test_url_2', - 'width' => 100, - 'height' => 50, - 'label' => 'test_label_2', - 'ratio' => 0.5, - 'custom_attributes' => 'name_1="value_1" name_2="value_2"', - 'resized_image_width' => 120, - 'resized_image_height' => 70, - ], + 'ratio' => 0.5, + 'custom_attributes' => 'name_1="value_1" name_2="value_2"', + 'resized_image_width' => 120, + 'resized_image_height' => 70, + 'product_id' => null ], ], ]; } + + /** + * @param array $data + * @param array $expected + * @dataProvider createDataProvider + */ + public function testCreateWithSimpleProduct($data, $expected) + { + $imageId = 'test_image_id'; + + $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); + $simpleProductMock = $this->createMock(\Magento\Catalog\Model\Product::class); + + $helperMock = $this->createMock(\Magento\Catalog\Helper\Image::class); + $helperMock->expects($this->once()) + ->method('init') + ->with($simpleProductMock, $imageId) + ->willReturnSelf(); + $helperMock->expects($this->once()) + ->method('getFrame') + ->willReturn($data['frame']); + $helperMock->expects($this->once()) + ->method('getUrl') + ->willReturn($data['url']); + $helperMock->expects($this->exactly(2)) + ->method('getWidth') + ->willReturn($data['width']); + $helperMock->expects($this->exactly(2)) + ->method('getHeight') + ->willReturn($data['height']); + $helperMock->expects($this->once()) + ->method('getLabel') + ->willReturn($data['label']); + $helperMock->expects($this->once()) + ->method('getResizedImageInfo') + ->willReturn($data['imagesize']); + + $this->helperFactory->expects($this->once()) + ->method('create') + ->willReturn($helperMock); + + $imageMock = $this->createMock(\Magento\Catalog\Block\Product\Image::class); + + $this->imageFactory->expects($this->once()) + ->method('create') + ->with($expected) + ->willReturn($imageMock); + + $this->model->setProduct($productMock); + $this->model->setImageId($imageId); + $this->model->setAttributes($data['custom_attributes']); + + $this->assertInstanceOf(\Magento\Catalog\Block\Product\Image::class, $this->model->create()); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php index b42357db89041..c53168b7bf7c3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php @@ -192,7 +192,7 @@ public function testGetIdentities() ->will($this->returnValue($this->toolbarMock)); $this->assertEquals( - [$productTag, $categoryTag], + [$categoryTag, $productTag], $this->block->getIdentities() ); $this->assertEquals( @@ -217,7 +217,7 @@ public function testGetAddToCartPostParams() ->will($this->returnValue(true)); $this->cartHelperMock->expects($this->any()) ->method('getAddUrl') - ->with($this->equalTo($this->productMock), $this->equalTo([])) + ->with($this->equalTo($this->productMock), $this->equalTo(['_escape' => false])) ->will($this->returnValue($url)); $this->productMock->expects($this->once()) ->method('getEntityId') diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php index 1d927a6e04ef5..deb84b7b2d3c4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/RelatedTest.php @@ -72,6 +72,9 @@ public function testCanItemsAddToCart($isComposite, $isSaleable, $hasRequiredOpt ); } + /** + * @return array + */ public function canItemsAddToCartDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php index dce0be8e62df3..1a6c25bbce2d0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php @@ -18,6 +18,11 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase */ protected $model; + /** + * @var \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer | \PHPUnit_Framework_MockObject_MockObject + */ + private $memorizer; + /** * @var \Magento\Framework\Url | \PHPUnit_Framework_MockObject_MockObject */ @@ -62,6 +67,16 @@ protected function setUp() 'getLimit', 'getCurrentPage' ]); + $this->memorizer = $this->createPartialMock( + \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer::class, + [ + 'getDirection', + 'getOrder', + 'getMode', + 'getLimit', + 'isMemorizingAllowed', + ] + ); $this->layout = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getChildName', 'getBlock']); $this->pagerBlock = $this->createPartialMock(\Magento\Theme\Block\Html\Pager::class, [ 'setUseContainer', @@ -116,6 +131,7 @@ protected function setUp() 'context' => $context, 'catalogConfig' => $this->catalogConfig, 'toolbarModel' => $this->model, + 'toolbarMemorizer' => $this->memorizer, 'urlEncoder' => $this->urlEncoder, 'productListHelper' => $this->productListHelper ] @@ -155,7 +171,7 @@ public function testGetPagerEncodedUrl() public function testGetCurrentOrder() { $order = 'price'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getOrder') ->will($this->returnValue($order)); $this->catalogConfig->expects($this->once()) @@ -169,7 +185,7 @@ public function testGetCurrentDirection() { $direction = 'desc'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getDirection') ->will($this->returnValue($direction)); @@ -183,7 +199,7 @@ public function testGetCurrentMode() $this->productListHelper->expects($this->once()) ->method('getAvailableViewMode') ->will($this->returnValue(['list' => 'List'])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); @@ -216,6 +232,9 @@ public function testSetModes($mode, $expected) $this->assertEquals($expected, $block->getModes()); } + /** + * @return array + */ public function setModesDataProvider() { return [ @@ -229,11 +248,11 @@ public function testGetLimit() $mode = 'list'; $limit = 10; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->productListHelper->expects($this->once()) @@ -263,7 +282,7 @@ public function testGetPagerHtml() $this->productListHelper->expects($this->exactly(2)) ->method('getAvailableLimit') ->will($this->returnValue([10 => 10, 20 => 20])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->pagerBlock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php new file mode 100644 index 0000000000000..2310b1f8b871c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Block\Product\View; + +use \PHPUnit\Framework\TestCase; +use \Magento\Framework\Phrase; +use \Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use \Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use \Magento\Catalog\Model\Product; +use \Magento\Framework\View\Element\Template\Context; +use \Magento\Framework\Registry; +use \Magento\Framework\Pricing\PriceCurrencyInterface; +use \Magento\Catalog\Block\Product\View\Attributes as AttributesBlock; + +/** + * Test class for \Magento\Catalog\Block\Product\View\Attributes + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AttributesTest extends TestCase +{ + /** + * @var \Magento\Framework\Phrase + */ + private $phrase; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Eav\Model\Entity\Attribute\AbstractAttribute + */ + private $attribute; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend + */ + private $frontendAttribute; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product + */ + private $product; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\View\Element\Template\Context + */ + private $context; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Registry + */ + private $registry; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Pricing\PriceCurrencyInterface + */ + private $priceCurrencyInterface; + + /** + * @var \Magento\Catalog\Block\Product\View\Attributes + */ + private $attributesBlock; + + protected function setUp() + { + $this->attribute = $this + ->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attribute + ->expects($this->any()) + ->method('getIsVisibleOnFront') + ->willReturn(true); + $this->attribute + ->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('phrase'); + $this->frontendAttribute = $this + ->getMockBuilder(AbstractFrontend::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attribute + ->expects($this->any()) + ->method('getFrontendInput') + ->willReturn('phrase'); + $this->attribute + ->expects($this->any()) + ->method('getFrontend') + ->willReturn($this->frontendAttribute); + $this->product = $this + ->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->product + ->expects($this->any()) + ->method('getAttributes') + ->willReturn([$this->attribute]); + $this->product + ->expects($this->any()) + ->method('hasData') + ->willReturn(true); + $this->context = $this + ->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->registry = $this + ->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->registry + ->expects($this->any()) + ->method('registry') + ->willReturn($this->product); + $this->priceCurrencyInterface = $this + ->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributesBlock = new AttributesBlock( + $this->context, + $this->registry, + $this->priceCurrencyInterface + ); + } + + /** + * Get attribute with no value phrase + * + * @param string $phrase + * @return void + * @dataProvider noValueProvider + */ + public function testGetAttributeNoValue(string $phrase) + { + $this->frontendAttribute->method('getValue') + ->willReturn($phrase); + $attributes = $this->attributesBlock->getAdditionalData(); + $this->assertArrayNotHasKey('phrase', $attributes); + } + + /** + * No value data provider + * + * @return array + */ + public function noValueProvider(): array + { + return [[' '], ['']]; + } + + /** + * @return void + */ + public function testGetAttributeHasValue() + { + $this->phrase = __('Yes'); + $this->frontendAttribute + ->expects($this->any()) + ->method('getValue') + ->willReturn($this->phrase); + $attributes = $this->attributesBlock->getAdditionalData(); + $this->assertNotTrue(empty($attributes['phrase'])); + $this->assertNotTrue(empty($attributes['phrase']['value'])); + $this->assertEquals('Yes', $attributes['phrase']['value']); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php new file mode 100644 index 0000000000000..102b810b0e0a8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Block\Product\View; + +use Magento\Catalog\Block\Product\Context; +use Magento\Catalog\Block\Product\View\Gallery; +use Magento\Catalog\Block\Product\View\GalleryOptions; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\View\Config; +use Magento\Framework\Config\View; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GalleryOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GalleryOptions + */ + private $model; + + /** + * @var Gallery|\PHPUnit_Framework_MockObject_MockObject + */ + private $gallery; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var View|\PHPUnit_Framework_MockObject_MockObject + */ + private $configView; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $viewConfig; + + /** + * @var Escaper + */ + private $escaper; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->escaper = $objectManager->getObject(Escaper::class); + $this->configView = $this->createMock(View::class); + + $this->viewConfig = $this->createConfiguredMock( + Config::class, + [ + 'getViewConfig' => $this->configView + ] + ); + + $this->context = $this->createConfiguredMock( + Context::class, + [ + 'getEscaper' => $this->escaper, + 'getViewConfig' => $this->viewConfig + ] + ); + + $this->gallery = $this->createMock(Gallery::class); + + $this->jsonSerializer = $objectManager->getObject( + Json::class + ); + + $this->model = $objectManager->getObject(GalleryOptions::class, [ + 'context' => $this->context, + 'jsonSerializer' => $this->jsonSerializer, + 'gallery' => $this->gallery + ]); + } + + public function testGetOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/nav', 'thumbs'], + ['Magento_Catalog', 'gallery/loop', false], + ['Magento_Catalog', 'gallery/keyboard', true], + ['Magento_Catalog', 'gallery/arrows', true], + ['Magento_Catalog', 'gallery/caption', false], + ['Magento_Catalog', 'gallery/allowfullscreen', true], + ['Magento_Catalog', 'gallery/navdir', 'horizontal'], + ['Magento_Catalog', 'gallery/navarrows', true], + ['Magento_Catalog', 'gallery/navtype', 'slides'], + ['Magento_Catalog', 'gallery/thumbmargin', '5'], + ['Magento_Catalog', 'gallery/transition/effect', 'slide'], + ['Magento_Catalog', 'gallery/transition/duration', '500'], + ]; + + $imageAttributesMap = [ + ['product_page_image_medium','height',null, 100], + ['product_page_image_medium','width',null, 200], + ['product_page_image_small','height',null, 300], + ['product_page_image_small','width',null, 400] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + $this->gallery->expects($this->any()) + ->method('getImageAttribute') + ->will($this->returnValueMap($imageAttributesMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertSame('thumbs', $decodedJson['nav']); + $this->assertSame(false, $decodedJson['loop']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['arrows']); + $this->assertSame(false, $decodedJson['showCaption']); + $this->assertSame(true, $decodedJson['allowfullscreen']); + $this->assertSame('horizontal', $decodedJson['navdir']); + $this->assertSame(true, $decodedJson['navarrows']); + $this->assertSame('slides', $decodedJson['navtype']); + $this->assertSame(5, $decodedJson['thumbmargin']); + $this->assertSame('slide', $decodedJson['transition']); + $this->assertSame(500, $decodedJson['transitionduration']); + $this->assertSame(100, $decodedJson['height']); + $this->assertSame(200, $decodedJson['width']); + $this->assertSame(300, $decodedJson['thumbheight']); + $this->assertSame(400, $decodedJson['thumbwidth']); + } + + public function testGetFSOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/nav', false], + ['Magento_Catalog', 'gallery/fullscreen/loop', true], + ['Magento_Catalog', 'gallery/fullscreen/keyboard', true], + ['Magento_Catalog', 'gallery/fullscreen/arrows', false], + ['Magento_Catalog', 'gallery/fullscreen/caption', true], + ['Magento_Catalog', 'gallery/fullscreen/navdir', 'vertical'], + ['Magento_Catalog', 'gallery/fullscreen/navarrows', false], + ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], + ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + //Note, this tests the special case for nav variable set to false. It + //Should not be converted to boolean. + $this->assertSame('false', $decodedJson['nav']); + $this->assertSame(true, $decodedJson['loop']); + $this->assertSame(false, $decodedJson['arrows']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['showCaption']); + $this->assertSame('vertical', $decodedJson['navdir']); + $this->assertSame(false, $decodedJson['navarrows']); + $this->assertSame(10, $decodedJson['thumbmargin']); + $this->assertSame('thumbs', $decodedJson['navtype']); + $this->assertSame('dissolve', $decodedJson['transition']); + $this->assertSame(300, $decodedJson['transitionduration']); + } + + public function testGetOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } + + public function testGetFSOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/keyboard', false], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('keyboard', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php index ec7779fcbb781..ae5176e78df7b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php @@ -77,6 +77,88 @@ protected function mockContext() ->willReturn($this->registry); } + public function testGetGalleryImagesJsonWithLabel() + { + $this->prepareGetGalleryImagesJsonMocks(); + $json = $this->model->getGalleryImagesJson(); + $decodedJson = json_decode($json, true); + $this->assertEquals('product_page_image_small_url', $decodedJson[0]['thumb']); + $this->assertEquals('product_page_image_medium_url', $decodedJson[0]['img']); + $this->assertEquals('product_page_image_large_url', $decodedJson[0]['full']); + $this->assertEquals('test_label', $decodedJson[0]['caption']); + $this->assertEquals('2', $decodedJson[0]['position']); + $this->assertEquals(false, $decodedJson[0]['isMain']); + $this->assertEquals('test_media_type', $decodedJson[0]['type']); + $this->assertEquals('test_video_url', $decodedJson[0]['videoUrl']); + } + + public function testGetGalleryImagesJsonWithoutLabel() + { + $this->prepareGetGalleryImagesJsonMocks(false); + $json = $this->model->getGalleryImagesJson(); + $decodedJson = json_decode($json, true); + $this->assertEquals('test_product_name', $decodedJson[0]['caption']); + } + + /** + * @param bool $hasLabel + */ + private function prepareGetGalleryImagesJsonMocks($hasLabel = true) + { + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $productTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + ->disableOriginalConstructor() + ->getMock(); + $productTypeMock->expects($this->any()) + ->method('getStoreFilter') + ->with($productMock) + ->willReturn($storeMock); + + $productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($productTypeMock); + $productMock->expects($this->any()) + ->method('getMediaGalleryImages') + ->willReturn($this->getImagesCollectionWithPopulatedDataObject($hasLabel)); + $productMock->expects($this->any()) + ->method('getName') + ->willReturn('test_product_name'); + + $this->registry->expects($this->any()) + ->method('registry') + ->with('product') + ->willReturn($productMock); + + $this->imageHelper->expects($this->any()) + ->method('init') + ->willReturnMap([ + [$productMock, 'product_page_image_small', [], $this->imageHelper], + [$productMock, 'product_page_image_medium_no_frame', [], $this->imageHelper], + [$productMock, 'product_page_image_large_no_frame', [], $this->imageHelper], + ]) + ->willReturnSelf(); + $this->imageHelper->expects($this->any()) + ->method('setImageFile') + ->with('test_file') + ->willReturnSelf(); + $this->imageHelper->expects($this->at(2)) + ->method('getUrl') + ->willReturn('product_page_image_small_url'); + $this->imageHelper->expects($this->at(5)) + ->method('getUrl') + ->willReturn('product_page_image_medium_url'); + $this->imageHelper->expects($this->at(8)) + ->method('getUrl') + ->willReturn('product_page_image_large_url'); + } + public function testGetGalleryImages() { $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) @@ -154,4 +236,30 @@ private function getImagesCollection() return $collectionMock; } + + /** + * @return \Magento\Framework\Data\Collection + */ + private function getImagesCollectionWithPopulatedDataObject($hasLabel) + { + $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + $items = [ + new \Magento\Framework\DataObject([ + 'file' => 'test_file', + 'label' => ($hasLabel ? 'test_label' : ''), + 'position' => '2', + 'media_type' => 'external-test_media_type', + "video_url" => 'test_video_url' + ]), + ]; + + $collectionMock->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator($items)); + + return $collectionMock; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php index 51bda60b419d0..bcb5540f14817 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ViewTest.php @@ -6,8 +6,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Block\Product; class ViewTest extends \PHPUnit\Framework\TestCase @@ -67,23 +65,19 @@ public function testGetIdentities() { $productTags = ['cat_p_1']; $product = $this->createMock(\Magento\Catalog\Model\Product::class); - $category = $this->createMock(\Magento\Catalog\Model\Category::class); $product->expects($this->once()) ->method('getIdentities') ->will($this->returnValue($productTags)); - $category->expects($this->once()) - ->method('getId') - ->will($this->returnValue(1)); $this->registryMock->expects($this->any()) ->method('registry') - ->will($this->returnValueMap( - [ - ['product', $product], - ['current_category', $category], - ] - ) - ); - $this->assertEquals(['cat_p_1', 'cat_c_1'], $this->view->getIdentities()); + ->will( + $this->returnValueMap( + [ + ['product', $product], + ] + ) + ); + $this->assertEquals($productTags, $this->view->getIdentities()); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php index e2e0fa2f27667..129dea37b185e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/NewProductsTest.php @@ -91,6 +91,9 @@ protected function setUp() ); } + /** + * @return array + */ public function isAllowedDataProvider() { return [ @@ -108,6 +111,9 @@ public function testIsAllowed($configValue, $expectedResult) $this->assertEquals($expectedResult, $this->block->isAllowed()); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function getItemMock() { $methods = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php index 6509aa138802e..3c9f19d61d16a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Rss/Product/SpecialTest.php @@ -167,6 +167,9 @@ public function testGetRssData() ); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function getItemMock() { $item = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) diff --git a/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php b/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php deleted file mode 100644 index 457ba7b94529b..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Console/Command/ImagesResizeCommandTest.php +++ /dev/null @@ -1,211 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Test\Unit\Console\Command; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Console\Command\ImagesResizeCommand; -use Magento\Catalog\Model\Product\Image\Cache as ImageCache; -use Magento\Catalog\Model\Product\Image\CacheFactory as ImageCacheFactory; -use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; -use Magento\Framework\App\State as AppState; -use Magento\Framework\Exception\NoSuchEntityException; -use Symfony\Component\Console\Tester\CommandTester; -use \Magento\Framework\App\Area; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ImagesResizeCommandTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ImagesResizeCommand - */ - protected $command; - - /** - * @var AppState | \PHPUnit_Framework_MockObject_MockObject - */ - protected $appState; - - /** - * @var ProductCollectionFactory | \PHPUnit_Framework_MockObject_MockObject - */ - protected $productCollectionFactory; - - /** - * @var ProductCollection | \PHPUnit_Framework_MockObject_MockObject - */ - protected $productCollection; - - /** - * @var ProductRepositoryInterface | \PHPUnit_Framework_MockObject_MockObject - */ - protected $productRepository; - - /** - * @var ImageCacheFactory | \PHPUnit_Framework_MockObject_MockObject - */ - protected $imageCacheFactory; - - /** - * @var ImageCache | \PHPUnit_Framework_MockObject_MockObject - */ - protected $imageCache; - - protected function setUp() - { - $this->appState = $this->getMockBuilder(\Magento\Framework\App\State::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - - $this->prepareProductCollection(); - $this->prepareImageCache(); - - $this->command = new ImagesResizeCommand( - $this->appState, - $this->productCollectionFactory, - $this->productRepository, - $this->imageCacheFactory - ); - } - - public function testExecuteNoProducts() - { - $this->appState->expects($this->once()) - ->method('setAreaCode') - ->with(Area::AREA_GLOBAL) - ->willReturnSelf(); - - $this->productCollection->expects($this->once()) - ->method('getAllIds') - ->willReturn([]); - - $commandTester = new CommandTester($this->command); - $commandTester->execute([]); - - $this->assertContains( - 'No product images to resize', - $commandTester->getDisplay() - ); - } - - public function testExecute() - { - $productsIds = [1, 2]; - - $this->appState->expects($this->once()) - ->method('setAreaCode') - ->with(Area::AREA_GLOBAL) - ->willReturnSelf(); - - $this->productCollection->expects($this->once()) - ->method('getAllIds') - ->willReturn($productsIds); - - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->productRepository->expects($this->at(0)) - ->method('getById') - ->with($productsIds[0]) - ->willReturn($productMock); - $this->productRepository->expects($this->at(1)) - ->method('getById') - ->with($productsIds[1]) - ->willThrowException(new NoSuchEntityException()); - - $this->imageCache->expects($this->exactly(count($productsIds) - 1)) - ->method('generate') - ->with($productMock) - ->willReturnSelf(); - - $commandTester = new CommandTester($this->command); - $commandTester->execute([]); - - $this->assertContains( - 'Product images resized successfully', - $commandTester->getDisplay() - ); - } - - public function testExecuteWithException() - { - $productsIds = [1]; - $exceptionMessage = 'Test exception text'; - - $this->appState->expects($this->once()) - ->method('setAreaCode') - ->with(Area::AREA_GLOBAL) - ->willReturnSelf(); - - $this->productCollection->expects($this->once()) - ->method('getAllIds') - ->willReturn($productsIds); - - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->productRepository->expects($this->exactly(count($productsIds))) - ->method('getById') - ->with($productsIds[0]) - ->willReturn($productMock); - - $this->imageCache->expects($this->once()) - ->method('generate') - ->with($productMock) - ->willThrowException(new \Exception($exceptionMessage)); - - $commandTester = new CommandTester($this->command); - $commandTester->execute([]); - - $this->assertContains( - $exceptionMessage, - $commandTester->getDisplay() - ); - } - - protected function prepareProductCollection() - { - $this->productCollectionFactory = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->productCollection = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Product\Collection::class - ) - ->disableOriginalConstructor() - ->getMock(); - - $this->productCollectionFactory->expects($this->any()) - ->method('create') - ->willReturn($this->productCollection); - } - - protected function prepareImageCache() - { - $this->imageCacheFactory = $this->getMockBuilder(\Magento\Catalog\Model\Product\Image\CacheFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->imageCache = $this->getMockBuilder(\Magento\Catalog\Model\Product\Image\Cache::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->imageCacheFactory->expects($this->any()) - ->method('create') - ->willReturn($this->imageCache); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php index af1ded6987196..2cae2c07cc85a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php @@ -42,8 +42,9 @@ protected function setUp() false, true, true, - ['getParam', 'getPost'] + ['getParam', 'getPost', 'isPost'] ); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $auth = $this->createPartialMock(\Magento\Backend\Model\Auth::class, ['getAuthStorage']); $this->authStorage = $this->createPartialMock( \Magento\Backend\Model\Auth\StorageInterface::class, @@ -71,7 +72,7 @@ protected function setUp() false, true, true, - ['addSuccess'] + ['addSuccessMessage'] ); $this->categoryRepository = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); $context->expects($this->any()) diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php index 07dacae7298cf..e2cd01fd1c23a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Image/UploadTest.php @@ -23,6 +23,9 @@ protected function setUp() $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); } + /** + * @return array + */ public function executeDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php index d729d0ffbdccc..8649dc7990c3c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php @@ -83,9 +83,10 @@ private function fillContext() { $this->request = $this ->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost']) + ->setMethods(['getPost', 'isPost']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->context->expects($this->once())->method('getRequest')->will($this->returnValue($this->request)); $this->messageManager = $this->createMock(ManagerInterface::class); $this->context->expects($this->once())->method('getMessageManager')->willReturn($this->messageManager); @@ -134,13 +135,10 @@ public function testExecuteWithGenericException() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwigConfig]]); $categoryMock->expects($this->once()) ->method('move') - ->willThrowException(new \Exception( - __('Some exception') - )); + ->willThrowException(new \Exception(__('Some exception'))); $this->messageManager->expects($this->once()) ->method('addErrorMessage') ->with(__('There was a category move error.')); @@ -208,7 +206,6 @@ public function testExecuteWithLocaliedException() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwigConfig]]); $this->messageManager->expects($this->once()) ->method('addExceptionMessage'); @@ -236,9 +233,7 @@ public function testExecuteWithLocaliedException() ->willReturn(true); $categoryMock->expects($this->once()) ->method('move') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException( - __($exceptionMessage) - )); + ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__($exceptionMessage))); $this->resultJsonFactoryMock ->expects($this->once()) ->method('create') @@ -280,7 +275,6 @@ public function testSuccessfullCategorySave() ->willReturn($categoryMock); $this->objectManager->expects($this->any()) ->method('get') - ->withConsecutive([Registry::class], [Registry::class], [\Magento\Cms\Model\Wysiwyg\Config::class]) ->willReturnMap([[Registry::class, $registry], [\Magento\Cms\Model\Wysiwyg\Config::class, $wysiwigConfig]]); $this->messageManager->expects($this->once()) ->method('getMessages') diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php new file mode 100644 index 0000000000000..4077ecb11f355 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category; + +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Catalog\Controller\Adminhtml\Category\RefreshPath; +use Magento\Backend\App\Action\Context; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Test for class RefreshPath. + */ +class RefreshPathTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultJsonFactoryMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * {@inheritDoc} + */ + protected function setUp() + { + $this->resultJsonFactoryMock = $this->getMockBuilder(JsonFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create', 'setData']) + ->getMock(); + + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->setMethods(['getRequest']) + ->getMock(); + } + + /** + * Sets object non-public property. + * + * @param mixed $object + * @param string $propertyName + * @param mixed $value + * + * @return void + */ + private function setObjectProperty($object, string $propertyName, $value) + { + $reflectionClass = new \ReflectionClass($object); + $reflectionProperty = $reflectionClass->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + } + + /** + * @return void + */ + public function testExecute() + { + $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2]; + $result = '{"id":3,"path":"1/2/3","parentId":"2"}'; + + $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); + + $refreshPath = $this->getMockBuilder(RefreshPath::class) + ->setMethods(['getRequest', 'create']) + ->setConstructorArgs([ + $this->contextMock, + $this->resultJsonFactoryMock, + ])->getMock(); + + $refreshPath->expects($this->any())->method('getRequest')->willReturn($requestMock); + $requestMock->expects($this->any())->method('getParam')->with('id')->willReturn($value['id']); + + $categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + ->disableOriginalConstructor() + ->setMethods(['getPath', 'getParentId', 'getResource']) + ->getMock(); + + $categoryMock->expects($this->any())->method('getPath')->willReturn($value['path']); + $categoryMock->expects($this->any())->method('getParentId')->willReturn($value['parentId']); + + $categoryResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category::class); + + $objectManagerMock = $this->getMockBuilder(ObjectManager::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->setObjectProperty($refreshPath, '_objectManager', $objectManagerMock); + $this->setObjectProperty($categoryMock, '_resource', $categoryResource); + + $objectManagerMock->expects($this->once()) + ->method('create') + ->with(\Magento\Catalog\Model\Category::class) + ->willReturn($categoryMock); + + $this->resultJsonFactoryMock->expects($this->any())->method('create')->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->any()) + ->method('setData') + ->with($value) + ->willReturn($result); + + $this->assertEquals($result, $refreshPath->execute()); + } + + /** + * @return void + */ + public function testExecuteWithoutCategoryId() + { + $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); + + $refreshPath = $this->getMockBuilder(RefreshPath::class) + ->setMethods(['getRequest', 'create']) + ->setConstructorArgs([ + $this->contextMock, + $this->resultJsonFactoryMock, + ])->getMock(); + + $refreshPath->expects($this->any())->method('getRequest')->willReturn($requestMock); + $requestMock->expects($this->any())->method('getParam')->with('id')->willReturn(null); + + $objectManagerMock = $this->getMockBuilder(ObjectManager::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->setObjectProperty($refreshPath, '_objectManager', $objectManagerMock); + + $objectManagerMock->expects($this->never()) + ->method('create') + ->with(\Magento\Catalog\Model\Category::class) + ->willReturnSelf(); + + $refreshPath->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php index f6a586950afdc..74173dc926d97 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/SaveTest.php @@ -5,8 +5,6 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category; -use Magento\Catalog\Controller\Adminhtml\Category\Save as Model; - /** * Class SaveTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -104,7 +102,7 @@ protected function setUp() false, true, true, - ['addSuccess', 'getMessages'] + ['addSuccessMessage', 'getMessages'] ); $this->save = $this->objectManager->getObject( @@ -394,7 +392,7 @@ public function testExecute($categoryId, $storeId, $parentId) $categoryMock->expects($this->once()) ->method('save'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the category.')); $categoryMock->expects($this->at(1)) ->method('getId') @@ -463,9 +461,25 @@ public function dataProviderExecute() */ public function imagePreprocessingDataProvider() { + $dataWithImage = [ + 'image' => 'path.jpg', + 'name' => 'category', + 'description' => '', + 'parent' => 0 + ]; + $expectedSameAsDataWithImage = $dataWithImage; + + $dataWithoutImage = [ + 'name' => 'category', + 'description' => '', + 'parent' => 0 + ]; + $expectedIfDataWithoutImage = $dataWithoutImage; + $expectedIfDataWithoutImage['image'] = ''; + return [ - [['attribute1' => null, 'attribute2' => 123]], - [['attribute2' => 123]] + 'categoryPostData contains image' => [$dataWithImage, $expectedSameAsDataWithImage], + 'categoryPostData doesn\'t contain image' => [$dataWithoutImage, $expectedIfDataWithoutImage], ]; } @@ -473,8 +487,9 @@ public function imagePreprocessingDataProvider() * @dataProvider imagePreprocessingDataProvider * * @param array $data + * @param array $expected */ - public function testImagePreprocessingWithoutValue($data) + public function testImagePreprocessing($data, $expected) { $eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getEntityType']); @@ -484,49 +499,17 @@ public function testImagePreprocessingWithoutValue($data) $collection = new \Magento\Framework\DataObject(['attribute_collection' => [ new \Magento\Framework\DataObject([ - 'attribute_code' => 'attribute1', + 'attribute_code' => 'image', 'backend' => $imageBackendModel ]), new \Magento\Framework\DataObject([ - 'attribute_code' => 'attribute2', + 'attribute_code' => 'name', 'backend' => new \Magento\Framework\DataObject() - ]) - ]]); - - $eavConfig->expects($this->once()) - ->method('getEntityType') - ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) - ->will($this->returnValue($collection)); - - $model = $this->objectManager->getObject(\Magento\Catalog\Controller\Adminhtml\Category\Save::class, [ - 'eavConfig' => $eavConfig - ]); - - $result = $model->imagePreprocessing($data); - - $this->assertEquals([ - 'attribute1' => false, - 'attribute2' => 123 - ], $result); - } - - public function testImagePreprocessingWithValue() - { - $eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getEntityType']); - - $imageBackendModel = $this->objectManager->getObject( - \Magento\Catalog\Model\Category\Attribute\Backend\Image::class - ); - - $collection = new \Magento\Framework\DataObject(['attribute_collection' => [ - new \Magento\Framework\DataObject([ - 'attribute_code' => 'attribute1', - 'backend' => $imageBackendModel ]), new \Magento\Framework\DataObject([ - 'attribute_code' => 'attribute2', + 'attribute_code' => 'level', 'backend' => new \Magento\Framework\DataObject() - ]) + ]), ]]); $eavConfig->expects($this->once()) @@ -534,18 +517,12 @@ public function testImagePreprocessingWithValue() ->with(\Magento\Catalog\Api\Data\CategoryAttributeInterface::ENTITY_TYPE_CODE) ->will($this->returnValue($collection)); - $model = $this->objectManager->getObject(Model::class, [ + $model = $this->objectManager->getObject(\Magento\Catalog\Controller\Adminhtml\Category\Save::class, [ 'eavConfig' => $eavConfig ]); - $result = $model->imagePreprocessing([ - 'attribute1' => 'somevalue', - 'attribute2' => null - ]); + $result = $model->imagePreprocessing($data); - $this->assertEquals([ - 'attribute1' => 'somevalue', - 'attribute2' => null - ], $result); + $this->assertEquals($expected, $result); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/CategoriesJsonTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/CategoriesJsonTest.php index ea1229688e9d0..2fd4616f71f01 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/CategoriesJsonTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/CategoriesJsonTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category\Widget; /** @@ -69,13 +66,14 @@ protected function setUp() $context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) ->setMethods(['getRequest', 'getResponse', 'getMessageManager', 'getSession']) - ->setConstructorArgs($helper->getConstructArguments( + ->setConstructorArgs( + $helper->getConstructArguments( \Magento\Backend\App\Action\Context::class, [ 'response' => $this->responseMock, 'request' => $this->requestMock, 'view' => $this->viewMock, - 'objectManager' => $this->objectManagerMock + 'objectManager' => $this->objectManagerMock, ] ) ) @@ -106,7 +104,10 @@ protected function setUp() $context->expects($this->once())->method('getResponse')->will($this->returnValue($this->responseMock)); $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $this->controller = new \Magento\Catalog\Controller\Adminhtml\Category\Widget\CategoriesJson( - $context, $layoutFactory, $resultJsonFactory, $this->registryMock + $context, + $layoutFactory, + $resultJsonFactory, + $this->registryMock ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/ChooserTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/ChooserTest.php index 941a65a7e4e07..9de5cc74e1d0b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/ChooserTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/Widget/ChooserTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Category\Widget; /** @@ -64,13 +61,14 @@ protected function setUp() $context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) ->setMethods(['getRequest', 'getResponse', 'getMessageManager', 'getSession']) - ->setConstructorArgs($helper->getConstructArguments( - \Magento\Backend\App\Action\Context::class, + ->setConstructorArgs( + $helper->getConstructArguments( + \Magento\Backend\App\Action\Context::class, [ 'response' => $this->responseMock, 'request' => $this->requestMock, 'view' => $this->viewMock, - 'objectManager' => $this->objectManagerMock + 'objectManager' => $this->objectManagerMock, ] ) ) @@ -99,7 +97,9 @@ protected function setUp() $context->expects($this->once())->method('getRequest')->will($this->returnValue($this->requestMock)); $context->expects($this->once())->method('getResponse')->will($this->returnValue($this->responseMock)); $this->controller = new \Magento\Catalog\Controller\Adminhtml\Category\Widget\Chooser( - $context, $layoutFactory, $resultRawFactory + $context, + $layoutFactory, + $resultRawFactory ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php index 5a977b7934670..0ddd89afeac22 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/EditTest.php @@ -94,7 +94,7 @@ private function prepareContext() $messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->setMethods([]) ->disableOriginalConstructor()->getMock(); - $messageManager->expects($this->any())->method('addError')->willReturn(true); + $messageManager->expects($this->any())->method('addErrorMessage')->willReturn(true); $this->context = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) ->setMethods(['getRequest', 'getObjectManager', 'getMessageManager', 'getResultRedirectFactory']) ->disableOriginalConstructor()->getMock(); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php index c88a008efb19b..9dd0f6ef3ff71 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php @@ -229,6 +229,8 @@ protected function prepareContext() $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], ])); + + $this->request->expects($this->any())->method('isPost')->willReturn(true); } public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() @@ -250,8 +252,8 @@ public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() ['inventory', [], [7]], ])); - $this->messageManager->expects($this->never())->method('addError'); - $this->messageManager->expects($this->never())->method('addException'); + $this->messageManager->expects($this->never())->method('addErrorMessage'); + $this->messageManager->expects($this->never())->method('addExceptionMessage'); $this->object->execute(); } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php deleted file mode 100644 index f493cbc88f18e..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ /dev/null @@ -1,206 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; - -use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; -use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; -use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; -use Magento\Catalog\Model\Product\AttributeSet\Build; -use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; -use Magento\Eav\Api\Data\AttributeSetInterface; -use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; -use Magento\Framework\Filter\FilterManager; -use Magento\Catalog\Helper\Product as ProductHelper; -use Magento\Framework\View\LayoutFactory; -use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; -use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends AttributeTest -{ - /** - * @var BuildFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $buildFactoryMock; - - /** - * @var FilterManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterManagerMock; - - /** - * @var ProductHelper|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productHelperMock; - - /** - * @var AttributeFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $attributeFactoryMock; - - /** - * @var ValidatorFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $validatorFactoryMock; - - /** - * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $groupCollectionFactoryMock; - - /** - * @var LayoutFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $layoutFactoryMock; - - /** - * @var ResultRedirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $redirectMock; - - /** - * @var AttributeSet|\PHPUnit_Framework_MockObject_MockObject - */ - protected $attributeSetMock; - - /** - * @var Build|\PHPUnit_Framework_MockObject_MockObject - */ - protected $builderMock; - - /** - * @var InputTypeValidator|\PHPUnit_Framework_MockObject_MockObject - */ - protected $inputTypeValidatorMock; - - protected function setUp() - { - parent::setUp(); - $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorFactoryMock = $this->getMockBuilder(ValidatorFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->groupCollectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->buildFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->builderMock); - $this->validatorFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->inputTypeValidatorMock); - } - - /** - * {@inheritdoc} - */ - protected function getModel() - { - return $this->objectManager->getObject(Save::class, [ - 'context' => $this->contextMock, - 'attributeLabelCache' => $this->attributeLabelCacheMock, - 'coreRegistry' => $this->coreRegistryMock, - 'resultPageFactory' => $this->resultPageFactoryMock, - 'buildFactory' => $this->buildFactoryMock, - 'filterManager' => $this->filterManagerMock, - 'productHelper' => $this->productHelperMock, - 'attributeFactory' => $this->attributeFactoryMock, - 'validatorFactory' => $this->validatorFactoryMock, - 'groupCollectionFactory' => $this->groupCollectionFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock, - ]); - } - - public function testExecuteWithEmptyData() - { - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn([]); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->redirectMock); - $this->redirectMock->expects($this->any()) - ->method('setPath') - ->willReturnSelf(); - - $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); - } - - public function testExecute() - { - $data = [ - 'new_attribute_set_name' => 'Test attribute set name', - 'frontend_input' => 'test_frontend_input', - ]; - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($data); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->redirectMock); - $this->redirectMock->expects($this->any()) - ->method('setPath') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setEntityTypeId') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setSkeletonId') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setName') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('getAttributeSet') - ->willReturn($this->attributeSetMock); - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['set', null, 1], - ['attribute_code', null, 'test_attribute_code'] - ]); - $this->inputTypeValidatorMock->expects($this->once()) - ->method('getMessages') - ->willReturn([]); - - $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index 6373712066695..750d38f60e13d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; +use Magento\Framework\Serialize\Serializer\FormData; use Magento\Framework\Controller\Result\Json as ResultJson; use Magento\Framework\Controller\Result\JsonFactory as ResultJsonFactory; use Magento\Framework\Escaper; @@ -61,6 +62,11 @@ class ValidateTest extends AttributeTest */ protected $layoutMock; + /** + * @var FormData|\PHPUnit_Framework_MockObject_MockObject + */ + private $formDataSerializer; + protected function setUp() { parent::setUp(); @@ -86,6 +92,9 @@ protected function setUp() ->getMock(); $this->layoutMock = $this->getMockBuilder(LayoutInterface::class) ->getMockForAbstractClass(); + $this->formDataSerializer = $this->getMockBuilder(FormData::class) + ->disableOriginalConstructor() + ->getMock(); $this->contextMock->expects($this->any()) ->method('getObjectManager') @@ -100,25 +109,28 @@ protected function getModel() return $this->objectManager->getObject( Validate::class, [ - 'context' => $this->contextMock, - 'attributeLabelCache' => $this->attributeLabelCacheMock, - 'coreRegistry' => $this->coreRegistryMock, - 'resultPageFactory' => $this->resultPageFactoryMock, - 'resultJsonFactory' => $this->resultJsonFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock, - 'multipleAttributeList' => ['select' => 'option'] + 'context' => $this->contextMock, + 'attributeLabelCache' => $this->attributeLabelCacheMock, + 'coreRegistry' => $this->coreRegistryMock, + 'resultPageFactory' => $this->resultPageFactoryMock, + 'resultJsonFactory' => $this->resultJsonFactoryMock, + 'layoutFactory' => $this->layoutFactoryMock, + 'multipleAttributeList' => ['select' => 'option'], + 'formDataSerializer' => $this->formDataSerializer, ] ); } public function testExecute() { + $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap([ ['frontend_label', null, 'test_frontend_label'], ['attribute_code', null, 'test_attribute_code'], ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['serialized_options', '[]', $serializedOptions], ]); $this->objectManagerMock->expects($this->exactly(2)) ->method('create') @@ -160,6 +172,7 @@ public function testExecute() */ public function testUniqueValidation(array $options, $isError) { + $serializedOptions = '{"key":"value"}'; $countFunctionCalls = ($isError) ? 6 : 5; $this->requestMock->expects($this->exactly($countFunctionCalls)) ->method('getParam') @@ -167,10 +180,15 @@ public function testUniqueValidation(array $options, $isError) ['frontend_label', null, null], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], - ['option', null, $options], - ['message_key', null, Validate::DEFAULT_MESSAGE_KEY] + ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], + ['serialized_options', '[]', $serializedOptions], ]); + $this->formDataSerializer->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn($options); + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($this->attributeMock); @@ -195,59 +213,92 @@ public function testUniqueValidation(array $options, $isError) $this->assertInstanceOf(ResultJson::class, $this->getModel()->execute()); } + /** + * @return array + */ public function provideUniqueData() { return [ 'no values' => [ [ - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] - ], false + 'option' => [ + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], + ], + + ], + false, ], 'valid options' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [2, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [2, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] - ], false + ], + false, ], 'duplicate options' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [1, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [1, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] - ], true + ], + true, ], 'duplicate and deleted' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [1, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [1, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "1", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "1", - "option_2" => "", - ] - ], false + ], + false, + ], + 'empty and deleted' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [2, 0], + "option_2" => ["", ""], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "1", + ], + ], + ], + false, ], ]; } @@ -261,6 +312,7 @@ public function provideUniqueData() */ public function testEmptyOption(array $options, $result) { + $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap([ @@ -268,10 +320,15 @@ public function testEmptyOption(array $options, $result) ['frontend_input', 'select', 'multipleselect'], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], - ['option', null, $options], ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], ]); + $this->formDataSerializer->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn($options); + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($this->attributeMock); @@ -303,25 +360,111 @@ public function provideEmptyOption() return [ 'empty admin scope options' => [ [ - 'value' => [ - "option_0" => [''], + 'option' => [ + 'value' => [ + "option_0" => [''], + ], ], ], (object) [ 'error' => true, 'message' => 'The value of Admin scope can\'t be empty.', - ] + ], ], 'not empty admin scope options' => [ [ - 'value' => [ - "option_0" => ['asdads'], + 'option' => [ + 'value' => [ + "option_0" => ['asdads'], + ], ], ], (object) [ 'error' => false, - ] - ] + ], + ], + 'empty admin scope options and deleted' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [''], + ], + 'delete' => [ + 'option_0' => '1', + ], + ], + ], + (object) [ + 'error' => false, + ], + ], + 'empty admin scope options and not deleted' => [ + [ + 'option' => [ + 'value' => [ + "option_0" => [''], + ], + 'delete' => [ + 'option_0' => '0', + ], + ], + ], + (object) [ + 'error' => true, + 'message' => 'The value of Admin scope can\'t be empty.', + ], + ], ]; } + + /** + * @return void + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithOptionsDataError() + { + $serializedOptions = '{"key":"value"}'; + $message = "The attribute couldn't be validated due to an error. Verify your information and try again. " + . "If the error persists, please try again later."; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], + ]); + + $this->formDataSerializer->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willThrowException(new \InvalidArgumentException('Some exception')); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturnMap([ + [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], + [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + ]); + + $this->attributeMock->expects($this->once()) + ->method('loadByCode') + ->willReturnSelf(); + $this->attributeSetMock->expects($this->never()) + ->method('setEntityTypeId') + ->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->with(json_encode([ + 'error' => true, + 'message' => $message, + ])) + ->willReturnSelf(); + + $this->getModel()->execute(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php index b85b03852b621..3a0b2b4bf7229 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php @@ -9,8 +9,9 @@ use Magento\Catalog\Controller\Adminhtml\Product\Attribute; use Magento\Framework\App\RequestInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Message\ManagerInterface; use Magento\Framework\Registry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\ResultFactory; @@ -20,7 +21,7 @@ class AttributeTest extends \PHPUnit\Framework\TestCase { /** - * @var ObjectManager + * @var ObjectManagerHelper */ protected $objectManager; @@ -54,9 +55,14 @@ class AttributeTest extends \PHPUnit\Framework\TestCase */ protected $resultFactoryMock; + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $messageManager; + protected function setUp() { - $this->objectManager = new ObjectManager($this); + $this->objectManager = new ObjectManagerHelper($this); $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); @@ -74,6 +80,9 @@ protected function setUp() $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->messageManager = $this->getMockBuilder(ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->contextMock->expects($this->any()) ->method('getRequest') @@ -81,6 +90,9 @@ protected function setUp() $this->contextMock->expects($this->any()) ->method('getResultFactory') ->willReturn($this->resultFactoryMock); + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManager); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php index 4113ce636d66b..c71fa90fb02dd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/BuilderTest.php @@ -14,6 +14,9 @@ use Magento\Framework\Registry; use Magento\Cms\Model\Wysiwyg\Config as WysiwygConfig; use Magento\Framework\App\Request\Http; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Model\Product\Type as ProductTypes; /** * Class BuilderTest @@ -67,6 +70,11 @@ class BuilderTest extends \PHPUnit\Framework\TestCase */ protected $storeFactoryMock; + /** + * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $productRepositoryMock; + /** * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -90,9 +98,10 @@ protected function setUp() ->setMethods(['load']) ->getMockForAbstractClass(); - $this->storeFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->storeMock); + $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->setMethods(['getById']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->builder = $this->objectManager->getObject( Builder::class, @@ -102,140 +111,198 @@ protected function setUp() 'registry' => $this->registryMock, 'wysiwygConfig' => $this->wysiwygConfigMock, 'storeFactory' => $this->storeFactoryMock, + 'productRepository' => $this->productRepositoryMock ] ); } public function testBuildWhenProductExistAndPossibleToLoadProduct() { + $productId = 2; + $productType = 'type_id'; + $productStore = 'store'; + $productSet = 3; + $valueMap = [ - ['id', null, 2], - ['store', 0, 'some_store'], - ['type', null, 'type_id'], - ['set', null, 3], - ['store', null, 'store'], + ['id', null, $productId], + ['type', null, $productType], + ['set', null, $productSet], + ['store', 0, $productStore], ]; + $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap($valueMap); - $this->productFactoryMock->expects($this->once()) + + $this->productRepositoryMock->expects($this->any()) + ->method('getById') + ->with($productId, true, $productStore) + ->willReturn($this->productMock); + + $this->storeFactoryMock->expects($this->any()) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once()) - ->method('setStoreId') - ->with('some_store') - ->willReturnSelf(); - $this->productMock->expects($this->never()) - ->method('setTypeId'); - $this->productMock->expects($this->once()) + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->any()) ->method('load') - ->with(2) - ->will($this->returnSelf()); - $this->productMock->expects($this->once()) - ->method('setAttributeSetId') - ->with(3) - ->will($this->returnSelf()); + ->with($productStore) + ->willReturnSelf(); + $registryValueMap = [ ['product', $this->productMock, $this->registryMock], ['current_product', $this->productMock, $this->registryMock], + ['current_store', $this->registryMock, $this->storeMock], ]; + $this->registryMock->expects($this->any()) ->method('register') ->willReturn($registryValueMap); + $this->wysiwygConfigMock->expects($this->once()) ->method('setStoreId') - ->with('store'); + ->with($productStore); + $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); } public function testBuildWhenImpossibleLoadProduct() { + $productId = 2; + $productType = 'type_id'; + $productStore = 'store'; + $productSet = 3; + $valueMap = [ - ['id', null, 15], - ['store', 0, 'some_store'], - ['type', null, 'type_id'], - ['set', null, 3], - ['store', null, 'store'], + ['id', null, $productId], + ['type', null, $productType], + ['set', null, $productSet], + ['store', 0, $productStore], ]; + $this->requestMock->expects($this->any()) ->method('getParam') - ->will($this->returnValueMap($valueMap)); + ->willReturnMap($valueMap); + + $this->productRepositoryMock->expects($this->any()) + ->method('getById') + ->with($productId, true, $productStore) + ->willThrowException(new NoSuchEntityException()); + $this->productFactoryMock->expects($this->once()) ->method('create') - ->willReturn($this->productMock); - $this->productMock->expects($this->once()) - ->method('setStoreId') - ->with('some_store') - ->willReturnSelf(); - $this->productMock->expects($this->once()) + ->will($this->returnValue($this->productMock)); + + $this->productMock->expects($this->any()) + ->method('setData') + ->with('_edit_mode', true); + + $this->productMock->expects($this->any()) ->method('setTypeId') - ->with(\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE) - ->willReturnSelf(); - $this->productMock->expects($this->once()) - ->method('load') - ->with(15) - ->willThrowException(new \Exception()); + ->with(ProductTypes::DEFAULT_TYPE); + + $this->productMock->expects($this->any()) + ->method('setStoreId') + ->with($productStore); + + $this->productMock->expects($this->any()) + ->method('setAttributeSetId') + ->with($productSet); + $this->loggerMock->expects($this->once()) ->method('critical'); - $this->productMock->expects($this->once()) - ->method('setAttributeSetId') - ->with(3) - ->will($this->returnSelf()); + + $this->storeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->any()) + ->method('load') + ->with($productStore) + ->willReturnSelf(); + $registryValueMap = [ ['product', $this->productMock, $this->registryMock], ['current_product', $this->productMock, $this->registryMock], + ['current_store', $this->registryMock, $this->storeMock], ]; + $this->registryMock->expects($this->any()) ->method('register') - ->will($this->returnValueMap($registryValueMap)); + ->willReturn($registryValueMap); + $this->wysiwygConfigMock->expects($this->once()) ->method('setStoreId') - ->with('store'); + ->with($productStore); + $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); } public function testBuildWhenProductNotExist() { + $productId = 0; + $productType = 'type_id'; + $productStore = 'store'; + $productSet = 3; + $valueMap = [ - ['id', null, null], - ['store', 0, 'some_store'], - ['type', null, 'type_id'], - ['set', null, 3], - ['store', null, 'store'], + ['id', null, $productId], + ['type', null, $productType], + ['set', null, $productSet], + ['store', 0, $productStore], ]; + $this->requestMock->expects($this->any()) ->method('getParam') - ->will($this->returnValueMap($valueMap)); + ->willReturnMap($valueMap); + + $this->productRepositoryMock->expects($this->any()) + ->method('getById') + ->with($productId, true, $productStore) + ->willThrowException(new NoSuchEntityException()); + $this->productFactoryMock->expects($this->once()) ->method('create') - ->willReturn($this->productMock); - $this->productMock->expects($this->once()) - ->method('setStoreId') - ->with('some_store') - ->willReturnSelf(); - $productValueMap = [ - ['type_id', $this->productMock], - [\Magento\Catalog\Model\Product\Type::DEFAULT_TYPE, $this->productMock], - ]; + ->will($this->returnValue($this->productMock)); + + $this->productMock->expects($this->any()) + ->method('setData') + ->with('_edit_mode', true); + $this->productMock->expects($this->any()) ->method('setTypeId') - ->willReturnMap($productValueMap); - $this->productMock->expects($this->never()) - ->method('load'); - $this->productMock->expects($this->once()) + ->with($productType); + + $this->productMock->expects($this->any()) + ->method('setStoreId') + ->with($productStore); + + $this->productMock->expects($this->any()) ->method('setAttributeSetId') - ->with(3) - ->will($this->returnSelf()); + ->with($productSet); + + $this->storeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->storeMock); + + $this->storeMock->expects($this->any()) + ->method('load') + ->with($productStore) + ->willReturnSelf(); + $registryValueMap = [ ['product', $this->productMock, $this->registryMock], ['current_product', $this->productMock, $this->registryMock], + ['current_store', $this->registryMock, $this->storeMock], ]; + $this->registryMock->expects($this->any()) ->method('register') - ->will($this->returnValueMap($registryValueMap)); + ->willReturn($registryValueMap); + $this->wysiwygConfigMock->expects($this->once()) ->method('setStoreId') - ->with('store'); + ->with($productStore); + $this->assertEquals($this->productMock, $this->builder->build($this->requestMock)); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php index 28617addc6d27..137eadb07d529 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use PHPUnit_Framework_MockObject_MockObject; class AttributeFilterTest extends \PHPUnit\Framework\TestCase { @@ -16,12 +19,12 @@ class AttributeFilterTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var PHPUnit_Framework_MockObject_MockObject */ protected $objectManagerMock; /** - * @var Product|\PHPUnit_Framework_MockObject_MockObject + * @var Product|PHPUnit_Framework_MockObject_MockObject */ protected $productMock; @@ -44,15 +47,25 @@ public function testPrepareProductAttributes( $expectedProductData, $initialProductData ) { + /** @var PHPUnit_Framework_MockObject_MockObject | Product $productMockMap */ $productMockMap = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getData']) + ->setMethods(['getData', 'getAttributes']) ->getMock(); if (!empty($initialProductData)) { $productMockMap->expects($this->any())->method('getData')->willReturnMap($initialProductData); } + if ($useDefaults) { + $productMockMap + ->expects($this->once()) + ->method('getAttributes') + ->willReturn( + $this->getProductAttributesMock($useDefaults) + ); + } + $actualProductData = $this->model->prepareProductAttributes($productMockMap, $requestProductData, $useDefaults); $this->assertEquals($expectedProductData, $actualProductData); } @@ -169,7 +182,7 @@ public function setupInputDataProvider() 'description' => 'descr modified' ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], ['description', null, 'descr text'] @@ -180,7 +193,8 @@ public function setupInputDataProvider() 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', - 'special_price' => '100' + 'special_price' => '100', + 'description' => 'descr modified', ], 'useDefaults' => [ 'description' => '1' @@ -190,9 +204,10 @@ public function setupInputDataProvider() 'sku' => 'testSku3', 'price' => '103', 'special_price' => '100', + 'description' => false ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], ['description', null, 'descr text'] @@ -200,4 +215,27 @@ public function setupInputDataProvider() ], ]; } + + /** + * @param array $useDefaults + * @return array + */ + private function getProductAttributesMock(array $useDefaults): array + { + $returnArray = []; + foreach ($useDefaults as $attributecode => $isDefault) { + if ($isDefault === '1') { + /** @var Attribute | PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $attribute->expects($this->any()) + ->method('getBackendType') + ->willReturn('varchar'); + + $returnArray[$attributecode] = $attribute; + } + } + return $returnArray; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index dce3f5886d1a8..c889c58e3df3a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -95,6 +95,14 @@ class HelperTest extends \PHPUnit\Framework\TestCase */ protected $attributeFilterMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFilterMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = new ObjectManager($this); @@ -167,6 +175,11 @@ protected function setUp() $resolverProperty = $helperReflection->getProperty('linkResolver'); $resolverProperty->setAccessible(true); $resolverProperty->setValue($this->helper, $this->linkResolverMock); + + $this->dateTimeFilterMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); + $dateTimeFilterProperty = $helperReflection->getProperty('dateTimeFilter'); + $dateTimeFilterProperty->setAccessible(true); + $dateTimeFilterProperty->setValue($this->helper, $this->dateTimeFilterMock); } /** @@ -198,14 +211,22 @@ public function testInitialize( 'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'], 'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14'] ]; + $specialFromDate = '2018-03-03 19:30:00'; $productData = [ 'stock_data' => ['stock_data'], 'options' => $optionsData, - 'website_ids' => $websiteIds + 'website_ids' => $websiteIds, + 'special_from_date' => $specialFromDate, ]; if (!empty($tierPrice)) { $productData = array_merge($productData, ['tier_price' => $tierPrice]); } + + $this->dateTimeFilterMock->expects($this->once()) + ->method('filter') + ->with($specialFromDate) + ->willReturn($specialFromDate); + $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->getMock(); @@ -306,6 +327,7 @@ public function testInitialize( } $this->assertEquals($expectedLinks, $resultLinks); + $this->assertEquals($specialFromDate, $productData['special_from_date']); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php index d41de5f67503c..e2b9b06175b13 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php @@ -50,6 +50,9 @@ class MassStatusTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\Pro */ private $actionMock; + /** + * @inheritdoc + */ protected function setUp() { $this->priceProcessorMock = $this->getMockBuilder(Processor::class) @@ -111,6 +114,7 @@ protected function setUp() ]; /** @var \Magento\Backend\App\Action\Context $context */ $context = $this->initContext($additionalParams, [[Action::class, $this->actionMock]]); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->action = new \Magento\Catalog\Controller\Adminhtml\Product\MassStatus( $context, diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php index a10814371577e..22dbc32c2c3ab 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php @@ -5,10 +5,15 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product; +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product\Copier; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** + * Unit tests for \Magento\Catalog\Controller\Adminhtml\Product\Save class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTest @@ -28,6 +33,21 @@ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTe /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ private $product; + /** + * @var Copier|\PHPUnit_Framework_MockObject_MockObject + */ + private $productCopierMock; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttributeMock; + + /** + * @var CategoryLinkManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryLinkManagementMock; + /** @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ private $resultRedirectFactory; @@ -42,6 +62,7 @@ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTe /** * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { @@ -49,11 +70,33 @@ protected function setUp() \Magento\Catalog\Controller\Adminhtml\Product\Builder::class, ['build'] ); - $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)->disableOriginalConstructor() - ->setMethods(['addData', 'getSku', 'getTypeId', 'getStoreId', '__sleep', '__wakeup'])->getMock(); + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addData', + 'unsetData', + 'getData', + 'getSku', + 'getCategoryIds', + 'getAttributes', + 'getTypeId', + 'getStoreId', + 'save', + '__sleep', + '__wakeup', + ]) + ->getMock(); $this->product->expects($this->any())->method('getTypeId')->will($this->returnValue('simple')); $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue('1')); $this->productBuilder->expects($this->any())->method('build')->will($this->returnValue($this->product)); + $this->productCopierMock = $this->getMockBuilder(Copier::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['getIsUnique', 'getIsUserDefined', 'getAttributeCode', 'getDefaultFrontendLabel']) + ->getMockForAbstractClass(); + $this->categoryLinkManagementMock = $this->getMockBuilder(CategoryLinkManagementInterface::class) + ->getMockForAbstractClass(); $this->messageManagerMock = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class @@ -155,4 +198,86 @@ public function exceptionTypeDataProvider() ['Exception', 'addErrorMessage'] ]; } + + /** + * @return void + */ + public function testExecuteCheckUniqueAttributesOnDuplicate() + { + $productSku = 'test_sku'; + $attributeCode = 'test_attribute_code'; + + $productData = [ + 'product' => [ + 'name' => 'test-name', + 'sku' => $productSku, + $attributeCode => 'test_attribute', + ] + ]; + + $this->request->expects($this->at(1)) + ->method('getParam') + ->with('back', false) + ->willReturn('duplicate'); + + $this->request->expects($this->any())->method('getPostValue')->willReturn($productData); + $this->initializationHelper->expects($this->any())->method('initialize') + ->willReturn($this->product); + + $this->product->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $this->product->expects($this->any()) + ->method('getSku') + ->willReturn($productSku); + $this->product->expects($this->any()) + ->method('getCategoryIds') + ->willReturn([]); + + $this->categoryLinkManagementMock->expects($this->any()) + ->method('assignProductToCategories') + ->with($productSku, []) + ->willReturn(true); + + $this->product->expects($this->once()) + ->method('unsetData') + ->with('quantity_and_stock_status') + ->willReturnSelf(); + + $this->productCopierMock->expects($this->any()) + ->method('copy') + ->with($this->product) + ->willReturn($this->product); + + $this->product->expects($this->once()) + ->method('getAttributes') + ->willReturn([$this->productAttributeMock]); + + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getIsUnique') + ->willReturn('1'); + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getIsUserDefined') + ->willReturn('1'); + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $this->product->expects($this->any()) + ->method('getData') + ->willReturnMap([ + [$attributeCode, null, $productData['product'][$attributeCode]] + ]); + + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getDefaultFrontendLabel') + ->willReturn('Test Attribute Label'); + + $this->messageManagerMock->expects($this->once()) + ->method('addErrorMessage'); + $this->messageManagerMock->expects($this->atLeastOnce()) + ->method('addSuccessMessage'); + + $this->action->execute(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php index ba716fdb53c89..47a60a1916142 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php @@ -58,7 +58,7 @@ protected function getContext() $objectManagerMock = $this->getMockForAbstractClass(\Magento\Framework\ObjectManagerInterface::class); $objectManagerMock->expects($this->any()) ->method('get') - ->willreturn($productActionMock); + ->willReturn($productActionMock); $eventManager = $this->getMockBuilder(\Magento\Framework\Event\Manager::class) ->setMethods(['dispatch']) diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Widget/ChooserTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Widget/ChooserTest.php new file mode 100644 index 0000000000000..0a93c619a5148 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Widget/ChooserTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Widget; + +use Magento\Catalog\Controller\Adminhtml\Product\Widget\Chooser; +use Magento\Framework\App\Action\Context; +use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\View\LayoutFactory; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Unit tests for Magento\Catalog\Controller\Adminhtml\Product\Widget\Chooser. + */ +class ChooserTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Chooser + */ + private $controller; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var RawFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $rawFactoryMock; + + /** + * @var LayoutFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $layoutFactoryMock; + + /** + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestInterfaceMock; + + /** + * @var Http|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManagerHelper = new ObjectManagerHelper($this); + + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->rawFactoryMock = $this->createMock(\Magento\Framework\Controller\Result\RawFactory::class); + $this->layoutFactoryMock = $this->createMock(\Magento\Framework\View\LayoutFactory::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->requestInterfaceMock = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + true, + true, + ['isPost'] + ); + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $this->controller = $objectManagerHelper->getObject( + \Magento\Catalog\Controller\Adminhtml\Product\Widget\Chooser::class, + [ + 'context' => $this->contextMock, + 'resultRawFactory' => $this->rawFactoryMock, + 'layoutFactory' => $this->layoutFactoryMock, + ] + ); + } + + /** + * Check that error throws when request is not a POST. + * + * @return void + * @expectedException \Magento\Framework\Exception\NotFoundException + * @expectedExceptionMessage Page not found. + */ + public function testExecuteWithNonPostRequest() + { + $this->requestMock->expects($this->once())->method('isPost')->willReturn(false); + + $this->controller->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php index 5d08f80847da2..7c3b78d5cf05a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php @@ -67,7 +67,7 @@ protected function initContext(array $additionalParams = [], array $objectManage ->setMethods(['add', 'prepend'])->disableOriginalConstructor()->getMock(); $title->expects($this->any())->method('prepend')->withAnyParameters()->will($this->returnSelf()); $requestInterfaceMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class)->setMethods( - ['getParam', 'getPost', 'getFullActionName', 'getPostValue'] + ['getParam', 'getPost', 'getFullActionName', 'getPostValue', 'isPost'] )->disableOriginalConstructor()->getMock(); $responseInterfaceMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class)->setMethods( diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Product/Compare/IndexTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Product/Compare/IndexTest.php index d5a7ddb0b3e65..490bf1474baa1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Product/Compare/IndexTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Product/Compare/IndexTest.php @@ -1,17 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Controller\Product\Compare; use Magento\Catalog\Controller\Product\Compare\Index; use Magento\Catalog\Model\ResourceModel\Product\Compare\Item; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory; +use Magento\Catalog\Model\Session; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -28,7 +27,7 @@ class IndexTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Model\Product\Compare\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $itemFactoryMock; - /** @var Item\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $collectionFactoryMock; /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ @@ -40,7 +39,7 @@ class IndexTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Model\Product\Compare\ListCompare|\PHPUnit_Framework_MockObject_MockObject */ protected $listCompareMock; - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Session|\PHPUnit_Framework_MockObject_MockObject */ protected $catalogSession; /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -69,22 +68,31 @@ class IndexTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->contextMock = $this->createPartialMock(\Magento\Framework\App\Action\Context::class, ['getRequest', 'getResponse', 'getResultRedirectFactory']); + $this->contextMock = $this->createPartialMock( + \Magento\Framework\App\Action\Context::class, + ['getRequest', 'getResponse', 'getResultRedirectFactory'] + ); $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->response = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->redirectFactoryMock = $this->createPartialMock(\Magento\Framework\Controller\Result\RedirectFactory::class, ['create']); + $this->redirectFactoryMock = $this->createPartialMock( + \Magento\Framework\Controller\Result\RedirectFactory::class, + ['create'] + ); $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->request); $this->contextMock->expects($this->any())->method('getResponse')->willReturn($this->response); $this->contextMock->expects($this->any()) ->method('getResultRedirectFactory') ->willReturn($this->redirectFactoryMock); - $this->itemFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Compare\ItemFactory::class, ['create']); - $this->collectionFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory::class, ['create']); + $this->itemFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Compare\ItemFactory::class, + ['create'] + ); + $this->collectionFactoryMock = $this->createPartialMock(CollectionFactory::class, ['create']); $this->sessionMock = $this->createMock(\Magento\Customer\Model\Session::class); $this->visitorMock = $this->createMock(\Magento\Customer\Model\Visitor::class); $this->listCompareMock = $this->createMock(\Magento\Catalog\Model\Product\Compare\ListCompare::class); - $this->catalogSession = $this->createPartialMock(\Magento\Catalog\Model\Session::class, ['setBeforeCompareUrl']); + $this->catalogSession = $this->createPartialMock(Session::class, ['setBeforeCompareUrl']); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->formKeyValidatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php new file mode 100644 index 0000000000000..1a9d7959dda9c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Cron; + +use Magento\Catalog\Cron\DeleteAbandonedStoreFlatTables; +use Magento\Catalog\Helper\Product\Flat\Indexer; + +/** + * @covers \Magento\Catalog\Cron\DeleteAbandonedStoreFlatTables + */ +class DeleteAbandonedStoreFlatTablesTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var DeleteAbandonedStoreFlatTables + */ + private $deleteAbandonedStoreFlatTables; + + /** + * @var Indexer|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->indexerMock = $this->createMock(Indexer::class); + $this->deleteAbandonedStoreFlatTables = new DeleteAbandonedStoreFlatTables($this->indexerMock); + } + + /** + * Test execute method + * + * @return void + */ + public function testExecute() + { + $this->indexerMock->expects($this->once())->method('deleteAbandonedStoreFlatTables'); + $this->deleteAbandonedStoreFlatTables->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php new file mode 100644 index 0000000000000..c59d86aa3d5f1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Cron; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Cron\DeleteOutdatedPriceValues; +use Magento\Eav\Api\AttributeRepositoryInterface as AttributeRepository; +use Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Model\Entity\Attribute\Backend\BackendInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface as ScopeConfig; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Store\Model\Store; + +/** + * @covers \Magento\Catalog\Cron\DeleteOutdatedPriceValues + */ +class DeleteOutdatedPriceValuesTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var DeleteOutdatedPriceValues + */ + private $deleteOutdatedPriceValues; + + /** + * @var AttributeRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepositoryMock; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var ScopeConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var Attribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $dbAdapterMock; + + /** + * @var BackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeBackendMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->attributeRepositoryMock = $this->createMock(AttributeRepository::class); + $this->attributeMock = $this->createMock(Attribute::class); + $this->scopeConfigMock = $this->createMock(ScopeConfig::class); + $this->dbAdapterMock = $this->createMock(AdapterInterface::class); + $this->attributeBackendMock = $this->createMock(BackendInterface::class); + $this->deleteOutdatedPriceValues = new DeleteOutdatedPriceValues( + $this->resourceConnectionMock, + $this->attributeRepositoryMock, + $this->scopeConfigMock + ); + } + + /** + * Test execute method + * + * @return void + */ + public function testExecute() + { + $table = 'catalog_product_entity_decimal'; + $attributeId = 15; + $conditions = ['first', 'second']; + + $this->scopeConfigMock->expects($this->once())->method('getValue')->with(Store::XML_PATH_PRICE_SCOPE) + ->willReturn(Store::XML_PATH_PRICE_SCOPE); + $this->attributeRepositoryMock->expects($this->once())->method('get') + ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE) + ->willReturn($this->attributeMock); + $this->attributeMock->expects($this->once())->method('getId')->willReturn($attributeId); + $this->attributeMock->expects($this->once())->method('getBackend')->willReturn($this->attributeBackendMock); + $this->attributeBackendMock->expects($this->once())->method('getTable')->willReturn($table); + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->dbAdapterMock); + $this->dbAdapterMock->expects($this->exactly(2))->method('quoteInto')->willReturnMap([ + ['attribute_id = ?', $attributeId, null, null, $conditions[0]], + ['store_id != ?', Store::DEFAULT_STORE_ID, null, null, $conditions[1]], + ]); + $this->dbAdapterMock->expects($this->once())->method('delete')->with($table, $conditions); + $this->deleteOutdatedPriceValues->execute(); + } + + /** + * Test execute method + * The price scope config option is not equal to global value + * + * @return void + */ + public function testExecutePriceConfigIsNotSetToGlobal() + { + $this->scopeConfigMock->expects($this->once())->method('getValue')->with(Store::XML_PATH_PRICE_SCOPE) + ->willReturn(null); + $this->attributeRepositoryMock->expects($this->never())->method('get'); + $this->dbAdapterMock->expects($this->never())->method('delete'); + + $this->deleteOutdatedPriceValues->execute(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php index 29d9736e02442..46eac31af986e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php @@ -87,13 +87,14 @@ public function testExecute() 'recently_viewed_product' ]); + $time = time() - 1500; $connectionMock->expects($this->once()) ->method('quoteInto') - ->with('added_at < ?', time() - 1500) - ->willReturn(['added_at < ?', time() - 1500]); + ->with('added_at < ?', $time) + ->willReturn(['added_at < ?', $time]); $connectionMock->expects($this->once()) ->method('delete') - ->with('catalog_product_frontend_action', [['added_at < ?', time() - 1500]]); + ->with('catalog_product_frontend_action', [['added_at < ?', $time]]); $this->model->execute(); } diff --git a/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php b/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php new file mode 100644 index 0000000000000..e30ddda0b70b9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/CustomerData/CompareProductsTest.php @@ -0,0 +1,286 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\CustomerData; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\CustomerData\CompareProducts; +use Magento\Catalog\Helper\Output; +use Magento\Catalog\Helper\Product\Compare; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Url; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class CompareProductsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CompareProducts + */ + private $model; + + /** + * @var Compare|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * @var Url|\PHPUnit_Framework_MockObject_MockObject + */ + private $productUrlMock; + + /** + * @var Output|\PHPUnit_Framework_MockObject_MockObject + */ + private $outputHelperMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var array + */ + private $productValueMap = [ + 'id' => 'getId', + ProductInterface::NAME => 'getName' + ]; + + protected function setUp() + { + parent::setUp(); + + $this->helperMock = $this->getMockBuilder(Compare::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productUrlMock = $this->getMockBuilder(Url::class) + ->disableOriginalConstructor() + ->getMock(); + $this->outputHelperMock = $this->getMockBuilder(Output::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + CompareProducts::class, + [ + 'helper' => $this->helperMock, + 'productUrl' => $this->productUrlMock, + 'outputHelper' => $this->outputHelperMock + ] + ); + } + + /** + * Prepare compare items collection. + * + * @param array $items + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getItemCollectionMock(array $items) : \PHPUnit_Framework_MockObject_MockObject + { + $itemCollectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + $itemCollectionMock->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator($items)); + + return $itemCollectionMock; + } + + /** + * Prepare product mocks objects and add corresponding method mocks for helpers. + * + * @param array $dataSet + * @return array + */ + private function prepareProductsWithCorrespondingMocks(array $dataSet) : array + { + $items = []; + $urlMap = []; + $outputMap = []; + $helperMap = []; + + $count = count($dataSet); + + foreach ($dataSet as $data) { + $item = $this->getProductMock($data); + $items[] = $item; + + $outputMap[] = [$item, $data['name'], 'name', 'productName#' . $data['id']]; + $helperMap[] = [$item, 'http://remove.url/' . $data['id']]; + $urlMap[] = [$item, [], 'http://product.url/' . $data['id']]; + } + + $this->productUrlMock->expects($this->exactly($count)) + ->method('getUrl') + ->will($this->returnValueMap($urlMap)); + + $this->outputHelperMock->expects($this->exactly($count)) + ->method('productAttribute') + ->will($this->returnValueMap($outputMap)); + + $this->helperMock->expects($this->exactly($count)) + ->method('getPostDataRemove') + ->will($this->returnValueMap($helperMap)); + + return $items; + } + + /** + * Prepare mock of product object. + * + * @param array $data + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getProductMock(array $data) : \PHPUnit_Framework_MockObject_MockObject + { + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + + foreach ($data as $index => $value) { + $product->expects($this->once()) + ->method($this->productValueMap[$index]) + ->willReturn($value); + } + + return $product; + } + + public function testGetSectionData() + { + $dataSet = [ + ['id' => 1, 'name' => 'product#1'], + ['id' => 2, 'name' => 'product#2'], + ['id' => 3, 'name' => 'product#3'] + ]; + + $count = count($dataSet); + + $this->helperMock->expects($this->once()) + ->method('getItemCount') + ->willReturn($count); + + $items = $this->prepareProductsWithCorrespondingMocks($dataSet); + + $itemCollectionMock = $this->getItemCollectionMock($items); + + $this->helperMock->expects($this->once()) + ->method('getItemCollection') + ->willReturn($itemCollectionMock); + + $this->helperMock->expects($this->once()) + ->method('getListUrl') + ->willReturn('http://list.url'); + + $this->assertEquals( + [ + 'count' => $count, + 'countCaption' => __('%1 items', $count), + 'listUrl' => 'http://list.url', + 'items' => [ + [ + 'id' => 1, + 'product_url' => 'http://product.url/1', + 'name' => 'productName#1', + 'remove_url' => 'http://remove.url/1' + ], + [ + 'id' => 2, + 'product_url' => 'http://product.url/2', + 'name' => 'productName#2', + 'remove_url' => 'http://remove.url/2' + ], + [ + 'id' => 3, + 'product_url' => 'http://product.url/3', + 'name' => 'productName#3', + 'remove_url' => 'http://remove.url/3' + ] + ] + ], + $this->model->getSectionData() + ); + } + + public function testGetSectionDataNoItems() + { + $count = 0; + + $this->helperMock->expects($this->once()) + ->method('getItemCount') + ->willReturn($count); + + $this->helperMock->expects($this->never()) + ->method('getItemCollection'); + + $this->helperMock->expects($this->once()) + ->method('getListUrl') + ->willReturn('http://list.url'); + + $this->assertEquals( + [ + 'count' => $count, + 'countCaption' => __('%1 items', $count), + 'listUrl' => 'http://list.url', + 'items' => [] + ], + $this->model->getSectionData() + ); + } + + public function testGetSectionDataSingleItem() + { + $count = 1; + + $this->helperMock->expects($this->once()) + ->method('getItemCount') + ->willReturn($count); + + $items = $this->prepareProductsWithCorrespondingMocks( + [ + [ + 'id' => 12345, + 'name' => 'SingleProduct' + ] + ] + ); + + $itemCollectionMock = $this->getItemCollectionMock($items); + + $this->helperMock->expects($this->once()) + ->method('getItemCollection') + ->willReturn($itemCollectionMock); + + $this->helperMock->expects($this->once()) + ->method('getListUrl') + ->willReturn('http://list.url'); + + $this->assertEquals( + [ + 'count' => 1, + 'countCaption' => __('1 item'), + 'listUrl' => 'http://list.url', + 'items' => [ + [ + 'id' => 12345, + 'product_url' => 'http://product.url/12345', + 'name' => 'productName#12345', + 'remove_url' => 'http://remove.url/12345' + ] + ] + ], + $this->model->getSectionData() + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php new file mode 100644 index 0000000000000..70b75e7ff2790 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/FactoryTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder; + +use Magento\Eav\Model\Config as EavConfig; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\Factory; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionInterface; +use Magento\Framework\Api\Filter; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Model\Product; + +class FactoryTest extends \PHPUnit\Framework\TestCase +{ + + private $productResourceMock; + + private $eavConfigMock; + + private $eavAttrConditionBuilderMock; + + private $nativeAttrConditionBuilderMock; + + private $conditionBuilderFactory; + + protected function setUp() + { + $this->productResourceMock = $this->getMockBuilder(ProductResource::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityTable']) + ->getMock(); + + $this->eavConfigMock = $this->getMockBuilder(EavConfig::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + + $this->eavAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->nativeAttrConditionBuilderMock = $this->getMockBuilder(CustomConditionInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->conditionBuilderFactory = $objectManagerHelper->getObject( + Factory::class, + [ + 'eavConfig' => $this->eavConfigMock, + 'productResource' => $this->productResourceMock, + 'eavAttributeConditionBuilder' => $this->eavAttrConditionBuilderMock, + 'nativeAttributeConditionBuilder' => $this->nativeAttrConditionBuilderMock, + ] + ); + } + + public function testNativeAttrConditionBuilder() + { + $fieldName = 'super_field'; + $attributeTable = 'my-table'; + $productResourceTable = 'my-table'; + + $filterMock = $this->getMockBuilder(Filter::class) + ->disableOriginalConstructor() + ->setMethods(['getField']) + ->getMock(); + + $filterMock + ->method('getField') + ->willReturn($fieldName); + + $attributeMock = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getBackendTable']) + ->getMock(); + + $this->eavConfigMock + ->method('getAttribute') + ->with(Product::ENTITY, $fieldName) + ->willReturn($attributeMock); + + $attributeMock + ->method('getBackendTable') + ->willReturn($attributeTable); + + $this->productResourceMock + ->method('getEntityTable') + ->willReturn($productResourceTable); + + $this->assertEquals( + $this->nativeAttrConditionBuilderMock, + $this->conditionBuilderFactory->createByFilter($filterMock) + ); + } + + public function testEavAttrConditionBuilder() + { + $fieldName = 'super_field'; + $attributeTable = 'my-table'; + $productResourceTable = 'not-my-table'; + + $filterMock = $this->getMockBuilder(Filter::class) + ->disableOriginalConstructor() + ->setMethods(['getField']) + ->getMock(); + + $filterMock + ->method('getField') + ->willReturn($fieldName); + + $attributeMock = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getBackendTable']) + ->getMock(); + + $this->eavConfigMock + ->method('getAttribute') + ->with(Product::ENTITY, $fieldName) + ->willReturn($attributeMock); + + $attributeMock + ->method('getBackendTable') + ->willReturn($attributeTable); + + $this->productResourceMock + ->method('getEntityTable') + ->willReturn($productResourceTable); + + $this->assertEquals( + $this->eavAttrConditionBuilderMock, + $this->conditionBuilderFactory->createByFilter($filterMock) + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php index f667638cc4da8..157c72fcedf10 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Api/SearchCriteria/CollectionProcessor/FilterProcessor/ProductCategoryFilterTest.php @@ -31,7 +31,7 @@ public function testApply() ->disableOriginalConstructor() ->getMock(); - $filterMock->expects($this->exactly(2)) + $filterMock->expects($this->exactly(1)) ->method('getConditionType') ->willReturn('condition'); $filterMock->expects($this->once()) @@ -66,7 +66,7 @@ public function testApplyWithoutCondition() $collectionMock->expects($this->once()) ->method('addCategoriesFilter') - ->with(['eq' => ['value']]); + ->with(['in' => ['value']]); $this->assertTrue($this->model->apply($filterMock, $collectionMock)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php deleted file mode 100644 index 1973c88fd39f9..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/CustomlayoutupdateTest.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -// @codingStandardsIgnoreFile - -namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend; - -use Magento\Framework\DataObject; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -class CustomlayoutupdateTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var string - */ - private $attributeName = 'private'; - - /** - * @var \Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate - */ - private $model; - - /** - * @expectedException \Magento\Eav\Model\Entity\Attribute\Exception - */ - public function testValidateException() - { - $object = new DataObject(); - $object->setData($this->attributeName, 'exception'); - $this->model->validate($object); - } - - /** - * @param string - * @dataProvider validateProvider - */ - public function testValidate($data) - { - $object = new DataObject(); - $object->setData($this->attributeName, $data); - - $this->assertTrue($this->model->validate($object)); - $this->assertTrue($this->model->validate($object)); - } - - /** - * @return array - */ - public function validateProvider() - { - return [[''], ['xml']]; - } - - protected function setUp() - { - $helper = new ObjectManager($this); - $this->model = $helper->getObject( - \Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate::class, - [ - 'layoutUpdateValidatorFactory' => $this->getMockedLayoutUpdateValidatorFactory() - ] - ); - $this->model->setAttribute($this->getMockedAttribute()); - } - - /** - * @return \Magento\Framework\View\Model\Layout\Update\ValidatorFactory - */ - private function getMockedLayoutUpdateValidatorFactory() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\View\Model\Layout\Update\ValidatorFactory::class); - $mockBuilder->disableOriginalConstructor(); - $mockBuilder->setMethods(['create']); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('create') - ->will($this->returnValue($this->getMockedValidator())); - - return $mock; - } - - /** - * @return \Magento\Framework\View\Model\Layout\Update\Validator - */ - private function getMockedValidator() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\View\Model\Layout\Update\Validator::class); - $mockBuilder->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('isValid') - ->will( - /** - * @param string $xml - * $return bool - */ - $this->returnCallback( - function ($xml) { - if ($xml == 'exception') { - return false; - } else { - return true; - } - } - ) - ); - - $mock->expects($this->any()) - ->method('getMessages') - ->will($this->returnValue(['error'])); - - return $mock; - } - - /** - * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute - */ - private function getMockedAttribute() - { - $mockBuilder = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class); - $mockBuilder->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('getName') - ->will($this->returnValue($this->attributeName)); - - $mock->expects($this->any()) - ->method('getIsRequired') - ->will($this->returnValue(false)); - - return $mock; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php new file mode 100644 index 0000000000000..027eb5ad505ed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler + */ +class SaveHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var SaveHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $saveHandler; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepository; + + /** + * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupManagement; + + /** + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoll; + + /** + * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceResource; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAllCustomersGroup']) + ->getMockForAbstractClass(); + $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata']) + ->getMock(); + $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->saveHandler = $this->objectManager->getObject( + \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler::class, + [ + 'storeManager' => $this->storeManager, + 'attributeRepository' => $this->attributeRepository, + 'groupManagement' => $this->groupManagement, + 'metadataPoll' => $this->metadataPoll, + 'tierPriceResource' => $this->tierPriceResource + ] + ); + } + + public function testExecute() + { + $tierPrices = [ + ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], + ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] + ]; + $linkField = 'entity_id'; + $productId = 10; + + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( + [ + ['tier_price', $tierPrices], + ['entity_id', $productId] + ] + ); + $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); + $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId']) + ->getMockForAbstractClass(); + $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); + $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getLinkField']) + ->getMockForAbstractClass(); + $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); + $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($productMetadata); + $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); + $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') + ->willReturn($customerGroup); + $this->tierPriceResource->expects($this->atLeastOnce())->method('savePriceData')->willReturnSelf(); + + $this->assertEquals($product, $this->saveHandler->execute($product)); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Tier prices data should be array, but actually other type is received + */ + public function testExecuteWithException() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); + + $this->saveHandler->execute($product); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php new file mode 100644 index 0000000000000..be2f0c5185fff --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php @@ -0,0 +1,185 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler + */ +class UpdateHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var UpdateHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $updateHandler; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepository; + + /** + * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupManagement; + + /** + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoll; + + /** + * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceResource; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAllCustomersGroup']) + ->getMockForAbstractClass(); + $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata']) + ->getMock(); + $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->updateHandler = $this->objectManager->getObject( + \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler::class, + [ + 'storeManager' => $this->storeManager, + 'attributeRepository' => $this->attributeRepository, + 'groupManagement' => $this->groupManagement, + 'metadataPoll' => $this->metadataPoll, + 'tierPriceResource' => $this->tierPriceResource + ] + ); + } + + public function testExecute() + { + $newTierPrices = [ + ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 15], + ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] + ]; + $priceIdToDelete = 2; + $originalTierPrices = [ + ['price_id' => 1, 'website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], + ['price_id' => $priceIdToDelete, 'website_id' => 0, 'price_qty' => 4, 'cust_group' => 0, 'price' => 20], + ]; + $linkField = 'entity_id'; + $productId = 10; + + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( + [ + ['tier_price', $newTierPrices], + ['entity_id', $productId] + ] + ); + $product->expects($this->atLeastOnce())->method('getOrigData')->with('tier_price') + ->willReturn($originalTierPrices); + $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); + $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId']) + ->getMockForAbstractClass(); + $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); + $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getLinkField']) + ->getMockForAbstractClass(); + $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); + $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($productMetadata); + $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); + $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') + ->willReturn($customerGroup); + $this->tierPriceResource->expects($this->exactly(2))->method('savePriceData')->willReturnSelf(); + $this->tierPriceResource->expects($this->once())->method('deletePriceData') + ->with($productId, null, $priceIdToDelete); + + $this->assertEquals($product, $this->updateHandler->execute($product)); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Tier prices data should be array, but actually other type is received + */ + public function testExecuteWithException() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); + + $this->updateHandler->execute($product); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php index a0a563e1e070e..779630b9559c6 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php @@ -39,6 +39,9 @@ public function testExemplarXml($fixtureXml, array $expectedErrors) $this->assertEquals($expectedErrors, $actualErrors); } + /** + * @return array + */ public function exemplarXmlDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index d4e09714d0522..4442785706173 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Catalog\Test\Unit\Model\Category\Attribute\Backend; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\WriteInterface; + +/** + * Test for Magento\Catalog\Model\Category\Attribute\Backend\Image class. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ImageTest extends \PHPUnit\Framework\TestCase { /** @@ -27,6 +34,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $logger; + /** + * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $filesystem; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -57,8 +72,12 @@ protected function setUp() $this->imageUploader = $this->createPartialMock( \Magento\Catalog\Model\ImageUploader::class, - ['moveFileFromTmp'] + ['moveFileFromTmp', 'getBasePath'] ); + + $this->filesystem = $this->getMockBuilder(\Magento\Framework\Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); } /** @@ -82,13 +101,11 @@ public function testBeforeSaveValueDeletion($value) $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => $value - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => $value]); $model->beforeSave($object); - $this->assertEquals('', $object->getTestAttribute()); + $this->assertEquals(null, $object->getTestAttribute()); } /** @@ -119,57 +136,84 @@ public function testBeforeSaveValueInvalid($value) $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => $value - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => $value]); $model->beforeSave($object); $this->assertEquals('', $object->getTestAttribute()); } + /** + * Test beforeSaveAttributeFileName. + */ public function testBeforeSaveAttributeFileName() { - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); - $model->setAttribute($this->attribute); + $model = $this->setUpModelForAfterSave(); + $mediaDirectoryMock = $this->createMock(WriteInterface::class); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + $this->imageUploader->expects($this->once())->method('getBasePath')->willReturn('base/path'); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with('base/path/test123.jpg') + ->willReturn('absolute/path/base/path/test123.jpg'); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => [ - ['name' => 'test123.jpg'] + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => [ + ['name' => 'test123.jpg'], + ], ] - ]); + ); $model->beforeSave($object); $this->assertEquals('test123.jpg', $object->getTestAttribute()); } + /** + * Test beforeSaveTemporaryAttribute. + */ public function testBeforeSaveTemporaryAttribute() { - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); + $model = $this->setUpModelForAfterSave(); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => [ - ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] + $mediaDirectoryMock = $this->createMock(WriteInterface::class); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + + $object = new \Magento\Framework\DataObject( + [ + 'test_attribute' => [ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'], + ], ] - ]); + ); $model->beforeSave($object); - $this->assertEquals([ - ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'] - ], $object->getData('_additional_data_test_attribute')); + $this->assertEquals( + [ + ['name' => 'test123.jpg', 'tmp_name' => 'abc123', 'url' => 'http://www.example.com/test123.jpg'], + ], + $object->getData('_additional_data_test_attribute') + ); } + /** + * Test beforeSaveAttributeStringValue. + */ public function testBeforeSaveAttributeStringValue() { $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class); $model->setAttribute($this->attribute); - $object = new \Magento\Framework\DataObject([ - 'test_attribute' => 'test123.jpg' - ]); + $object = new \Magento\Framework\DataObject(['test_attribute' => 'test123.jpg']); $model->beforeSave($object); @@ -188,23 +232,34 @@ private function setUpModelForAfterSave() $objectManagerMock->expects($this->any()) ->method('get') - ->will($this->returnCallback(function ($class, $params = []) use ($imageUploaderMock) { - if ($class == \Magento\Catalog\CategoryImageUpload::class) { - return $imageUploaderMock; - } - - return $this->objectManager->get($class, $params); - })); - - $model = $this->objectManager->getObject(\Magento\Catalog\Model\Category\Attribute\Backend\Image::class, [ - 'objectManager' => $objectManagerMock, - 'logger' => $this->logger - ]); + ->will( + $this->returnCallback( + function ($class, $params = []) use ($imageUploaderMock) { + if ($class == \Magento\Catalog\CategoryImageUpload::class) { + return $imageUploaderMock; + } + + return $this->objectManager->get($class, $params); + } + ) + ); + + $model = $this->objectManager->getObject( + \Magento\Catalog\Model\Category\Attribute\Backend\Image::class, + [ + 'objectManager' => $objectManagerMock, + 'logger' => $this->logger, + 'filesystem' => $this->filesystem, + ] + ); $this->objectManager->setBackwardCompatibleProperty($model, 'imageUploader', $this->imageUploader); return $model->setAttribute($this->attribute); } + /** + * @return array + */ public function attributeValueDataProvider() { return [ @@ -259,6 +314,9 @@ public function testAfterSaveWithoutAdditionalData($value) $model->afterSave($object); } + /** + * Test afterSaveWithExceptions. + */ public function testAfterSaveWithExceptions() { $model = $this->setUpModelForAfterSave(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php index 3b99f2697f6b8..9d75e6fe68671 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/SortbyTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Category\Attribute\Backend; class SortbyTest extends \PHPUnit\Framework\TestCase @@ -70,6 +68,9 @@ public function testBeforeSave($attributeCode, $data, $expected) $this->assertSame($expected, $object->getData($attributeCode)); } + /** + * @return array + */ public function beforeSaveDataProvider() { return [ @@ -116,6 +117,9 @@ public function testAfterLoad($attributeCode, $data, $expected) $this->assertSame($expected, $object->getData($attributeCode)); } + /** + * @return array + */ public function afterLoadDataProvider() { return [ @@ -158,6 +162,9 @@ public function testValidate($attributeData, $data, $expected) $this->assertSame($expected, $this->_model->validate($object)); } + /** + * @return array + */ public function validateDataProvider() { return [ @@ -250,6 +257,9 @@ public function testValidateDefaultSort($attributeCode, $data) $this->assertTrue($this->_model->validate($object)); } + /** + * @return array + */ public function validateDefaultSortDataProvider() { return [ @@ -271,8 +281,8 @@ public function validateDefaultSortDataProvider() [ 'default_sort_by', [ - 'available_sort_by' => NULL, - 'default_sort_by' => NULL, + 'available_sort_by' => null, + 'default_sort_by' => null, 'use_post_data_config' => ['available_sort_by', 'default_sort_by', 'filter_price_range'] ] ], @@ -293,20 +303,23 @@ public function testValidateDefaultSortException($attributeCode, $data) $this->_model->validate($object); } + /** + * @return array + */ public function validateDefaultSortException() { return [ [ 'default_sort_by', [ - 'available_sort_by' => NULL, + 'available_sort_by' => null, 'use_post_data_config' => ['default_sort_by'] ], ], [ 'default_sort_by', [ - 'available_sort_by' => NULL, + 'available_sort_by' => null, 'use_post_data_config' => [] ] ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/AttributeRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/AttributeRepositoryTest.php index 0dd1a3daad254..c13224f85f60f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/AttributeRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/AttributeRepositoryTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Category; use Magento\Catalog\Model\Category\AttributeRepository; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php index 9545e5eb4b37d..a2424c7521e18 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php @@ -105,7 +105,7 @@ public function testGetPositions() $this->select->expects($this->once()) ->method('where') ->willReturnSelf(); - $this->select->expects($this->once()) + $this->select->expects($this->exactly(2)) ->method('order') ->willReturnSelf(); $this->select->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php index 5e6436eac053b..a718a7afbb842 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\Collection; + class TreeTest extends \PHPUnit\Framework\TestCase { /** @@ -21,7 +21,7 @@ class TreeTest extends \PHPUnit\Framework\TestCase protected $storeManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Catalog\Model\ResourceModel\Category\Collection + * @var \PHPUnit_Framework_MockObject_MockObject | Collection */ protected $categoryCollection; @@ -49,19 +49,16 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->categoryTreeMock = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Category\Tree::class - )->disableOriginalConstructor() + $this->categoryTreeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category\Tree::class) + ->disableOriginalConstructor() ->getMock(); - $this->categoryCollection = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Category\Collection::class - )->disableOriginalConstructor() + $this->categoryCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() ->getMock(); - $this->storeManagerMock = $this->getMockBuilder( - \Magento\Store\Model\StoreManagerInterface::class - )->disableOriginalConstructor() + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->disableOriginalConstructor() ->getMock(); $methods = ['create']; @@ -82,15 +79,13 @@ protected function setUp() public function testGetNode() { - $category = $this->getMockBuilder( - \Magento\Catalog\Model\Category::class - )->disableOriginalConstructor() + $category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + ->disableOriginalConstructor() ->getMock(); $category->expects($this->exactly(2))->method('getId')->will($this->returnValue(1)); - $node = $this->getMockBuilder( - \Magento\Framework\Data\Tree\Node::class - )->disableOriginalConstructor() + $node = $this->getMockBuilder(\Magento\Framework\Data\Tree\Node::class) + ->disableOriginalConstructor() ->getMock(); $node->expects($this->once())->method('loadChildren'); $this->categoryTreeMock->expects($this->once())->method('loadNode') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php index 1bc5e450ae153..7d448302666cc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php @@ -255,7 +255,8 @@ public function testSaveWithException() */ public function testSaveWithValidateCategoryException($error, $expectedException, $expectedExceptionMessage) { - $this->expectException($expectedException, $expectedExceptionMessage); + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); $categoryId = 5; $categoryMock = $this->createMock(\Magento\Catalog\Model\Category::class); $this->extensibleDataObjectConverterMock @@ -279,6 +280,9 @@ public function testSaveWithValidateCategoryException($error, $expectedException $this->model->save($categoryMock); } + /** + * @return array + */ public function saveWithValidateCategoryExceptionDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index b9e9bd14d6924..8c743b364aaf5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model; +use Magento\Catalog\Api\CategoryAttributeRepositoryInterface; use Magento\Catalog\Model\Indexer; use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -88,7 +88,7 @@ class CategoryTest extends \PHPUnit\Framework\TestCase private $productIndexer; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + * @var CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject */ private $categoryUrlPathGenerator; @@ -108,7 +108,7 @@ class CategoryTest extends \PHPUnit\Framework\TestCase private $indexerRegistry; /** - * @var \Magento\Catalog\Api\CategoryAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CategoryAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ private $metadataServiceMock; @@ -128,22 +128,34 @@ protected function setUp() $this->registry = $this->createMock(\Magento\Framework\Registry::class); $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->categoryTreeResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category\Tree::class); - $this->categoryTreeFactory = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class, ['create']); + $this->categoryTreeFactory = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class, + ['create'] + ); $this->categoryRepository = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); - $this->storeCollectionFactory = $this->createPartialMock(\Magento\Store\Model\ResourceModel\Store\CollectionFactory::class, ['create']); + $this->storeCollectionFactory = $this->createPartialMock( + \Magento\Store\Model\ResourceModel\Store\CollectionFactory::class, + ['create'] + ); $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->productCollectionFactory = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create']); + $this->productCollectionFactory = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, + ['create'] + ); $this->catalogConfig = $this->createMock(\Magento\Catalog\Model\Config::class); - $this->filterManager = $this->createPartialMock(\Magento\Framework\Filter\FilterManager::class, ['translitUrl']); + $this->filterManager = $this->createPartialMock( + \Magento\Framework\Filter\FilterManager::class, + ['translitUrl'] + ); $this->flatState = $this->createMock(\Magento\Catalog\Model\Indexer\Category\Flat\State::class); $this->flatIndexer = $this->createMock(\Magento\Framework\Indexer\IndexerInterface::class); $this->productIndexer = $this->createMock(\Magento\Framework\Indexer\IndexerInterface::class); - $this->categoryUrlPathGenerator = $this->createMock(\Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class); + $this->categoryUrlPathGenerator = $this->createMock(CategoryUrlPathGenerator::class); $this->urlFinder = $this->createMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class); $this->resource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category::class); $this->indexerRegistry = $this->createPartialMock(\Magento\Framework\Indexer\IndexerRegistry::class, ['get']); - $this->metadataServiceMock = $this->createMock(\Magento\Catalog\Api\CategoryAttributeRepositoryInterface::class); + $this->metadataServiceMock = $this->createMock(CategoryAttributeRepositoryInterface::class); $this->attributeValueFactory = $this->getMockBuilder(\Magento\Framework\Api\AttributeValueFactory::class) ->disableOriginalConstructor()->getMock(); @@ -170,7 +182,10 @@ public function testFormatUrlKey() public function testMoveWhenCannotFindParentCategory() { $this->markTestIncomplete('MAGETWO-31165'); - $parentCategory = $this->createPartialMock(\Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load']); + $parentCategory = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + ['getId', 'setStoreId', 'load'] + ); $parentCategory->expects($this->any())->method('setStoreId')->will($this->returnSelf()); $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($parentCategory)); @@ -189,7 +204,10 @@ public function testMoveWhenCannotFindParentCategory() */ public function testMoveWhenCannotFindNewCategory() { - $parentCategory = $this->createPartialMock(\Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load']); + $parentCategory = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + ['getId', 'setStoreId', 'load'] + ); $parentCategory->expects($this->any())->method('getId')->will($this->returnValue(5)); $parentCategory->expects($this->any())->method('setStoreId')->will($this->returnSelf()); $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); @@ -210,7 +228,10 @@ public function testMoveWhenCannotFindNewCategory() public function testMoveWhenParentCategoryIsSameAsChildCategory() { $this->markTestIncomplete('MAGETWO-31165'); - $parentCategory = $this->createPartialMock(\Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load']); + $parentCategory = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + ['getId', 'setStoreId', 'load'] + ); $parentCategory->expects($this->any())->method('getId')->will($this->returnValue(5)); $parentCategory->expects($this->any())->method('setStoreId')->will($this->returnSelf()); $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); @@ -231,7 +252,10 @@ public function testMovePrimaryWorkflow() ->method('get') ->with('catalog_category_product') ->will($this->returnValue($indexer)); - $parentCategory = $this->createPartialMock(\Magento\Catalog\Model\Category::class, ['getId', 'setStoreId', 'load']); + $parentCategory = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + ['getId', 'setStoreId', 'load'] + ); $parentCategory->expects($this->any())->method('getId')->will($this->returnValue(5)); $parentCategory->expects($this->any())->method('setStoreId')->will($this->returnSelf()); $parentCategory->expects($this->any())->method('load')->will($this->returnSelf()); @@ -259,6 +283,9 @@ public function testGetUseFlatResourceTrue() $this->assertEquals(true, $category->getUseFlatResource()); } + /** + * @return object + */ protected function getCategoryModel() { return $this->objectManager->getObject( @@ -287,6 +314,9 @@ protected function getCategoryModel() ); } + /** + * @return array + */ public function reindexFlatEnabledTestDataProvider() { return [ @@ -305,8 +335,12 @@ public function reindexFlatEnabledTestDataProvider() * * @dataProvider reindexFlatEnabledTestDataProvider */ - public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expectedFlatReindexCalls, $expectedProductReindexCall) - { + public function testReindexFlatEnabled( + $flatScheduled, + $productScheduled, + $expectedFlatReindexCalls, + $expectedProductReindexCall + ) { $affectedProductIds = ["1", "2"]; $this->category->setAffectedProductIds($affectedProductIds); $pathIds = ['path/1/2', 'path/2/3']; @@ -315,13 +349,16 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec $this->flatState->expects($this->any()) ->method('isFlatEnabled') - ->will($this->returnValue(true)); + ->willReturn(true); - $this->flatIndexer->expects($this->exactly(1))->method('isScheduled')->will($this->returnValue($flatScheduled)); + $this->flatIndexer->expects($this->exactly(1))->method('isScheduled')->willReturn($flatScheduled); $this->flatIndexer->expects($this->exactly($expectedFlatReindexCalls))->method('reindexList')->with(['123']); - $this->productIndexer->expects($this->exactly(1))->method('isScheduled')->will($this->returnValue($productScheduled)); - $this->productIndexer->expects($this->exactly($expectedProductReindexCall))->method('reindexList')->with($pathIds); + $this->productIndexer->expects($this->exactly(1))->method('isScheduled')->willReturn($productScheduled); + $this->productIndexer + ->expects($this->exactly($expectedProductReindexCall)) + ->method('reindexList') + ->with($pathIds); $this->indexerRegistry->expects($this->at(0)) ->method('get') @@ -336,16 +373,21 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec $this->category->reindex(); } + /** + * @return array + */ public function reindexFlatDisabledTestDataProvider() { return [ - [false, null, null, null, 0], - [true, null, null, null, 0], - [false, [], null, null, 0], - [false, ["1", "2"], null, null, 1], - [false, null, 1, null, 1], - [false, ["1", "2"], 0, 1, 1], - [false, null, 1, 1, 0], + [false, null, null, null, null, null, 0], + [true, null, null, null, null, null, 0], + [false, [], null, null, null, null, 0], + [false, ["1", "2"], null, null, null, null, 1], + [false, null, 1, null, null, null, 1], + [false, ["1", "2"], 0, 1, null, null, 1], + [false, null, 1, 1, null, null, 0], + [false, ["1", "2"], null, null, 0, 1, 1], + [false, ["1", "2"], null, null, 1, 0, 1], ]; } @@ -354,6 +396,8 @@ public function reindexFlatDisabledTestDataProvider() * @param array $affectedIds * @param int|string $isAnchorOrig * @param int|string $isAnchor + * @param mixed $isActiveOrig + * @param mixed $isActive, * @param int $expectedProductReindexCall * * @dataProvider reindexFlatDisabledTestDataProvider @@ -363,12 +407,16 @@ public function testReindexFlatDisabled( $affectedIds, $isAnchorOrig, $isAnchor, + $isActiveOrig, + $isActive, $expectedProductReindexCall ) { $this->category->setAffectedProductIds($affectedIds); $this->category->setData('is_anchor', $isAnchor); $this->category->setOrigData('is_anchor', $isAnchorOrig); $this->category->setAffectedProductIds($affectedIds); + $this->category->setData('is_active', $isActive); + $this->category->setOrigData('is_active', $isActiveOrig); $pathIds = ['path/1/2', 'path/2/3']; $this->category->setData('path_ids', $pathIds); @@ -378,7 +426,7 @@ public function testReindexFlatDisabled( ->method('isFlatEnabled') ->will($this->returnValue(false)); - $this->productIndexer->expects($this->exactly(1)) + $this->productIndexer->expects($this->any()) ->method('isScheduled') ->willReturn($productScheduled); $this->productIndexer->expects($this->exactly($expectedProductReindexCall)) @@ -426,7 +474,7 @@ public function testGetCustomAttributes() $this->assertEquals("description", $this->category->getCustomAttribute($descriptionAttributeCode)->getValue()); //Change the attribute value, should reflect in getCustomAttribute - $this->category->setData($descriptionAttributeCode, "new description"); + $this->category->setCustomAttribute($descriptionAttributeCode, "new description"); $this->assertEquals(1, count($this->category->getCustomAttributes())); $this->assertNotNull($this->category->getCustomAttribute($descriptionAttributeCode)); $this->assertEquals( @@ -506,4 +554,33 @@ public function testGetImageWithoutAttributeCode() $this->assertEquals('http://www.example.com/catalog/category/myimage', $result); } + + /** + * @return void + */ + public function testGetIdentities() + { + $category = $this->getCategoryModel(); + + //Without an ID no identities can be given. + $this->assertEmpty($category->getIdentities()); + + //Now because ID is set we can get some + $category->setId(42); + + $this->assertNotEmpty($category->getIdentities()); + } + + /** + * @return void + */ + public function testGetIdentitiesWithAffectedCategories() + { + $category = $this->getCategoryModel(); + $expectedIdentities = ['cat_c_1', 'cat_c_2', 'cat_c_3', 'cat_c_p_1']; + $category->setId(1); + $category->setAffectedCategoryIds([1,2,3]); + + $this->assertEquals($expectedIdentities, $category->getIdentities()); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php index 9df0a6bc1eac0..b9e8abe18f4ac 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php @@ -57,10 +57,14 @@ public function testGetCollection() $linkedProductOneMock = $this->createMock(Product::class); $linkedProductTwoMock = $this->createMock(Product::class); $linkedProductThreeMock = $this->createMock(Product::class); + $linkedProductFourMock = $this->createMock(Product::class); + $linkedProductFiveMock = $this->createMock(Product::class); $linkedProductOneMock->expects($this->once())->method('getId')->willReturn(1); $linkedProductTwoMock->expects($this->once())->method('getId')->willReturn(2); $linkedProductThreeMock->expects($this->once())->method('getId')->willReturn(3); + $linkedProductFourMock->expects($this->once())->method('getId')->willReturn(4); + $linkedProductFiveMock->expects($this->once())->method('getId')->willReturn(5); $this->converterPoolMock->expects($this->once()) ->method('getConverter') @@ -71,9 +75,11 @@ public function testGetCollection() [$linkedProductOneMock, ['name' => 'Product One', 'position' => 10]], [$linkedProductTwoMock, ['name' => 'Product Two', 'position' => 2]], [$linkedProductThreeMock, ['name' => 'Product Three', 'position' => 2]], + [$linkedProductFourMock, ['name' => 'Product Four', 'position' => null]], + [$linkedProductFiveMock, ['name' => 'Product Five']], ]; - $this->converterMock->expects($this->exactly(3))->method('convert')->willReturnMap($map); + $this->converterMock->expects($this->exactly(5))->method('convert')->willReturnMap($map); $this->providerMock->expects($this->once()) ->method('getLinkedProducts') @@ -82,14 +88,18 @@ public function testGetCollection() [ $linkedProductOneMock, $linkedProductTwoMock, - $linkedProductThreeMock + $linkedProductThreeMock, + $linkedProductFourMock, + $linkedProductFiveMock, ] ); $expectedResult = [ - 2 => ['name' => 'Product Two', 'position' => 2], - 3 => ['name' => 'Product Three', 'position' => 2], - 10 => ['name' => 'Product One', 'position' => 10], + 0 => ['name' => 'Product Four', 'position' => 0], + 1 => ['name' => 'Product Five', 'position' => 0], + 2 => ['name' => 'Product Three', 'position' => 2], + 3 => ['name' => 'Product Two', 'position' => 2], + 4 => ['name' => 'Product One', 'position' => 10], ]; $actualResult = $this->model->getCollection($this->productMock, 'crosssell'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php index 5b1d3bf7943fc..0688ad5bde19d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php @@ -36,6 +36,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $attribute; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -62,54 +70,78 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->getMockBuilder( + \Magento\Framework\Escaper::class + ) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + $helper = new ObjectManager($this); $this->model = $helper->getObject( \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, [ 'eavConfig' => $this->eavConfig, - 'attributeCollectionFactory' => $this->attributeCollectionFactory + 'attributeCollectionFactory' => $this->attributeCollectionFactory, + 'escaper' => $this->escaperMock, ] ); } - public function testGetPrefixes() + /** + * @param string $actualLabel + * @param string $expectedLabel + * @return void + * @dataProvider getPrefixesDataProvider + */ + public function testGetPrefixes(string $actualLabel, string $expectedLabel) { $entityTypeId = 3; /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) ->disableOriginalConstructor() ->getMock(); - $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); + $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) ->setMethods(['getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); + $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( - $this->equalTo($entityTypeId) - ); - $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( - $this->equalTo('media_image') - ); + $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); + $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - $this->attribute->expects($this->once())->method('getAttributeCode')->will( - $this->returnValue('attributeCode') - ); - $this->attribute->expects($this->once())->method('getFrontend')->will( - $this->returnValue($frontend) - ); + $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); + $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - $this->attributeCollection->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$this->attribute])) - ); + $this->attributeCollection->expects($this->any())->method('getIterator') + ->willReturn(new \ArrayIterator([$this->attribute])); + + $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) + ->willReturn($entityType); + + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) + ->willReturn($expectedLabel); - $this->eavConfig->expects($this->any())->method('getEntityType')->with( - $this->equalTo(Product::ENTITY) - )->will($this->returnValue($entityType)); + $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); + } - $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); + /** + * @return array + */ + public function getPrefixesDataProvider(): array + { + return [ + [ + 'actual_label' => 'testLabel', + 'expected_label' => 'testLabel', + ], + [ + 'actual_label' => '<media-image-attributelabel', + 'expected_label' => '<media-image-attributelabel', + ], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php index b8196fcd8bea3..4435446f77bfb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php @@ -69,10 +69,17 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase /** * Allowed extensions * - * @var string + * @var array */ private $allowedExtensions; + /** + * Allowed mime types + * + * @var array + */ + private $allowedMimeTypes; + protected function setUp() { $this->coreFileStorageDatabaseMock = $this->createMock( @@ -97,6 +104,7 @@ protected function setUp() $this->baseTmpPath = 'base/tmp/'; $this->basePath = 'base/real/'; $this->allowedExtensions = ['.jpg']; + $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; $this->imageUploader = new \Magento\Catalog\Model\ImageUploader( @@ -107,13 +115,20 @@ protected function setUp() $this->loggerMock, $this->baseTmpPath, $this->basePath, - $this->allowedExtensions + $this->allowedExtensions, + $this->allowedMimeTypes ); } public function testSaveFileToTmpDir() { $fileId = 'file.jpg'; + $allowedMimeTypes = [ + 'image/jpg', + 'image/jpeg', + 'image/gif', + 'image/png' + ]; /** @var \Magento\MediaStorage\Model\File\Uploader|\PHPUnit_Framework_MockObject_MockObject $uploader */ $uploader = $this->createMock(\Magento\MediaStorage\Model\File\Uploader::class); $this->uploaderFactoryMock->expects($this->once())->method('create')->willReturn($uploader); @@ -123,6 +138,7 @@ public function testSaveFileToTmpDir() ->willReturn($this->basePath); $uploader->expects($this->once())->method('save')->with($this->basePath) ->willReturn(['tmp_name' => $this->baseTmpPath, 'file' => $fileId, 'path' => $this->basePath]); + $uploader->expects($this->atLeastOnce())->method('checkMimeType')->with($allowedMimeTypes)->willReturn(true); $storeMock = $this->createPartialMock( \Magento\Store\Model\Store::class, ['getBaseUrl'] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php index 56bd04594018c..f69cbeb91631f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/Plugin/IndexerConfigDataTest.php @@ -48,6 +48,9 @@ public function testAroundGet($isFlat, $path, $default, $inputData, $outputData) $this->assertEquals($outputData, $this->model->afterGet($this->subjectMock, $inputData, $path, $default)); } + /** + * @return array + */ public function aroundGetDataProvider() { $flatIndexerData = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php index 3beb9a3ffb773..6916cef2dfa61 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/StateTest.php @@ -102,6 +102,9 @@ public function testIsAvailable($isAvailable, $isFlatEnabled, $isValid, $result) $this->assertEquals($result, $this->model->isAvailable()); } + /** + * @return array + */ public function isAvailableDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php index 3b3941d116fde..fb02b80a60175 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Flat/System/Config/ModeTest.php @@ -57,6 +57,9 @@ protected function setUp() ); } + /** + * @return array + */ public function dataProviderProcessValueEqual() { return [['0', '0'], ['', '0'], ['0', ''], ['1', '1']]; @@ -92,6 +95,9 @@ public function testProcessValueEqual($oldValue, $value) $this->model->processValue(); } + /** + * @return array + */ public function dataProviderProcessValueOn() { return [['0', '1'], ['', '1']]; @@ -143,6 +149,9 @@ public function testProcessValueOn($oldValue, $value) $this->model->processValue(); } + /** + * @return array + */ public function dataProviderProcessValueOff() { return [['1', '0'], ['1', '']]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php index 8310f3692d966..e134407d547ac 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreGroupTest.php @@ -89,6 +89,9 @@ public function testBeforeAndAfterSaveNotNew($valueMap) $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->groupMock)); } + /** + * @return array + */ public function changedDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php index 6e3cd6ed30b52..4da831f5257d0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/StoreViewTest.php @@ -33,6 +33,11 @@ class StoreViewTest extends \PHPUnit\Framework\TestCase */ protected $indexerRegistryMock; + /** + * @var \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $tableMaintainer; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -51,15 +56,30 @@ protected function setUp() ); $this->subject = $this->createMock(Group::class); $this->indexerRegistryMock = $this->createPartialMock(IndexerRegistry::class, ['get']); - $this->storeMock = $this->createPartialMock(Store::class, ['isObjectNew', 'dataHasChangedFor', '__wakeup']); + $this->storeMock = $this->createPartialMock( + Store::class, + [ + 'isObjectNew', + 'getId', + 'dataHasChangedFor', + '__wakeup' + ] + ); + $this->tableMaintainer = $this->createPartialMock( + \Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer::class, + [ + 'createTablesForStore' + ] + ); - $this->model = new StoreView($this->indexerRegistryMock); + $this->model = new StoreView($this->indexerRegistryMock, $this->tableMaintainer); } public function testAroundSaveNewObject() { $this->mockIndexerMethods(); - $this->storeMock->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->storeMock->expects($this->atLeastOnce())->method('isObjectNew')->willReturn(true); + $this->storeMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); $this->model->beforeSave($this->subject, $this->storeMock); $this->assertSame($this->subject, $this->model->afterSave($this->subject, $this->subject, $this->storeMock)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php index 58654136ab5a8..3f86b577d0213 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php @@ -22,6 +22,11 @@ class AbstractActionTest extends \PHPUnit\Framework\TestCase */ protected $_eavSourceFactoryMock; + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + protected function setUp() { $this->_eavDecimalFactoryMock = $this->createPartialMock( @@ -32,9 +37,16 @@ protected function setUp() \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class, ['create'] ); + $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->_model = $this->getMockForAbstractClass( \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction::class, - [$this->_eavDecimalFactoryMock, $this->_eavSourceFactoryMock, []] + [ + $this->_eavDecimalFactoryMock, + $this->_eavSourceFactoryMock, + $this->scopeConfig + ] ); } @@ -110,14 +122,27 @@ public function testReindexWithoutArgumentsExecutesReindexAll() ->method('create') ->will($this->returnValue($eavDecimal)); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn(1); + $this->_model->reindex(); } - public function testReindexWithNotNullArgumentExecutesReindexEntities() - { - $childIds = [1, 2, 3]; - $parentIds = [4]; - $reindexIds = array_merge($childIds, $parentIds); + /** + * @param array $ids + * @param array $parentIds + * @param array $childIds + * @throws \Exception + * @dataProvider reindexEntitiesDataProvider + */ + public function testReindexWithNotNullArgumentExecutesReindexEntities( + array $ids, + array $parentIds, + array $childIds + ) { + $reindexIds = array_unique(array_merge($ids, $parentIds, $childIds)); + $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->getMockForAbstractClass(); @@ -129,11 +154,23 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities() ->disableOriginalConstructor() ->getMock(); - $eavSource->expects($this->once())->method('getRelationsByChild')->with($childIds)->willReturn($childIds); - $eavSource->expects($this->once())->method('getRelationsByParent')->with($childIds)->willReturn($parentIds); + $eavSource->expects($this->once()) + ->method('getRelationsByChild') + ->with($ids) + ->willReturn($parentIds); + $eavSource->expects($this->once()) + ->method('getRelationsByParent') + ->with(array_unique(array_merge($parentIds, $ids))) + ->willReturn($childIds); - $eavDecimal->expects($this->once())->method('getRelationsByChild')->with($reindexIds)->willReturn($reindexIds); - $eavDecimal->expects($this->once())->method('getRelationsByParent')->with($reindexIds)->willReturn([]); + $eavDecimal->expects($this->once()) + ->method('getRelationsByChild') + ->with($reindexIds) + ->willReturn($parentIds); + $eavDecimal->expects($this->once()) + ->method('getRelationsByParent') + ->with(array_unique(array_merge($parentIds, $reindexIds))) + ->willReturn($childIds); $eavSource->expects($this->once())->method('getConnection')->willReturn($connectionMock); $eavDecimal->expects($this->once())->method('getConnection')->willReturn($connectionMock); @@ -153,6 +190,34 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities() ->method('create') ->will($this->returnValue($eavDecimal)); - $this->_model->reindex($childIds); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn(1); + + $this->_model->reindex($ids); + } + + public function testReindexWithDisabledEavIndexer() + { + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn(0); + + $this->_eavSourceFactoryMock->expects($this->never())->method('create'); + $this->_eavDecimalFactoryMock->expects($this->never())->method('create'); + + $this->_model->reindex(); + } + + /** + * @return array + */ + public function reindexEntitiesDataProvider() + { + return [ + [[4], [], [1, 2, 3]], + [[3], [4], []], + [[5], [], []] + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php index eb5fdabe53303..967a2167c688a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php @@ -5,52 +5,104 @@ */ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\Action\Full; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use PHPUnit\Framework\MockObject\MockObject as MockObject; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FullTest extends \PHPUnit\Framework\TestCase { - public function testExecuteWithAdapterErrorThrowsException() - { - $eavDecimalFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory::class, - ['create'] - ); - $eavSourceFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class, - ['create'] - ); + /** + * @var Full|MockObject + */ + private $model; + + /** + * @var DecimalFactory|MockObject + */ + private $eavDecimalFactory; + + /** + * @var SourceFactory|MockObject + */ + private $eavSourceFactory; - $exceptionMessage = 'exception message'; - $exception = new \Exception($exceptionMessage); + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; - $eavDecimalFactory->expects($this->once()) - ->method('create') - ->will($this->throwException($exception)); + /** + * @var BatchProviderInterface|MockObject + */ + private $batchProvider; - $metadataMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); - $batchProviderMock = $this->createMock(\Magento\Framework\Indexer\BatchProviderInterface::class); + /** + * @var BatchSizeCalculator|MockObject + */ + private $batchSizeCalculator; - $batchManagementMock = $this->createMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class - ); + /** + * @var ActiveTableSwitcher|MockObject + */ + private $activeTableSwitcher; - $tableSwitcherMock = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class - )->disableOriginalConstructor()->getMock(); - - $model = new \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full( - $eavDecimalFactory, - $eavSourceFactory, - $metadataMock, - $batchProviderMock, - $batchManagementMock, - $tableSwitcherMock - ); + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; - $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $exceptionMessage); + /** + * @var Generator + */ + private $batchQueryGenerator; - $model->execute(); + /** + * @inheritdoc + */ + protected function setUp() + { + $this->eavDecimalFactory = $this->createPartialMock(DecimalFactory::class, ['create']); + $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']); + $this->metadataPool = $this->createMock(MetadataPool::class); + $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class); + $this->batchQueryGenerator = $this->createMock(Generator::class); + $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class); + $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject( + Full::class, + [ + 'eavDecimalFactory' => $this->eavDecimalFactory, + 'eavSourceFactory' => $this->eavSourceFactory, + 'metadataPool' => $this->metadataPool, + 'batchProvider' => $this->batchProvider, + 'batchSizeCalculator' => $this->batchSizeCalculator, + 'activeTableSwitcher' => $this->activeTableSwitcher, + 'scopeConfig' => $this->scopeConfig, + 'batchQueryGenerator' => $this->batchQueryGenerator, + ] + ); } /** @@ -58,25 +110,18 @@ public function testExecuteWithAdapterErrorThrowsException() */ public function testExecute() { - $eavDecimalFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory::class, - ['create'] - ); - $eavSourceFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class, - ['create'] - ); + $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1); $ids = [1, 2, 3]; - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + $connectionMock = $this->getMockBuilder(AdapterInterface::class) ->getMockForAbstractClass(); $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); - $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) + $eavSource = $this->getMockBuilder(Source::class) ->disableOriginalConstructor() ->getMock(); - $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) + $eavDecimal = $this->getMockBuilder(Decimal::class) ->disableOriginalConstructor() ->getMock(); @@ -89,43 +134,36 @@ public function testExecute() $eavSource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock); $eavDecimal->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock); - $eavDecimal->expects($this->once()) - ->method('reindexEntities') - ->with($ids); + $eavDecimal->expects($this->once())->method('reindexEntities')->with($ids); - $eavSource->expects($this->once()) - ->method('reindexEntities') - ->with($ids); + $eavSource->expects($this->once())->method('reindexEntities')->with($ids); - $eavDecimalFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($eavSource)); + $this->eavDecimalFactory->expects($this->once())->method('create')->will($this->returnValue($eavSource)); - $eavSourceFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($eavDecimal)); + $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal)); - $metadataMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); - $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + $entityMetadataMock = $this->getMockBuilder(EntityMetadataInterface::class) ->getMockForAbstractClass(); - $metadataMock->expects($this->atLeastOnce()) + $this->metadataPool->expects($this->atLeastOnce()) ->method('getMetadata') - ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->with(ProductInterface::class) ->willReturn($entityMetadataMock); - $batchProviderMock = $this->createMock(\Magento\Framework\Indexer\BatchProviderInterface::class); - $batchProviderMock->expects($this->atLeastOnce()) - ->method('getBatches') - ->willReturn([['from' => 10, 'to' => 100]]); - $batchProviderMock->expects($this->atLeastOnce()) - ->method('getBatchIds') + // Super inefficient algorithm in some cases + $this->batchProvider->expects($this->never()) + ->method('getBatches'); + + $batchQuery = $this->createMock(Select::class); + + $connectionMock->method('fetchCol') + ->with($batchQuery) ->willReturn($ids); - $batchManagementMock = $this->createMock( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class - ); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + $this->batchQueryGenerator->method('generate') + ->willReturn([$batchQuery]); + + $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); @@ -133,19 +171,16 @@ public function testExecute() $selectMock->expects($this->atLeastOnce())->method('distinct')->willReturnSelf(); $selectMock->expects($this->atLeastOnce())->method('from')->willReturnSelf(); - $tableSwitcherMock = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class - )->disableOriginalConstructor()->getMock(); - - $model = new \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full( - $eavDecimalFactory, - $eavSourceFactory, - $metadataMock, - $batchProviderMock, - $batchManagementMock, - $tableSwitcherMock - ); + $this->model->execute(); + } - $model->execute(); + /** + * @return void + */ + public function testExecuteWithDisabledEavIndexer() + { + $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(0); + $this->metadataPool->expects($this->never())->method('getMetadata'); + $this->model->execute(); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php index e1a76426bd0a4..d4f1762424e0b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action; class EraserTest extends \PHPUnit\Framework\TestCase diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php index 41b3d36227431..11d07872fef91 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RowTest extends \PHPUnit\Framework\TestCase { /** @@ -18,72 +21,116 @@ class RowTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $store; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $productIndexerHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $resource; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $connection; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatItemWriter; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatItemEraser; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatTableBuilder; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); + $attributeTable = 'catalog_product_entity_int'; + $statusId = 22; $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); $this->resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->resource->expects($this->any())->method('getConnection') ->with('default') - ->will($this->returnValue($this->connection)); + ->willReturn($this->connection); $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->store = $this->createMock(\Magento\Store\Model\Store::class); - $this->store->expects($this->any())->method('getId')->will($this->returnValue('store_id_1')); - $this->storeManager->expects($this->any())->method('getStores')->will($this->returnValue([$this->store])); - $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); + $this->store->expects($this->any())->method('getId')->willReturn('store_id_1'); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$this->store]); $this->flatItemEraser = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Eraser::class); $this->flatItemWriter = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer::class); - $this->flatTableBuilder = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class); + $this->flatTableBuilder = $this->createMock( + \Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class + ); + $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); + $statusAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productIndexerHelper->expects($this->any())->method('getAttribute') + ->with('status') + ->willReturn($statusAttributeMock); + $backendMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) + ->disableOriginalConstructor() + ->getMock(); + $backendMock->expects($this->any())->method('getTable')->willReturn($attributeTable); + $statusAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendMock); + $statusAttributeMock->expects($this->any())->method('getId')->willReturn($statusId); + $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any())->method('select')->willReturn($selectMock); + $selectMock->method('from') + ->willReturnSelf(); + $selectMock->method('joinLeft') + ->willReturnSelf(); + $selectMock->expects($this->any())->method('where')->willReturnSelf(); + $selectMock->expects($this->any())->method('order')->willReturnSelf(); + $selectMock->expects($this->any())->method('limit')->willReturnSelf(); + $pdoMock = $this->createMock(\Zend_Db_Statement_Pdo::class); + $this->connection->expects($this->any())->method('query')->with($selectMock)->willReturn($pdoMock); + $pdoMock->expects($this->any())->method('fetchColumn')->willReturn('1'); + + $metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $metadataPool->expects($this->any())->method('getMetadata')->with(ProductInterface::class) + ->willReturn($productMetadata); + $productMetadata->expects($this->any())->method('getLinkField')->willReturn('entity_id'); $this->model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Flat\Action\Row::class, [ - 'resource' => $this->resource, - 'storeManager' => $this->storeManager, - 'productHelper' => $this->productIndexerHelper, - 'flatItemEraser' => $this->flatItemEraser, - 'flatItemWriter' => $this->flatItemWriter, - 'flatTableBuilder' => $this->flatTableBuilder - ]); + \Magento\Catalog\Model\Indexer\Product\Flat\Action\Row::class, + [ + 'resource' => $this->resource, + 'storeManager' => $this->storeManager, + 'productHelper' => $this->productIndexerHelper, + 'flatItemEraser' => $this->flatItemEraser, + 'flatItemWriter' => $this->flatItemWriter, + 'flatTableBuilder' => $this->flatTableBuilder, + ] + ); + + $objectManager->setBackwardCompatibleProperty($this->model, 'metadataPool', $metadataPool); } /** @@ -98,9 +145,9 @@ public function testExecuteWithEmptyId() public function testExecuteWithNonExistingFlatTablesCreatesTables() { $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') - ->will($this->returnValue('store_flat_table')); + ->willReturn('store_flat_table'); $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') - ->will($this->returnValue(false)); + ->willReturn(false); $this->flatItemEraser->expects($this->never())->method('removeDeletedProducts'); $this->flatTableBuilder->expects($this->once())->method('build')->with('store_id_1', ['product_id_1']); $this->flatItemWriter->expects($this->once())->method('write')->with('store_id_1', 'product_id_1'); @@ -110,12 +157,11 @@ public function testExecuteWithNonExistingFlatTablesCreatesTables() public function testExecuteWithExistingFlatTablesCreatesTables() { $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') - ->will($this->returnValue('store_flat_table')); + ->willReturn('store_flat_table'); $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') - ->will($this->returnValue(true)); + ->willReturn(true); $this->flatItemEraser->expects($this->once())->method('removeDeletedProducts'); $this->flatTableBuilder->expects($this->never())->method('build')->with('store_id_1', ['product_id_1']); $this->model->execute('product_id_1'); } } - diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowsTest.php index 0c2708004612a..ecf4ffc7cb954 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowsTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -75,17 +73,21 @@ protected function setUp() $this->_productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); $this->_flatItemEraser = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Eraser::class); $this->_flatItemWriter = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer::class); - $this->_flatTableBuilder = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class); + $this->_flatTableBuilder = $this->createMock( + \Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class + ); $this->_model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Flat\Action\Rows::class, [ - 'resource' => $this->_resource, - 'storeManager' => $this->_storeManager, - 'productHelper' => $this->_productIndexerHelper, - 'flatItemEraser' => $this->_flatItemEraser, - 'flatItemWriter' => $this->_flatItemWriter, - 'flatTableBuilder' => $this->_flatTableBuilder - ]); + \Magento\Catalog\Model\Indexer\Product\Flat\Action\Rows::class, + [ + 'resource' => $this->_resource, + 'storeManager' => $this->_storeManager, + 'productHelper' => $this->_productIndexerHelper, + 'flatItemEraser' => $this->_flatItemEraser, + 'flatItemWriter' => $this->_flatItemWriter, + 'flatTableBuilder' => $this->_flatTableBuilder + ] + ); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php index 8c63d77b74f53..d30a8da0e77a2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/ProcessorTest.php @@ -129,6 +129,9 @@ public function testReindexRow( $this->_model->reindexRow(1, $forceReindex); } + /** + * @return array + */ public function dataProviderReindexRow() { return [ @@ -198,6 +201,9 @@ public function testReindexList( $this->_model->reindexList([1], $forceReindex); } + /** + * @return array + */ public function dataProviderReindexList() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php index ca67185203738..34cc5c70418b9 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/System/Config/ModeTest.php @@ -50,6 +50,9 @@ protected function setUp() ); } + /** + * @return array + */ public function dataProviderProcessValueEqual() { return [['0', '0'], ['', '0'], ['0', ''], ['1', '1']]; @@ -84,6 +87,9 @@ public function testProcessValueEqual($oldValue, $value) $this->model->processValue(); } + /** + * @return array + */ public function dataProviderProcessValueOn() { return [['0', '1'], ['', '1']]; @@ -134,6 +140,9 @@ public function testProcessValueOn($oldValue, $value) $this->model->processValue(); } + /** + * @return array + */ public function dataProviderProcessValueOff() { return [['1', '0'], ['1', '']]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php index d551822d975ea..1a5fea5e12769 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Plugin/WebsiteTest.php @@ -5,43 +5,179 @@ */ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Price\Plugin; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration; + class WebsiteTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $_objectManager; + protected $objectManager; /** * @var \Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website */ - protected $_model; + protected $model; + + /** + * @var \Magento\Framework\Indexer\DimensionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $dimensionFactory; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $tableMaintainer; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor|\PHPUnit_Framework_MockObject_MockObject + * @var DimensionModeConfiguration|\PHPUnit_Framework_MockObject_MockObject */ - protected $_priceProcessorMock; + protected $dimensionModeConfiguration; protected function setUp() { - $this->_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_priceProcessorMock = $this->createPartialMock( - \Magento\Catalog\Model\Indexer\Product\Price\Processor::class, - ['markIndexerAsInvalid'] + $this->dimensionFactory = $this->createPartialMock( + \Magento\Framework\Indexer\DimensionFactory::class, + ['create'] ); - $this->_model = $this->_objectManager->getObject( + $this->tableMaintainer = $this->createPartialMock( + \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class, + ['dropTablesForDimensions', 'createTablesForDimensions'] + ); + + $this->dimensionModeConfiguration = $this->createPartialMock( + DimensionModeConfiguration::class, + ['getDimensionConfiguration'] + ); + + $this->model = $this->objectManager->getObject( \Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website::class, - ['processor' => $this->_priceProcessorMock] + [ + 'dimensionFactory' => $this->dimensionFactory, + 'tableMaintainer' => $this->tableMaintainer, + 'dimensionModeConfiguration' => $this->dimensionModeConfiguration, + ] ); } public function testAfterDelete() { - $this->_priceProcessorMock->expects($this->once())->method('markIndexerAsInvalid'); + $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + + $this->dimensionFactory->expects($this->once())->method('create')->willReturn( + $dimensionMock + ); + $this->tableMaintainer->expects($this->once())->method('dropTablesForDimensions')->with( + [$dimensionMock] + ); + + $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn( + [\Magento\Store\Model\Indexer\WebsiteDimensionProvider::DIMENSION_NAME] + ); + + $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $objectResourceMock, + $this->model->afterDelete($subjectMock, $objectResourceMock, $websiteMock) + ); + } + + public function testAfterDeleteOnModeWithoutWebsiteDimension() + { + $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + + $this->dimensionFactory->expects($this->never())->method('create')->willReturn( + $dimensionMock + ); + $this->tableMaintainer->expects($this->never())->method('dropTablesForDimensions')->with( + [$dimensionMock] + ); + + $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn( + [] + ); + + $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $objectResourceMock, + $this->model->afterDelete($subjectMock, $objectResourceMock, $websiteMock) + ); + } + + public function testAfterSave() + { + $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + + $this->dimensionFactory->expects($this->once())->method('create')->willReturn( + $dimensionMock + ); + $this->tableMaintainer->expects($this->once())->method('createTablesForDimensions')->with( + [$dimensionMock] + ); + + $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn( + [\Magento\Store\Model\Indexer\WebsiteDimensionProvider::DIMENSION_NAME] + ); + + $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $websiteMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(true); - $websiteMock = $this->createMock(\Magento\Store\Model\ResourceModel\Website::class); - $this->assertEquals('return_value', $this->_model->afterDelete($websiteMock, 'return_value')); + $this->assertEquals( + $objectResourceMock, + $this->model->afterSave($subjectMock, $objectResourceMock, $websiteMock) + ); + } + + public function testAfterSaveOnModeWithoutWebsiteDimension() + { + $dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + + $this->dimensionFactory->expects($this->never())->method('create')->willReturn( + $dimensionMock + ); + $this->tableMaintainer->expects($this->never())->method('createTablesForDimensions')->with( + [$dimensionMock] + ); + + $this->dimensionModeConfiguration->expects($this->once())->method('getDimensionConfiguration')->willReturn( + [] + ); + + $subjectMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $objectResourceMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class); + $websiteMock = $this->createMock(\Magento\Framework\Model\AbstractModel::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $websiteMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(true); + + $this->assertEquals( + $objectResourceMock, + $this->model->afterSave($subjectMock, $objectResourceMock, $websiteMock) + ); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/AvailabilityFlagTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/AvailabilityFlagTest.php index 7ba217317c88f..008fd04afadd4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/AvailabilityFlagTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/AvailabilityFlagTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layer\Category; use \Magento\Catalog\Model\Layer\Category\AvailabilityFlag; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/CollectionFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/CollectionFilterTest.php index 8f489995004c9..d03efdc6994de 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/CollectionFilterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/CollectionFilterTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layer\Category; use \Magento\Catalog\Model\Layer\Category\CollectionFilter; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/FilterableAttributeListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/FilterableAttributeListTest.php index f7ec16e06ef22..8950dc7bec4d5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/FilterableAttributeListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Category/FilterableAttributeListTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layer\Category; class FilterableAttributeListTest extends \PHPUnit\Framework\TestCase @@ -27,7 +25,10 @@ class FilterableAttributeListTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->collectionFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, ['create']); + $this->collectionFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); @@ -35,7 +36,6 @@ protected function setUp() $this->collectionFactoryMock, $this->storeManagerMock ); - } public function testGetList() diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php index 3e5daf1a98a9c..257a84e50248d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/CategoryTest.php @@ -77,6 +77,9 @@ protected function setUp() ); } + /** + * @return \Magento\Catalog\Model\Layer\Filter\DataProvider\Category + */ public function testGetCategoryWithAppliedId() { $storeId = 1234; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php index 3a23ebcdf4518..8ca23df31cdee 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php @@ -152,7 +152,7 @@ public function testGetMaxPrice() $this->productCollection->expects($this->once()) ->method('getMaxPrice') ->will($this->returnValue($maxPrice)); - $this->assertSame(floatval($maxPrice), $this->target->getMaxPrice()); + $this->assertSame((float)$maxPrice, $this->target->getMaxPrice()); } /** @@ -165,6 +165,9 @@ public function testValidateFilter($filter, $expectedResult) $this->assertSame($expectedResult, $this->target->validateFilter($filter)); } + /** + * @return array + */ public function validateFilterDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php index db810e2f5c0f4..8733f305ce091 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layer; use \Magento\Catalog\Model\Layer\FilterList; @@ -40,7 +38,9 @@ class FilterListTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->attributeListMock = $this->createMock(\Magento\Catalog\Model\Layer\Category\FilterableAttributeList::class); + $this->attributeListMock = $this->createMock( + \Magento\Catalog\Model\Layer\Category\FilterableAttributeList::class + ); $this->attributeMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $filters = [ FilterList::CATEGORY_FILTER => 'CategoryFilterClass', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Search/FilterableAttributeListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Search/FilterableAttributeListTest.php index 75af901edc98e..62d49a12c8981 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Search/FilterableAttributeListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Search/FilterableAttributeListTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layer\Search; class FilterableAttributeListTest extends \PHPUnit\Framework\TestCase @@ -27,7 +25,10 @@ class FilterableAttributeListTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->collectionFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, ['create']); + $this->collectionFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); @@ -35,7 +36,6 @@ protected function setUp() $this->collectionFactoryMock, $this->storeManagerMock ); - } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layout/DepersonalizePluginTest.php index d82c58c3a2a01..2d2d448e09eb9 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layout/DepersonalizePluginTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layout/DepersonalizePluginTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Layout; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -39,8 +37,12 @@ protected function setUp() $this->resultLayout = $this->createMock(\Magento\Framework\View\Layout::class); $this->depersonalizeCheckerMock = $this->createMock(\Magento\PageCache\Model\DepersonalizeChecker::class); - $this->plugin = (new ObjectManager($this))->getObject(\Magento\Catalog\Model\Layout\DepersonalizePlugin::class, - ['catalogSession' => $this->catalogSessionMock, 'depersonalizeChecker' => $this->depersonalizeCheckerMock] + $this->plugin = (new ObjectManager($this))->getObject( + \Magento\Catalog\Model\Layout\DepersonalizePlugin::class, + [ + 'catalogSession' => $this->catalogSessionMock, + 'depersonalizeChecker' => $this->depersonalizeCheckerMock, + ] ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php index 8cf075f4d8504..a97e4650b49bd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ActionTest.php @@ -164,6 +164,9 @@ public function testUpdateWebsites($type, $methodName) $this->assertEquals($this->model->getDataByKey('action_type'), $type); } + /** + * @return array + */ public function updateWebsitesDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php index 5963d8b161633..3003c2f8085e4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/GroupPrice/AbstractTest.php @@ -47,7 +47,7 @@ protected function setUp() 'scopeOverriddenValue' => $scopeOverriddenValue ] ); - $resource = $this->createPartialMock(\StdClass::class, ['getMainTable']); + $resource = $this->createPartialMock(\stdClass::class, ['getMainTable']); $resource->expects($this->any())->method('getMainTable')->will($this->returnValue('table')); $this->_model->expects($this->any())->method('_getResource')->will($this->returnValue($resource)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php index 115a333a38b5b..3ceedddc2b713 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/ImageTest.php @@ -3,45 +3,71 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Model\Product\Attribute\Frontend; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Frontend\Image; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; -class ImageTest extends \PHPUnit\Framework\TestCase +class ImageTest extends TestCase { /** - * @var \Magento\Catalog\Model\Product\Attribute\Frontend\Image + * @var Image */ private $model; - public function testGetUrl() + /** + * @dataProvider getUrlDataProvider + * @param string $expectedImage + * @param string $productImage + */ + public function testGetUrl(string $expectedImage, string $productImage) + { + $this->assertEquals($expectedImage, $this->model->getUrl($this->getMockedProduct($productImage))); + } + + /** + * Data provider for testGetUrl + * + * @return array + */ + public function getUrlDataProvider(): array { - $this->assertEquals('catalog/product/img.jpg', $this->model->getUrl($this->getMockedProduct())); + return [ + ['catalog/product/img.jpg', 'img.jpg'], + ['catalog/product/img.jpg', '/img.jpg'], + ]; } protected function setUp() { $helper = new ObjectManager($this); $this->model = $helper->getObject( - \Magento\Catalog\Model\Product\Attribute\Frontend\Image::class, + Image::class, ['storeManager' => $this->getMockedStoreManager()] ); $this->model->setAttribute($this->getMockedAttribute()); } /** - * @return \Magento\Catalog\Model\Product + * @param string $productImage + * @return Product */ - private function getMockedProduct() + private function getMockedProduct(string $productImage): Product { - $mockBuilder = $this->getMockBuilder(\Magento\Catalog\Model\Product::class); + $mockBuilder = $this->getMockBuilder(Product::class); $mock = $mockBuilder->setMethods(['getData', 'getStore', '__wakeup']) ->disableOriginalConstructor() ->getMock(); $mock->expects($this->any()) ->method('getData') - ->will($this->returnValue('img.jpg')); + ->will($this->returnValue($productImage)); $mock->expects($this->any()) ->method('getStore'); @@ -50,13 +76,13 @@ private function getMockedProduct() } /** - * @return \Magento\Store\Model\StoreManagerInterface + * @return StoreManagerInterface */ - private function getMockedStoreManager() + private function getMockedStoreManager(): StoreManagerInterface { $mockedStore = $this->getMockedStore(); - $mockBuilder = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class); + $mockBuilder = $this->getMockBuilder(StoreManagerInterface::class); $mock = $mockBuilder->setMethods(['getStore']) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -69,11 +95,11 @@ private function getMockedStoreManager() } /** - * @return \Magento\Store\Model\Store + * @return Store */ - private function getMockedStore() + private function getMockedStore(): Store { - $mockBuilder = $this->getMockBuilder(\Magento\Store\Model\Store::class); + $mockBuilder = $this->getMockBuilder(Store::class); $mock = $mockBuilder->setMethods(['getBaseUrl', '__wakeup']) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -86,11 +112,11 @@ private function getMockedStore() } /** - * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + * @return AbstractAttribute */ - private function getMockedAttribute() + private function getMockedAttribute(): AbstractAttribute { - $mockBuilder = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class); + $mockBuilder = $this->getMockBuilder(AbstractAttribute::class); $mockBuilder->setMethods(['getAttributeCode', '__wakeup']); $mockBuilder->disableOriginalConstructor(); $mock = $mockBuilder->getMockForAbstractClass(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/ManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/ManagementTest.php index 3b717eac4fe8c..21d87f7e1de1c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/ManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/ManagementTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Attribute; class ManagementTest extends \PHPUnit\Framework\TestCase @@ -67,7 +64,8 @@ public function testGetAttributes() $this->attrManagementMock->expects($this->once()) ->method('getAttributes') ->with( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeSetId + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSetId )->willReturn([$attributeMock]); $this->assertEquals([$attributeMock], $this->model->getAttributes($attributeSetId)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php index ca63ba4a30761..62cc7536f9c55 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php @@ -1,18 +1,14 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Attribute; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Product\Attribute\Repository; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\Eav\Api\Data\AttributeFrontendLabelInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -87,7 +83,10 @@ protected function setUp() $this->eavConfigMock = $this->createMock(\Magento\Eav\Model\Config::class); $this->eavConfigMock->expects($this->any())->method('getEntityType') ->willReturn(new \Magento\Framework\DataObject(['default_attribute_set_id' => 4])); - $this->validatorFactoryMock = $this->createPartialMock(\Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory::class, ['create']); + $this->validatorFactoryMock = $this->createPartialMock( + \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory::class, + ['create'] + ); $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class); $this->searchResultMock = @@ -210,7 +209,10 @@ public function testSaveNoSuchEntityException() */ public function testSaveInputExceptionRequiredField() { - $attributeMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, ['getFrontendLabels', 'getDefaultFrontendLabel', '__wakeup', 'getAttributeId', 'setAttributeId']); + $attributeMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, + ['getFrontendLabels', 'getDefaultFrontendLabel', '__wakeup', 'getAttributeId', 'setAttributeId'] + ); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn(null); $attributeMock->expects($this->once())->method('setAttributeId')->with(null)->willReturnSelf(); $attributeMock->expects($this->once())->method('getFrontendLabels')->willReturn(null); @@ -225,12 +227,15 @@ public function testSaveInputExceptionRequiredField() */ public function testSaveInputExceptionInvalidFieldValue() { - $attributeMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, ['getFrontendLabels', 'getDefaultFrontendLabel', 'getAttributeId', '__wakeup', 'setAttributeId']); + $attributeMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, + ['getFrontendLabels', 'getDefaultFrontendLabel', 'getAttributeId', '__wakeup', 'setAttributeId'] + ); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn(null); $attributeMock->expects($this->once())->method('setAttributeId')->with(null)->willReturnSelf(); - $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class); - $attributeMock->expects($this->exactly(4))->method('getFrontendLabels')->willReturn([$labelMock]); - $attributeMock->expects($this->exactly(2))->method('getDefaultFrontendLabel')->willReturn('test'); + $labelMock = $this->createMock(\Magento\Eav\Model\Entity\Attribute\FrontendLabel::class); + $attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]); + $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null); $labelMock->expects($this->once())->method('getStoreId')->willReturn(0); $labelMock->expects($this->once())->method('getLabel')->willReturn(null); @@ -253,7 +258,7 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload() ->method('get') ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) ->willReturn($existingModelMock); - + $existingModelMock->expects($this->once())->method('getDefaultFrontendLabel')->willReturn('default_label'); // Attribute code must not be changed after attribute creation $attributeMock->expects($this->once())->method('setAttributeCode')->with($attributeCode); $this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock); @@ -264,7 +269,7 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload() public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload() { - $labelMock = $this->createMock(AttributeFrontendLabelInterface::class); + $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeFrontendLabelInterface::class); $labelMock->expects($this->any())->method('getStoreId')->willReturn(1); $labelMock->expects($this->any())->method('getLabel')->willReturn('Store Scope Label'); @@ -273,11 +278,12 @@ public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload() $attributeMock = $this->createMock(Attribute::class); $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); $attributeMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId); - $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label'); + $attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null); $attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]); $attributeMock->expects($this->any())->method('getOptions')->willReturn([]); $existingModelMock = $this->createMock(Attribute::class); + $existingModelMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label'); $existingModelMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId); $existingModelMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); @@ -288,12 +294,7 @@ public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload() $attributeMock->expects($this->once()) ->method('setDefaultFrontendLabel') - ->with( - [ - 0 => 'Default Label', - 1 => 'Store Scope Label' - ] - ); + ->with('Default Label'); $this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock); $this->model->save($attributeMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/StatusTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/StatusTest.php index 7baef8d10e20f..606f04b8ba001 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/StatusTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/StatusTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Attribute\Source; use Magento\Eav\Model\Entity\AbstractEntity; @@ -54,7 +52,10 @@ protected function setUp() 'getEntity', 'getAttribute' ]); - $this->backendAttributeModel = $this->createPartialMock(\Magento\Catalog\Model\Product\Attribute\Backend\Sku::class, ['__wakeup', 'getTable']); + $this->backendAttributeModel = $this->createPartialMock( + \Magento\Catalog\Model\Product\Attribute\Backend\Sku::class, + ['__wakeup', 'getTable'] + ); $this->status = $this->objectManagerHelper->getObject( \Magento\Catalog\Model\Product\Attribute\Source\Status::class ); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/TypesListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/TypesListTest.php index 31ba735d51304..19d44d667f002 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/TypesListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/TypesListTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Attribute; use Magento\Catalog\Model\Product\Attribute\TypesList; @@ -35,7 +32,10 @@ class TypesListTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->inputTypeFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Attribute\Source\InputtypeFactory::class, ['create', '__wakeup']); + $this->inputTypeFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Attribute\Source\InputtypeFactory::class, + ['create', '__wakeup'] + ); $this->attributeTypeFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductAttributeTypeInterfaceFactory::class, [ 'create', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php index 6f1f5e120b100..2144cf34c2a09 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CartConfigurationTest.php @@ -21,6 +21,9 @@ public function testIsProductConfigured($productType, $config, $expected) $this->assertEquals($expected, $cartConfiguration->isProductConfigured($productMock, $config)); } + /** + * @return array + */ public function isProductConfiguredDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php index d80cf3cb9bfa2..1c876e8a2ac0a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Gallery; class GalleryManagementTest extends \PHPUnit\Framework\TestCase @@ -268,7 +265,7 @@ public function testGetWithNonExistingProduct() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionText Such image doesn't exist + * @expectedExceptionMessage Such image doesn't exist */ public function testGetWithNonExistingImage() { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/MimeTypeExtensionMapTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/MimeTypeExtensionMapTest.php index 3896ac5347992..8da7a677b36c2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/MimeTypeExtensionMapTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/MimeTypeExtensionMapTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Gallery; class MimeTypeExtensionMapTest extends \PHPUnit\Framework\TestCase diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php index d52aad50f05f3..15f003282dc04 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/ProcessorTest.php @@ -151,6 +151,9 @@ public function testValidate($value) $this->assertEquals(!$value, $this->model->validate($this->dataObject)); } + /** + * @return array + */ public function validateDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php index 3ff3601da8ccc..428ef432888f0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/CacheTest.php @@ -189,6 +189,58 @@ public function testGenerate() $this->model->generate($this->product); } + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + */ + public function testGenerateWithException() + { + $imageFile = 'image.jpg'; + $imageItem = $this->objectManager->getObject( + \Magento\Framework\DataObject::class, + [ + 'data' => ['file' => $imageFile] + ] + ); + $this->mediaGalleryCollection->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$imageItem])); + + $this->product->expects($this->atLeastOnce()) + ->method('getMediaGalleryImages') + ->willReturn($this->mediaGalleryCollection); + + $data = $this->getTestData(); + $this->config->expects($this->once()) + ->method('getMediaEntities') + ->with('Magento_Catalog') + ->willReturn($data); + + $themeMock = $this->getMockBuilder(\Magento\Theme\Model\Theme::class) + ->disableOriginalConstructor() + ->getMock(); + $themeMock->expects($this->exactly(3)) + ->method('getCode') + ->willReturn('Magento\theme'); + + $this->themeCollection->expects($this->once()) + ->method('loadRegisteredThemes') + ->willReturn([$themeMock]); + + $this->viewConfig->expects($this->once()) + ->method('getViewConfig') + ->with([ + 'area' => Area::AREA_FRONTEND, + 'themeModel' => $themeMock, + ]) + ->willReturn($this->config); + + $this->imageHelper->expects($this->exactly(3)) + ->method('init') + ->willThrowException(new \Exception('Some text ')); + + $this->model->generate($this->product); + } + /** * @return array */ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php index f918692cb2753..f5ed96882b981 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php @@ -5,12 +5,11 @@ */ namespace Magento\Catalog\Test\Unit\Model\Product; -use Magento\Catalog\Model\View\Asset\Image\ContextFactory; use Magento\Catalog\Model\View\Asset\ImageFactory; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\View\Asset\ContextInterface; +use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -73,11 +72,29 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $viewAssetPlaceholderFactory; + /** + * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializer; + + /** + * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheManager; + + /** + * @var ScopeConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->context = $this->createMock(\Magento\Framework\Model\Context::class); - + $this->cacheManager = $this->getMockBuilder(\Magento\Framework\App\CacheInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->context->expects($this->any())->method('getCacheManager')->will($this->returnValue($this->cacheManager)); $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) ->disableOriginalConstructor() ->setMethods(['getStore', 'getWebsite'])->getMock(); @@ -86,24 +103,20 @@ protected function setUp() $store->expects($this->any())->method('getId')->will($this->returnValue(1)); $store->expects($this->any())->method('getBaseUrl')->will($this->returnValue('http://magento.com/media/')); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->config = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) ->setMethods(['getBaseMediaPath'])->disableOriginalConstructor()->getMock(); $this->config->expects($this->any())->method('getBaseMediaPath')->will($this->returnValue('catalog/product')); $this->coreFileHelper = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) ->setMethods(['saveFile', 'deleteFolder'])->disableOriginalConstructor()->getMock(); - $this->mediaDirectory = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Write::class) ->disableOriginalConstructor() ->setMethods(['create', 'isFile', 'isExist', 'getAbsolutePath']) ->getMock(); - $this->filesystem = $this->createMock(\Magento\Framework\Filesystem::class); $this->filesystem->expects($this->once())->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) ->will($this->returnValue($this->mediaDirectory)); $this->factory = $this->createMock(\Magento\Framework\Image\Factory::class); - $this->viewAssetImageFactory = $this->getMockBuilder(ImageFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) @@ -112,17 +125,41 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); + $this->serializer = $this->getMockBuilder( + \Magento\Framework\Serialize\SerializerInterface::class + )->getMockForAbstractClass(); + $this->serializer->expects($this->any()) + ->method('serialize') + ->willReturnCallback( + function ($value) { + return json_encode($value); + } + ); + $this->serializer->expects($this->any()) + ->method('unserialize') + ->willReturnCallback( + function ($value) { + return json_decode($value, true); + } + ); + $this->scopeConfig = $this->getMockBuilder(ScopeConfig::class) + ->setMethods(['getValue'])->getMockForAbstractClass(); + $this->scopeConfig->expects($this->any())->method('getValue') + ->with('system/upload_configuration/jpeg_quality')->willReturn(80); $this->image = $objectManager->getObject( \Magento\Catalog\Model\Product\Image::class, [ + 'context' => $this->context, 'storeManager' => $this->storeManager, 'catalogProductMediaConfig' => $this->config, 'coreFileStorageDatabase' => $this->coreFileHelper, 'filesystem' => $this->filesystem, 'imageFactory' => $this->factory, 'viewAssetImageFactory' => $this->viewAssetImageFactory, - 'viewAssetPlaceholderFactory' => $this->viewAssetPlaceholderFactory + 'viewAssetPlaceholderFactory' => $this->viewAssetPlaceholderFactory, + 'serializer' => $this->serializer, + 'scopeConfig' => $this->scopeConfig ] ); @@ -354,12 +391,16 @@ public function testIsCached() $this->testSetGetBaseFile(); $absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png'; $this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath); + $this->cacheManager->expects($this->once())->method('load')->willReturn( + json_encode(['size' => ['image data']]) + ); $this->assertTrue($this->image->isCached()); } public function testClearCache() { $this->coreFileHelper->expects($this->once())->method('deleteFolder')->will($this->returnValue(true)); + $this->cacheManager->expects($this->once())->method('clean'); $this->image->clearCache(); } @@ -383,4 +424,24 @@ public function testIsBaseFilePlaceholder() { $this->assertFalse($this->image->isBaseFilePlaceholder()); } + + public function testGetResizedImageInfoWithCache() + { + $absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png'; + $this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath); + $this->cacheManager->expects($this->once())->method('load')->willReturn( + json_encode(['size' => ['image data']]) + ); + $this->cacheManager->expects($this->never())->method('save'); + $this->assertEquals(['image data'], $this->image->getResizedImageInfo()); + } + + public function testGetResizedImageInfoEmptyCache() + { + $absolutePath = dirname(dirname(__DIR__)) . '/_files/catalog/product/watermark/somefile.png'; + $this->imageAsset->expects($this->any())->method('getPath')->willReturn($absolutePath); + $this->cacheManager->expects($this->once())->method('load')->willReturn(false); + $this->cacheManager->expects($this->once())->method('save'); + $this->assertTrue(is_array($this->image->getResizedImageInfo())); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php index 0e29aeab697af..e242b77f1a5fc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/LinkTypeProviderTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product; class LinkTypeProviderTest extends \PHPUnit\Framework\TestCase @@ -37,9 +35,18 @@ class LinkTypeProviderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->linkTypeFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductLinkTypeInterfaceFactory::class, ['create']); - $this->linkAttributeFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductLinkAttributeInterfaceFactory::class, ['create']); - $this->linkFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\Product\LinkFactory::class, ['create']); + $this->linkTypeFactoryMock = $this->createPartialMock( + \Magento\Catalog\Api\Data\ProductLinkTypeInterfaceFactory::class, + ['create'] + ); + $this->linkAttributeFactoryMock = $this->createPartialMock( + \Magento\Catalog\Api\Data\ProductLinkAttributeInterfaceFactory::class, + ['create'] + ); + $this->linkFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\LinkFactory::class, + ['create'] + ); $this->linkTypes = [ 'test_product_link_1' => 'test_code_1', 'test_product_link_2' => 'test_code_2', @@ -111,6 +118,9 @@ public function testGetItemAttributes($type, $typeId) $this->assertEquals($expectedResult, $this->model->getItemAttributes($type)); } + /** + * @return array + */ public function getItemAttributesDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php index 6fe0594be08f2..7b830124a365b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php @@ -48,7 +48,12 @@ public function setUp() $this->model = new SaveHandler($this->optionRepository); } - public function testExecute() + /** + * @dataProvider testExecuteDataProvider + * @param bool $dataHasChangedFor + * @return void + */ + public function testExecute(bool $dataHasChangedFor) { $this->optionMock->expects($this->any())->method('getOptionId')->willReturn(5); $this->entity->expects($this->once())->method('getOptions')->willReturn([$this->optionMock]); @@ -63,10 +68,27 @@ public function testExecute() ->method('getProductOptions') ->with($this->entity) ->willReturn([$this->optionMock, $secondOptionMock]); - + $this->entity->expects($this->once()) + ->method('dataHasChangedFor') + ->with('sku') + ->willReturn($dataHasChangedFor); + $this->entity->expects($this->once()) + ->method('getSku') + ->willReturn('product_sku'); $this->optionRepository->expects($this->once())->method('delete'); $this->optionRepository->expects($this->once())->method('save')->with($this->optionMock); $this->assertEquals($this->entity, $this->model->execute($this->entity)); } + + /** + * @return array + */ + public function testExecuteDataProvider(): array + { + return [ + [true], + [false], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php index 1eb5f1a2dacd2..5a8dba5c8c2b2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php @@ -60,10 +60,10 @@ public function isValidTitleDataProvider() { $mess = ['option required fields' => 'Missed values for option required fields']; return [ - ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), [], true], - [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], + ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), [], true], + [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], ]; } @@ -71,20 +71,19 @@ public function isValidTitleDataProvider() * @param $title * @param $type * @param $priceType - * @param $price * @param $product * @param $messages * @param $result * @dataProvider isValidTitleDataProvider */ - public function testIsValidTitle($title, $type, $priceType, $price, $product, $messages, $result) + public function testIsValidTitle($title, $type, $priceType, $product, $messages, $result) { - $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; + $methods = ['getTitle', 'getType', 'getPriceType', '__wakeup', 'getProduct']; $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); $valueMock->expects($this->any())->method('getType')->will($this->returnValue($type)); $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); - $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); + // $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); $this->assertEquals($result, $this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); @@ -124,41 +123,4 @@ public function testIsValidFail($product) $this->assertFalse($this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); } - - /** - * Data provider for testValidationNegativePrice - * @return array - */ - public function validationNegativePriceDataProvider() - { - return [ - ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 1])], - ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 0])], - ]; - } - - /** - * @param $title - * @param $type - * @param $priceType - * @param $price - * @param $product - * @dataProvider validationNegativePriceDataProvider - */ - public function testValidationNegativePrice($title, $type, $priceType, $price, $product) - { - $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; - $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); - $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); - $valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue($type)); - $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); - $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); - $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); - - $messages = [ - 'option values' => 'Invalid option value', - ]; - $this->assertFalse($this->validator->isValid($valueMock)); - $this->assertEquals($messages, $this->validator->getMessages()); - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php index 3c06db0e7ce5f..d8b48d0cc984e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php @@ -58,8 +58,10 @@ public function testIsValidSuccess() { $this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title')); $this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1')); - $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed')); - $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10)); + $this->valueMock->method('getPriceType') + ->willReturn('fixed'); + $this->valueMock->method('getPrice') + ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(15)); $this->assertEmpty($this->validator->getMessages()); @@ -70,8 +72,10 @@ public function testIsValidWithNegativeImageSize() { $this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title')); $this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1')); - $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed')); - $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10)); + $this->valueMock->method('getPriceType') + ->willReturn('fixed'); + $this->valueMock->method('getPrice') + ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(-10)); $this->valueMock->expects($this->never())->method('getImageSizeY'); $messages = [ @@ -85,8 +89,10 @@ public function testIsValidWithNegativeImageSizeY() { $this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title')); $this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1')); - $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed')); - $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10)); + $this->valueMock->method('getPriceType') + ->willReturn('fixed'); + $this->valueMock->method('getPrice') + ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(-10)); $messages = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/PoolTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/PoolTest.php index 42b8215f40615..2bf04332bd73d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/PoolTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/PoolTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Option\Validator; class PoolTest extends \PHPUnit\Framework\TestCase @@ -27,7 +25,9 @@ class PoolTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->defaultValidatorMock = $this->createMock(\Magento\Catalog\Model\Product\Option\Validator\DefaultValidator::class); + $this->defaultValidatorMock = $this->createMock( + \Magento\Catalog\Model\Product\Option\Validator\DefaultValidator::class + ); $this->selectValidatorMock = $this->createMock(\Magento\Catalog\Model\Product\Option\Validator\Select::class); $this->pool = new \Magento\Catalog\Model\Product\Option\Validator\Pool( ['default' => $this->defaultValidatorMock, 'select' => $this->selectValidatorMock] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php index 046ee703c850e..675821fcda111 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php @@ -69,6 +69,9 @@ public function testIsValidSuccess($expectedResult, array $value) $this->assertEquals($expectedResult, $this->validator->isValid($this->valueMock)); } + /** + * @return array + */ public function isValidSuccessDataProvider() { return [ @@ -87,7 +90,7 @@ public function isValidSuccessDataProvider() ] ], [ - false, + true, [ 'title' => 'Some Title', 'price_type' => 'fixed', @@ -154,10 +157,12 @@ public function testIsValidateWithInvalidData($priceType, $price, $title) $this->assertEquals($messages, $this->validator->getMessages()); } + /** + * @return array + */ public function isValidateWithInvalidDataDataProvider() { return [ - 'invalid_price' => ['fixed', -10, 'Title'], 'invalid_price_type' => ['some_value', '10', 'Title'], 'empty_title' => ['fixed', 10, null] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php index cf31d67817684..ffd858c3d433e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php @@ -58,8 +58,10 @@ public function testIsValidSuccess() { $this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title')); $this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1')); - $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed')); - $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10)); + $this->valueMock->method('getPriceType') + ->willReturn('fixed'); + $this->valueMock->method('getPrice') + ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(10)); $this->assertTrue($this->validator->isValid($this->valueMock)); $this->assertEmpty($this->validator->getMessages()); @@ -69,8 +71,10 @@ public function testIsValidWithNegativeMaxCharacters() { $this->valueMock->expects($this->once())->method('getTitle')->will($this->returnValue('option_title')); $this->valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue('name 1.1')); - $this->valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue('fixed')); - $this->valueMock->expects($this->once())->method('getPrice')->will($this->returnValue(10)); + $this->valueMock->method('getPriceType') + ->willReturn('fixed'); + $this->valueMock->method('getPrice') + ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(-10)); $messages = [ 'option values' => 'Invalid option value', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php index a2d31f377e925..a3769fa13beca 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php @@ -19,6 +19,32 @@ class ValueTest extends \PHPUnit\Framework\TestCase */ private $model; + /** + * @var \Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator + */ + private $customOptionPriceCalculatorMock; + + protected function setUp() + { + $mockedResource = $this->getMockedResource(); + $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); + + $this->customOptionPriceCalculatorMock = $this->createMock( + \Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator::class + ); + + $helper = new ObjectManager($this); + $this->model = $helper->getObject( + \Magento\Catalog\Model\Product\Option\Value::class, + [ + 'resource' => $mockedResource, + 'valueCollectionFactory' => $mockedCollectionFactory, + 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock, + ] + ); + $this->model->setOption($this->getMockedOption()); + } + public function testSaveProduct() { $this->model->setValues([100]) @@ -35,11 +61,16 @@ public function testSaveProduct() public function testGetPrice() { - $this->model->setPrice(1000); + $price = 1000; + $this->model->setPrice($price); $this->model->setPriceType(Value::TYPE_PERCENT); - $this->assertEquals(1000, $this->model->getPrice(false)); + $this->assertEquals($price, $this->model->getPrice(false)); - $this->assertEquals(100, $this->model->getPrice(true)); + $percentPice = 100; + $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce()) + ->method('getOptionPriceByPriceCode') + ->willReturn($percentPice); + $this->assertEquals($percentPice, $this->model->getPrice(true)); } public function testGetValuesCollection() @@ -78,23 +109,6 @@ public function testDeleteValue() $this->assertInstanceOf(\Magento\Catalog\Model\Product\Option\Value::class, $this->model->deleteValue(1)); } - protected function setUp() - { - $mockedResource = $this->getMockedResource(); - $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); - $mockedContext = $this->getMockedContext(); - $helper = new ObjectManager($this); - $this->model = $helper->getObject( - \Magento\Catalog\Model\Product\Option\Value::class, - [ - 'resource' => $mockedResource, - 'valueCollectionFactory' => $mockedCollectionFactory, - 'context' => $mockedContext - ] - ); - $this->model->setOption($this->getMockedOption()); - } - /** * @return \Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory */ @@ -164,13 +178,27 @@ private function getMockedOption() private function getMockedProduct() { $mockBuilder = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['getFinalPrice', '__wakeup']) + ->setMethods(['getPriceInfo', '__wakeup']) ->disableOriginalConstructor(); $mock = $mockBuilder->getMock(); - $mock->expects($this->any()) - ->method('getFinalPrice') - ->will($this->returnValue(10)); + $priceInfoMock = $this->getMockForAbstractClass( + \Magento\Framework\Pricing\PriceInfoInterface::class, + [], + '', + false, + false, + true, + ['getPrice'] + ); + + $priceMock = $this->getMockForAbstractClass(\Magento\Framework\Pricing\Price\PriceInterface::class); + + $priceInfoMock->expects($this->any())->method('getPrice')->willReturn($priceMock); + + $mock->expects($this->any())->method('getPriceInfo')->willReturn($priceInfoMock); + + $priceMock->expects($this->any())->method('getValue')->willReturn(10); return $mock; } @@ -229,61 +257,4 @@ private function getMockedResource() return $mock; } - - /** - * @return \Magento\Framework\Model\Context - */ - private function getMockedContext() - { - $mockedRemoveAction = $this->getMockedRemoveAction(); - $mockEventManager = $this->getMockedEventManager(); - - $mockBuilder = $this->getMockBuilder(\Magento\Framework\Model\Context::class) - ->setMethods(['getActionValidator', 'getEventDispatcher']) - ->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('getActionValidator') - ->will($this->returnValue($mockedRemoveAction)); - - $mock->expects($this->any()) - ->method('getEventDispatcher') - ->will($this->returnValue($mockEventManager)); - - return $mock; - } - - /** - * @return RemoveAction - */ - private function getMockedRemoveAction() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\Model\Context::class) - ->setMethods(['isAllowed']) - ->disableOriginalConstructor(); - $mock = $mockBuilder->getMock(); - - $mock->expects($this->any()) - ->method('isAllowed') - ->will($this->returnValue(true)); - - return $mock; - } - - /** - * @return \Magento\Framework\Event\ManagerInterface - */ - private function getMockedEventManager() - { - $mockBuilder = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->setMethods(['dispatch']) - ->disableOriginalConstructor(); - $mock = $mockBuilder->getMockForAbstractClass(); - - $mock->expects($this->any()) - ->method('dispatch'); - - return $mock; - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php index cd0af47180974..83c69ba14a290 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/OptionTest.php @@ -8,6 +8,9 @@ use \Magento\Catalog\Model\Product\Option; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests \Magento\Catalog\Model\Product\Option. + */ class OptionTest extends \PHPUnit\Framework\TestCase { /** @@ -68,4 +71,41 @@ public function testGetRegularPrice() $this->model->setPriceType(null); $this->assertEquals(50, $this->model->getRegularPrice()); } + + /** + * Tests removing ineligible characters from file_extension. + * + * @param string $rawExtensions + * @param string $expectedExtensions + * @dataProvider beforeSaveFileOptionDataProvider + */ + public function testBeforeSaveFileOption($rawExtensions, $expectedExtensions) + { + $this->model->setType(Option::OPTION_GROUP_FILE); + $this->model->setFileExtension($rawExtensions); + $this->model->beforeSave(); + $actualExtensions = $this->model->getFileExtension(); + $this->assertEquals( + $expectedExtensions, + $actualExtensions + ); + } + + /** + * Data provider for testBeforeSaveFileOption. + * + * @return array + */ + public function beforeSaveFileOptionDataProvider() + { + return [ + ['JPG, PNG, GIF', 'jpg, png, gif'], + ['jpg, jpg, jpg', 'jpg'], + ['jpg, png, gif', 'jpg, png, gif'], + ['jpg png gif', 'jpg, png, gif'], + ['!jpg@png#gif%', 'jpg, png, gif'], + ['jpg, png, 123', 'jpg, png, 123'], + ['', ''], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php index c9288790ed6e1..1cc865632e545 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php @@ -6,46 +6,44 @@ namespace Magento\Catalog\Test\Unit\Model\Product\Price; +use Magento\Catalog\Api\Data\TierPriceInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product\Price\TierPriceFactory; +use Magento\Catalog\Model\Product\Price\TierPricePersistence; +use Magento\Catalog\Model\Product\Price\Validation\Result as PriceValidationResult; +use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; +use Magento\Catalog\Model\ProductIdLocatorInterface; + /** * TierPriceStorage test. */ class TierPriceStorageTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Price\TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject + * @var TierPricePersistence|\PHPUnit_Framework_MockObject_MockObject */ private $tierPricePersistence; /** - * @var \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject + * @var TierPriceValidator|\PHPUnit_Framework_MockObject_MockObject */ private $tierPriceValidator; /** - * @var \Magento\Catalog\Model\Product\Price\TierPriceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var TierPriceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $tierPriceFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price|\PHPUnit_Framework_MockObject_MockObject + * @var PriceIndexerProcessor|\PHPUnit_Framework_MockObject_MockObject */ - private $priceIndexer; + private $priceIndexProcessor; /** - * @var \Magento\Catalog\Model\ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ProductIdLocatorInterface|\PHPUnit_Framework_MockObject_MockObject */ private $productIdLocator; - /** - * @var \Magento\PageCache\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $config; - - /** - * @var \Magento\Framework\App\Cache\TypeListInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $typeList; - /** * @var \Magento\Catalog\Model\Product\Price\TierPriceStorage */ @@ -56,36 +54,13 @@ class TierPriceStorageTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->tierPricePersistence = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\TierPricePersistence::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->tierPricePersistence->expects($this->any()) - ->method('getEntityLinkField') - ->willReturn('row_id'); - $this->tierPriceValidator = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->tierPriceFactory = $this->getMockBuilder( - \Magento\Catalog\Model\Product\Price\TierPriceFactory::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->priceIndexer = $this->getMockBuilder(\Magento\Catalog\Model\Indexer\Product\Price::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productIdLocator = $this->getMockBuilder(\Magento\Catalog\Model\ProductIdLocatorInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->config = $this->getMockBuilder(\Magento\PageCache\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - $this->typeList = $this->getMockBuilder(\Magento\Framework\App\Cache\TypeListInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $this->tierPricePersistence = $this->createMock(TierPricePersistence::class); + $this->tierPricePersistence->method('getEntityLinkField') + ->willReturn('entity_id'); + $this->tierPriceValidator = $this->createMock(TierPriceValidator::class); + $this->tierPriceFactory = $this->createMock(TierPriceFactory::class); + $this->priceIndexProcessor = $this->createMock(PriceIndexerProcessor::class); + $this->productIdLocator = $this->createMock(ProductIdLocatorInterface::class); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->tierPriceStorage = $objectManager->getObject( @@ -94,10 +69,8 @@ protected function setUp() 'tierPricePersistence' => $this->tierPricePersistence, 'tierPriceValidator' => $this->tierPriceValidator, 'tierPriceFactory' => $this->tierPriceFactory, - 'priceIndexer' => $this->priceIndexer, + 'priceIndexProcessor' => $this->priceIndexProcessor, 'productIdLocator' => $this->productIdLocator, - 'config' => $this->config, - 'typeList' => $this->typeList, ] ); } @@ -125,7 +98,7 @@ public function testGet() [ [ 'value_id' => 1, - 'row_id' => 2, + 'entity_id' => 2, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 2.0000, @@ -135,7 +108,7 @@ public function testGet() ], [ 'value_id' => 2, - 'row_id' => 3, + 'entity_id' => 3, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 3.0000, @@ -145,7 +118,7 @@ public function testGet() ] ] ); - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); + $price = $this->getMockBuilder(TierPriceInterface::class)->getMockForAbstractClass(); $this->tierPriceFactory->expects($this->atLeastOnce())->method('create')->willReturn($price); $prices = $this->tierPriceStorage->get($skus); $this->assertNotEmpty($prices); @@ -159,36 +132,37 @@ public function testGet() */ public function testUpdate() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + $price = $this->createMock(TierPriceInterface::class); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); - $this->tierPriceValidator - ->expects($this->atLeastOnce()) + $this->tierPriceValidator->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 2, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 2, - 'value' => 3, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton') + ->willReturn( + [ + 'entity_id' => 2, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 2, + 'value' => 3, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); $this->tierPricePersistence->expects($this->once()) ->method('get') ->willReturn( [ [ 'value_id' => 1, - 'row_id' => 2, + 'entity_id' => 2, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 2.0000, @@ -198,11 +172,15 @@ public function testUpdate() ] ] ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('update'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('simple'); + $this->tierPricePersistence->expects($this->once()) + ->method('update'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2, 3]); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('simple'); + $this->assertEmpty($this->tierPriceStorage->update([$price])); } @@ -213,35 +191,41 @@ public function testUpdate() */ public function testReplace() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('virtual'); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); + $price = $this->createMock(TierPriceInterface::class); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('virtual'); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); $this->productIdLocator->expects($this->atLeastOnce()) ->method('retrieveProductIdsBySkus') ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); $this->tierPriceValidator - ->expects($this->atLeastOnce()) + ->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 3, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 3, - 'value' => 7, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('replace'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton') + ->willReturn( + [ + 'entity_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->once()) + ->method('replace'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2, 3]); + $this->assertEmpty($this->tierPriceStorage->replace([$price])); } @@ -252,13 +236,15 @@ public function testReplace() */ public function testDelete() { - $price = $this->getMockBuilder(\Magento\Catalog\Api\Data\TierPriceInterface::class)->getMockForAbstractClass(); - $price->expects($this->atLeastOnce())->method('getSku')->willReturn('simple'); - $result = $this->getMockBuilder(\Magento\Catalog\Model\Product\Price\Validation\Result::class) - ->disableOriginalConstructor() - ->getMock(); - $result->expects($this->atLeastOnce())->method('getFailedRowIds')->willReturn([]); - $this->tierPriceValidator->expects($this->atLeastOnce()) + $price = $this->createMock(TierPriceInterface::class); + $price->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn('simple'); + $result = $this->createMock(PriceValidationResult::class); + $result->expects($this->once()) + ->method('getFailedRowIds') + ->willReturn([]); + $this->tierPriceValidator->expects($this->once()) ->method('retrieveValidationResult') ->willReturn($result); $this->productIdLocator->expects($this->atLeastOnce()) @@ -270,7 +256,7 @@ public function testDelete() [ [ 'value_id' => 7, - 'row_id' => 7, + 'entity_id' => 7, 'all_groups' => 1, 'customer_group_id' => 0, 'qty' => 5.0000, @@ -280,21 +266,24 @@ public function testDelete() ] ] ); - $this->tierPriceFactory->expects($this->atLeastOnce())->method('createSkeleton')->willReturn( - [ - 'row_id' => 3, - 'all_groups' => 1, - 'customer_group_id' => 0, - 'qty' => 3, - 'value' => 7, - 'percentage_value' => null, - 'website_id' => 0 - ] - ); - $this->tierPricePersistence->expects($this->atLeastOnce())->method('delete'); - $this->priceIndexer->expects($this->atLeastOnce())->method('execute'); - $this->config->expects($this->atLeastOnce())->method('isEnabled')->willReturn(true); - $this->typeList->expects($this->atLeastOnce())->method('invalidate'); + $this->tierPriceFactory->expects($this->once()) + ->method('createSkeleton')->willReturn( + [ + 'entity_id' => 3, + 'all_groups' => 1, + 'customer_group_id' => 0, + 'qty' => 3, + 'value' => 7, + 'percentage_value' => null, + 'website_id' => 0 + ] + ); + $this->tierPricePersistence->expects($this->once()) + ->method('delete'); + $this->priceIndexProcessor->expects($this->once()) + ->method('reindexList') + ->with([2]); + $this->assertEmpty($this->tierPriceStorage->delete([$price])); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php index 75fbad0aab262..6029a2b820086 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product; class PriceModifierTest extends \PHPUnit\Framework\TestCase @@ -56,7 +54,7 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedMessage This product doesn't have tier price + * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '1', website = 1, qty = 3 */ public function testRemoveWhenTierPricesNotExists() { @@ -72,7 +70,7 @@ public function testRemoveWhenTierPricesNotExists() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedMessage For current customerGroupId = '10' with 'qty' = 15 any tier price exist'. + * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '10', website = 1, qty = 5 */ public function testRemoveTierPriceForNonExistingCustomerGroup() { @@ -83,7 +81,7 @@ public function testRemoveTierPriceForNonExistingCustomerGroup() ->will($this->returnValue($this->prices)); $this->productMock->expects($this->never())->method('setData'); $this->productRepositoryMock->expects($this->never())->method('save'); - $this->priceModifier->removeTierPrice($this->productMock, 10, 15, 1); + $this->priceModifier->removeTierPrice($this->productMock, 10, 5, 1); } public function testSuccessfullyRemoveTierPriceSpecifiedForAllGroups() diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php index fce4a02622d9e..2fd787e216118 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php @@ -78,27 +78,20 @@ protected function setUp() ); } - public function testFilterProductActions() + /** + * @dataProvider filterProductActionsDataProvider + * + * @param array $productsData + * @param bool $correct + * @return void + */ + public function testFilterProductActions(array $productsData, bool $correct) { - $productsData = [ - 1 => [ - 'added_at' => 12, - 'product_id' => 1, - ], - 2 => [ - 'added_at' => 13, - 'product_id' => 2, - ], - 3 => [ - 'added_at' => 14, - 'product_id' => 3, - ] - ]; $frontendConfiguration = $this->createMock(\Magento\Catalog\Model\FrontendStorageConfigurationInterface::class); $frontendConfiguration->expects($this->once()) ->method('get') ->willReturn([ - 'lifetime' => 2 + 'lifetime' => 2, ]); $this->frontendStorageConfigurationPoolMock->expects($this->once()) ->method('get') @@ -110,7 +103,6 @@ public function testFilterProductActions() $action2 = $this->getMockBuilder(ProductFrontendActionInterface::class) ->getMockForAbstractClass(); - $frontendAction = $this->createMock(ProductFrontendActionInterface::class); $collection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -126,47 +118,91 @@ public function testFilterProductActions() $collection->expects($this->once()) ->method('addFilterByUserIdentities') ->with(1, 34); - $collection->expects($this->any()) - ->method('addFieldToFilter') - ->withConsecutive(['type_id'], ['product_id']); - $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); - $collection->expects($this->once()) - ->method('getIterator') - ->willReturn($iterator); - $this->entityManagerMock->expects($this->once()) - ->method('delete') - ->with($frontendAction); - $this->productFrontendActionFactoryMock->expects($this->exactly(2)) - ->method('create') - ->withConsecutive( - [ + if ($correct) { + $frontendAction = $this->createMock(ProductFrontendActionInterface::class); + $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); + $collection->expects($this->any()) + ->method('addFieldToFilter') + ->withConsecutive(['type_id'], ['product_id']); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn($iterator); + $this->entityManagerMock->expects($this->once()) + ->method('delete') + ->with($frontendAction); + $this->entityManagerMock->expects($this->exactly(2)) + ->method('save') + ->withConsecutive([$action1], [$action2]); + $this->productFrontendActionFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( [ - 'data' => [ - 'visitor_id' => null, - 'customer_id' => 1, - 'added_at' => 12, - 'product_id' => 1, - 'type_id' => 'recently_compared_product' - ] - ] - ], - [ + [ + 'data' => [ + 'visitor_id' => null, + 'customer_id' => 1, + 'added_at' => 12, + 'product_id' => 1, + 'type_id' => 'recently_compared_product', + ], + ], + ], [ - 'data' => [ - 'visitor_id' => null, - 'customer_id' => 1, - 'added_at' => 13, - 'product_id' => 2, - 'type_id' => 'recently_compared_product' - ] + [ + 'data' => [ + 'visitor_id' => null, + 'customer_id' => 1, + 'added_at' => 13, + 'product_id' => 2, + 'type_id' => 'recently_compared_product', + ], + ], ] - ] - ) - ->willReturnOnConsecutiveCalls($action1, $action2); - $this->entityManagerMock->expects($this->exactly(2)) - ->method('save') - ->withConsecutive([$action1], [$action2]); + ) + ->willReturnOnConsecutiveCalls($action1, $action2); + } else { + $this->entityManagerMock->expects($this->never()) + ->method('delete'); + $this->entityManagerMock->expects($this->never()) + ->method('save'); + } + $this->model->syncActions($productsData, 'recently_compared_product'); } + + /** + * @return array + */ + public function filterProductActionsDataProvider(): array + { + return [ + [ + 'productsData' => [ + 1 => [ + 'added_at' => 12, + 'product_id' => 1, + ], + 2 => [ + 'added_at' => 13, + 'product_id' => 2, + ], + 3 => [ + 'added_at' => 14, + 'product_id' => 3, + ], + ], + 'correct' => true, + ], + [ + 'productsData' => [ + 1 => [ + 'added_at' => 12, + 'product_id' => 'test', + ], + ], + 'correct' => false, + ], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php new file mode 100644 index 0000000000000..699931b5fe89d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarMemorizerTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\Product\ProductList; + +use Magento\Catalog\Model\Product\ProductList\Toolbar; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class for testing toolbal memorizer. + */ +class ToolbarMemorizerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ToolbarMemorizer + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Toolbar + */ + private $toolbarMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CatalogSession + */ + private $catalogSessionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ScopeConfigInterface + */ + private $scopeConfigMock; + + /** + * @var ObjectManager $objectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->toolbarMock = $this->getMockBuilder(Toolbar::class) + ->disableOriginalConstructor() + ->setMethods(['getOrder', 'getDirection', 'getLimit', 'getMode']) + ->getMock(); + $this->catalogSessionMock = $this->getMockBuilder(CatalogSession::class) + ->disableOriginalConstructor() + ->setMethods(['getParamsMemorizeDisabled', 'getData']) + ->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject( + ToolbarMemorizer::class, + [ + 'toolbarModel' => $this->toolbarMock, + 'catalogSession' => $this->catalogSessionMock, + 'scopeConfig' => $this->scopeConfigMock, + ] + ); + } + + /** + * @return array + */ + public function getMainDataProvider(): array + { + return [ + ['any_value',null,null,null,'any_value'], + [null, 'any_value', false, null, 'any_value'], + [null, null, false, null, null], + [null, null, true, 'data', 'data'], + ]; + } + + /** + * Test get order. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetOrder($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'order', $variable); + $this->toolbarMock->method('getOrder')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getOrder()); + } + + /** + * Test get direction. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetDirection($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'direction', $variable); + $this->toolbarMock->method('getDirection')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getDirection()); + } + + /** + * Test get mode. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetMode($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'mode', $variable); + $this->toolbarMock->method('getMode')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getMode()); + } + + /** + * Test getting limit. + * + * @param string|null $variable + * @param string|null $variableValue + * @param bool|null $flag + * @param string|null $data + * @param string|null $expected + * @return void + * + * @dataProvider getMainDataProvider + */ + public function testGetLimit($variable, $variableValue, $flag, $data, $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'limit', $variable); + $this->toolbarMock->method('getLimit')->willReturn($variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->catalogSessionMock->method('getData')->willReturn($data); + $this->assertEquals($expected, $this->model->getLimit()); + } + + /** + * Test memorizing parameters. + * + * @return void + */ + public function testMemorizeParams() + { + $this->catalogSessionMock->method('getParamsMemorizeDisabled')->willReturn(false); + $this->objectManager->setBackwardCompatibleProperty($this->model, 'isMemorizingAllowed', true); + $this->model->memorizeParams(); + } + + /** + * @return array + */ + public function getMemorizedDataProvider(): array + { + return [ + [null, false, false], + [null, true, true], + [false, false, false], + [false, true, false], + [true, false, true], + [true, true, true], + ]; + } + + /** + * Test method isMemorizingAllowed. + * + * @param bool|null $variableValue + * @param bool $flag + * @param bool $expected + * @return void + * + * @dataProvider getMemorizedDataProvider + */ + public function testIsMemorizingAllowed($variableValue, bool $flag, bool $expected) + { + $this->objectManager->setBackwardCompatibleProperty($this->model, 'isMemorizingAllowed', $variableValue); + $this->scopeConfigMock->method('isSetFlag')->willReturn($flag); + $this->assertEquals($expected, $this->model->isMemorizingAllowed()); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php index 84a9e9ded094b..3789ba4ee126d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductList/ToolbarTest.php @@ -112,6 +112,9 @@ public function testGetCurrentPageNoParam() $this->assertEquals(1, $this->toolbarModel->getCurrentPage()); } + /** + * @return array + */ public function stringParamProvider() { return [ @@ -119,6 +122,9 @@ public function stringParamProvider() ]; } + /** + * @return array + */ public function intParamProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php index 7506b4adc1d3a..5080e64f46e27 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ReservedAttributeListTest.php @@ -40,6 +40,9 @@ public function testIsReservedAttribute($isUserDefined, $attributeCode, $expecte $this->assertEquals($expected, $this->model->isReservedAttribute($attribute)); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php index 6d42a33d1fc73..046beb29be71e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product; use Magento\Catalog\Model\Product\TierPriceManagement; @@ -72,11 +70,17 @@ class TierPriceManagementTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->repositoryMock = $this->createMock(\Magento\Catalog\Model\ProductRepository::class); - $this->priceFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class, ['create']); + $this->priceFactoryMock = $this->createPartialMock( + \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getId', '__wakeup']); - $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData', 'getIdBySku', 'load', '__wakeup', 'save', 'validate', 'setData']); + $this->productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getData', 'getIdBySku', 'load', '__wakeup', 'save', 'validate', 'setData'] + ); $this->configMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->priceModifierMock = $this->createMock(\Magento\Catalog\Model\Product\PriceModifier::class); @@ -147,6 +151,9 @@ public function testGetList($configValue, $customerGroupId, $groupData, $expecte } } + /** + * @return array + */ public function getListDataProvider() { return [ @@ -188,7 +195,7 @@ public function testSuccessDeleteTierPrice() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @message Such product doesn't exist + * @expectedExceptionMessage No such entity. */ public function testDeleteTierPriceFromNonExistingProduct() { @@ -399,6 +406,9 @@ public function testAddWithInvalidData($price, $qty) $this->service->add('product_sku', 1, $price, $qty); } + /** + * @return array + */ public function addDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php index dcddab60fb0b9..b34375256a959 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/AbstractTypeTest.php @@ -96,6 +96,9 @@ public function testAttributesCompare($attr1, $attr2, $expectedResult) $this->assertEquals($expectedResult, $this->model->attributesCompare($attribute, $attribute2)); } + /** + * @return array + */ public function attributeCompareProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php index 995aac371e598..99151d1c8dd39 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\Product\Type; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php index 9fa820d64bae1..ef7aad2cbb802 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/UrlTest.php @@ -169,6 +169,9 @@ public function testGetUrl( } } + /** + * @return array + */ public function getUrlDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/ManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/ManagementTest.php index 0365e44b7f800..ea518b7737257 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/ManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/ManagementTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\ProductLink; use Magento\Framework\Exception\NoSuchEntityException; @@ -205,8 +203,9 @@ public function testSetProductLinksNoProductException() $this->productRepositoryMock->expects($this->once()) ->method('get') - ->will($this->throwException( - new \Magento\Framework\Exception\NoSuchEntityException(__('Requested product doesn\'t exist')))); + ->willThrowException( + new \Magento\Framework\Exception\NoSuchEntityException(__('Requested product doesn\'t exist')) + ); $this->model->setProductLinks($productSku, $links); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php index 05aee02a05d28..56aca8d205302 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductLink/RepositoryTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\ProductLink; /** @@ -71,8 +69,12 @@ protected function setUp() { $linkManagementMock = $this->createMock(\Magento\Catalog\Model\ProductLink\Management::class); $this->productRepositoryMock = $this->createMock(\Magento\Catalog\Model\ProductRepository::class); - $this->entityCollectionProviderMock = $this->createMock(\Magento\Catalog\Model\ProductLink\CollectionProvider::class); - $this->linkInitializerMock = $this->createMock(\Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks::class); + $this->entityCollectionProviderMock = $this->createMock( + \Magento\Catalog\Model\ProductLink\CollectionProvider::class + ); + $this->linkInitializerMock = $this->createMock( + \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks::class + ); $this->metadataPoolMock = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); $this->hydratorPoolMock = $this->createMock(\Magento\Framework\EntityManager\HydratorPool::class); $this->hydratorMock = $this->createPartialMock(\Magento\Framework\EntityManager\Hydrator::class, ['extract']); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php index 034b04b6a757d..cfb54c3aefd0f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php @@ -29,12 +29,12 @@ ], ], 'renderer_attribute_with_invalid_value' => [ - '<?xml version="1.0"?><config><option name="name_one" renderer="true12"><inputType name="name_one"/>' . + '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . '</option></config>', [ - "Element 'option', attribute 'renderer': [facet 'pattern'] The value 'true12' is not accepted by the " . - "pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", - "Element 'option', attribute 'renderer': 'true12' is not a valid value of the atomic" . + "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic" . " type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php new file mode 100644 index 0000000000000..02773b2fb3d70 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepository/MediaGalleryProcessorTest.php @@ -0,0 +1,227 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Model\ProductRepository; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests for ProductRepository/MediaGalleryProcessor. + */ +class MediaGalleryProcessorTest extends TestCase +{ + /** + * Test subject. + * + * @var MediaGalleryProcessor + */ + private $model; + + /** + * @var Processor|\PHPUnit_Framework_MockObject_MockObject + */ + private $processor; + + /** + * @var ImageContentInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $contentFactory; + + /** + * @var ImageProcessorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $imageProcessor; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $product; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ + 'hasGalleryAttribute', + 'getMediaConfig', + 'getMediaAttributes', + 'getMediaGalleryEntries', + ] + ); + $this->product->expects($this->any()) + ->method('hasGalleryAttribute') + ->willReturn(true); + $this->processor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contentFactory = $this->getMockBuilder(ImageContentInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->imageProcessor = $this->getMockBuilder(ImageProcessorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject( + MediaGalleryProcessor::class, + [ + 'processor' => $this->processor, + 'contentFactory' => $this->contentFactory, + 'imageProcessor' => $this->imageProcessor, + ] + ); + } + + /** + * Test add image. + * + * @return void + */ + public function testProcessWithNewMediaEntry() + { + $mediaGalleryEntries = [ + [ + 'value_id' => null, + 'label' => 'label_text', + 'position' => 10, + 'disabled' => false, + 'types' => ['image', 'small_image'], + 'content' => [ + ImageContentInterface::NAME => 'filename', + ImageContentInterface::TYPE => 'image/jpeg', + ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content', + ], + 'media_type' => 'media_type', + ], + ]; + + //setup media attribute backend. + $mediaTmpPath = '/tmp'; + $absolutePath = '/a/b/filename.jpg'; + $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaConfigMock->expects($this->once()) + ->method('getTmpMediaShortUrl') + ->with($absolutePath) + ->willReturn($mediaTmpPath . $absolutePath); + $this->product->setData('media_gallery', ['images' => $mediaGalleryEntries]); + $this->product->expects($this->any()) + ->method('getMediaAttributes') + ->willReturn(['image' => 'imageAttribute', 'small_image' => 'small_image_attribute']); + $this->product->expects($this->once()) + ->method('getMediaConfig') + ->willReturn($mediaConfigMock); + $this->processor->expects($this->once())->method('clearMediaAttribute') + ->with($this->product, ['image', 'small_image']); + + //verify new entries. + $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class) + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + $this->contentFactory->expects($this->once()) + ->method('create') + ->willReturn($contentDataObject); + + $this->imageProcessor->expects($this->once()) + ->method('processImageContent') + ->willReturn($absolutePath); + + $imageFileUri = 'imageFileUri'; + $this->processor->expects($this->once())->method('addImage') + ->with($this->product, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false) + ->willReturn($imageFileUri); + $this->processor->expects($this->once())->method('updateImage') + ->with( + $this->product, + $imageFileUri, + [ + 'label' => 'label_text', + 'position' => 10, + 'disabled' => false, + 'media_type' => 'media_type', + ] + ); + + $this->model->processMediaGallery($this->product, $mediaGalleryEntries); + } + + /** + * Test update(delete) images. + */ + public function testProcessExistingWithMediaGalleryEntries() + { + //update one entry, delete one entry. + $newEntries = [ + [ + 'id' => 5, + 'label' => 'new_label_text', + 'file' => 'filename1', + 'position' => 10, + 'disabled' => false, + 'types' => ['image', 'small_image'], + ], + ]; + + $existingMediaGallery = [ + 'images' => [ + [ + 'value_id' => 5, + 'label' => 'label_text', + 'file' => 'filename1', + 'position' => 10, + 'disabled' => true, + ], + [ + 'value_id' => 6, //will be deleted. + 'file' => 'filename2', + ], + ], + ]; + + $expectedResult = [ + [ + 'value_id' => 5, + 'id' => 5, + 'label' => 'new_label_text', + 'file' => 'filename1', + 'position' => 10, + 'disabled' => false, + 'types' => ['image', 'small_image'], + ], + [ + 'value_id' => 6, //will be deleted. + 'file' => 'filename2', + 'removed' => true, + ], + ]; + + $this->product->setData('media_gallery', $existingMediaGallery); + $this->product->expects($this->any()) + ->method('getMediaAttributes') + ->willReturn(['image' => 'filename1', 'small_image' => 'filename2']); + + $this->processor->expects($this->once())->method('clearMediaAttribute') + ->with($this->product, ['image', 'small_image']); + $this->processor->expects($this->once()) + ->method('setMediaAttribute') + ->with($this->product, ['image', 'small_image'], 'filename1'); + $this->model->processMediaGallery($this->product, $newEntries); + $this->assertEquals($expectedResult, $this->product->getMediaGallery('images')); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index c98705b4eda63..ff680916bd15f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -5,21 +5,39 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +declare(strict_types=1); namespace Magento\Catalog\Test\Unit\Model; -use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; +use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\ImageContentValidator; +use Magento\Framework\Api\ImageContentValidatorInterface; +use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\DB\Adapter\ConnectionException; +use Magento\Framework\Filesystem; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Class ProductRepositoryTest - * @package Magento\Catalog\Test\Unit\Model * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -27,132 +45,127 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ - protected $productMock; + private $product; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ - protected $initializedProductMock; + private $initializedProduct; /** - * @var \Magento\Catalog\Model\ProductRepository + * @var ProductRepository */ - protected $model; + private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Helper|MockObject */ - protected $initializationHelperMock; + private $initializationHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ - protected $resourceModelMock; + private $resourceModel; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ProductFactory|MockObject */ - protected $productFactoryMock; + private $productFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|MockObject */ - protected $collectionFactoryMock; + private $collectionFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var SearchCriteriaBuilder|MockObject */ - protected $searchCriteriaBuilderMock; + private $searchCriteriaBuilder; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var FilterBuilder|MockObject */ - protected $filterBuilderMock; + private $filterBuilder; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ProductAttributeRepositoryInterface|MockObject */ - protected $metadataServiceMock; + private $metadataService; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ProductSearchResultsInterfaceFactory|MockObject */ - protected $searchResultsFactoryMock; + private $searchResultsFactory; /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + * @var ExtensibleDataObjectConverter|MockObject */ - protected $eavConfigMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $extensibleDataObjectConverterMock; + private $extensibleDataObjectConverter; /** * @var array data to create product */ - protected $productData = [ + private $productData = [ 'sku' => 'exisiting', 'name' => 'existing product', ]; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Filesystem + * @var Filesystem|MockObject */ - protected $fileSystemMock; + private $fileSystem; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap + * @var MimeTypeExtensionMap|MockObject */ - protected $mimeTypeExtensionMapMock; + private $mimeTypeExtensionMap; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ImageContentInterfaceFactory|MockObject */ - protected $contentFactoryMock; + private $contentFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageContentValidator + * @var ImageContentValidator|MockObject */ - protected $contentValidatorMock; + private $contentValidator; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var LinkTypeProvider|MockObject */ - protected $linkTypeProviderMock; + private $linkTypeProvider; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\ImageProcessorInterface + * @var ImageProcessorInterface|MockObject */ - protected $imageProcessorMock; + private $imageProcessor; /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var ObjectManager */ - protected $objectManager; + private $objectManager; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ - protected $storeManagerMock; + private $storeManager; /** - * @var \Magento\Catalog\Model\Product\Gallery\Processor|\PHPUnit_Framework_MockObject_MockObject + * @var MediaGalleryProcessor|MockObject */ - protected $mediaGalleryProcessor; + private $mediaGalleryProcessor; /** - * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionProcessorInterface|MockObject */ - private $collectionProcessorMock; + private $collectionProcessor; /** - * @var Json|\PHPUnit_Framework_MockObject_MockObject + * @var ProductExtensionInterface|MockObject */ - private $serializerMock; + private $productExtension; /** * Product repository cache limit. @@ -166,9 +179,11 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->productFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create', 'setData']); + $this->productFactory = $this->createPartialMock(ProductFactory::class, ['create', 'setData']); - $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $this->product = $this->createPartialMock( + Product::class, + [ 'getId', 'getSku', 'setWebsiteIds', @@ -176,15 +191,20 @@ protected function setUp() 'load', 'setData', 'getStoreId', - 'getMediaGalleryEntries' - ]); + 'getMediaGalleryEntries', + 'getExtensionAttributes' + ] + ); - $this->initializedProductMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $this->initializedProduct = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ 'getWebsiteIds', 'setProductOptions', 'load', 'getOptions', 'getSku', + 'getId', 'hasGalleryAttribute', 'getMediaConfig', 'getMediaAttributes', @@ -193,86 +213,88 @@ protected function setUp() 'validate', 'save', 'getMediaGalleryEntries', + 'getExtensionAttributes' ] ); - $this->initializedProductMock->expects($this->any()) + $this->initializedProduct->expects($this->any()) ->method('hasGalleryAttribute') ->willReturn(true); - $this->filterBuilderMock = $this->createMock(\Magento\Framework\Api\FilterBuilder::class); - $this->initializationHelperMock = $this->createMock(\Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::class); - $this->collectionFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class, ['create']); - $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class); - $this->metadataServiceMock = $this->createMock(\Magento\Catalog\Api\ProductAttributeRepositoryInterface::class); - $this->searchResultsFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory::class, ['create']); - $this->resourceModelMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); + $this->filterBuilder = $this->createMock(FilterBuilder::class); + $this->initializationHelper = $this->createMock(Helper::class); + $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']); + $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); + $this->metadataService = $this->createMock(ProductAttributeRepositoryInterface::class); + $this->searchResultsFactory = $this->createPartialMock(ProductSearchResultsInterfaceFactory::class, ['create']); + $this->resourceModel = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); $this->objectManager = new ObjectManager($this); - $this->extensibleDataObjectConverterMock = $this - ->getMockBuilder(\Magento\Framework\Api\ExtensibleDataObjectConverter::class) + $this->extensibleDataObjectConverter = $this + ->getMockBuilder(ExtensibleDataObjectConverter::class) ->setMethods(['toNestedArray']) ->disableOriginalConstructor() ->getMock(); - $this->fileSystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) + $this->fileSystem = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor()->getMock(); - $this->mimeTypeExtensionMapMock = - $this->getMockBuilder(\Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap::class)->getMock(); - $this->contentFactoryMock = $this->createPartialMock(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class, ['create']); - $this->contentValidatorMock = $this->getMockBuilder( - \Magento\Framework\Api\ImageContentValidatorInterface::class) + $this->mimeTypeExtensionMap = $this->getMockBuilder(MimeTypeExtensionMap::class) + ->getMock(); + $this->contentFactory = $this->createPartialMock(ImageContentInterfaceFactory::class, ['create']); + $this->contentValidator = $this->getMockBuilder(ImageContentValidatorInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->linkTypeProviderMock = $this->createPartialMock(\Magento\Catalog\Model\Product\LinkTypeProvider::class, ['getLinkTypes']); - $this->imageProcessorMock = $this->createMock(\Magento\Framework\Api\ImageProcessorInterface::class); + $this->linkTypeProvider = $this->createPartialMock(LinkTypeProvider::class, ['getLinkTypes']); + $this->imageProcessor = $this->createMock(ImageProcessorInterface::class); - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) ->disableOriginalConstructor() ->setMethods([]) ->getMockForAbstractClass(); - $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + $this->productExtension = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['__toArray']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->productExtension + ->method('__toArray') + ->willReturn([]); + $this->product + ->method('getExtensionAttributes') + ->willReturn($this->productExtension); + $this->initializedProduct + ->method('getExtensionAttributes') + ->willReturn($this->productExtension); + $storeMock = $this->getMockBuilder(StoreInterface::class) ->disableOriginalConstructor() ->setMethods([]) ->getMockForAbstractClass(); $storeMock->expects($this->any())->method('getWebsiteId')->willReturn('1'); $storeMock->expects($this->any())->method('getCode')->willReturn(\Magento\Store\Model\Store::ADMIN_CODE); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); - $this->mediaGalleryProcessor = $this->createMock(\Magento\Catalog\Model\Product\Gallery\Processor::class); + $this->mediaGalleryProcessor = $this->createMock(MediaGalleryProcessor::class); - $this->collectionProcessorMock = $this->getMockBuilder(CollectionProcessorInterface::class) + $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) ->getMock(); - $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); - $this->serializerMock->expects($this->any()) - ->method('unserialize') - ->will( - $this->returnCallback( - function ($value) { - return json_decode($value, true); - } - ) - ); - $this->model = $this->objectManager->getObject( - \Magento\Catalog\Model\ProductRepository::class, + ProductRepository::class, [ - 'productFactory' => $this->productFactoryMock, - 'initializationHelper' => $this->initializationHelperMock, - 'resourceModel' => $this->resourceModelMock, - 'filterBuilder' => $this->filterBuilderMock, - 'collectionFactory' => $this->collectionFactoryMock, - 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, - 'metadataServiceInterface' => $this->metadataServiceMock, - 'searchResultsFactory' => $this->searchResultsFactoryMock, - 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock, - 'contentValidator' => $this->contentValidatorMock, - 'fileSystem' => $this->fileSystemMock, - 'contentFactory' => $this->contentFactoryMock, - 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMapMock, - 'linkTypeProvider' => $this->linkTypeProviderMock, - 'imageProcessor' => $this->imageProcessorMock, - 'storeManager' => $this->storeManagerMock, + 'productFactory' => $this->productFactory, + 'initializationHelper' => $this->initializationHelper, + 'resourceModel' => $this->resourceModel, + 'filterBuilder' => $this->filterBuilder, + 'collectionFactory' => $this->collectionFactory, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'metadataServiceInterface' => $this->metadataService, + 'searchResultsFactory' => $this->searchResultsFactory, + 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, + 'contentValidator' => $this->contentValidator, + 'fileSystem' => $this->fileSystem, + 'contentFactory' => $this->contentFactory, + 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMap, + 'linkTypeProvider' => $this->linkTypeProvider, + 'imageProcessor' => $this->imageProcessor, + 'storeManager' => $this->storeManager, 'mediaGalleryProcessor' => $this->mediaGalleryProcessor, - 'collectionProcessor' => $this->collectionProcessorMock, - 'serializer' => $this->serializerMock, + 'collectionProcessor' => $this->collectionProcessor, + 'serializer' => new Json(), 'cacheLimit' => $this->cacheLimit ] ); @@ -284,50 +306,53 @@ function ($value) { */ public function testGetAbsentProduct() { - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with('test_sku') + $this->productFactory->expects($this->never())->method('create') + ->will($this->returnValue($this->product)); + $this->resourceModel->expects($this->once())->method('getIdBySku')->with('test_sku') ->will($this->returnValue(null)); - $this->productFactoryMock->expects($this->never())->method('setData'); + $this->productFactory->expects($this->never())->method('setData'); $this->model->get('test_sku'); } public function testCreateCreatesProduct() { $sku = 'test_sku'; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) + $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) ->will($this->returnValue('test_id')); - $this->productMock->expects($this->once())->method('load')->with('test_id'); - $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); - $this->assertEquals($this->productMock, $this->model->get($sku)); + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); + $this->product->expects($this->once())->method('load')->with('test_id'); + $this->product->expects($this->any())->method('getId')->willReturn('test_id'); + $this->product->expects($this->any())->method('getSku')->willReturn($sku); + $this->assertEquals($this->product, $this->model->get($sku)); } public function testGetProductInEditMode() { $sku = 'test_sku'; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) + $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) ->will($this->returnValue('test_id')); - $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true); - $this->productMock->expects($this->once())->method('load')->with('test_id'); - $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); - $this->assertEquals($this->productMock, $this->model->get($sku, true)); + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); + $this->product->expects($this->once())->method('setData')->with('_edit_mode', true); + $this->product->expects($this->once())->method('load')->with('test_id'); + $this->product->expects($this->any())->method('getId')->willReturn('test_id'); + $this->product->expects($this->any())->method('getSku')->willReturn($sku); + $this->assertEquals($this->product, $this->model->get($sku, true)); } public function testGetBySkuWithSpace() { $trimmedSku = 'test_sku'; $sku = 'test_sku '; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) + $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) ->will($this->returnValue('test_id')); - $this->productMock->expects($this->once())->method('load')->with('test_id'); - $this->productMock->expects($this->once())->method('getSku')->willReturn($trimmedSku); - $this->assertEquals($this->productMock, $this->model->get($sku)); + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); + $this->product->expects($this->once())->method('load')->with('test_id'); + $this->product->expects($this->any())->method('getId')->willReturn('test_id'); + $this->product->expects($this->any())->method('getSku')->willReturn($trimmedSku); + $this->assertEquals($this->product, $this->model->get($sku)); } public function testGetWithSetStoreId() @@ -335,13 +360,13 @@ public function testGetWithSetStoreId() $productId = 123; $sku = 'test-sku'; $storeId = 7; - $this->productFactoryMock->expects($this->once())->method('create')->willReturn($this->productMock); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId); - $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); - $this->productMock->expects($this->once())->method('load')->with($productId); - $this->productMock->expects($this->once())->method('getId')->willReturn($productId); - $this->productMock->expects($this->once())->method('getSku')->willReturn($sku); - $this->assertSame($this->productMock, $this->model->get($sku, false, $storeId)); + $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku)->willReturn($productId); + $this->productFactory->expects($this->once())->method('create')->willReturn($this->product); + $this->product->expects($this->once())->method('setData')->with('store_id', $storeId); + $this->product->expects($this->once())->method('load')->with($productId); + $this->product->expects($this->any())->method('getId')->willReturn($productId); + $this->product->expects($this->any())->method('getSku')->willReturn($sku); + $this->assertSame($this->product, $this->model->get($sku, false, $storeId)); } /** @@ -350,22 +375,28 @@ public function testGetWithSetStoreId() */ public function testGetByIdAbsentProduct() { - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('load')->with('product_id'); - $this->productMock->expects($this->once())->method('getId')->willReturn(null); + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); + $this->product->expects($this->once())->method('load')->with('product_id'); + $this->product->expects($this->once())->method('getId')->willReturn(null); $this->model->getById('product_id'); } public function testGetByIdProductInEditMode() { $productId = 123; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true); - $this->productMock->expects($this->once())->method('load')->with($productId); - $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); - $this->assertEquals($this->productMock, $this->model->getById($productId, true)); + $this->productFactory->method('create') + ->willReturn($this->product); + $this->product->method('setData') + ->with('_edit_mode', true); + $this->product->method('load') + ->with($productId); + $this->product->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($productId); + $this->product->method('getSku') + ->willReturn('simple'); + $this->assertEquals($this->product, $this->model->getById($productId, true)); } /** @@ -379,20 +410,24 @@ public function testGetByIdProductInEditMode() public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId) { $callIndex = 0; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); if ($editMode) { - $this->productMock->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode); + $this->product->expects($this->at($callIndex))->method('setData')->with('_edit_mode', $editMode); ++$callIndex; } if ($storeId !== null) { - $this->productMock->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId); + $this->product->expects($this->at($callIndex))->method('setData')->with('store_id', $storeId); } - $this->productMock->expects($this->once())->method('load')->with($identifier); - $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($identifier); - $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + $this->product->method('load')->with($identifier); + $this->product->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($identifier); + $this->product->method('getSku') + ->willReturn('simple'); + $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); //Second invocation should just return from cache - $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); } /** @@ -406,17 +441,22 @@ public function testGetByIdForcedReload() $editMode = false; $storeId = 0; - $this->productFactoryMock->expects($this->exactly(2))->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->exactly(2))->method('load'); - $this->serializerMock->expects($this->exactly(3))->method('serialize'); + $this->productFactory->expects($this->exactly(2))->method('create') + ->willReturn($this->product); + $this->product->expects($this->exactly(2)) + ->method('load'); - $this->productMock->expects($this->exactly(4))->method('getId')->willReturn($identifier); - $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + $this->product->expects($this->exactly(4)) + ->method('getId') + ->willReturn($identifier); + $this->product->method('getSku') + ->willReturn('simple'); + + $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); //second invocation should just return from cache - $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); + $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId)); //force reload - $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId, true)); + $this->assertEquals($this->product, $this->model->getById($identifier, $editMode, $storeId, true)); } /** @@ -431,10 +471,9 @@ public function testGetByIdWhenCacheReduced() $productsCount = $this->cacheLimit * 2; $productMocks = $this->getProductMocksForReducedCache($productsCount); - $productFactoryInvMock = $this->productFactoryMock->expects($this->exactly($productsCount)) + $productFactoryInvMock = $this->productFactory->expects($this->exactly($productsCount)) ->method('create'); call_user_func_array([$productFactoryInvMock, 'willReturnOnConsecutiveCalls'], $productMocks); - $this->serializerMock->expects($this->atLeastOnce())->method('serialize'); for ($i = 1; $i <= $productsCount; $i++) { $product = $this->model->getById($i, false, 0); @@ -486,87 +525,101 @@ public function testGetForcedReload() $editMode = false; $storeId = 0; - $this->productFactoryMock->expects($this->exactly(2))->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->exactly(2))->method('load'); - $this->productMock->expects($this->exactly(2))->method('getId')->willReturn($sku); - $this->resourceModelMock->expects($this->exactly(2))->method('getIdBySku') + $this->resourceModel->expects($this->exactly(2))->method('getIdBySku') ->with($sku)->willReturn($id); - $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn($sku); - $this->serializerMock->expects($this->exactly(3))->method('serialize'); + $this->productFactory->expects($this->exactly(2))->method('create') + ->will($this->returnValue($this->product)); + $this->product->expects($this->exactly(2))->method('load'); + $this->product->expects($this->any())->method('getId')->willReturn($id); + $this->product->expects($this->any())->method('getSku')->willReturn($sku); - $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); + $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId)); //second invocation should just return from cache - $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId)); + $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId)); //force reload - $this->assertEquals($this->productMock, $this->model->get($sku, $editMode, $storeId, true)); + $this->assertEquals($this->product, $this->model->get($sku, $editMode, $storeId, true)); } public function testGetByIdWithSetStoreId() { $productId = 123; $storeId = 1; - $this->productFactoryMock->expects($this->atLeastOnce())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); - $this->productMock->expects($this->once())->method('load')->with($productId); - $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); - $this->assertEquals($this->productMock, $this->model->getById($productId, false, $storeId)); + $this->productFactory->method('create') + ->willReturn($this->product); + $this->product->method('setData') + ->with('store_id', $storeId); + $this->product->method('load') + ->with($productId); + $this->product->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($productId); + $this->product->method('getSku') + ->willReturn('simple'); + $this->assertEquals($this->product, $this->model->getById($productId, false, $storeId)); } public function testGetBySkuFromCacheInitializedInGetById() { $productId = 123; $productSku = 'product_123'; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('load')->with($productId); - $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); - $this->productMock->expects($this->once())->method('getSku')->willReturn($productSku); - $this->assertEquals($this->productMock, $this->model->getById($productId)); - $this->assertEquals($this->productMock, $this->model->get($productSku)); + $this->productFactory->method('create') + ->willReturn($this->product); + $this->product->method('load') + ->with($productId); + $this->product->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($productId); + $this->product->method('getSku') + ->willReturn($productSku); + $this->assertEquals($this->product, $this->model->getById($productId)); + $this->assertEquals($this->product, $this->model->get($productSku)); } public function testSaveExisting() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $id = 100; + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn($id); + $this->productFactory->expects($this->any()) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->product) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); - $this->extensibleDataObjectConverterMock + $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); - $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + ->willReturn($this->productData); + $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + $this->product->expects($this->at(0))->method('getId')->willReturn(null); + $this->product->expects($this->any())->method('getId')->willReturn($id); - $this->assertEquals($this->productMock, $this->model->save($this->productMock)); + $this->assertEquals($this->product, $this->model->save($this->product)); } public function testSaveNew() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); - $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $id = 100; + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->at(0))->method('getIdBySku')->willReturn(null); + $this->resourceModel->expects($this->at(3))->method('getIdBySku')->willReturn($id); + $this->product->expects($this->at(0))->method('getId')->willReturn(null); + $this->product->expects($this->any())->method('getId')->willReturn($id); + $this->productFactory->expects($this->any()) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->product) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); - $this->extensibleDataObjectConverterMock + $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + ->willReturn($this->productData); + $this->product->method('getSku')->willReturn('simple'); - $this->assertEquals($this->productMock, $this->model->save($this->productMock)); + $this->assertEquals($this->product, $this->model->save($this->product)); } /** @@ -575,23 +628,25 @@ public function testSaveNew() */ public function testSaveUnableToSaveException() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); - $this->productFactoryMock->expects($this->exactly(2)) + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->exactly(1)) + ->method('getIdBySku') + ->willReturn(null); + $this->productFactory->expects($this->exactly(1)) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->product) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock) + $this->resourceModel->expects($this->once())->method('save')->with($this->product) ->willThrowException(new \Exception()); - $this->extensibleDataObjectConverterMock + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + ->willReturn($this->productData); + $this->product->method('getSku')->willReturn('simple'); - $this->model->save($this->productMock); + $this->model->save($this->product); } /** @@ -600,24 +655,26 @@ public function testSaveUnableToSaveException() */ public function testSaveException() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); - $this->productFactoryMock->expects($this->exactly(2)) + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->exactly(1)) + ->method('getIdBySku') + ->willReturn(null); + $this->productFactory->expects($this->exactly(1)) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->product) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock) + $this->resourceModel->expects($this->once())->method('save')->with($this->product) ->willThrowException(new \Magento\Eav\Model\Entity\Attribute\Exception(__('123'))); - $this->productMock->expects($this->once())->method('getId')->willReturn(null); - $this->extensibleDataObjectConverterMock + $this->product->expects($this->exactly(2))->method('getId')->willReturn(null); + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + ->willReturn($this->productData); + $this->product->method('getSku')->willReturn('simple'); - $this->model->save($this->productMock); + $this->model->save($this->product); } /** @@ -626,22 +683,28 @@ public function testSaveException() */ public function testSaveInvalidProductException() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); - $this->productFactoryMock->expects($this->exactly(2)) + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel + ->expects($this->exactly(1)) + ->method('getIdBySku') + ->willReturn(null); + $this->productFactory->expects($this->exactly(1)) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel + ->expects($this->once()) + ->method('validate') + ->with($this->product) ->willReturn(['error1', 'error2']); - $this->productMock->expects($this->never())->method('getId'); - $this->extensibleDataObjectConverterMock + $this->product->expects($this->once())->method('getId')->willReturn(null); + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + ->willReturn($this->productData); + $this->product->method('getSku')->willReturn('simple'); - $this->model->save($this->productMock); + $this->model->save($this->product); } /** @@ -650,38 +713,37 @@ public function testSaveInvalidProductException() */ public function testSaveThrowsTemporaryStateExceptionIfDatabaseConnectionErrorOccurred() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->productFactoryMock->expects($this->any()) + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->productFactory->expects($this->any()) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never()) + ->willReturn($this->product); + $this->initializationHelper->expects($this->never()) ->method('initialize'); - $this->resourceModelMock->expects($this->once()) + $this->resourceModel->expects($this->once()) ->method('validate') - ->with($this->productMock) + ->with($this->product) ->willReturn(true); - $this->resourceModelMock->expects($this->once()) + $this->resourceModel->expects($this->once()) ->method('save') - ->with($this->productMock) + ->with($this->product) ->willThrowException(new ConnectionException('Connection lost')); - $this->extensibleDataObjectConverterMock + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once()) - ->method('getWebsiteIds') - ->willReturn([]); + ->willReturn($this->productData); + $this->product->method('getSku') + ->willReturn('simple'); - $this->model->save($this->productMock); + $this->model->save($this->product); } public function testDelete() { - $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); - $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42); - $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock) + $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); + $this->product->expects($this->exactly(2))->method('getId')->willReturn(42); + $this->resourceModel->expects($this->once())->method('delete')->with($this->product) ->willReturn(true); - $this->assertTrue($this->model->delete($this->productMock)); + $this->assertTrue($this->model->delete($this->product)); } /** @@ -690,22 +752,23 @@ public function testDelete() */ public function testDeleteException() { - $this->productMock->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); - $this->productMock->expects($this->exactly(2))->method('getId')->willReturn(42); - $this->resourceModelMock->expects($this->once())->method('delete')->with($this->productMock) + $this->product->expects($this->exactly(2))->method('getSku')->willReturn('product-42'); + $this->product->expects($this->exactly(2))->method('getId')->willReturn(42); + $this->resourceModel->expects($this->once())->method('delete')->with($this->product) ->willThrowException(new \Exception()); - $this->model->delete($this->productMock); + $this->model->delete($this->product); } public function testDeleteById() { $sku = 'product-42'; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->resourceModelMock->expects($this->once())->method('getIdBySku')->with($sku) + $this->productFactory->expects($this->once())->method('create') + ->will($this->returnValue($this->product)); + $this->resourceModel->expects($this->once())->method('getIdBySku')->with($sku) ->will($this->returnValue('42')); - $this->productMock->expects($this->once())->method('load')->with('42'); - $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($sku); + $this->product->expects($this->once())->method('load')->with('42'); + $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($sku); + $this->product->expects($this->atLeastOnce())->method('getId')->willReturn(42); $this->assertTrue($this->model->deleteById($sku)); } @@ -714,24 +777,27 @@ public function testGetList() $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); - $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock); + $this->collectionFactory->expects($this->once())->method('create')->willReturn($collectionMock); + + $this->product->method('getSku') + ->willReturn('simple'); $collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); $collectionMock->expects($this->exactly(2))->method('joinAttribute')->withConsecutive( ['status', 'catalog_product/status', 'entity_id', null, 'inner'], ['visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'] ); - $this->collectionProcessorMock->expects($this->once()) + $this->collectionProcessor->expects($this->once()) ->method('process') ->with($searchCriteriaMock, $collectionMock); $collectionMock->expects($this->once())->method('load'); $collectionMock->expects($this->once())->method('addCategoryIds'); - $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->productMock]); + $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->product]); $collectionMock->expects($this->once())->method('getSize')->willReturn(128); $searchResultsMock = $this->createMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterface::class); $searchResultsMock->expects($this->once())->method('setSearchCriteria')->with($searchCriteriaMock); - $searchResultsMock->expects($this->once())->method('setItems')->with([$this->productMock]); - $this->searchResultsFactoryMock->expects($this->once())->method('create')->willReturn($searchResultsMock); + $searchResultsMock->expects($this->once())->method('setItems')->with([$this->product]); + $this->searchResultsFactory->expects($this->once())->method('create')->willReturn($searchResultsMock); $this->assertEquals($searchResultsMock, $this->model->getList($searchCriteriaMock)); } @@ -797,33 +863,42 @@ public function cacheKeyDataProvider() * @param array $existingOptions * @param array $expectedData * @dataProvider saveExistingWithOptionsDataProvider + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function testSaveExistingWithOptions(array $newOptions, array $existingOptions, array $expectedData) { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $id = 100; + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->any())->method('getIdBySku')->will($this->returnValue($id)); + $this->productFactory->expects($this->any()) ->method('create') - ->will($this->returnValue($this->initializedProductMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) + ->willReturn($this->initializedProduct); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel + ->expects($this->once())->method('validate') + ->with($this->initializedProduct) + ->willReturn(true); + $this->resourceModel + ->expects($this->once())->method('save') + ->with($this->initializedProduct) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save') - ->with($this->initializedProductMock)->willReturn(true); //option data $this->productData['options'] = $newOptions; - $this->extensibleDataObjectConverterMock + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); + ->willReturn($this->productData); - $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); - $this->initializedProductMock->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); - $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + $this->initializedProduct + ->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn($this->productData['sku']); + $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + $this->initializedProduct->expects($this->at(0))->method('getId')->willReturn(null); + $this->initializedProduct->expects($this->any())->method('getId')->willReturn($id); - $this->assertEquals($this->initializedProductMock, $this->model->save($this->productMock)); + $this->assertEquals($this->initializedProduct, $this->model->save($this->product)); } /** @@ -863,7 +938,7 @@ public function saveExistingWithOptionsDataProvider() ], ]; - /** @var \Magento\Catalog\Model\Product\Option|\PHPUnit_Framework_MockObject_MockObject $existingOption1 */ + /** @var \Magento\Catalog\Model\Product\Option|MockObject $existingOption1 */ $existingOption1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) ->disableOriginalConstructor() ->setMethods(null) @@ -968,32 +1043,40 @@ public function saveExistingWithOptionsDataProvider() * @param array $existingLinks * @param array $expectedData * @dataProvider saveWithLinksDataProvider + * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\InputException */ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $expectedData) { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn(100); + $this->productFactory + ->expects($this->any()) ->method('create') - ->will($this->returnValue($this->initializedProductMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) + ->will($this->returnValue($this->initializedProduct)); + $this->initializedProduct->method('getId')->willReturn(100); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel + ->expects($this->once())->method('validate') + ->with($this->initializedProduct) + ->willReturn(true); + $this->resourceModel + ->expects($this->once())->method('save') + ->with($this->initializedProduct) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save') - ->with($this->initializedProductMock)->willReturn(true); - $this->initializedProductMock->setData("product_links", $existingLinks); + $this->initializedProduct->setData("product_links", $existingLinks); if (!empty($newLinks)) { $linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3]; - $this->linkTypeProviderMock->expects($this->once()) + $this->linkTypeProvider + ->expects($this->once()) ->method('getLinkTypes') ->willReturn($linkTypes); - $this->initializedProductMock->setData("ignore_links_flag", false); - $this->resourceModelMock + $this->initializedProduct->setData("ignore_links_flag", false); + $this->resourceModel ->expects($this->any())->method('getProductsIdsBySkus') ->willReturn([$newLinks['linked_product_sku'] => $newLinks['linked_product_sku']]); @@ -1010,32 +1093,34 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $ $this->productData['product_links'] = [$inputLink]; - $this->initializedProductMock->expects($this->any()) + $this->initializedProduct + ->expects($this->any()) ->method('getProductLinks') ->willReturn([$inputLink]); } else { - $this->resourceModelMock + $this->resourceModel ->expects($this->any())->method('getProductsIdsBySkus') ->willReturn([]); $this->productData['product_links'] = []; - $this->initializedProductMock->setData('ignore_links_flag', true); - $this->initializedProductMock->expects($this->never()) + $this->initializedProduct->setData('ignore_links_flag', true); + $this->initializedProduct + ->expects($this->never()) ->method('getProductLinks') ->willReturn([]); } - $this->extensibleDataObjectConverterMock + $this->extensibleDataObjectConverter ->expects($this->at(0)) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); + ->willReturn($this->productData); if (!empty($newLinks)) { - $this->extensibleDataObjectConverterMock + $this->extensibleDataObjectConverter ->expects($this->at(1)) ->method('toNestedArray') - ->will($this->returnValue($newLinks)); + ->willReturn($newLinks); } $outputLinks = []; @@ -1054,23 +1139,29 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $ $outputLinks[] = $outputLink; } } - $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); if (!empty($outputLinks)) { - $this->initializedProductMock->expects($this->once()) + $this->initializedProduct + ->expects($this->once()) ->method('setProductLinks') ->with($outputLinks); } else { - $this->initializedProductMock->expects($this->never()) + $this->initializedProduct + ->expects($this->never()) ->method('setProductLinks'); } - $this->initializedProductMock->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); + $this->initializedProduct + ->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn($this->productData['sku']); - $results = $this->model->save($this->initializedProductMock); - $this->assertEquals($this->initializedProductMock, $results); + $results = $this->model->save($this->initializedProduct); + $this->assertEquals($this->initializedProduct, $results); } + /** + * @return mixed + */ public function saveWithLinksDataProvider() { // Scenario 1 @@ -1142,20 +1233,21 @@ public function saveWithLinksDataProvider() protected function setupProductMocksForSave() { - $this->resourceModelMock->expects($this->any())->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $this->resourceModel->expects($this->any())->method('getIdBySku')->willReturn(100); + $this->productFactory + ->expects($this->any()) ->method('create') - ->will($this->returnValue($this->initializedProductMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->initializedProductMock) + ->willReturn($this->initializedProduct); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->initializedProduct) ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save') - ->with($this->initializedProductMock)->willReturn(true); + $this->resourceModel->expects($this->once())->method('save') + ->with($this->initializedProduct)->willReturn(true); } public function testSaveExistingWithNewMediaGalleryEntries() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); $newEntriesData = [ 'images' => [ [ @@ -1175,74 +1267,62 @@ public function testSaveExistingWithNewMediaGalleryEntries() ] ] ]; - + $expectedEntriesData = [ + [ + 'id' => null, + 'label' => "label_text", + 'position' => 10, + 'disabled' => false, + 'types' => ['image', 'small_image'], + 'content' => [ + ImageContentInterface::NAME => 'filename', + ImageContentInterface::TYPE => 'image/jpeg', + ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content', + ], + 'media_type' => 'media_type', + ], + ]; $this->setupProductMocksForSave(); //media gallery data - $this->productData['media_gallery'] = $newEntriesData; - $this->extensibleDataObjectConverterMock + $this->productData['media_gallery_entries'] = [ + [ + 'id' => null, + 'label' => "label_text", + 'position' => 10, + 'disabled' => false, + 'types' => ['image', 'small_image'], + 'content' => [ + ImageContentInterface::NAME => 'filename', + ImageContentInterface::TYPE => 'image/jpeg', + ImageContentInterface::BASE64_ENCODED_DATA => 'encoded_content', + ], + 'media_type' => 'media_type', + ] + ]; + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - - $this->initializedProductMock->setData('media_gallery', $newEntriesData); - $this->initializedProductMock->expects($this->any()) - ->method('getMediaAttributes') - ->willReturn(["image" => "imageAttribute", "small_image" => "small_image_attribute"]); - - //setup media attribute backend - $mediaTmpPath = '/tmp'; - $absolutePath = '/a/b/filename.jpg'; - - $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') - ->with($this->initializedProductMock, ['image', 'small_image']); - - $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) - ->disableOriginalConstructor() - ->getMock(); - $mediaConfigMock->expects($this->once()) - ->method('getTmpMediaShortUrl') - ->with($absolutePath) - ->willReturn($mediaTmpPath . $absolutePath); - $this->initializedProductMock->expects($this->once()) - ->method('getMediaConfig') - ->willReturn($mediaConfigMock); - - //verify new entries - $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class) - ->disableOriginalConstructor() - ->setMethods(null) - ->getMock(); - $this->contentFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($contentDataObject); - - $this->imageProcessorMock->expects($this->once()) - ->method('processImageContent') - ->willReturn($absolutePath); - - $imageFileUri = "imageFileUri"; - $this->mediaGalleryProcessor->expects($this->once())->method('addImage') - ->with($this->initializedProductMock, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false) - ->willReturn($imageFileUri); - $this->mediaGalleryProcessor->expects($this->once())->method('updateImage') - ->with( - $this->initializedProductMock, - $imageFileUri, - [ - 'label' => 'label_text', - 'position' => 10, - 'disabled' => false, - 'media_type' => 'media_type', - ] - ); - $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); - $this->initializedProductMock->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); - $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + ->willReturn($this->productData); - $this->model->save($this->productMock); + $this->initializedProduct->setData('media_gallery', $newEntriesData); + $this->mediaGalleryProcessor + ->expects($this->once()) + ->method('processMediaGallery') + ->with($this->initializedProduct, $expectedEntriesData); + $this->initializedProduct + ->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn($this->productData['sku']); + $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + $this->initializedProduct->expects($this->at(0))->method('getId')->willReturn(null); + $this->initializedProduct->expects($this->any())->method('getId')->willReturn(100); + + $this->model->save($this->product); } + /** + * @return array + */ public function websitesProvider() { return [ @@ -1253,42 +1333,47 @@ public function websitesProvider() public function testSaveWithDifferentWebsites() { $storeMock = $this->createMock(StoreInterface::class); - $this->resourceModelMock->expects($this->at(0))->method('getIdBySku')->will($this->returnValue(null)); - $this->resourceModelMock->expects($this->at(3))->method('getIdBySku')->will($this->returnValue(100)); - $this->productFactoryMock->expects($this->any()) + $this->resourceModel->expects($this->at(0))->method('getIdBySku')->willReturn(null); + $this->resourceModel->expects($this->at(3))->method('getIdBySku')->willReturn(100); + $this->productFactory + ->expects($this->any()) ->method('create') - ->will($this->returnValue($this->productMock)); - $this->initializationHelperMock->expects($this->never())->method('initialize'); - $this->resourceModelMock->expects($this->once())->method('validate')->with($this->productMock) - ->willReturn(true); - $this->resourceModelMock->expects($this->once())->method('save')->with($this->productMock)->willReturn(true); - $this->extensibleDataObjectConverterMock + ->willReturn($this->product); + $this->initializationHelper->expects($this->never())->method('initialize'); + $this->resourceModel->expects($this->once())->method('validate')->with($this->product)->willReturn(true); + $this->resourceModel->expects($this->once())->method('save')->with($this->product)->willReturn(true); + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') - ->will($this->returnValue($this->productData)); - $this->storeManagerMock->expects($this->any()) + ->willReturn($this->productData); + $this->storeManager + ->expects($this->any()) ->method('getStore') ->willReturn($storeMock); - $this->storeManagerMock->expects($this->once()) + $this->storeManager + ->expects($this->once()) ->method('getWebsites') ->willReturn([ 1 => ['first'], 2 => ['second'], 3 => ['third'] ]); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1,2,3]); - $this->productMock->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]); + $this->product->method('setWebsiteIds')->willReturn([2,3]); + $this->product->method('getSku') + ->willReturn('simple'); + $this->product->expects($this->at(0))->method('getId')->willReturn(null); + $this->product->expects($this->any())->method('getId')->willReturn(100); - $this->assertEquals($this->productMock, $this->model->save($this->productMock)); + $this->assertEquals($this->product, $this->model->save($this->product)); } public function testSaveExistingWithMediaGalleryEntries() { - $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); + $this->storeManager->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); //update one entry, delete one entry $newEntries = [ [ - 'value_id' => 5, + 'id' => 5, "label" => "new_label_text", 'file' => 'filename1', 'position' => 10, @@ -1312,48 +1397,27 @@ public function testSaveExistingWithMediaGalleryEntries() ], ], ]; - - $expectedResult = [ - [ - 'value_id' => 5, - 'value_id' => 5, - "label" => "new_label_text", - 'file' => 'filename1', - 'position' => 10, - 'disabled' => false, - 'types' => ['image', 'small_image'], - ], - [ - 'value_id' => 6, //will be deleted - 'file' => 'filename2', - 'removed' => true, - ], - ]; - $this->setupProductMocksForSave(); //media gallery data - $this->productData['media_gallery']['images'] = $newEntries; - $this->extensibleDataObjectConverterMock + $this->productData['media_gallery_entries'] = $newEntries; + $this->extensibleDataObjectConverter ->expects($this->once()) ->method('toNestedArray') ->will($this->returnValue($this->productData)); - $this->initializedProductMock->setData('media_gallery', $existingMediaGallery); - $this->initializedProductMock->expects($this->any()) - ->method('getMediaAttributes') - ->willReturn(["image" => "filename1", "small_image" => "filename2"]); - - $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') - ->with($this->initializedProductMock, ['image', 'small_image']); - $this->mediaGalleryProcessor->expects($this->once()) - ->method('setMediaAttribute') - ->with($this->initializedProductMock, ['image', 'small_image'], 'filename1'); - $this->initializedProductMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); - $this->initializedProductMock->expects($this->atLeastOnce()) - ->method('getSku')->willReturn($this->productData['sku']); - $this->productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); - $this->productMock->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null); - $this->model->save($this->productMock); - $this->assertEquals($expectedResult, $this->initializedProductMock->getMediaGallery('images')); + $this->initializedProduct->setData('media_gallery', $existingMediaGallery); + + $this->mediaGalleryProcessor + ->expects($this->once()) + ->method('processMediaGallery') + ->with($this->initializedProduct, $newEntries); + $this->initializedProduct + ->expects($this->atLeastOnce()) + ->method('getSku') + ->willReturn($this->productData['sku']); + $this->product->expects($this->atLeastOnce())->method('getSku')->willReturn($this->productData['sku']); + $this->product->expects($this->any())->method('getMediaGalleryEntries')->willReturn(null); + $this->initializedProduct->expects($this->any())->method('getId')->willReturn(100); + $this->model->save($this->product); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 92e5b8c7ecb21..007b4ea055ef0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model; use Magento\Catalog\Api\Data\ProductExtensionFactory; use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\ExtensibleDataInterface; @@ -200,6 +199,16 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $extensionAttributes; + /** + * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheInterfaceMock; + + /** + * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -246,11 +255,13 @@ protected function setUp() \Magento\Framework\Model\ActionValidator\RemoveAction::class ); $actionValidatorMock->expects($this->any())->method('isAllowed')->will($this->returnValue(true)); - $cacheInterfaceMock = $this->createMock(\Magento\Framework\App\CacheInterface::class); - + $this->cacheInterfaceMock = $this->createMock(\Magento\Framework\App\CacheInterface::class); $contextMock = $this->createPartialMock( \Magento\Framework\Model\Context::class, - ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'], [], '', false + ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'], + [], + '', + false ); $contextMock->expects($this->any())->method('getAppState')->will($this->returnValue($this->appStateMock)); $contextMock->expects($this->any()) @@ -258,7 +269,7 @@ protected function setUp() ->will($this->returnValue($this->eventManagerMock)); $contextMock->expects($this->any()) ->method('getCacheManager') - ->will($this->returnValue($cacheInterfaceMock)); + ->will($this->returnValue($this->cacheInterfaceMock)); $contextMock->expects($this->any()) ->method('getActionValidator') ->will($this->returnValue($actionValidatorMock)); @@ -345,8 +356,7 @@ protected function setUp() $this->mediaGalleryEntryConverterPoolMock->expects($this->any())->method('getConverterByMediaType')->willReturn( $this->converterMock ); - $this->productLinkRepositoryMock = $this->getMockBuilder( - \Magento\Catalog\Api\ProductLinkRepositoryInterface::class) + $this->productLinkRepositoryMock = $this->getMockBuilder(ProductLinkRepositoryInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->extensionAttributesFactory = $this->getMockBuilder(ExtensionAttributesFactory::class) @@ -360,6 +370,7 @@ protected function setUp() ->setMethods(['create']) ->getMock(); $this->mediaConfig = $this->createMock(\Magento\Catalog\Model\Product\Media\Config::class); + $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); $this->extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) ->setMethods(['getStockItem']) @@ -397,7 +408,8 @@ protected function setUp() 'catalogProductMediaConfig' => $this->mediaConfig, '_filesystem' => $this->filesystemMock, '_collectionFactory' => $this->collectionFactoryMock, - 'data' => ['id' => 1] + 'data' => ['id' => 1], + 'eavConfig' => $this->eavConfig ] ); } @@ -502,6 +514,9 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection, $this->assertEquals($initCategoryCollection, $result); } + /** + * @return array + */ public function getCategoryCollectionCollectionNullDataProvider() { return [ @@ -529,6 +544,7 @@ public function testSetCategoryCollection() public function testGetCategory() { + $this->model->setData('category_ids', [10]); $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($this->category)); @@ -537,7 +553,8 @@ public function testGetCategory() public function testGetCategoryId() { - $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); + $this->model->setData('category_ids', [10]); + $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->at(0))->method('registry'); $this->registry->expects($this->at(1))->method('registry')->will($this->returnValue($this->category)); @@ -545,6 +562,14 @@ public function testGetCategoryId() $this->assertEquals(10, $this->model->getCategoryId()); } + public function testGetCategoryIdWhenProductNotInCurrentCategory() + { + $this->model->setData('category_ids', [12]); + $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); + $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); + $this->assertFalse($this->model->getCategoryId()); + } + public function testGetIdBySku() { $this->resource->expects($this->once())->method('getIdBySku')->will($this->returnValue(5)); @@ -612,13 +637,16 @@ public function testReindex($productChanged, $isScheduled, $productFlatCount, $c $this->model->reindex(); } + /** + * @return array + */ public function getProductReindexProvider() { - return array( + return [ 'set 1' => [true, false, 1, 1], 'set 2' => [true, true, 1, 0], 'set 3' => [false, false, 1, 0] - ); + ]; } public function testPriceReindexCallback() @@ -1159,19 +1187,18 @@ public function testSetMediaGalleryEntries() ]; $entryMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface::class) - ->setMethods( - [ - 'getId', - 'getFile', - 'getLabel', - 'getPosition', - 'isDisabled', - 'types', - 'getContent', - 'getMediaType' - ] - ) - ->getMockForAbstractClass(); + ->setMethods( + [ + 'getId', + 'getFile', + 'getLabel', + 'getPosition', + 'isDisabled', + 'types', + 'getContent', + 'getMediaType' + ] + )->getMockForAbstractClass(); $result = [ 'value_id' => 1, @@ -1242,7 +1269,10 @@ public function testGetMediaGalleryImagesMerging() ->disableOriginalConstructor() ->getMock(); $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($imagesCollectionMock); - $imagesCollectionMock->expects($this->at(2))->method('getItemById')->with(1)->willReturn($expectedImageDataObject); + $imagesCollectionMock->expects($this->at(2)) + ->method('getItemById') + ->with(1) + ->willReturn($expectedImageDataObject); $this->mediaConfig->expects($this->at(0)) ->method('getMediaUrl') ->willReturn('http://magento.dev/pub/imageFile.jpg'); @@ -1265,18 +1295,11 @@ public function testGetCustomAttributes() { $priceCode = 'price'; $colorAttributeCode = 'color'; - $interfaceAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class); - $interfaceAttribute->expects($this->once()) - ->method('getAttributeCode') - ->willReturn($priceCode); - $colorAttribute = $this->createMock(\Magento\Framework\Api\MetadataObjectInterface::class); - $colorAttribute->expects($this->once()) - ->method('getAttributeCode') - ->willReturn($colorAttributeCode); - $customAttributesMetadata = [$interfaceAttribute, $colorAttribute]; - - $this->metadataServiceMock->expects($this->once()) - ->method('getCustomAttributesMetadata') + $customAttributesMetadata = [$priceCode => 'attribute1', $colorAttributeCode => 'attribute2']; + + $this->metadataServiceMock->expects($this->never())->method('getCustomAttributesMetadata'); + $this->eavConfig->expects($this->once()) + ->method('getEntityAttributes') ->willReturn($customAttributesMetadata); $this->model->setData($priceCode, 10); @@ -1284,20 +1307,20 @@ public function testGetCustomAttributes() $this->assertEquals([], $this->model->getCustomAttributes()); //Set the color attribute; - $this->model->setData($colorAttributeCode, "red"); + $this->model->setData($colorAttributeCode, 'red'); $attributeValue = new \Magento\Framework\Api\AttributeValue(); $attributeValue2 = new \Magento\Framework\Api\AttributeValue(); $this->attributeValueFactory->expects($this->exactly(2))->method('create') ->willReturnOnConsecutiveCalls($attributeValue, $attributeValue2); $this->assertEquals(1, count($this->model->getCustomAttributes())); $this->assertNotNull($this->model->getCustomAttribute($colorAttributeCode)); - $this->assertEquals("red", $this->model->getCustomAttribute($colorAttributeCode)->getValue()); + $this->assertEquals('red', $this->model->getCustomAttribute($colorAttributeCode)->getValue()); //Change the attribute value, should reflect in getCustomAttribute - $this->model->setData($colorAttributeCode, "blue"); + $this->model->setCustomAttribute($colorAttributeCode, 'blue'); $this->assertEquals(1, count($this->model->getCustomAttributes())); $this->assertNotNull($this->model->getCustomAttribute($colorAttributeCode)); - $this->assertEquals("blue", $this->model->getCustomAttribute($colorAttributeCode)->getValue()); + $this->assertEquals('blue', $this->model->getCustomAttribute($colorAttributeCode)->getValue()); } /** @@ -1393,7 +1416,20 @@ public function testGetFinalPricePreset() $qty = 1; $this->model->setQty($qty); $this->model->setFinalPrice($finalPrice); - $this->productTypeInstanceMock->expects($this->never())->method('priceFactory'); + $productTypePriceMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Type\Price::class, + ['getFinalPrice'] + ); + $productTypePriceMock->expects($this->any()) + ->method('getFinalPrice') + ->with($qty, $this->model) + ->will($this->returnValue($finalPrice)); + + $this->productTypeInstanceMock->expects($this->any()) + ->method('priceFactory') + ->with($this->model->getTypeId()) + ->will($this->returnValue($productTypePriceMock)); + $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty)); } @@ -1434,4 +1470,17 @@ public function testGetOptionByIdForProductWithoutOptions() { $this->assertNull($this->model->getOptionById(100)); } + + public function testGetCacheTags() + { + //If entity is identified getCacheTags has to return the same values + //as getIdentities + $this->model->setId(null); + $this->assertEquals([Product::CACHE_TAG], $this->model->getCacheTags()); + $this->model->setId(1); + $this->assertEquals( + $this->model->getIdentities(), + $this->model->getCacheTags() + ); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php index e1847bea53fcb..868252da8190c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php @@ -23,7 +23,7 @@ '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', [ "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . - " pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -57,7 +57,7 @@ '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', [ "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . - "by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -66,7 +66,7 @@ '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', [ "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . - "the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -83,7 +83,7 @@ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', [ "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . - "accepted by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic " . "type 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml index 7edbc399a9476..701338774baa5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml @@ -15,6 +15,14 @@ <stockIndexerModel instance="instance_name"/> </type> <type label="some_label" name="some_name2" modelInstance="model_name"> + <allowedSelectionTypes> + <type name="some_name" /> + </allowedSelectionTypes> + <priceModel instance="instance_name_with_digits_123" /> + <indexerModel instance="instance_name_with_digits_123" /> + <stockIndexerModel instance="instance_name_with_digits_123"/> + </type> + <type label="some_label" name="some_name3" modelInstance="model_name"> <allowedSelectionTypes> <type name="some_name" /> </allowedSelectionTypes> @@ -25,5 +33,6 @@ <composableTypes> <type name="some_name"/> <type name="some_name2"/> + <type name="some_name3"/> </composableTypes> </config> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php index e6de1d33e564d..fb289c7beaac6 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/ConfigTest.php @@ -68,6 +68,9 @@ public function testGetType($value, $expected) $this->assertEquals($expected, $this->config->getType('global')); } + /** + * @return array + */ public function getTypeDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php index 64416301faa06..96336d2b0706a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AbstractTest.php @@ -44,11 +44,13 @@ public function testWalkAttributes() $code = 'test_attr'; $set = 10; + $storeId = 100; $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['__wakeup']); $object->setData('test_attr', 'test_attr'); $object->setData('attribute_set_id', $set); + $object->setData('store_id', $storeId); $entityType = new \Magento\Framework\DataObject(); $entityType->setEntityTypeCode('test'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php new file mode 100644 index 0000000000000..29a579bbae5c7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php @@ -0,0 +1,230 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Attribute; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\ResourceModel\Entity\Type; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\DB\Adapter\AdapterInterface as Adapter; +use Magento\ResourceConnections\DB\Select; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Model\Attribute\LockValidatorInterface; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AttributeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var Adapter|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceMock; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Eav\Model\ResourceModel\Entity\Type|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavEntityTypeMock; + + /** + * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfigMock; + + /** + * @var LockValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $lockValidatorMock; + + /** + * @var EntityMetadataInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityMetaDataInterfaceMock; + + /** + * {@inheritDoc} + */ + protected function setUp() + { + $this->selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->setMethods(['from', 'where', 'join', 'deleteFromSelect']) + ->getMock(); + + $this->connectionMock = $this->getMockBuilder(Adapter::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock); + $this->connectionMock->expects($this->once())->method('query')->willReturn($this->selectMock); + $this->connectionMock->expects($this->once())->method('delete')->willReturn($this->selectMock); + $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); + $this->selectMock->expects($this->once())->method('join')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('deleteFromSelect')->willReturnSelf(); + + $this->resourceMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->setMethods(['delete', 'getConnection']) + ->getMock(); + + $this->contextMock = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->eavEntityTypeMock = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + $this->lockValidatorMock = $this->getMockBuilder(LockValidatorInterface::class) + ->disableOriginalConstructor() + ->setMethods(['validate']) + ->getMock(); + $this->entityMetaDataInterfaceMock = $this->getMockBuilder(EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Sets object non-public property. + * + * @param mixed $object + * @param string $propertyName + * @param mixed $value + * + * @return void + */ + private function setObjectProperty($object, string $propertyName, $value) + { + $reflectionClass = new \ReflectionClass($object); + $reflectionProperty = $reflectionClass->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + } + + /** + * @return void + */ + public function testDeleteEntity() + { + $entityAttributeId = 196; + $entityTypeId = 4; + $result = [ + 'entity_attribute_id' => 196, + 'entity_type_id' => 4, + 'attribute_set_id'=> 4, + 'attribute_group_id' => 7, + 'attribute_id' => 177, + 'sort_order' => 3, + ]; + + $backendTableName = 'weee_tax'; + $backendFieldName = 'value_id'; + + $attributeModel = $this->getMockBuilder(Attribute::class) + ->setMethods(['getEntityAttribute', 'getMetadataPool', 'getConnection', 'getTable']) + ->setConstructorArgs([ + $this->contextMock, + $this->storeManagerMock, + $this->eavEntityTypeMock, + $this->eavConfigMock, + $this->lockValidatorMock, + null, + ])->getMock(); + $attributeModel->expects($this->any()) + ->method('getEntityAttribute') + ->with($entityAttributeId) + ->willReturn($result); + $metadataPoolMock = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata']) + ->getMock(); + + $this->setObjectProperty($attributeModel, 'metadataPool', $metadataPoolMock); + + $eavAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMock(); + + $eavAttributeMock->expects($this->any())->method('getId')->willReturn($result['attribute_id']); + + $this->eavConfigMock->expects($this->any()) + ->method('getAttribute') + ->with($entityTypeId, $result['attribute_id']) + ->willReturn($eavAttributeMock); + + $abstractModelMock = $this->getMockBuilder(AbstractModel::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityAttributeId','getEntityTypeId']) + ->getMockForAbstractClass(); + $abstractModelMock->expects($this->any())->method('getEntityAttributeId')->willReturn($entityAttributeId); + $abstractModelMock->expects($this->any())->method('getEntityTypeId')->willReturn($entityTypeId); + + $this->lockValidatorMock->expects($this->any()) + ->method('validate') + ->with($eavAttributeMock, $result['attribute_set_id']) + ->willReturn(true); + + $backendModelMock = $this->getMockBuilder(AbstractBackend::class) + ->disableOriginalConstructor() + ->setMethods(['getBackend', 'getTable', 'getEntityIdField']) + ->getMock(); + + $abstractAttributeMock = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->setMethods(['getEntity']) + ->getMockForAbstractClass(); + + $eavAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendModelMock); + $eavAttributeMock->expects($this->any())->method('getEntity')->willReturn($abstractAttributeMock); + + $backendModelMock->expects($this->any())->method('getTable')->willReturn($backendTableName); + $backendModelMock->expects($this->once())->method('getEntityIdField')->willReturn($backendFieldName); + + $metadataPoolMock->expects($this->any()) + ->method('getMetadata') + ->with(ProductInterface::class) + ->willReturn($this->entityMetaDataInterfaceMock); + + $this->entityMetaDataInterfaceMock->expects($this->any()) + ->method('getLinkField') + ->willReturn('row_id'); + + $attributeModel->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); + $attributeModel->expects($this->any()) + ->method('getTable') + ->with('eav_entity_attribute') + ->willReturn('eav_entity_attribute'); + + $attributeModel->deleteEntity($abstractModelMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/TreeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/TreeTest.php index 0d5c0552d11fe..fcc42e8e17e4f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/TreeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/TreeTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; use Magento\Catalog\Api\Data\CategoryInterface; @@ -73,7 +71,9 @@ protected function setUp() ); $eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $this->_attributeConfig = $this->createMock(\Magento\Catalog\Model\Attribute\Config::class); - $this->_collectionFactory = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category\Collection\Factory::class); + $this->_collectionFactory = $this->createMock( + \Magento\Catalog\Model\ResourceModel\Category\Collection\Factory::class + ); $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) ->disableOriginalConstructor() @@ -159,15 +159,10 @@ public function testAddCollectionData() $attributeConfig = $this->createMock(\Magento\Catalog\Model\Attribute\Config::class); $attributes = ['attribute_one', 'attribute_two']; - $attributeConfig->expects( - $this->once() - )->method( - 'getAttributeNames' - )->with( - 'catalog_category' - )->will( - $this->returnValue($attributes) - ); + $attributeConfig->expects($this->once()) + ->method('getAttributeNames') + ->with('catalog_category') + ->willReturn($attributes); $collection = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); $collection->expects($this->never())->method('getAllIds')->will($this->returnValue([])); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php index 4812751792f18..2ac0b65c22e03 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel; use Magento\Catalog\Model\Factory; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Model\ResourceModel\Category; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Eav\Model\Config; @@ -91,6 +92,11 @@ class CategoryTest extends \PHPUnit\Framework\TestCase */ private $serializerMock; + /** + * @var Processor|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerProcessorMock; + /** * {@inheritDoc} */ @@ -121,6 +127,9 @@ protected function setUp() $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->indexerProcessorMock = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); @@ -132,7 +141,8 @@ protected function setUp() $this->treeFactoryMock, $this->collectionFactoryMock, [], - $this->serializerMock + $this->serializerMock, + $this->indexerProcessorMock ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Eav/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Eav/AttributeTest.php index 4a6ae7466f3af..71698882f8e1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Eav/AttributeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Eav/AttributeTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Eav; /** @@ -56,7 +54,10 @@ protected function setUp() $actionValidatorMock = $this->createMock(\Magento\Framework\Model\ActionValidator\RemoveAction::class); $actionValidatorMock->expects($this->any())->method('isAllowed')->will($this->returnValue(true)); - $this->contextMock = $this->createPartialMock(\Magento\Framework\Model\Context::class, ['getEventDispatcher', 'getCacheManager', 'getActionValidator']); + $this->contextMock = $this->createPartialMock( + \Magento\Framework\Model\Context::class, + ['getEventDispatcher', 'getCacheManager', 'getActionValidator'] + ); $this->contextMock->expects($this->any()) ->method('getEventDispatcher') @@ -93,14 +94,14 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_model = $objectManager->getObject( \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, - [ - 'context' => $this->contextMock, - 'productFlatIndexerProcessor' => $this->_processor, - 'indexerEavProcessor' => $this->_eavProcessor, - 'resource' => $this->resourceMock, - 'data' => ['id' => 1], - 'eavConfig' => $this->eavConfigMock - ] + [ + 'context' => $this->contextMock, + 'productFlatIndexerProcessor' => $this->_processor, + 'indexerEavProcessor' => $this->_eavProcessor, + 'resource' => $this->resourceMock, + 'data' => ['id' => 1], + 'eavConfig' => $this->eavConfigMock, + ] ); } @@ -118,7 +119,10 @@ public function testIndexerAfterSaveScopeChangeAttribute() { $this->_processor->expects($this->once())->method('markIndexerAsInvalid'); - $this->_model->setOrigData('is_global', \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE); + $this->_model->setOrigData( + 'is_global', + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE + ); $this->_model->setOrigData('used_in_product_listing', 1); $this->_model->setIsGlobal(\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL); $this->_model->afterSave(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php deleted file mode 100644 index 9fba7d833c25a..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Layer/Filter/PriceTest.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Layer\Filter; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -class PriceTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price - */ - private $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $resourceMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManager($this); - - $contextMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\Context::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock); - $this->model = $objectManagerHelper->getObject( - \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price::class, - [ - 'context' => $contextMock - ] - ); - } - - public function testGetMainTable() - { - $expectedTableName = 'expectedTableName'; - $this->resourceMock->expects($this->once())->method('getTableName')->willReturn($expectedTableName); - $this->assertEquals($expectedTableName, $this->model->getMainTable()); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index c73e772de3702..6d3316a0610cd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Framework\DB\Select; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -239,7 +240,7 @@ public function testAddMediaGalleryData() $mediaGalleriesMock = [[$linkField => $rowId]]; $itemMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() - ->setMethods(['getData']) + ->setMethods(['getOrigData']) ->getMock(); $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -254,8 +255,10 @@ public function testAddMediaGalleryData() $this->galleryResourceMock->expects($this->once())->method('createBatchBaseSelect')->willReturn($selectMock); $attributeMock->expects($this->once())->method('getAttributeId')->willReturn($attributeId); $this->entityMock->expects($this->once())->method('getAttribute')->willReturn($attributeMock); - $itemMock->expects($this->atLeastOnce())->method('getData')->willReturn($rowId); - $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$rowId]); + $itemMock->expects($this->atLeastOnce())->method('getOrigData')->willReturn($rowId); + $selectMock->expects($this->once())->method('reset')->with(Select::ORDER)->willReturnSelf(); + $selectMock->expects($this->once())->method('where')->with('entity.' . $linkField . ' IN (?)', [$rowId]) + ->willReturnSelf(); $this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock); $metadataMock->expects($this->once())->method('getLinkField')->willReturn($linkField); @@ -315,7 +318,7 @@ public function testAddTierPriceDataByGroupId() [ '(customer_group_id=? AND all_groups=0) OR all_groups=1', $customerGroupId] ) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) @@ -367,7 +370,7 @@ public function testAddTierPriceData() $select->expects($this->exactly(1))->method('where') ->with('entity_id IN(?)', [1]) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php index dfed4e4f37385..47ef3c999125f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php @@ -281,6 +281,9 @@ public function testBindValueToEntityRecordExists() $this->resource->bindValueToEntity($valueId, $entityId); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testLoadGallery() { $productId = 5; @@ -329,7 +332,8 @@ public function testLoadGallery() 'main.value_id = entity.value_id', ['entity_id'] )->willReturnSelf(); - $this->product->expects($this->at(0))->method('getData')->with('entity_id')->willReturn($productId); + $this->product->expects($this->at(0))->method('getData') + ->with('entity_id')->willReturn($productId); $this->product->expects($this->at(1))->method('getStoreId')->will($this->returnValue($storeId)); $this->connection->expects($this->exactly(2))->method('quoteInto')->withConsecutive( ['value.store_id = ?'], @@ -338,26 +342,50 @@ public function testLoadGallery() 'value.store_id = ' . $storeId, 'default_value.store_id = ' . 0 ); + $this->connection->expects($this->any())->method('getIfNullSql')->will( + $this->returnValueMap([ + [ + '`value`.`label`', + '`default_value`.`label`', + 'IFNULL(`value`.`label`, `default_value`.`label`)' + ], + [ + '`value`.`position`', + '`default_value`.`position`', + 'IFNULL(`value`.`position`, `default_value`.`position`)' + ], + [ + '`value`.`disabled`', + '`default_value`.`disabled`', + 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)' + ] + ]) + ); $this->select->expects($this->at(2))->method('joinLeft')->with( ['value' => $getTableReturnValue], $quoteInfoReturnValue, - [ - 'label', - 'position', - 'disabled' - ] + [] )->willReturnSelf(); $this->select->expects($this->at(3))->method('joinLeft')->with( ['default_value' => $getTableReturnValue], $quoteDefaultInfoReturnValue, - ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] + [] )->willReturnSelf(); - $this->select->expects($this->at(4))->method('where')->with( + $this->select->expects($this->at(4))->method('columns')->with([ + 'label' => 'IFNULL(`value`.`label`, `default_value`.`label`)', + 'position' => 'IFNULL(`value`.`position`, `default_value`.`position`)', + 'disabled' => 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)', + 'label_default' => 'default_value.label', + 'position_default' => 'default_value.position', + 'disabled_default' => 'default_value.disabled' + ])->willReturnSelf(); + $this->select->expects($this->at(5))->method('where')->with( 'main.attribute_id = ?', $attributeId )->willReturnSelf(); - $this->select->expects($this->at(5))->method('where')->with('main.disabled = 0')->willReturnSelf(); - $this->select->expects($this->at(7))->method('where') + $this->select->expects($this->at(6))->method('where') + ->with('main.disabled = 0')->willReturnSelf(); + $this->select->expects($this->at(8))->method('where') ->with('entity.entity_id = ?', $productId) ->willReturnSelf(); $this->select->expects($this->once())->method('order') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php new file mode 100644 index 0000000000000..44f66b6cbf66e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Image; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\App\ResourceConnection; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Framework\DB\Query\BatchIteratorInterface; + +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @var AdapterInterface | MockObject + */ + protected $connectionMock; + + /** + * @var Generator | MockObject + */ + protected $generatorMock; + + /** + * @var ResourceConnection | MockObject + */ + protected $resourceMock; + + protected function setUp() + { + $this->objectManager = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->resourceMock->method('getConnection') + ->willReturn($this->connectionMock); + $this->resourceMock->method('getTableName') + ->willReturnArgument(0); + $this->generatorMock = $this->createMock(Generator::class); + } + + /** + * @return MockObject + */ + protected function getVisibleImagesSelectMock(): MockObject + { + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $selectMock->expects($this->once()) + ->method('distinct') + ->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('from') + ->with( + ['images' => Gallery::GALLERY_TABLE], + 'value as filepath' + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('where') + ->with('disabled = 0') + ->willReturnSelf(); + + return $selectMock; + } + + /** + * @param int $imagesCount + * @dataProvider dataProvider + */ + public function testGetCountAllProductImages(int $imagesCount) + { + $selectMock = $this->getVisibleImagesSelectMock(); + $selectMock->expects($this->exactly(2)) + ->method('reset') + ->withConsecutive( + ['columns'], + ['distinct'] + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('columns') + ->with(new \Zend_Db_Expr('count(distinct value)')) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($selectMock); + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($selectMock) + ->willReturn($imagesCount); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock + ] + ); + + $this->assertSame( + $imagesCount, + $imageModel->getCountAllProductImages() + ); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @dataProvider dataProvider + */ + public function testGetAllProductImages( + int $imagesCount, + int $batchSize + ) { + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->getVisibleImagesSelectMock()); + + $batchCount = (int)ceil($imagesCount / $batchSize); + $fetchResultsCallback = $this->getFetchResultCallbackForBatches($imagesCount, $batchSize); + $this->connectionMock->expects($this->exactly($batchCount)) + ->method('fetchAll') + ->will($this->returnCallback($fetchResultsCallback)); + + /** @var Select | MockObject $selectMock */ + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->generatorMock->expects($this->once()) + ->method('generate') + ->with( + 'value_id', + $selectMock, + $batchSize, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + )->will( + $this->returnCallback( + $this->getBatchIteratorCallback($selectMock, $batchCount) + ) + ); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock, + 'batchSize' => $batchSize + ] + ); + + $this->assertCount($imagesCount, $imageModel->getAllProductImages()); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @return \Closure + */ + protected function getFetchResultCallbackForBatches( + int $imagesCount, + int $batchSize + ): \Closure { + $fetchResultsCallback = function () use (&$imagesCount, $batchSize) { + $batchSize = + ($imagesCount >= $batchSize) ? $batchSize : $imagesCount; + $imagesCount -= $batchSize; + + $getFetchResults = function ($batchSize): array { + $result = []; + $count = $batchSize; + while ($count) { + $count--; + $result[$count] = $count; + } + + return $result; + }; + + return $getFetchResults($batchSize); + }; + + return $fetchResultsCallback; + } + + /** + * @param Select | MockObject $selectMock + * @param int $batchCount + * @return \Closure + */ + protected function getBatchIteratorCallback( + MockObject $selectMock, + int $batchCount + ): \Closure { + $iteratorCallback = function () use ($batchCount, $selectMock): array { + $result = []; + $count = $batchCount; + while ($count) { + $count--; + $result[$count] = $selectMock; + } + + return $result; + }; + + return $iteratorCallback; + } + + /** + * Data Provider + * @return array + */ + public function dataProvider(): array + { + return [ + [300, 300], + [300, 100], + [139, 100], + [67, 10], + [154, 47], + [0, 100] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php index 6b908d317aa5b..6f3d8e1a84b17 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPriceTest.php @@ -7,6 +7,9 @@ use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkedProductSelectBuilderByIndexPriceTest extends \PHPUnit\Framework\TestCase { /** @@ -56,12 +59,26 @@ protected function setUp() $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); + + $this->indexScopeResolverMock = $this->createMock( + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class + ); + $this->dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + $this->dimensionFactoryMock = $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class); + $this->dimensionFactoryMock->method('create')->willReturn($this->dimensionMock); + $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $storeMock->method('getId')->willReturn(1); + $storeMock->method('getWebsiteId')->willReturn(1); + $this->storeManagerMock->method('getStore')->willReturn($storeMock); + $this->model = new \Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice( $this->storeManagerMock, $this->resourceMock, $this->customerSessionMock, $this->metadataPoolMock, - $this->baseSelectProcessorMock + $this->baseSelectProcessorMock, + $this->indexScopeResolverMock, + $this->dimensionFactoryMock ); } @@ -79,12 +96,12 @@ public function testBuild() $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->getMockForAbstractClass(); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $this->customerSessionMock->expects($this->once())->method('getCustomerGroupId'); + $this->customerSessionMock->expects($this->once())->method('getCustomerGroupId')->willReturn(1); $connection->expects($this->any())->method('select')->willReturn($select); $select->expects($this->any())->method('from')->willReturnSelf(); $select->expects($this->any())->method('joinInner')->willReturnSelf(); $select->expects($this->any())->method('where')->willReturnSelf(); - $select->expects($this->once())->method('order')->willReturnSelf(); + $select->expects($this->exactly(2))->method('order')->willReturnSelf(); $select->expects($this->once())->method('limit')->willReturnSelf(); $this->resourceMock->expects($this->any())->method('getConnection')->willReturn($connection); $this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadata); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Option/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Option/CollectionTest.php index 4055149103d4a..e52c58e123d07 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Option/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Option/CollectionTest.php @@ -10,7 +10,6 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @codingStandardsIgnoreFile */ class CollectionTest extends \PHPUnit\Framework\TestCase { @@ -82,17 +81,29 @@ class CollectionTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->entityFactoryMock = $this->createPartialMock(\Magento\Framework\Data\Collection\EntityFactory::class, ['create']); + $this->entityFactoryMock = $this->createPartialMock( + \Magento\Framework\Data\Collection\EntityFactory::class, + ['create'] + ); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->fetchStrategyMock = $this->createPartialMock(\Magento\Framework\Data\Collection\Db\FetchStrategy\Query::class, ['fetchAll']); + $this->fetchStrategyMock = $this->createPartialMock( + \Magento\Framework\Data\Collection\Db\FetchStrategy\Query::class, + ['fetchAll'] + ); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\Manager::class); - $this->optionsFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory::class, ['create']); + $this->optionsFactoryMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); $this->joinProcessor = $this->getMockBuilder( - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class) - ->disableOriginalConstructor() + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class + )->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->resourceMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Product\Option::class, ['getConnection', '__wakeup', 'getMainTable', 'getTable']); + $this->resourceMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Product\Option::class, + ['getConnection', '__wakeup', 'getMainTable', 'getTable'] + ); $this->selectMock = $this->createPartialMock(\Magento\Framework\DB\Select::class, ['from', 'reset', 'join']); $this->connection = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['select']); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php index 517b5949ee8ea..405c1ced44ba3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php @@ -145,6 +145,9 @@ public function testGetUrl($filePath, $miscParams) ); } + /** + * @return array + */ public function getPathDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php index 96a6c15e35651..58007145d21a4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php @@ -147,6 +147,9 @@ public function testGetUrl($imageType, $placeholderPath) $this->assertEquals($expectedResult, $imageModel->getUrl()); } + /** + * @return array + */ public function getPathDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Observer/MenuCategoryDataTest.php b/app/code/Magento/Catalog/Test/Unit/Observer/MenuCategoryDataTest.php index 4132f7cd7189f..f37c2e58ce5b4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Observer/MenuCategoryDataTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Observer/MenuCategoryDataTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -39,11 +37,15 @@ class MenuCategoryDataTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->_catalogCategory = $this->createPartialMock(\Magento\Catalog\Helper\Category::class, ['getStoreCategories', 'getCategoryUrl']); + $this->_catalogCategory = $this->createPartialMock( + \Magento\Catalog\Helper\Category::class, + ['getStoreCategories', 'getCategoryUrl'] + ); $layerResolver = $this->createMock(\Magento\Catalog\Model\Layer\Resolver::class); $layerResolver->expects($this->once())->method('get')->willReturn(null); - $this->_observer = (new ObjectManager($this))->getObject(\Magento\Catalog\Observer\MenuCategoryData::class, + $this->_observer = (new ObjectManager($this))->getObject( + \Magento\Catalog\Observer\MenuCategoryData::class, [ 'layerResolver' => $layerResolver, 'catalogCategory' => $this->_catalogCategory, diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php index 2d67db77d430b..c5a3e5dab7678 100644 --- a/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Block/TopmenuTest.php @@ -87,7 +87,7 @@ protected function setUp() \Magento\Catalog\Model\ResourceModel\Category\Collection::class ); $this->categoryCollectionFactoryMock = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory::class, + \Magento\Catalog\Model\ResourceModel\Category\StateDependentCollectionFactory::class, ['create'] ); diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php new file mode 100644 index 0000000000000..efd78cac6e512 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Framework/App/Action/ContextPluginTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Plugin\Framework\App\Action; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Http\Context as HttpContext; + +/** + * Class for testing ContextPlugin class. + */ +class ContextPluginTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ContextPlugin + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ToolbarMemorizer + */ + private $toolbarMemorizerMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CatalogSession + */ + private $catalogSessionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|HttpContext + */ + private $httpContextMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->toolbarMemorizerMock = $this->getMockBuilder(ToolbarMemorizer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->catalogSessionMock = $this->getMockBuilder(CatalogSession::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpContextMock = $this->getMockBuilder(HttpContext::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + ContextPlugin::class, + [ + 'toolbarMemorizer' => $this->toolbarMemorizerMock, + 'catalogSession' => $this->catalogSessionMock, + 'httpContext' => $this->httpContextMock, + ] + ); + } + + /** + * Test beforeDispatch method. + * + * @return void + */ + public function testBeforeDispatch() + { + $this->toolbarMemorizerMock->method('isMemorizingAllowed')->willReturn(true); + $this->catalogSessionMock->method('getData')->willReturn('any_value'); + + $this->model->beforeDispatch(); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php new file mode 100644 index 0000000000000..944dc234e928c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Plugin\Model\Attribute\Backend; + +use Magento\Catalog\Plugin\Model\Attribute\Backend\AttributeValidation; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\DataObject; + +class AttributeValidationTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AttributeValidation + */ + private $attributeValidation; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var array + */ + private $allowedEntityTypes; + + /** + * @var \Callable + */ + private $proceedMock; + + /** + * @var bool + */ + private $isProceedMockCalled = false; + + /** + * @var AbstractBackend|\PHPUnit_Framework_MockObject_MockObject + */ + private $subjectMock; + + /** + * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMock; + + /** + * @var DataObject|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->attributeMock = $this->getMockBuilder(AbstractBackend::class) + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $this->subjectMock = $this->getMockBuilder(AbstractBackend::class) + ->setMethods(['getAttribute']) + ->getMockForAbstractClass(); + $this->subjectMock->expects($this->any()) + ->method('getAttribute') + ->willReturn($this->attributeMock); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->entityMock = $this->getMockBuilder(DataObject::class) + ->setMethods(['getData']) + ->getMock(); + + $this->allowedEntityTypes = [$this->entityMock]; + + $this->proceedMock = function () { + $this->isProceedMockCalled = true; + }; + + $this->attributeValidation = $objectManager->getObject( + AttributeValidation::class, + [ + 'storeManager' => $this->storeManagerMock, + 'allowedEntityTypes' => $this->allowedEntityTypes, + ] + ); + } + + /** + * @param bool $shouldProceedRun + * @param bool $defaultStoreUsed + * @param null|int|string $storeId + * @dataProvider aroundValidateDataProvider + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @return void + */ + public function testAroundValidate(bool $shouldProceedRun, bool $defaultStoreUsed, $storeId) + { + $this->isProceedMockCalled = false; + $attributeCode = 'code'; + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + if ($defaultStoreUsed) { + $this->attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->entityMock->expects($this->at(0)) + ->method('getData') + ->willReturn([$attributeCode => null]); + $this->entityMock->expects($this->at(1)) + ->method('getData') + ->with($attributeCode) + ->willReturn(null); + } + + $this->attributeValidation->aroundValidate($this->subjectMock, $this->proceedMock, $this->entityMock); + $this->assertSame($shouldProceedRun, $this->isProceedMockCalled); + } + + /** + * Data provider for testAroundValidate + * @return array + */ + public function aroundValidateDataProvider(): array + { + return [ + [true, false, '0'], + [true, false, 0], + [true, false, null], + [false, true, 1], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php new file mode 100644 index 0000000000000..712aeba59dffe --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/AttributeSetRepository/RemoveProductsTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\Unit\Plugin\Model\AttributeSetRepository; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Plugin\Model\AttributeSetRepository\RemoveProducts; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Eav\Api\Data\AttributeSetInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests for RemoveProducts plugin. + */ +class RemoveProductsTest extends TestCase +{ + /** + * @var RemoveProducts + */ + private $testSubject; + + /** + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $collectionFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->testSubject = $objectManager->getObject( + RemoveProducts::class, + [ + 'collectionFactory' => $this->collectionFactory, + ] + ); + } + + /** + * Test plugin will delete all related products for given attribute set. + */ + public function testAfterDelete() + { + $attributeSetId = '1'; + + /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collection */ + $collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $collection->expects(self::once()) + ->method('addFieldToFilter') + ->with(self::identicalTo('attribute_set_id'), self::identicalTo(['eq' => $attributeSetId])); + $collection->expects(self::once()) + ->method('delete'); + + $this->collectionFactory->expects(self::once()) + ->method('create') + ->willReturn($collection); + + /** @var AttributeSetRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $attributeSetRepository */ + $attributeSetRepository = $this->getMockBuilder(AttributeSetRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + /** @var AttributeSetInterface|\PHPUnit_Framework_MockObject_MockObject $attributeSet */ + $attributeSet = $this->getMockBuilder(AttributeSetInterface::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $attributeSet->expects(self::once()) + ->method('getId') + ->willReturn($attributeSetId); + + self::assertTrue($this->testSubject->afterDelete($attributeSetRepository, true, $attributeSet)); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php index 25c3c3ab24ad8..b823549391257 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/BasePriceTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Pricing\Price; /** @@ -108,6 +106,9 @@ public function testGetValue($specialPriceValue, $expectedResult) $this->assertSame($expectedResult, $this->basePrice->getValue()); } + /** + * @return array + */ public function getValueDataProvider() { return [[77, 77], [0, 0], [false, 99]]; diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php index f3436b8f9f09f..4a206d023ec16 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CustomOptionPriceTest.php @@ -77,6 +77,11 @@ protected function setUp() ); } + /** + * @param array $optionsData + * + * @return array + */ protected function setupOptions(array $optionsData) { $options = []; @@ -105,6 +110,11 @@ protected function setupOptions(array $optionsData) return $options; } + /** + * @param $optionsData + * + * @return array + */ protected function setupSingleValueOptions($optionsData) { $options = []; @@ -279,7 +289,7 @@ protected function getOptionValueMock($price) { $optionValueMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class) ->disableOriginalConstructor() - ->setMethods(['getPriceType', 'getPrice', 'getId', '__wakeup']) + ->setMethods(['getPriceType', 'getPrice', 'getId', '__wakeup', 'getOption', 'getData']) ->getMock(); $optionValueMock->expects($this->any()) ->method('getPriceType') @@ -288,6 +298,29 @@ protected function getOptionValueMock($price) ->method('getPrice') ->with($this->equalTo(true)) ->will($this->returnValue($price)); + + $optionValueMock->expects($this->any()) + ->method('getData') + ->with(\Magento\Catalog\Model\Product\Option\Value::KEY_PRICE) + ->willReturn($price); + + $optionMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + + $optionValueMock->expects($this->any())->method('getOption')->willReturn($optionMock); + + $optionMock->expects($this->any())->method('getProduct')->willReturn($this->product); + + $priceMock = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMockForAbstractClass(); + $priceMock->method('getValue')->willReturn($price); + + $this->priceInfo->method('getPrice')->willReturn($priceMock); + return $optionValueMock; } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php index d04bb4c681e67..1c50271976d15 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php @@ -61,6 +61,9 @@ public function setUp() ); } + /** + * @return int + */ private function getValueTierPricesExistShouldReturnMinTierPrice() { $minPrice = 5; diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php index fd13439d7d34c..993f641367a89 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/TierPriceTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Test\Unit\Pricing\Price; use Magento\Catalog\Pricing\Price\TierPrice; @@ -81,7 +79,10 @@ protected function setUp() { $this->priceInfo = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class); - $this->product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getPriceInfo', 'hasCustomerGroupId', 'getCustomerGroupId', 'getResource', '__wakeup']); + $this->product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getPriceInfo', 'hasCustomerGroupId', 'getCustomerGroupId', 'getResource', '__wakeup'] + ); $this->product->expects($this->any())->method('getPriceInfo')->will($this->returnValue($this->priceInfo)); $this->customerGroupRetriever = $this->getMockBuilder(\Magento\Customer\Model\Group\RetrieverInterface::class) ->disableOriginalConstructor()->getMock(); @@ -119,11 +120,10 @@ public function testBaseInitialization($tierPrices, $expectedValue) $convertedExpectedValue = $expectedValue - 1; $this->priceCurrencyMock->expects($this->any()) ->method('convertAndRound') - ->will($this->returnCallback( + ->willReturnCallback( function ($arg) { return $arg -1; } - ) ); $this->product->setData(TierPrice::PRICE_CODE, $tierPrices); $group = $this->createMock(\Magento\Customer\Model\Data\Group::class); @@ -264,11 +264,10 @@ public function testGetterTierPriceList($tierPrices, $basePrice, $expectedResult ->willReturn($price); $this->priceCurrencyMock->expects($this->any()) ->method('convertAndRound') - ->will($this->returnCallback( + ->willReturnCallback( function ($arg) { return round(0.5 * $arg, 2); } - ) ); $group = $this->createMock(\Magento\Customer\Model\Data\Group::class); @@ -411,6 +410,9 @@ public function testGetQuantity($quantity, $expectedValue) $this->assertEquals($expectedValue, $tierPrice->getQuantity()); } + /** + * @return array + */ public function getQuantityDataProvider() { return [ @@ -422,6 +424,5 @@ public function getQuantityDataProvider() ['0.7', 0.7], ['0.0000000', 1] ]; - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index fc45a2e0c2146..42f537228ddf8 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -246,7 +246,8 @@ public function testRenderMsrpEnabled() //assert price wrapper $this->assertEquals( - '<div class="price-box price-final_price" data-role="priceBox" data-product-id="">test</div>', + '<div class="price-box price-final_price" data-role="priceBox" data-product-id="" ' . + 'data-price-box="product-id-">test</div>', $result ); } @@ -346,6 +347,9 @@ public function testHasSpecialPrice($regularPrice, $finalPrice, $expectedResult) $this->assertEquals($expectedResult, $this->object->hasSpecialPrice()); } + /** + * @return array + */ public function hasSpecialPriceProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php index 986a1f7710919..e4d531e91fa07 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/PriceBoxTest.php @@ -88,6 +88,9 @@ public function testGetCanDisplayQty($typeCode, $expected) $this->assertEquals($expected, $this->object->getCanDisplayQty($product)); } + /** + * @return array + */ public function getCanDisplayQtyDataProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php new file mode 100644 index 0000000000000..0f22da282fec0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject() + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, $filter) + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + '__disableTmpl' => ['label' => true] + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php new file mode 100644 index 0000000000000..9f1a86a5b3bf6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/Product/MassActionTest.php @@ -0,0 +1,267 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component\Product; + +use Magento\Catalog\Ui\Component\Product\MassAction; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\UiComponent\ContextInterface; + +/** + * Test for Magento\Catalog\Ui\Component\Product\MassAction class. + */ +class MassActionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var MassAction + */ + private $massAction; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->contextMock = $this->getMockBuilder(ContextInterface::class) + ->getMockForAbstractClass(); + $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) + ->getMockForAbstractClass(); + + $this->massAction = $this->objectManager->getObject( + MassAction::class, + [ + 'authorization' => $this->authorizationMock, + 'context' => $this->contextMock, + 'data' => [], + ] + ); + } + + /** + * @return void + */ + public function testGetComponentName() + { + $this->assertTrue($this->massAction->getComponentName() === MassAction::NAME); + } + + /** + * @param string $componentName + * @param array $componentData + * @param bool $isAllowed + * @param bool $expectActionConfig + * @return void + * @dataProvider getPrepareDataProvider + */ + public function testPrepare( + string $componentName, + array $componentData, + bool $isAllowed = true, + bool $expectActionConfig = true + ) { + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->atLeastOnce())->method('getProcessor')->willReturn($processor); + /** @var \Magento\Ui\Component\MassAction $action */ + $action = $this->objectManager->getObject( + \Magento\Ui\Component\MassAction::class, + [ + 'context' => $this->contextMock, + 'data' => [ + 'name' => $componentName, + 'config' => $componentData, + ] + ] + ); + $this->authorizationMock->method('isAllowed') + ->willReturn($isAllowed); + $this->massAction->addComponent('action', $action); + $this->massAction->prepare(); + $expected = $expectActionConfig ? ['actions' => [$action->getConfiguration()]] : []; + $this->assertEquals($expected, $this->massAction->getConfiguration()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getPrepareDataProvider() : array + { + return [ + [ + 'test_component1', + [ + 'type' => 'first_action', + 'label' => 'First Action', + 'url' => '/module/controller/firstAction', + '__disableTmpl' => true + ], + ], + [ + 'test_component2', + [ + 'type' => 'second_action', + 'label' => 'Second Action', + 'actions' => [ + [ + 'type' => 'second_sub_action1', + 'label' => 'Second Sub Action 1', + 'url' => '/module/controller/secondSubAction1', + ], + [ + 'type' => 'second_sub_action2', + 'label' => 'Second Sub Action 2', + 'url' => '/module/controller/secondSubAction2', + ], + ], + '__disableTmpl' => true + ], + ], + [ + 'status_component', + [ + 'type' => 'status', + 'label' => 'Status', + 'actions' => [ + [ + 'type' => 'enable', + 'label' => 'Second Sub Action 1', + 'url' => '/module/controller/enable', + ], + [ + 'type' => 'disable', + 'label' => 'Second Sub Action 2', + 'url' => '/module/controller/disable', + ], + ], + '__disableTmpl' => true + ], + ], + [ + 'status_component_not_allowed', + [ + 'type' => 'status', + 'label' => 'Status', + 'actions' => [ + [ + 'type' => 'enable', + 'label' => 'Second Sub Action 1', + 'url' => '/module/controller/enable', + ], + [ + 'type' => 'disable', + 'label' => 'Second Sub Action 2', + 'url' => '/module/controller/disable', + ], + ], + '__disableTmpl' => true + ], + false, + false, + ], + [ + 'delete_component', + [ + 'type' => 'delete', + 'label' => 'First Action', + 'url' => '/module/controller/delete', + '__disableTmpl' => true + ], + ], + [ + 'delete_component_not_allowed', + [ + 'type' => 'delete', + 'label' => 'First Action', + 'url' => '/module/controller/delete', + '__disableTmpl' => true + ], + false, + false, + ], + [ + 'attributes_component', + [ + 'type' => 'delete', + 'label' => 'First Action', + 'url' => '/module/controller/attributes', + '__disableTmpl' => true + ], + ], + [ + 'attributes_component_not_allowed', + [ + 'type' => 'delete', + 'label' => 'First Action', + 'url' => '/module/controller/attributes', + '__disableTmpl' => true + ], + false, + false, + ], + ]; + } + + /** + * @param bool $expected + * @param string $actionType + * @param int $callNum + * @param string $resource + * @param bool $isAllowed + * @return void + * @dataProvider isActionAllowedDataProvider + */ + public function testIsActionAllowed( + bool $expected, + string $actionType, + int $callNum, + string $resource = '', + bool $isAllowed = true + ) { + $this->authorizationMock->expects($this->exactly($callNum)) + ->method('isAllowed') + ->with($resource) + ->willReturn($isAllowed); + + $this->assertEquals($expected, $this->massAction->isActionAllowed($actionType)); + } + + /** + * @return array + */ + public function isActionAllowedDataProvider(): array + { + return [ + 'other' => [true, 'other', 0,], + 'delete-allowed' => [true, 'delete', 1, 'Magento_Catalog::products'], + 'delete-not-allowed' => [false, 'delete', 1, 'Magento_Catalog::products', false], + 'status-allowed' => [true, 'status', 1, 'Magento_Catalog::products'], + 'status-not-allowed' => [false, 'status', 1, 'Magento_Catalog::products', false], + 'attributes-allowed' => [true, 'attributes', 1, 'Magento_Catalog::update_attributes'], + 'attributes-not-allowed' => [false, 'attributes', 1, 'Magento_Catalog::update_attributes', false], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php index 9b0ade2b1288f..57b277a786ea3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/CatalogEavValidationRulesTest.php @@ -53,6 +53,9 @@ public function testBuild($frontendInput, $frontendClass, array $eavConfig, arra $this->assertEquals($expectedResult, $this->catalogEavValidationRules->build($attribute, $eavConfig)); } + /** + * @return array + */ public function buildDataProvider() { $data['required'] = true; diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php index af10eeea42fd3..7a86f806abd26 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AbstractModifierTest.php @@ -63,10 +63,11 @@ protected function setUp() 'getAttributes', 'getStore', 'getAttributeDefaultValue', - 'getExistsStoreValueFlag' + 'getExistsStoreValueFlag', + 'isLockedAttribute' ])->getMockForAbstractClass(); $this->storeMock = $this->getMockBuilder(StoreInterface::class) - ->setMethods(['load', 'getId', 'getConfig']) + ->setMethods(['load', 'getId', 'getConfig', 'getBaseCurrency', 'getBaseCurrencyCode']) ->getMockForAbstractClass(); $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) ->disableOriginalConstructor() @@ -81,9 +82,6 @@ protected function setUp() $this->arrayManagerMock->expects($this->any()) ->method('set') ->willReturnArgument(1); - $this->arrayManagerMock->expects($this->any()) - ->method('merge') - ->willReturnArgument(1); $this->arrayManagerMock->expects($this->any()) ->method('remove') ->willReturnArgument(1); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php index c22dde0b456ac..6280de9d3a611 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AttributeSetTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AttributeSet; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; @@ -84,7 +85,34 @@ protected function createModel() public function testModifyMeta() { - $this->assertNotEmpty($this->getModel()->modifyMeta(['test_group' => []])); + $modifyMeta = $this->getModel()->modifyMeta(['test_group' => []]); + $this->assertNotEmpty($modifyMeta); + } + + /** + * @param bool $locked + * @return void + * @dataProvider modifyMetaLockedDataProvider + */ + public function testModifyMetaLocked(bool $locked) + { + $this->productMock->expects($this->any()) + ->method('isLockedAttribute') + ->willReturn($locked); + $modifyMeta = $this->getModel()->modifyMeta([AbstractModifier::DEFAULT_GENERAL_PANEL => []]); + $children = $modifyMeta[AbstractModifier::DEFAULT_GENERAL_PANEL]['children']; + $this->assertEquals( + $locked, + $children['attribute_set_id']['arguments']['data']['config']['disabled'] + ); + } + + /** + * @return array + */ + public function modifyMetaLockedDataProvider() + { + return [[true], [false]]; } public function testModifyMetaToBeEmpty() diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 4daff7e7930e3..e741070547163 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -114,27 +114,13 @@ public function testModifyMeta() $this->assertArrayHasKey($groupCode, $this->getModel()->modifyMeta($meta)); } - public function testModifyMetaWithCaching() + /** + * @param bool $locked + * @return void + * @dataProvider modifyMetaLockedDataProvider + */ + public function testModifyMetaLocked(bool $locked) { - $this->arrayManagerMock->expects($this->exactly(2)) - ->method('findPath') - ->willReturn(true); - $cacheManager = $this->getMockBuilder(CacheInterface::class) - ->getMockForAbstractClass(); - $cacheManager->expects($this->once()) - ->method('load') - ->with(Categories::CATEGORY_TREE_ID . '_'); - $cacheManager->expects($this->once()) - ->method('save'); - - $modifier = $this->createModel(); - $cacheContextProperty = new \ReflectionProperty( - Categories::class, - 'cacheManager' - ); - $cacheContextProperty->setAccessible(true); - $cacheContextProperty->setValue($modifier, $cacheManager); - $groupCode = 'test_group_code'; $meta = [ $groupCode => [ @@ -145,6 +131,28 @@ public function testModifyMetaWithCaching() ], ], ]; - $modifier->modifyMeta($meta); + + $this->arrayManagerMock->expects($this->any()) + ->method('findPath') + ->willReturn('path'); + + $this->productMock->expects($this->any()) + ->method('isLockedAttribute') + ->willReturn($locked); + + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(2); + + $modifyMeta = $this->createModel()->modifyMeta($meta); + $this->assertEquals($locked, $modifyMeta['arguments']['data']['config']['disabled']); + } + + /** + * @return array + */ + public function modifyMetaLockedDataProvider() + { + return [[true], [false]]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php index 921a8dcdfe6b8..dd9819cdbc5ab 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php @@ -154,7 +154,16 @@ public function testModifyMeta() ->method('getAll') ->willReturn([]); - $this->assertArrayHasKey(CustomOptions::GROUP_CUSTOM_OPTIONS_NAME, $this->getModel()->modifyMeta([])); + $meta = $this->getModel()->modifyMeta([]); + + $this->assertArrayHasKey(CustomOptions::GROUP_CUSTOM_OPTIONS_NAME, $meta); + + $buttonAdd = $meta['custom_options']['children']['container_header']['children']['button_add']; + $buttonAddTargetName = $buttonAdd['arguments']['data']['config']['actions'][0]['targetName']; + $expectedTargetName = '${ $.ns }.${ $.ns }.' . CustomOptions::GROUP_CUSTOM_OPTIONS_NAME + . '.' . CustomOptions::GRID_OPTIONS_NAME; + + $this->assertEquals($expectedTargetName, $buttonAddTargetName); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index d2adf50035cc8..0c4b12857d469 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -5,11 +5,10 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Framework\App\RequestInterface; -use Magento\Framework\EntityManager\EventManager; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Ui\DataProvider\EavValidationRules; @@ -19,6 +18,7 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Eav\Model\Entity\Type as EntityType; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -87,6 +87,11 @@ class EavTest extends AbstractModifierTest */ private $entityTypeMock; + /** + * @var AttributeCollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeCollectionFactoryMock; + /** * @var AttributeCollection|\PHPUnit_Framework_MockObject_MockObject */ @@ -225,6 +230,10 @@ protected function setUp() $this->entityTypeMock = $this->getMockBuilder(EntityType::class) ->disableOriginalConstructor() ->getMock(); + $this->attributeCollectionFactoryMock = $this->getMockBuilder(AttributeCollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); $this->attributeCollectionMock = $this->getMockBuilder(AttributeCollection::class) ->disableOriginalConstructor() ->getMock(); @@ -256,7 +265,15 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->setMethods([ + 'load', + 'getAttributeGroupCode', + 'getApplyTo', + 'getFrontendInput', + 'getAttributeCode', + 'usesSource', + 'getSource' + ]) ->disableOriginalConstructor() ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) @@ -323,7 +340,7 @@ protected function setUp() $this->eavAttributeMock->expects($this->any()) ->method('load') ->willReturnSelf(); - + $this->eav =$this->getModel(); $this->objectManager->setBackwardCompatibleProperty( $this->eav, @@ -352,7 +369,8 @@ protected function createModel() 'attributeRepository' => $this->attributeRepositoryMock, 'arrayManager' => $this->arrayManagerMock, 'eavAttributeFactory' => $this->eavAttributeFactoryMock, - '_eventManager' => $this->eventManagerMock + '_eventManager' => $this->eventManagerMock, + 'attributeCollectionFactory' => $this->attributeCollectionFactoryMock ]); } @@ -366,139 +384,131 @@ public function testModifyData() ] ]; - $this->locatorMock->expects($this->any()) - ->method('getProduct') + $this->attributeCollectionFactoryMock->expects($this->once())->method('create') + ->willReturn($this->attributeCollectionMock); + + $this->attributeCollectionMock->expects($this->any())->method('getItems') + ->willReturn([ + $this->eavAttributeMock + ]); + + $this->locatorMock->expects($this->any())->method('getProduct') ->willReturn($this->productMock); - $this->productMock->expects($this->any()) - ->method('getId') + $this->productMock->expects($this->any())->method('getId') ->willReturn(1); - $this->productMock->expects($this->once()) - ->method('getAttributeSetId') + $this->productMock->expects($this->once())->method('getAttributeSetId') ->willReturn(4); - $this->productMock->expects($this->once()) - ->method('getData') + $this->productMock->expects($this->once())->method('getData') ->with(ProductAttributeInterface::CODE_PRICE)->willReturn('19.9900'); - $this->searchCriteriaBuilderMock->expects($this->any()) - ->method('addFilter') + $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter') ->willReturnSelf(); - $this->searchCriteriaBuilderMock->expects($this->any()) - ->method('create') + $this->searchCriteriaBuilderMock->expects($this->any())->method('create') ->willReturn($this->searchCriteriaMock); - $this->attributeGroupRepositoryMock->expects($this->any()) - ->method('getList') + $this->attributeGroupRepositoryMock->expects($this->any())->method('getList') ->willReturn($this->searchCriteriaMock); - $this->searchCriteriaMock->expects($this->once()) - ->method('getItems') + $this->searchCriteriaMock->expects($this->once())->method('getItems') ->willReturn([$this->attributeGroupMock]); - $this->sortOrderBuilderMock->expects($this->once()) - ->method('setField') + $this->sortOrderBuilderMock->expects($this->once())->method('setField') ->willReturnSelf(); - $this->sortOrderBuilderMock->expects($this->once()) - ->method('setAscendingDirection') + $this->sortOrderBuilderMock->expects($this->once())->method('setAscendingDirection') ->willReturnSelf(); $dataObjectMock = $this->createMock(\Magento\Framework\Api\AbstractSimpleObject::class); - $this->sortOrderBuilderMock->expects($this->once()) - ->method('create') + $this->sortOrderBuilderMock->expects($this->once())->method('create') ->willReturn($dataObjectMock); - $this->searchCriteriaBuilderMock->expects($this->any()) - ->method('addFilter') + $this->searchCriteriaBuilderMock->expects($this->any())->method('addFilter') ->willReturnSelf(); - $this->searchCriteriaBuilderMock->expects($this->once()) - ->method('addSortOrder') + $this->searchCriteriaBuilderMock->expects($this->once())->method('addSortOrder') ->willReturnSelf(); - $this->searchCriteriaBuilderMock->expects($this->any()) - ->method('create') + $this->searchCriteriaBuilderMock->expects($this->any())->method('create') ->willReturn($this->searchCriteriaMock); - $this->attributeRepositoryMock->expects($this->once()) - ->method('getList') + $this->attributeRepositoryMock->expects($this->once())->method('getList') ->with($this->searchCriteriaMock) ->willReturn($this->searchResultsMock); - $this->eavAttributeMock->expects($this->any()) - ->method('getAttributeGroupCode') + $this->eavAttributeMock->expects($this->any())->method('getAttributeGroupCode') ->willReturn('product-details'); - $this->eavAttributeMock->expects($this->once()) - ->method('getApplyTo') + $this->eavAttributeMock->expects($this->once())->method('getApplyTo') ->willReturn([]); - $this->eavAttributeMock->expects($this->once()) - ->method('getFrontendInput') + $this->eavAttributeMock->expects($this->once())->method('getFrontendInput') ->willReturn('price'); - $this->eavAttributeMock->expects($this->any()) - ->method('getAttributeCode') + $this->eavAttributeMock->expects($this->any())->method('getAttributeCode') ->willReturn(ProductAttributeInterface::CODE_PRICE); - $this->searchResultsMock->expects($this->once()) - ->method('getItems') + $this->searchResultsMock->expects($this->once())->method('getItems') ->willReturn([$this->eavAttributeMock]); - $this->storeMock->expects(($this->once())) - ->method('getBaseCurrencyCode') + $this->storeMock->expects(($this->once()))->method('getBaseCurrencyCode') ->willReturn('en_US'); - $this->storeManagerMock->expects($this->once()) - ->method('getStore') + $this->storeManagerMock->expects($this->once())->method('getStore') ->willReturn($this->storeMock); - $this->currencyMock->expects($this->once()) - ->method('toCurrency') + $this->currencyMock->expects($this->once())->method('toCurrency') ->willReturn('19.99'); - $this->currencyLocaleMock->expects($this->once()) - ->method('getCurrency') + $this->currencyLocaleMock->expects($this->once())->method('getCurrency') ->willReturn($this->currencyMock); $this->assertEquals($sourceData, $this->eav->modifyData([])); } /** - * @param int $productId + * @param int|null $productId * @param bool $productRequired - * @param string $attrValue + * @param string|null $attrValue * @param array $expected + * @param bool $locked + * @return void * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta * @dataProvider setupAttributeMetaDataProvider */ - public function testSetupAttributeMetaDefaultAttribute($productId, $productRequired, $attrValue, $expected) - { - $configPath = 'arguments/data/config'; + public function testSetupAttributeMetaDefaultAttribute( + $productId, + bool $productRequired, + $attrValue, + array $expected, + bool $locked = false + ) { + $configPath = 'arguments/data/config'; $groupCode = 'product-details'; $sortOrder = '0'; + $attributeOptions = [ + ['value' => 1, 'label' => 'Int label'], + ['value' => 1.5, 'label' => 'Float label'], + ['value' => true, 'label' => 'Boolean label'], + ['value' => 'string', 'label' => 'String label'], + ['value' => ['test1', 'test2'], 'label' => 'Array label'] + ]; + $attributeOptionsExpected = [ + ['value' => '1', 'label' => 'Int label', '__disableTmpl' => true], + ['value' => '1.5', 'label' => 'Float label', '__disableTmpl' => true], + ['value' => '1', 'label' => 'Boolean label', '__disableTmpl' => true], + ['value' => 'string', 'label' => 'String label', '__disableTmpl' => true], + ['value' => ['test1', 'test2'], 'label' => 'Array label', '__disableTmpl' => true] + ]; - $this->productMock->expects($this->any()) - ->method('getId') - ->willReturn($productId); - - $this->productAttributeMock->expects($this->any()) - ->method('getIsRequired') - ->willReturn($productRequired); - - $this->productAttributeMock->expects($this->any()) - ->method('getDefaultValue') - ->willReturn('required_value'); - - $this->productAttributeMock->expects($this->any()) - ->method('getAttributeCode') - ->willReturn('code'); - - $this->productAttributeMock->expects($this->any()) - ->method('getValue') - ->willReturn('value'); + $this->productMock->method('getId')->willReturn($productId); + $this->productMock->expects($this->any())->method('isLockedAttribute')->willReturn($locked); + $this->productAttributeMock->method('getIsRequired')->willReturn($productRequired); + $this->productAttributeMock->method('getDefaultValue')->willReturn('required_value'); + $this->productAttributeMock->method('getAttributeCode')->willReturn('code'); + $this->productAttributeMock->method('getValue')->willReturn('value'); $attributeMock = $this->getMockBuilder(AttributeInterface::class) ->setMethods(['getValue']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attributeMock->expects($this->any()) - ->method('getValue') - ->willReturn($attrValue); + $attributeMock->method('getValue')->willReturn($attrValue); + $this->productMock->method('getCustomAttribute')->willReturn($attributeMock); + $this->eavAttributeMock->method('usesSource')->willReturn(true); - $this->productMock->expects($this->any()) - ->method('getCustomAttribute') - ->willReturn($attributeMock); + $attributeSource = $this->getMockBuilder(SourceInterface::class)->getMockForAbstractClass(); + $attributeSource->method('getAllOptions')->willReturn($attributeOptions); - $this->arrayManagerMock->expects($this->any()) - ->method('set') + $this->eavAttributeMock->method('getSource')->willReturn($attributeSource); + + $this->arrayManagerMock->method('set') ->with( $configPath, [], @@ -508,14 +518,20 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi $this->arrayManagerMock->expects($this->any()) ->method('merge') + ->with( + $this->anything(), + $this->anything(), + $this->callback( + function ($value) use ($attributeOptionsExpected) { + return isset($value['options']) ? $value['options'] === $attributeOptionsExpected : true; + } + ) + ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->any()) - ->method('get') - ->willReturn([]); + $this->arrayManagerMock->method('get')->willReturn([]); - $this->arrayManagerMock->expects($this->any()) - ->method('exists'); + $this->arrayManagerMock->method('exists')->willReturn(true); $this->assertEquals( $expected, @@ -525,84 +541,110 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function setupAttributeMetaDataProvider() { return [ 'default_null_prod_not_new_and_required' => [ - 'productId' => 1, - 'productRequired' => true, - 'attrValue' => 'val', - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => true, - 'notice' => null, - 'default' => null, - 'label' => null, - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], + 'default_null_prod_not_new_locked_and_required' => [ + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] + ], + 'locked' => true, + ], 'default_null_prod_not_new_and_not_required' => [ - 'productId' => 1, - 'productRequired' => false, - 'attrValue' => 'val', - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => null, - 'label' => null, - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'productId' => 1, + 'productRequired' => false, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => null, + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], 'default_null_prod_new_and_not_required' => [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => null, - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ], 'default_null_prod_new_and_required' => [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => null, - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => null, + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + '__disableTmpl' => ['label' => true, 'code' => true] ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php index b4460b314513b..0f247e7cca85f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php @@ -5,8 +5,11 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\General; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Stdlib\ArrayManager; /** * Class GeneralTest @@ -15,6 +18,38 @@ */ class GeneralTest extends AbstractModifierTest { + /** + * @var AttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepositoryMock; + + /** + * @var General + */ + private $generalModifier; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->attributeRepositoryMock = $this->getMockBuilder(AttributeRepositoryInterface::class) + ->getMockForAbstractClass(); + + $arrayManager = $this->objectManager->getObject(ArrayManager::class); + + $this->generalModifier = $this->objectManager->getObject( + General::class, + [ + 'attributeRepository' => $this->attributeRepositoryMock, + 'locator' => $this->locatorMock, + 'arrayManager' => $arrayManager, + ] + ); + } + /** * {@inheritdoc} */ @@ -26,8 +61,14 @@ protected function createModel() ]); } + /** + * @return void + */ public function testModifyMeta() { + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(2); $this->assertNotEmpty($this->getModel()->modifyMeta([ 'first_panel_code' => [ 'arguments' => [ @@ -40,4 +81,59 @@ public function testModifyMeta() ] ])); } + + /** + * @param array $data + * @param int $defaultStatusValue + * @param array $expectedResult + * @dataProvider modifyDataDataProvider + * @return void + */ + public function testModifyDataNewProduct(array $data, int $defaultStatusValue, array $expectedResult) + { + $attributeMock = $this->getMockBuilder(AttributeInterface::class) + ->getMockForAbstractClass(); + $attributeMock + ->method('getDefaultValue') + ->willReturn($defaultStatusValue); + $this->attributeRepositoryMock + ->method('get') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ) + ->willReturn($attributeMock); + $this->assertSame($expectedResult, $this->generalModifier->modifyData($data)); + } + + /** + * @return array + */ + public function modifyDataDataProvider(): array + { + return [ + 'With default status value' => [ + 'data' => [], + 'defaultStatusAttributeValue' => 5, + 'expectedResult' => [ + null => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 5, + ], + ], + ], + ], + 'Without default status value' => [ + 'data' => [], + 'defaultStatusAttributeValue' => 0, + 'expectedResult' => [ + null => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + ], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php index d4d4136bf4157..855a756b550a6 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ScheduleDesignUpdateTest.php @@ -22,8 +22,14 @@ protected function createModel() ]); } + /** + * @return void + */ public function testModifyMeta() { + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(1); $this->assertSame([], $this->getModel()->modifyMeta([])); } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php index 997b66861c21b..f38e2a0d426d1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php @@ -76,7 +76,10 @@ class WebsitesTest extends AbstractModifierTest protected function setUp() { - $this->objectManager = new ObjectManager($this); + parent::setUp(); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn(self::PRODUCT_ID); $this->assignedWebsites = [self::SECOND_WEBSITE_ID]; $this->websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class) ->setMethods(['getId', 'getName']) @@ -87,35 +90,28 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->websiteRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\WebsiteRepositoryInterface::class) - ->setMethods(['getList', 'getDefault']) + ->setMethods(['getDefault']) ->getMockForAbstractClass(); $this->websiteRepositoryMock->expects($this->any()) ->method('getDefault') ->willReturn($this->websiteMock); - $this->websiteRepositoryMock->expects($this->any()) - ->method('getList') - ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); $this->groupRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\GroupRepositoryInterface::class) ->setMethods(['getList']) ->getMockForAbstractClass(); $this->storeRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\StoreRepositoryInterface::class) ->setMethods(['getList']) ->getMockForAbstractClass(); - $this->locatorMock = $this->getMockBuilder(\Magento\Catalog\Model\Locator\LocatorInterface::class) - ->setMethods(['getProduct', 'getWebsiteIds']) - ->getMockForAbstractClass(); $this->productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) ->setMethods(['getId']) ->getMockForAbstractClass(); - $this->locatorMock->expects($this->any()) - ->method('getProduct') - ->willReturn($this->productMock); $this->locatorMock->expects($this->any()) ->method('getWebsiteIds') ->willReturn($this->assignedWebsites); $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->setMethods(['isSingleStoreMode']) + ->setMethods(['isSingleStoreMode', 'getWebsites']) ->getMockForAbstractClass(); + $this->storeManagerMock->method('getWebsites') + ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); $this->storeManagerMock->expects($this->any()) ->method('isSingleStoreMode') ->willReturn(false); @@ -148,9 +144,6 @@ protected function setUp() $this->storeRepositoryMock->expects($this->any()) ->method('getList') ->willReturn([$this->storeViewMock]); - $this->productMock->expects($this->any()) - ->method('getId') - ->willReturn(self::PRODUCT_ID); $this->secondWebsiteMock->expects($this->any()) ->method('getId') ->willReturn($this->assignedWebsites[0]); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 12bc9acfa4c51..2d6b082e35b17 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -6,15 +6,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Listing\Collector; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductRender\ImageInterface; use Magento\Catalog\Api\Data\ProductRenderInterface; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Catalog\Helper\ImageFactory; use Magento\Catalog\Model\Product; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Ui\DataProvider\Product\Listing\Collector\Image; use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\DesignLoader; use Magento\Store\Model\StoreManagerInterface; -use Magento\Catalog\Helper\ImageFactory; -use Magento\Catalog\Api\Data\ProductRender\ImageInterface; -use Magento\Catalog\Helper\Image as ImageHelper; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -42,8 +43,21 @@ class ImageTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Api\Data\ProductRender\ImageInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $imageInterfaceFactory; + /** @var DesignLoader|\PHPUnit_Framework_MockObject_MockObject */ + private $designLoader; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @inheritdoc + */ public function setUp() { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->imageFactory = $this->getMockBuilder(ImageFactory::class) ->disableOriginalConstructor() ->getMock(); @@ -60,14 +74,21 @@ public function setUp() ->getMock(); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->design = $this->createMock(DesignInterface::class); - $this->model = new Image( - $this->imageFactory, - $this->state, - $this->storeManager, - $this->design, - $this->imageInterfaceFactory, - $this->imageCodes - ); + $this->designLoader = $this->createMock(DesignLoader::class); + + $this->model = $this->objectManager + ->getObject( + Image::class, + [ + 'imageFactory' => $this->imageFactory, + 'state' => $this->state, + 'storeManager' => $this->storeManager, + 'design' => $this->design, + 'imageRenderInfoFactory' => $this->imageInterfaceFactory, + 'imageCodes' => $this->imageCodes, + 'designLoader' => $this->designLoader, + ] + ); } public function testGet() @@ -165,6 +186,7 @@ public function testEmulateImageCreating() $imageMock->expects($this->once()) ->method('setUrl') ->with('url'); + $this->designLoader->expects($this->once())->method('load'); $this->assertEquals( $imageHelperMock, diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php index 6d7c8814bd474..35daac491d583 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php @@ -54,7 +54,16 @@ protected function setUp() ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) + ->setMethods([ + 'load', + 'getSelect', + 'getTable', + 'getIterator', + 'isLoaded', + 'toArray', + 'getSize', + 'setStoreId', + ]) ->getMockForAbstractClass(); $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php new file mode 100644 index 0000000000000..01221a8fbbb7f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php @@ -0,0 +1,185 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\ViewModel\Product; + +use Magento\Catalog\Helper\Data as CatalogHelper; +use Magento\Catalog\Model\Product; +use Magento\Catalog\ViewModel\Product\Breadcrumbs; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Unit test for Magento\Catalog\ViewModel\Product\Breadcrumbs. + */ +class BreadcrumbsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Breadcrumbs + */ + private $viewModel; + + /** + * @var CatalogHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $catalogHelper; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->catalogHelper = $this->getMockBuilder(CatalogHelper::class) + ->setMethods(['getProduct']) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->setMethods(['getValue', 'isSetFlag']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $escaper = $this->getObjectManager()->getObject(\Magento\Framework\Escaper::class); + + $this->viewModel = $this->getObjectManager()->getObject( + Breadcrumbs::class, + [ + 'catalogData' => $this->catalogHelper, + 'scopeConfig' => $this->scopeConfig, + 'escaper' => $escaper + ] + ); + } + + /** + * @return void + */ + public function testGetCategoryUrlSuffix() + { + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with('catalog/seo/category_url_suffix', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('.html'); + + $this->assertEquals('.html', $this->viewModel->getCategoryUrlSuffix()); + } + + /** + * @return void + */ + public function testIsCategoryUsedInProductUrl() + { + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with('catalog/seo/product_use_categories', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn(false); + + $this->assertFalse($this->viewModel->isCategoryUsedInProductUrl()); + } + + /** + * @dataProvider productDataProvider + * + * @param Product|null $product + * @param string $expectedName + * @return void + */ + public function testGetProductName($product, $expectedName) + { + $this->catalogHelper->expects($this->atLeastOnce()) + ->method('getProduct') + ->willReturn($product); + + $this->assertEquals($expectedName, $this->viewModel->getProductName()); + } + + /** + * @return array + */ + public function productDataProvider() + { + return [ + [$this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test']]), 'Test'], + [null, ''], + ]; + } + + /** + * @dataProvider productJsonEncodeDataProvider + * + * @param Product|null $product + * @param string $expectedJson + * @return void + */ + public function testGetJsonConfiguration($product, string $expectedJson) + { + $this->catalogHelper->expects($this->atLeastOnce()) + ->method('getProduct') + ->willReturn($product); + + $this->scopeConfig->expects($this->any()) + ->method('isSetFlag') + ->with('catalog/seo/product_use_categories', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn(false); + + $this->scopeConfig->expects($this->any()) + ->method('getValue') + ->with('catalog/seo/category_url_suffix', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('."html'); + + $this->assertEquals($expectedJson, $this->viewModel->getJsonConfiguration()); + } + + /** + * @return array + */ + public function productJsonEncodeDataProvider() + { + return [ + [ + $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test ™']]), + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test \u2122"}}', + ], + [ + $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test "']]), + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test ""}}', + ], + [ + $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test <b>x</b>']]), + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":' + . '"Test <b>x<\/b>"}}', + ], + [ + $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test \'abc\'']]), + '{"breadcrumbs":' + . '{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test 'abc'"}}' + ], + ]; + } + + /** + * @return ObjectManager + */ + private function getObjectManager() + { + if (null === $this->objectManager) { + $this->objectManager = new ObjectManager($this); + } + + return $this->objectManager; + } +} diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index cbc67fee8a5a3..fb580fad184ba 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** * @api * @since 100.0.2 @@ -54,19 +56,25 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ */ public function create($attribute, $context, array $config = []) { + $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); + $columnName = $attribute->getAttributeCode(); $config = array_merge([ 'label' => __($attribute->getDefaultFrontendLabel()), 'dataType' => $this->getDataType($attribute), 'add_field' => true, 'visible' => $attribute->getIsVisibleInGrid(), - 'filter' => ($attribute->getIsFilterableInGrid()) + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, + '__disableTmpl' => ['label' => true], ], $config); if ($attribute->usesSource()) { $config['options'] = $attribute->getSource()->getAllOptions(); + foreach ($config['options'] as &$optionData) { + $optionData['__disableTmpl'] = true; + } } $config['component'] = $this->getJsComponent($config['dataType']); diff --git a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php index fcc500c891607..dd8eaffb0a658 100644 --- a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php @@ -71,8 +71,6 @@ public function create($attribute, $context, $config = []) */ protected function getFilterType($attribute) { - return isset($this->filterMap[$attribute->getFrontendInput()]) - ? $this->filterMap[$attribute->getFrontendInput()] - : $this->filterMap['default']; + return $this->filterMap[$attribute->getFrontendInput()] ?? $this->filterMap['default']; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php index c96498b054d25..8ea6d8b9e5a06 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php @@ -80,6 +80,6 @@ public function prepare() */ protected function getFilterType($frontendInput) { - return isset($this->filterMap[$frontendInput]) ? $this->filterMap[$frontendInput] : $this->filterMap['default']; + return $this->filterMap[$frontendInput] ?? $this->filterMap['default']; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php index 0c4efa87c1a32..596b0f4118599 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/ProductActions.php @@ -60,6 +60,7 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Edit'), 'hidden' => false, + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php new file mode 100644 index 0000000000000..d969dba00de24 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/Component/Product/MassAction.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\Component\Product; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Element\UiComponentInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\AbstractComponent; + +/** + * Provide validation of allowed massaction for user. + */ +class MassAction extends AbstractComponent +{ + const NAME = 'massaction'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param AuthorizationInterface $authorization + * @param ContextInterface $context + * @param UiComponentInterface[] $components + * @param array $data + */ + public function __construct( + AuthorizationInterface $authorization, + ContextInterface $context, + array $components = [], + array $data = [] + ) { + $this->authorization = $authorization; + parent::__construct($context, $components, $data); + } + + /** + * @inheritdoc + */ + public function prepare() + { + $config = $this->getConfiguration(); + + foreach ($this->getChildComponents() as $actionComponent) { + $actionType = $actionComponent->getConfiguration()['type']; + if ($this->isActionAllowed($actionType)) { + $config['actions'][] = array_merge($actionComponent->getConfiguration(), ['__disableTmpl' => true]); + } + } + $origConfig = $this->getConfiguration(); + if ($origConfig !== $config) { + $config = array_replace_recursive($config, $origConfig); + } + + $this->setData('config', $config); + $this->components = []; + + parent::prepare(); + } + + /** + * @inheritdoc + */ + public function getComponentName() : string + { + return static::NAME; + } + + /** + * Check if the given type of action is allowed. + * + * @param string $actionType + * @return bool + */ + public function isActionAllowed(string $actionType) : bool + { + $isAllowed = true; + switch ($actionType) { + case 'delete': + case 'status': + $isAllowed = $this->authorization->isAllowed('Magento_Catalog::products'); + break; + case 'attributes': + $isAllowed = $this->authorization->isAllowed('Magento_Catalog::update_attributes'); + break; + default: + break; + } + + return $isAllowed; + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index a8378c364a63e..e3da613cb1634 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -7,14 +7,15 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Locator\LocatorInterface; -use Magento\Customer\Model\Customer\Source\GroupSourceInterface; -use Magento\Directory\Helper\Data; -use Magento\Framework\App\ObjectManager; -use Magento\Store\Model\StoreManagerInterface; use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Customer\Model\Customer\Source\GroupSourceInterface; +use Magento\Directory\Helper\Data; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Module\Manager as ModuleManager; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; use Magento\Ui\Component\Form\Element\DataType\Number; use Magento\Ui\Component\Form\Element\DataType\Price; @@ -23,7 +24,6 @@ use Magento\Ui\Component\Form\Element\Select; use Magento\Ui\Component\Form\Field; use Magento\Ui\Component\Modal; -use Magento\Framework\Stdlib\ArrayManager; /** * Class AdvancedPricing @@ -139,7 +139,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -158,7 +159,8 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyData(array $data) @@ -381,6 +383,7 @@ private function addAdvancedPriceLink() ); $advancedPricingButton['arguments']['data']['config'] = [ + 'dataScope' => 'advanced_pricing_button', 'displayAsLink' => true, 'formElement' => Container::NAME, 'componentType' => Container::NAME, @@ -432,7 +435,8 @@ private function getTierPriceStructure($tierPricePath) 'dndConfig' => [ 'enabled' => false, ], - 'disabled' => false, + 'disabled' => + $this->arrayManager->get($tierPricePath . '/arguments/data/config/disabled', $this->meta), 'required' => false, 'sortOrder' => $this->arrayManager->get($tierPricePath . '/arguments/data/config/sortOrder', $this->meta), @@ -500,7 +504,8 @@ private function getTierPriceStructure($tierPricePath) 'validation' => [ 'required-entry' => true, 'validate-greater-than-zero' => true, - 'validate-digits' => true, + 'validate-digits' => false, + 'validate-number' => true, ], ], ], @@ -622,7 +627,7 @@ private function customizeAdvancedPricing() 'componentType' => Modal::NAME, 'dataScope' => '', 'provider' => 'product_form.product_form_data_source', - 'onCancel' => 'actionDone', + 'onCancel' => 'closeModal', 'options' => [ 'title' => __('Advanced Pricing'), 'buttons' => [ diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php index a1aacc91f2e47..474b139810267 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php @@ -78,7 +78,13 @@ public function getOptions() \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::SORT_ORDER_ASC ); - return $collection->getData(); + $collectionData = $collection->getData() ?? []; + + array_walk($collectionData, function (&$attribute) { + $attribute['__disableTmpl'] = true; + }); + + return $collectionData; } /** @@ -108,6 +114,7 @@ public function modifyMeta(array $meta) self::ATTRIBUTE_SET_FIELD_ORDER ), 'multiple' => false, + 'disabled' => $this->locator->getProduct()->isLockedAttribute('attribute_set_id'), ]; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php index aec6549f400fc..a6b9856a4a0ed 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Attributes.php @@ -67,7 +67,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyData(array $data) @@ -76,6 +77,8 @@ public function modifyData(array $data) } /** + * Check if can add attributes on product form. + * * @return boolean */ private function canAddAttributes() @@ -89,7 +92,8 @@ private function canAddAttributes() } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -111,6 +115,8 @@ public function modifyMeta(array $meta) } /** + * Modify meta customize attribute modal. + * * @param array $meta * @return array */ @@ -182,6 +188,11 @@ private function customizeAddAttributeModal(array $meta) . '.create_new_attribute_modal', 'actionName' => 'toggleModal', ], + [ + 'targetName' => 'product_form.product_form.add_attribute_modal' + . '.create_new_attribute_modal.product_attribute_add_form', + 'actionName' => 'destroyInserted' + ], [ 'targetName' => 'product_form.product_form.add_attribute_modal' @@ -202,6 +213,8 @@ private function customizeAddAttributeModal(array $meta) } /** + * Modify meta to customize create attribute modal. + * * @param array $meta * @return array */ @@ -284,6 +297,8 @@ private function customizeCreateAttributeModal(array $meta) } /** + * Modify meta to customize attribute grid. + * * @param array $meta * @return array */ @@ -304,7 +319,7 @@ private function customizeAttributesGrid(array $meta) 'immediateUpdateBySelection' => true, 'behaviourType' => 'edit', 'externalFilterMode' => true, - 'dataLinks' => ['imports' => false, 'exports' => true], + 'dataLinks' => ['imports' => false, 'exports' => false], 'formProvider' => 'ns = ${ $.namespace }, index = product_form', 'groupCode' => static::GROUP_CODE, 'groupName' => static::GROUP_NAME, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 7456c1bfef91f..2a01de46e6238 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Model\Locator\LocatorInterface; @@ -11,6 +13,7 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -202,6 +205,7 @@ protected function createNewCategoryModal(array $meta) * * @param array $meta * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function customizeCategoriesField(array $meta) @@ -228,6 +232,7 @@ protected function customizeCategoriesField(array $meta) 'componentType' => 'container', 'component' => 'Magento_Ui/js/form/components/group', 'scopeLabel' => __('[GLOBAL]'), + 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ], ], ], @@ -288,6 +293,7 @@ protected function customizeCategoriesField(array $meta) 'source' => 'product_details', 'displayArea' => 'insideGroup', 'sortOrder' => 20, + 'dataScope' => $fieldCode, ], ], ] @@ -304,20 +310,64 @@ protected function customizeCategoriesField(array $meta) * * @param string|null $filter * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function getCategoriesTree($filter = null) { - $categoryTree = $this->getCacheManager()->load(self::CATEGORY_TREE_ID . '_' . $filter); - if ($categoryTree) { - return $this->serializer->unserialize($categoryTree); + $storeId = (int) $this->locator->getStore()->getId(); + + $cachedCategoriesTree = $this->getCacheManager() + ->load($this->getCategoriesTreeCacheId($storeId, (string) $filter)); + if (!empty($cachedCategoriesTree)) { + return $this->serializer->unserialize($cachedCategoriesTree); } - $storeId = $this->locator->getStore()->getId(); + $categoriesTree = $this->retrieveCategoriesTree( + $storeId, + $this->retrieveShownCategoriesIds($storeId, (string) $filter) + ); + + $this->getCacheManager()->save( + $this->serializer->serialize($categoriesTree), + $this->getCategoriesTreeCacheId($storeId, (string) $filter), + [ + \Magento\Catalog\Model\Category::CACHE_TAG, + \Magento\Framework\App\Cache\Type\Block::CACHE_TAG + ] + ); + + return $categoriesTree; + } + + /** + * Get cache id for categories tree. + * + * @param int $storeId + * @param string $filter + * @return string + */ + private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + { + return self::CATEGORY_TREE_ID + . '_' . (string) $storeId + . '_' . $filter; + } + + /** + * Retrieve filtered list of categories id. + * + * @param int $storeId + * @param string $filter + * @return array + * @throws LocalizedException + */ + private function retrieveShownCategoriesIds(int $storeId, string $filter = '') : array + { /* @var $matchingNamesCollection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $matchingNamesCollection = $this->categoryCollectionFactory->create(); - if ($filter !== null) { + if (!empty($filter)) { $matchingNamesCollection->addAttributeToFilter( 'name', ['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])] @@ -337,6 +387,19 @@ protected function getCategoriesTree($filter = null) } } + return $shownCategoriesIds; + } + + /** + * Retrieve tree of categories with attributes. + * + * @param int $storeId + * @param array $shownCategoriesIds + * @return array|null + * @throws LocalizedException + */ + private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) + { /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $collection = $this->categoryCollectionFactory->create(); @@ -360,18 +423,10 @@ protected function getCategoriesTree($filter = null) $categoryById[$category->getId()]['is_active'] = $category->getIsActive(); $categoryById[$category->getId()]['label'] = $category->getName(); + $categoryById[$category->getId()]['__disableTmpl'] = true; $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; } - $this->getCacheManager()->save( - $this->serializer->serialize($categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']), - self::CATEGORY_TREE_ID . '_' . $filter, - [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Framework\App\Cache\Type\Block::CACHE_TAG - ] - ); - return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']; } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 73fecd17c69ce..cf65c2ff2b206 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Ui\Component\Form\Element\Hidden; use Magento\Ui\Component\Modal; use Magento\Ui\Component\Container; use Magento\Ui\Component\DynamicRows; @@ -348,7 +349,8 @@ protected function getHeaderContainerConfig($sortOrder) 'sortOrder' => 20, 'actions' => [ [ - 'targetName' => 'ns = ${ $.ns }, index = ' . static::GRID_OPTIONS_NAME, + 'targetName' => '${ $.ns }.${ $.ns }.' . static::GROUP_CUSTOM_OPTIONS_NAME + . '.' . static::GRID_OPTIONS_NAME, 'actionName' => 'processingAddChild', ] ] @@ -866,10 +868,9 @@ protected function getPositionFieldConfig($sortOrder) 'data' => [ 'config' => [ 'componentType' => Field::NAME, - 'formElement' => Input::NAME, + 'formElement' => Hidden::NAME, 'dataScope' => static::FIELD_SORT_ORDER_NAME, 'dataType' => Number::NAME, - 'visible' => false, 'sortOrder' => $sortOrder, ], ], @@ -922,7 +923,7 @@ protected function getPriceFieldConfig($sortOrder) 'addbeforePool' => $this->productOptionsPrice->prefixesToOptionArray(), 'sortOrder' => $sortOrder, 'validation' => [ - 'validate-zero-or-greater' => true + 'validate-number' => true ], ], ], @@ -1045,6 +1046,7 @@ protected function getFileExtensionFieldConfig($sortOrder) 'data' => [ 'config' => [ 'label' => __('Compatible File Extensions'), + 'notice' => __('Enter separated extensions, like: png, jpg, gif.'), 'componentType' => Field::NAME, 'formElement' => Input::NAME, 'dataScope' => static::FIELD_FILE_EXTENSION_NAME, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 970d485267ca0..17ceaa76d317c 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -17,11 +17,14 @@ use Magento\Eav\Api\Data\AttributeGroupInterface; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\Filter\Translit; use Magento\Framework\Locale\CurrencyInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -31,6 +34,8 @@ use Magento\Ui\Component\Form\Fieldset; use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; +use \Magento\Catalog\Model\Product\Type as ProductType; /** * Class Eav @@ -39,6 +44,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 101.0.0 */ class Eav extends AbstractModifier @@ -187,6 +193,38 @@ class Eav extends AbstractModifier */ private $localeCurrency; + /** + * internal cache for attribute models + * @var array + */ + private $attributesCache = []; + + /** + * @var AttributeCollectionFactory + */ + private $attributeCollectionFactory; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Product design attribute codes. + * + * @var array + */ + private $designAttributeCodes = [ + 'custom_design', + 'page_layout', + 'options_container', + 'custom_layout_update', + 'custom_design_from', + 'custom_design_to', + 'custom_layout', + 'custom_layout_update_file' + ]; + /** * @param LocatorInterface $locator * @param CatalogEavValidationRules $catalogEavValidationRules @@ -207,6 +245,8 @@ class Eav extends AbstractModifier * @param DataPersistorInterface $dataPersistor * @param array $attributesToDisable * @param array $attributesToEliminate + * @param AttributeCollectionFactory $attributeCollectionFactory + * @param AuthorizationInterface|null $authorization * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -228,7 +268,9 @@ public function __construct( ScopeOverriddenValue $scopeOverriddenValue, DataPersistorInterface $dataPersistor, $attributesToDisable = [], - $attributesToEliminate = [] + $attributesToEliminate = [], + AttributeCollectionFactory $attributeCollectionFactory = null, + AuthorizationInterface $authorization = null ) { $this->locator = $locator; $this->catalogEavValidationRules = $catalogEavValidationRules; @@ -249,6 +291,9 @@ public function __construct( $this->dataPersistor = $dataPersistor; $this->attributesToDisable = $attributesToDisable; $this->attributesToEliminate = $attributesToEliminate; + $this->attributeCollectionFactory = $attributeCollectionFactory + ?: ObjectManager::getInstance()->get(AttributeCollectionFactory::class); + $this->authorization = $authorization ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); } /** @@ -265,7 +310,7 @@ public function modifyMeta(array $meta) if ($attributes) { $meta[$groupCode]['children'] = $this->getAttributesMeta($attributes, $groupCode); $meta[$groupCode]['arguments']['data']['config']['componentType'] = Fieldset::NAME; - $meta[$groupCode]['arguments']['data']['config']['label'] = __('%1', $group->getAttributeGroupName()); + $meta[$groupCode]['arguments']['data']['config']['label'] = __($group->getAttributeGroupName()); $meta[$groupCode]['arguments']['data']['config']['collapsible'] = true; $meta[$groupCode]['arguments']['data']['config']['dataScope'] = self::DATA_SCOPE_PRODUCT; $meta[$groupCode]['arguments']['data']['config']['sortOrder'] = @@ -381,7 +426,7 @@ public function modifyData(array $data) foreach ($attributes as $attribute) { if (null !== ($attributeValue = $this->setupAttributeData($attribute))) { - if ($attribute->getFrontendInput() === 'price' && is_scalar($attributeValue)) { + if ($this->isPriceAttribute($attribute, $attributeValue)) { $attributeValue = $this->formatPrice($attributeValue); } $data[$productId][self::DATA_SOURCE_DEFAULT][$attribute->getAttributeCode()] = $attributeValue; @@ -392,6 +437,32 @@ public function modifyData(array $data) return $data; } + /** + * Obtain if given attribute is a price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @param string|integer $attributeValue + * @return bool + */ + private function isPriceAttribute(ProductAttributeInterface $attribute, $attributeValue) + { + return $attribute->getFrontendInput() === 'price' + && is_scalar($attributeValue) + && !$this->isBundleSpecialPrice($attribute); + } + + /** + * Obtain if current product is bundle and given attribute is special_price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return bool + */ + private function isBundleSpecialPrice(ProductAttributeInterface $attribute) + { + return $this->locator->getProduct()->getTypeId() === ProductType::TYPE_BUNDLE + && $attribute->getAttributeCode() === ProductAttributeInterface::CODE_SPECIAL_PRICE; + } + /** * Resolve data persistence * @@ -485,39 +556,59 @@ private function getAttributeSetId() private function getAttributes() { if (!$this->attributes) { - foreach ($this->getGroups() as $group) { - $this->attributes[$this->calculateGroupCode($group)] = $this->loadAttributes($group); - } + $this->attributes = $this->loadAttributesForGroups($this->getGroups()); } return $this->attributes; } /** - * Loading product attributes from group + * Loads attributes for specified groups at once * - * @param AttributeGroupInterface $group - * @return ProductAttributeInterface[] + * @param AttributeGroupInterface[] ...$groups + * @return @return ProductAttributeInterface[] */ - private function loadAttributes(AttributeGroupInterface $group) + private function loadAttributesForGroups(array $groups) { $attributes = []; + $groupIds = []; + + foreach ($groups as $group) { + $groupIds[$group->getAttributeGroupId()] = $this->calculateGroupCode($group); + $attributes[$this->calculateGroupCode($group)] = []; + } + + $collection = $this->attributeCollectionFactory->create(); + $collection->setAttributeGroupFilter(array_keys($groupIds)); + + $mapAttributeToGroup = []; + + foreach ($collection->getItems() as $attribute) { + $mapAttributeToGroup[$attribute->getAttributeId()] = $attribute->getAttributeGroupId(); + } + $sortOrder = $this->sortOrderBuilder ->setField('sort_order') ->setAscendingDirection() ->create(); + $searchCriteria = $this->searchCriteriaBuilder - ->addFilter(AttributeGroupInterface::GROUP_ID, $group->getAttributeGroupId()) + ->addFilter(AttributeGroupInterface::GROUP_ID, array_keys($groupIds), 'in') ->addFilter(ProductAttributeInterface::IS_VISIBLE, 1) ->addSortOrder($sortOrder) ->create(); + $groupAttributes = $this->attributeRepository->getList($searchCriteria)->getItems(); + $productType = $this->getProductType(); + foreach ($groupAttributes as $attribute) { $applyTo = $attribute->getApplyTo(); $isRelated = !$applyTo || in_array($productType, $applyTo); if ($isRelated) { - $attributes[] = $attribute; + $attributeGroupId = $mapAttributeToGroup[$attribute->getAttributeId()]; + $attributeGroupCode = $groupIds[$attributeGroupId]; + $attributes[$attributeGroupCode][] = $attribute; } } @@ -553,7 +644,7 @@ private function getPreviousSetAttributes() */ private function isProductExists() { - return (bool) $this->locator->getProduct()->getId(); + return (bool)$this->locator->getProduct()->getId(); } /** @@ -572,7 +663,7 @@ private function isProductExists() public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder) { $configPath = ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER); - + $attributeCode = $attribute->getAttributeCode(); $meta = $this->arrayManager->set($configPath, [], [ 'dataType' => $attribute->getFrontendInput(), 'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()), @@ -581,18 +672,29 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC 'notice' => $attribute->getNote(), 'default' => (!$this->isProductExists()) ? $attribute->getDefaultValue() : null, 'label' => $attribute->getDefaultFrontendLabel(), - 'code' => $attribute->getAttributeCode(), + 'code' => $attributeCode, 'source' => $groupCode, 'scopeLabel' => $this->getScopeLabel($attribute), 'globalScope' => $this->isScopeGlobal($attribute), 'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER, + '__disableTmpl' => ['label' => true, 'code' => true] ]); + $product = $this->locator->getProduct(); // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { + $source = $attributeModel->getSource(); + if ($source instanceof SpecificSourceInterface) { + $options = $source->getOptionsFor($product); + } else { + $options = $source->getAllOptions(true, true); + } + foreach ($options as &$option) { + $option['__disableTmpl'] = true; + } $meta = $this->arrayManager->merge($configPath, $meta, [ - 'options' => $attributeModel->getSource()->getAllOptions(), + 'options' => $this->convertOptionsValueToString($options), ]); } @@ -610,7 +712,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC ]); } - if (in_array($attribute->getAttributeCode(), $this->attributesToDisable)) { + if (in_array($attributeCode, $this->attributesToDisable) || $product->isLockedAttribute($attributeCode)) { $meta = $this->arrayManager->merge($configPath, $meta, [ 'disabled' => true, ]); @@ -642,9 +744,59 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC break; } + $meta = $this->disableInaccessibleAttribute($attribute, $configPath, $meta); + + return $meta; + } + + /** + * Disable inaccessible attributes. + * + * @param ProductAttributeInterface $attribute + * @param string $configPath + * @param array $meta + * @return array Updated meta. + */ + private function disableInaccessibleAttribute( + ProductAttributeInterface $attribute, + string $configPath, + array $meta + ): array { + if (in_array($attribute->getAttributeCode(), $this->designAttributeCodes, true)) { + //Checking access to design configurations + if (!$this->authorization->isAllowed('Magento_Catalog::edit_product_design')) { + return $this->arrayManager->merge( + $configPath, + $meta, + [ + 'disabled' => true, + 'validation' => ['required' => false], + 'required' => false, + 'serviceDisabled' => true, + ] + ); + } + } + return $meta; } + /** + * Convert options value to string + * + * @param array $options + * @return array + */ + private function convertOptionsValueToString(array $options): array + { + array_walk($options, function (&$value) { + if (isset($value['value']) && is_scalar($value['value'])) { + $value['value'] = (string)$value['value']; + } + }); + return $options; + } + /** * @param ProductAttributeInterface $attribute * @param array $meta @@ -687,6 +839,7 @@ public function setupAttributeContainerMeta(ProductAttributeInterface $attribute 'breakLine' => false, 'label' => $attribute->getDefaultFrontendLabel(), 'required' => $attribute->getIsRequired(), + '__disableTmpl' => ['label' => true] ] ); @@ -800,7 +953,7 @@ private function getFormElementsMapValue($value) { $valueMap = $this->formElementMapper->getMappings(); - return isset($valueMap[$value]) ? $valueMap[$value] : $value; + return $valueMap[$value] ?? $value; } /** @@ -854,6 +1007,9 @@ private function canDisplayUseDefault(ProductAttributeInterface $attribute) $attributeCode = $attribute->getAttributeCode(); /** @var Product $product */ $product = $this->locator->getProduct(); + if ($product->isLockedAttribute($attributeCode)) { + return false; + } if (isset($this->canDisplayUseDefault[$attributeCode])) { return $this->canDisplayUseDefault[$attributeCode]; @@ -888,7 +1044,13 @@ private function isScopeGlobal($attribute) */ private function getAttributeModel($attribute) { - return $this->eavAttributeFactory->create()->load($attribute->getAttributeId()); + $attributeId = $attribute->getAttributeId(); + + if (!array_key_exists($attributeId, $this->attributesCache)) { + $this->attributesCache[$attributeId] = $this->eavAttributeFactory->create()->load($attributeId); + } + + return $this->attributesCache[$attributeId]; } /** diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index ea69ebf4dda24..f745250826cfb 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Ui\Component\Form; use Magento\Framework\Stdlib\ArrayManager; @@ -35,16 +36,25 @@ class General extends AbstractModifier */ private $localeCurrency; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + /** * @param LocatorInterface $locator * @param ArrayManager $arrayManager + * @param AttributeRepositoryInterface|null $attributeRepository */ public function __construct( LocatorInterface $locator, - ArrayManager $arrayManager + ArrayManager $arrayManager, + AttributeRepositoryInterface $attributeRepository = null ) { $this->locator = $locator; $this->arrayManager = $arrayManager; + $this->attributeRepository = $attributeRepository + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeRepositoryInterface::class); } /** @@ -58,7 +68,12 @@ public function modifyData(array $data) $modelId = $this->locator->getProduct()->getId(); if (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { - $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = '1'; + $attributeStatus = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ); + $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = + $attributeStatus->getDefaultValue() ?: 1; } return $data; @@ -106,7 +121,7 @@ protected function customizeAdvancedPriceFormat(array $data) $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE] = $this->formatPrice($value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE]); $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY] = - (int)$value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY]; + (float) $value[ProductAttributeInterface::CODE_TIER_PRICE_FIELD_PRICE_QTY]; } } @@ -187,7 +202,7 @@ protected function customizeStatusField(array $meta) protected function customizeWeightField(array $meta) { $weightPath = $this->arrayManager->findPath(ProductAttributeInterface::CODE_WEIGHT, $meta, null, 'children'); - + $disabled = $this->arrayManager->get($weightPath . '/arguments/data/config/disabled', $meta); if ($weightPath) { $meta = $this->arrayManager->merge( $weightPath . static::META_CONFIG_PATH, @@ -199,7 +214,7 @@ protected function customizeWeightField(array $meta) ], 'additionalClasses' => 'admin__field-small', 'addafter' => $this->locator->getStore()->getConfig('general/locale/weight_unit'), - 'imports' => [ + 'imports' => $disabled ? [] : [ 'disabled' => '!${$.provider}:' . self::DATA_SCOPE_PRODUCT . '.product_has_weight:value' ] @@ -239,6 +254,7 @@ protected function customizeWeightField(array $meta) ], ], 'value' => (int)$this->locator->getProduct()->getTypeInstance()->hasWeight(), + 'disabled' => $disabled, ] ); } @@ -264,23 +280,34 @@ protected function customizeNewDateRangeField(array $meta) if ($fromFieldPath && $toFieldPath) { $fromContainerPath = $this->arrayManager->slicePath($fromFieldPath, 0, -2); $toContainerPath = $this->arrayManager->slicePath($toFieldPath, 0, -2); + $commonFieldsMeta = [ + 'options' => [ + 'showsTime' => true, + ] + ]; $meta = $this->arrayManager->merge( $fromFieldPath . self::META_CONFIG_PATH, $meta, - [ - 'label' => __('Set Product as New From'), - 'additionalClasses' => 'admin__field-date', - ] + array_merge( + [ + 'label' => __('Set Product as New From'), + 'additionalClasses' => 'admin__field-date', + ], + $commonFieldsMeta + ) ); $meta = $this->arrayManager->merge( $toFieldPath . self::META_CONFIG_PATH, $meta, - [ - 'label' => __('To'), - 'scopeLabel' => null, - 'additionalClasses' => 'admin__field-date', - ] + array_merge( + [ + 'label' => __('To'), + 'scopeLabel' => null, + 'additionalClasses' => 'admin__field-date', + ], + $commonFieldsMeta + ) ); $meta = $this->arrayManager->merge( $fromContainerPath . self::META_CONFIG_PATH, @@ -332,8 +359,10 @@ protected function customizeNameListeners(array $meta) 'allowImport' => !$this->locator->getProduct()->getId(), ]; - if (!in_array($listener, $textListeners)) { - $importsConfig['elementTmpl'] = 'ui/form/element/input'; + if (in_array($listener, $textListeners)) { + $importsConfig['cols'] = 15; + $importsConfig['rows'] = 2; + $importsConfig['elementTmpl'] = 'ui/form/element/textarea'; } $meta = $this->arrayManager->merge($listenerPath . static::META_CONFIG_PATH, $meta, $importsConfig); @@ -344,7 +373,8 @@ protected function customizeNameListeners(array $meta) $skuPath . static::META_CONFIG_PATH, $meta, [ - 'autoImportIfEmpty' => true + 'autoImportIfEmpty' => true, + 'validation' => ['no-marginal-whitespace' => true] ] ); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php new file mode 100644 index 0000000000000..453be0c1a1582 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; + +/** + * Additional logic on how to display the layout update field. + */ +class LayoutUpdate implements ModifierInterface +{ + /** + * @var LocatorInterface + */ + private $locator; + + /** + * @param LocatorInterface $locator + */ + public function __construct(LocatorInterface $locator) + { + $this->locator = $locator; + } + + /** + * Extract custom layout value. + * + * @param ProductInterface|Product $product + * @return mixed + */ + private function extractLayoutUpdate(ProductInterface $product) + { + if ($product instanceof Product && !$product->hasData(Product::CUSTOM_ATTRIBUTES)) { + return $product->getData('custom_layout_update'); + } + + $attr = $product->getCustomAttribute('custom_layout_update'); + + return $attr ? $attr->getValue() : null; + } + + /** + * @inheritdoc + * @since 101.1.0 + */ + public function modifyData(array $data) + { + $product = $this->locator->getProduct(); + if ($this->extractLayoutUpdate($product)) { + $data[$product->getId()][AbstractModifier::DATA_SOURCE_DEFAULT]['custom_layout_update_file'] + = \Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate::VALUE_USE_UPDATE_XML; + } + + return $data; + } + + /** + * @inheritDoc + */ + public function modifyMeta(array $meta) + { + return $meta; + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index 0eddca3322205..9111bf544d52a 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -45,7 +45,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ public function modifyData(array $data) @@ -54,8 +54,11 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * Add tier price info to meta array. + * * @since 101.1.0 + * @param array $meta + * @return array */ public function modifyMeta(array $meta) { @@ -112,7 +115,7 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'component' => 'Magento_Ui/js/form/components/group', 'label' => __('Price'), - 'enableLabel' => true, + 'showLabel' => false, 'dataScope' => '', 'additionalClasses' => 'control-grouped', 'sortOrder' => isset($priceMeta['arguments']['data']['config']['sortOrder']) @@ -150,8 +153,8 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'addbefore' => '%', 'validation' => [ - 'validate-number' => true, - 'less-than-equals-to' => 100 + 'required-entry' => true, + 'validate-positive-percent-decimal' => true, ], 'visible' => $firstOption && $firstOption['value'] == ProductPriceOptionsInterface::VALUE_PERCENT, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php index 298da3d5cd6f2..a71f509727c81 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php @@ -135,7 +135,6 @@ public function modifyMeta(array $meta) 'collapsible' => true, 'componentType' => Form\Fieldset::NAME, 'dataScope' => self::DATA_SCOPE_PRODUCT, - 'disabled' => false, 'sortOrder' => $this->getNextGroupSortOrder( $meta, 'search-engine-optimization', @@ -176,9 +175,11 @@ protected function getFieldsForFieldset() $label = __('Websites'); $defaultWebsiteId = $this->websiteRepository->getDefault()->getId(); + $isOnlyOneWebsiteAvailable = count($websitesList) === 1; foreach ($websitesList as $website) { $isChecked = in_array($website['id'], $websiteIds) - || ($defaultWebsiteId == $website['id'] && $isNewProduct); + || ($defaultWebsiteId == $website['id'] && $isNewProduct) + || $isOnlyOneWebsiteAvailable; $children[$website['id']] = [ 'arguments' => [ 'data' => [ @@ -187,6 +188,7 @@ protected function getFieldsForFieldset() 'componentType' => Form\Field::NAME, 'formElement' => Form\Element\Checkbox::NAME, 'description' => __($website['name']), + '__disableTmpl' => true, 'tooltip' => $tooltip, 'sortOrder' => $sortOrder, 'dataScope' => 'website_ids.' . $website['id'], @@ -196,6 +198,7 @@ protected function getFieldsForFieldset() 'false' => '0', ], 'value' => $isChecked ? (string)$website['id'] : '0', + 'disabled' => $this->locator->getProduct()->isLockedAttribute('website_ids'), ], ], ], @@ -351,18 +354,21 @@ protected function getWebsitesOptionsList() $websiteOption = [ 'value' => '0.' . $website['id'], 'label' => __($website['name']), + '__disableTmpl' => true, ]; $groupOptions = []; foreach ($website['groups'] as $group) { $groupOption = [ 'value' => '0.' . $website['id'] . '.' . $group['id'], 'label' => __($group['name']), + '__disableTmpl' => true, ]; $storeViewOptions = []; foreach ($group['stores'] as $storeView) { $storeViewOptions[] = [ 'value' => $storeView['id'], 'label' => __($storeView['name']), + '__disableTmpl' => true, ]; } if (!empty($storeViewOptions)) { @@ -397,8 +403,9 @@ protected function getWebsitesList() $this->websitesList = []; $groupList = $this->groupRepository->getList(); $storesList = $this->storeRepository->getList(); + $websiteList = $this->storeManager->getWebsites(true); - foreach ($this->websiteRepository->getList() as $website) { + foreach ($websiteList as $website) { $websiteId = $website->getId(); if (!$websiteId) { continue; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 2fc9ef76aa00d..34879ab29c185 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -11,17 +11,20 @@ use Magento\Catalog\Api\Data\ProductRender\ImageInterfaceFactory; use Magento\Catalog\Api\Data\ProductRenderInterface; use Magento\Catalog\Helper\ImageFactory; +use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException; use Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorInterface; use Magento\Framework\App\State; use Magento\Framework\View\DesignInterface; use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\View\DesignLoader; /** * Collect enough information about image rendering on front * If you want to add new image, that should render on front you need * to configure this class in di.xml * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image implements ProductRenderCollectorInterface { @@ -58,6 +61,11 @@ class Image implements ProductRenderCollectorInterface */ private $imageRenderInfoFactory; + /** + * @var DesignLoader + */ + private $designLoader; + /** * Image constructor. * @param ImageFactory $imageFactory @@ -66,6 +74,7 @@ class Image implements ProductRenderCollectorInterface * @param DesignInterface $design * @param ImageInterfaceFactory $imageRenderInfoFactory * @param array $imageCodes + * @param DesignLoader|null $designLoader */ public function __construct( ImageFactory $imageFactory, @@ -73,7 +82,8 @@ public function __construct( StoreManagerInterface $storeManager, DesignInterface $design, ImageInterfaceFactory $imageRenderInfoFactory, - array $imageCodes = [] + array $imageCodes = [], + DesignLoader $designLoader = null ) { $this->imageFactory = $imageFactory; $this->imageCodes = $imageCodes; @@ -81,6 +91,8 @@ public function __construct( $this->storeManager = $storeManager; $this->design = $design; $this->imageRenderInfoFactory = $imageRenderInfoFactory; + $this->designLoader = $designLoader ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(DesignLoader::class); } /** @@ -102,7 +114,12 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ [$this, "emulateImageCreating"], [$product, $imageCode, (int) $productRender->getStoreId(), $image] ); - $resizedInfo = $helper->getResizedImageInfo(); + + try { + $resizedInfo = $helper->getResizedImageInfo(); + } catch (NotLoadInfoImageException $exception) { + $resizedInfo = [$helper->getWidth(), $helper->getHeight()]; + } $image->setCode($imageCode); $image->setHeight($helper->getHeight()); @@ -118,6 +135,8 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ } /** + * Callback for emulating image creation. + * * Callback in which we emulate initialize default design theme, depends on current store, be settings store id * from render info * @@ -130,7 +149,7 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ public function emulateImageCreating(ProductInterface $product, $imageCode, $storeId, ImageInterface $image) { $this->storeManager->setCurrentStore($storeId); - $this->design->setDefaultDesignTheme(); + $this->designLoader->load(); $imageHelper = $this->imageFactory->create(); $imageHelper->init($product, $imageCode); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php new file mode 100644 index 0000000000000..29a19036f3bf3 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Ui\DataProvider\Product; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Eav\Model\Entity\Attribute\AttributeInterface; + +/** + * Collection which is used for rendering product list in the backend. + * + * Used for product grid and customizes behavior of the default Product collection for grid needs. + */ +class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection +{ + /** + * Disables using of price index for grid rendering + * + * Admin area shouldn't use price index and should rely on actual product data instead. + * + * @codeCoverageIgnore + * @return \Magento\Catalog\Model\ResourceModel\Product\Collection + */ + protected function _productLimitationJoinPrice() + { + $this->_productLimitationFilters->setUsePriceIndex(false); + return $this->_productLimitationPrice(true); + } + + /** + * Add attribute filter to collection + * + * @param AttributeInterface|integer|string|array $attribute + * @param null|string|array $condition + * @param string $joinType + * @return $this + * @throws LocalizedException + */ + public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') + { + $storeId = (int)$this->getStoreId(); + if ($attribute === 'is_saleable' + || is_array($attribute) + || $storeId !== $this->getDefaultStoreId() + ) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + if ($attribute instanceof AttributeInterface) { + $attributeModel = $attribute; + } else { + $attributeModel = $this->getEntity()->getAttribute($attribute); + if ($attributeModel === false) { + throw new LocalizedException( + __('Invalid attribute identifier for filter (%1)', get_class($attribute)) + ); + } + } + + if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + $this->addAttributeToFilterAllStores($attributeModel, $condition); + + return $this; + } + + /** + * Add attribute to filter by all stores + * + * @param Attribute $attributeModel + * @param array $condition + * @return void + */ + private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition) + { + $tableName = $this->getTable($attributeModel->getBackendTable()); + $entity = $this->getEntity(); + $fKey = 'e.' . $this->getEntityPkName($entity); + $pKey = $tableName . '.' . $this->getEntityPkName($entity); + $condition = "({$pKey} = {$fKey}) AND (" + . $this->_getConditionSql("{$tableName}.value", $condition) + . ')'; + $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); + $this->getSelect()->exists($selectExistsInAllStores, $condition); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index e31eca89f63c2..44aa33367c6be 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Ui\DataProvider\Product; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Store\Model\Store; /** * Class ProductDataProvider @@ -58,6 +59,7 @@ public function __construct( $this->collection = $collectionFactory->create(); $this->addFieldStrategies = $addFieldStrategies; $this->addFilterStrategies = $addFilterStrategies; + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); } /** diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php new file mode 100644 index 0000000000000..2c4144753cdee --- /dev/null +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\ViewModel\Product; + +use Magento\Catalog\Helper\Data; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\Escaper; + +/** + * Product breadcrumbs view model. + */ +class Breadcrumbs extends DataObject implements ArgumentInterface +{ + /** + * Catalog data. + * + * @var Data + */ + private $catalogData; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @param Data $catalogData + * @param ScopeConfigInterface $scopeConfig + * @param Json|null $json + * @param Escaper|null $escaper + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __construct( + Data $catalogData, + ScopeConfigInterface $scopeConfig, + Json $json = null, + Escaper $escaper = null + ) { + parent::__construct(); + + $this->catalogData = $catalogData; + $this->scopeConfig = $scopeConfig; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + } + + /** + * Returns category URL suffix. + * + * @return mixed + */ + public function getCategoryUrlSuffix() + { + return $this->scopeConfig->getValue( + 'catalog/seo/category_url_suffix', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + + /** + * Checks if categories path is used for product URLs. + * + * @return bool + */ + public function isCategoryUsedInProductUrl() + { + return $this->scopeConfig->isSetFlag( + 'catalog/seo/product_use_categories', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + + /** + * Returns product name. + * + * @return string + */ + public function getProductName() + { + return $this->catalogData->getProduct() !== null + ? $this->catalogData->getProduct()->getName() + : ''; + } + + /** + * Returns breadcrumb json with html escaped names + * + * @return string + */ + public function getJsonConfigurationHtmlEscaped() + { + return json_encode( + [ + 'breadcrumbs' => [ + 'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()), + 'useCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), + 'product' => $this->escaper->escapeHtml($this->getProductName()) + ] + ], + JSON_HEX_TAG + ); + } + + /** + * Returns breadcrumb json. + * + * @return string + * @deprecated in favor of new method with name {suffix}Html{postfix}() + */ + public function getJsonConfiguration() + { + return $this->getJsonConfigurationHtmlEscaped(); + } +} diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php new file mode 100644 index 0000000000000..00bac7e61b5b4 --- /dev/null +++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\ViewModel\Product\Checker; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; + +/** + * Check is available add to compare. + */ +class AddToCompareAvailability implements ArgumentInterface +{ + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct(StockConfigurationInterface $stockConfiguration) + { + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Is product available for comparison. + * + * @param ProductInterface $product + * @return bool + */ + public function isAvailableForCompare(ProductInterface $product): bool + { + if ((int)$product->getStatus() !== Status::STATUS_DISABLED) { + return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); + } + + return false; + } + + /** + * Get is in stock status. + * + * @param ProductInterface $product + * @return bool + */ + private function isInStock(ProductInterface $product): bool + { + $quantityAndStockStatus = $product->getQuantityAndStockStatus(); + if (!$quantityAndStockStatus) { + return $product->isSalable(); + } + + return $quantityAndStockStatus['is_in_stock'] ?? false; + } +} diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index b76c6a548c39a..efd675cd6091c 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-cms": "102.0.*", @@ -34,7 +34,7 @@ "magento/module-catalog-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "102.0.2", + "version": "102.0.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Catalog/etc/acl.xml b/app/code/Magento/Catalog/etc/acl.xml index 358a798fc7579..10cd0862990f5 100644 --- a/app/code/Magento/Catalog/etc/acl.xml +++ b/app/code/Magento/Catalog/etc/acl.xml @@ -11,8 +11,13 @@ <resource id="Magento_Backend::admin"> <resource id="Magento_Catalog::catalog" title="Catalog" translate="title" sortOrder="30"> <resource id="Magento_Catalog::catalog_inventory" title="Inventory" translate="title" sortOrder="10"> - <resource id="Magento_Catalog::products" title="Products" translate="title" sortOrder="10" /> - <resource id="Magento_Catalog::categories" title="Categories" translate="title" sortOrder="20" /> + <resource id="Magento_Catalog::products" title="Products" translate="title" sortOrder="10"> + <resource id="Magento_Catalog::edit_product_design" title="Edit Product Design" translate="title" /> + <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" /> + </resource> + <resource id="Magento_Catalog::categories" title="Categories" translate="title" sortOrder="20"> + <resource id="Magento_Catalog::edit_category_design" title="Edit Category Design" translate="title" /> + </resource> </resource> </resource> <resource id="Magento_Backend::stores"> @@ -23,7 +28,6 @@ </resource> <resource id="Magento_Backend::stores_attributes"> <resource id="Magento_Catalog::attributes_attributes" title="Product" translate="title" sortOrder="30" /> - <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" sortOrder="35" /> <resource id="Magento_Catalog::sets" title="Attribute Set" translate="title" sortOrder="40"/> </resource> </resource> diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index b97e6fc1aa318..ab83d007344af 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -78,6 +78,11 @@ <type name="Magento\Catalog\Model\ResourceModel\Attribute"> <plugin name="invalidate_pagecache_after_attribute_save" type="Magento\Catalog\Plugin\Model\ResourceModel\Attribute\Save" /> </type> + <virtualType name="\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">\Magento\Catalog\Ui\DataProvider\Product\ProductCollection</argument> + </arguments> + </virtualType> <type name="Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider"> <arguments> <argument name="addFieldStrategies" xsi:type="array"> @@ -86,6 +91,7 @@ <argument name="addFilterStrategies" xsi:type="array"> <item name="store_id" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\AddStoreFieldToCollection</item> </argument> + <argument name="collectionFactory" xsi:type="object">\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory</argument> </arguments> </type> <type name="Magento\Catalog\Model\Product\Action"> @@ -151,6 +157,10 @@ <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\TierPrice</item> <item name="sortOrder" xsi:type="number">150</item> </item> + <item name="custom_layout_update" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\LayoutUpdate</item> + <item name="sortOrder" xsi:type="number">160</item> + </item> </argument> </arguments> </virtualType> @@ -184,4 +194,19 @@ <argument name="scopeOverriddenValue" xsi:type="object">Magento\Catalog\Model\Attribute\ScopeOverriddenValue</argument> </arguments> </type> + <type name="Magento\Eav\Api\AttributeSetRepositoryInterface"> + <plugin name="remove_products" type="Magento\Catalog\Plugin\Model\AttributeSetRepository\RemoveProducts"/> + </type> + <type name="Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Attributes"> + <arguments> + <argument name="excludeFields" xsi:type="array"> + <item name="0" xsi:type="string">category_ids</item> + <item name="1" xsi:type="string">gallery</item> + <item name="2" xsi:type="string">image</item> + <item name="3" xsi:type="string">media_gallery</item> + <item name="4" xsi:type="string">quantity_and_stock_status</item> + <item name="5" xsi:type="string">tier_price</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml index f4fd7fc30398c..ad83f5898237a 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/events.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml @@ -9,4 +9,7 @@ <event name="cms_wysiwyg_images_static_urls_allowed"> <observer name="catalog_wysiwyg" instance="Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver" /> </event> + <event name="catalog_category_change_products"> + <observer name="category_product_indexer" instance="Magento\Catalog\Observer\CategoryProductIndexer"/> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/menu.xml b/app/code/Magento/Catalog/etc/adminhtml/menu.xml index aa910e6d5ade4..cfcce3a26cbec 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/menu.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/menu.xml @@ -12,7 +12,6 @@ <add id="Magento_Catalog::catalog_categories" title="Categories" translate="title" module="Magento_Catalog" sortOrder="20" parent="Magento_Catalog::inventory" action="catalog/category/" resource="Magento_Catalog::categories"/> <add id="Magento_Catalog::catalog_attributes_attributes" title="Product" translate="title" module="Magento_Catalog" sortOrder="30" parent="Magento_Backend::stores_attributes" action="catalog/product_attribute/" resource="Magento_Catalog::attributes_attributes"/> <add id="Magento_Catalog::catalog_attributes_sets" title="Attribute Set" translate="title" module="Magento_Catalog" sortOrder="40" parent="Magento_Backend::stores_attributes" action="catalog/product_set/" resource="Magento_Catalog::sets"/> - <add id="Magento_Catalog::inventory" title="Inventory" translate="title" module="Magento_Catalog" sortOrder="10" parent="Magento_Catalog::catalog" dependsOnModule="Magento_Catalog" resource="Magento_Catalog::catalog"/> </menu> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index e42eab787e3fc..6a432c1809ba5 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -36,10 +36,10 @@ </group> <group id="recently_products" translate="label" type="text" sortOrder="350" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Recently Viewed/Compared Products</label> - <field id="recently_viewed_lifetime" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="recently_viewed_lifetime" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Lifetime of products in Recently Viewed Widget</label> </field> - <field id="recently_compared_lifetime" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="recently_compared_lifetime" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Lifetime of products in Recently Compared Widget</label> </field> <field id="synchronize_with_backend" translate="label" type="select" showInDefault="1" canRestore="1"> @@ -56,7 +56,7 @@ <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> @@ -66,7 +66,7 @@ <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> @@ -85,6 +85,7 @@ </field> <field id="default_sort_by" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Product Listing Sort by</label> + <comment>Applies to category pages</comment> <source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model> </field> <field id="list_allow_all" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -97,13 +98,17 @@ <comment>E.g. {{media url="path/to/image.jpg"}} {{skin url="path/to/picture.gif"}}. Dynamic directives parsing impacts catalog performance.</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="remember_pagination" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Remember Category Pagination</label> + <comment>Changing may affect SEO and cache storage consumption.</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="placeholder" translate="label" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Product Image Placeholders</label> <clone_fields>1</clone_fields> <clone_model>Magento\Catalog\Model\Config\CatalogClone\Media\Image</clone_model> <field id="placeholder" type="image" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> - <label></label> <backend_model>Magento\Config\Model\Config\Backend\Image</backend_model> <upload_dir config="system/filesystem/media" scope_info="1">catalog/product/placeholder</upload_dir> <base_url type="media" scope_info="1">catalog/product/placeholder</base_url> @@ -186,5 +191,19 @@ </field> </group> </section> + <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> + <class>separator-top</class> + <label>System</label> + <tab>advanced</tab> + <resource>Magento_Config::config_system</resource> + <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Images Upload Configuration</label> + <field id="jpeg_quality" translate="label comment" type="text" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Quality</label> + <validate>validate-digits validate-digits-range digits-range-1-100 required-entry</validate> + <comment>Jpeg quality for resized images 1-100%.</comment> + </field> + </group> + </section> </system> </config> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index 3569c0a27b83f..fe1c8e7b87a7a 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -30,6 +30,7 @@ <flat_catalog_category>0</flat_catalog_category> <default_sort_by>position</default_sort_by> <parse_url_directives>1</parse_url_directives> + <remember_pagination>0</remember_pagination> </frontend> <product> <flat> @@ -52,6 +53,11 @@ <forbidden_extensions>php,exe</forbidden_extensions> </custom_options> </catalog> + <indexer> + <catalog_product_price> + <dimensions_mode>none</dimensions_mode> + </catalog_product_price> + </indexer> <system> <media_storage_configuration> <allowed_resources> @@ -60,6 +66,9 @@ <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> </media_storage_configuration> + <upload_configuration> + <jpeg_quality>80</jpeg_quality> + </upload_configuration> </system> <design> <watermark> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 114d46f63fdd3..524eb49b1d007 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -70,6 +70,7 @@ <preference for="Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoInterface" type="Magento\Catalog\Model\ProductRender\FormattedPriceInfo" /> <preference for="Magento\Framework\Indexer\BatchProviderInterface" type="Magento\Framework\Indexer\BatchProvider" /> <preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" /> + <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> @@ -211,6 +212,12 @@ <item name="gif" xsi:type="string">gif</item> <item name="png" xsi:type="string">png</item> </argument> + <argument name="allowedMimeTypes" xsi:type="array"> + <item name="jpg" xsi:type="string">image/jpg</item> + <item name="jpeg" xsi:type="string">image/jpeg</item> + <item name="gif" xsi:type="string">image/gif</item> + <item name="png" xsi:type="string">image/png</item> + </argument> </arguments> </virtualType> <type name="Magento\Catalog\Controller\Adminhtml\Category\Image\Upload"> @@ -224,7 +231,8 @@ </arguments> </type> <type name="Magento\Store\Model\ResourceModel\Website"> - <plugin name="priceIndexerOnWebsiteDelete" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website"/> + <plugin name="invalidatePriceIndexerOnWebsite" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\Website"/> + <plugin name="categoryProductWebsiteAfterDelete" type="\Magento\Catalog\Model\Indexer\Category\Product\Plugin\Website"/> </type> <type name="Magento\Store\Model\ResourceModel\Store"> <plugin name="storeViewResourceAroundSave" type="Magento\Catalog\Model\Indexer\Category\Flat\Plugin\StoreView"/> @@ -356,6 +364,7 @@ <item name="base_price" xsi:type="string">Magento\Catalog\Pricing\Price\BasePrice</item> <item name="custom_option_price" xsi:type="string">Magento\Catalog\Pricing\Price\CustomOptionPrice</item> <item name="configured_price" xsi:type="string">Magento\Catalog\Pricing\Price\ConfiguredPrice</item> + <item name="configured_regular_price" xsi:type="string">Magento\Catalog\Pricing\Price\ConfiguredRegularPrice</item> </argument> </arguments> </virtualType> @@ -591,6 +600,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="custom_options" xsi:type="object">Magento\Catalog\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\Model\Entity\RepositoryFactory"> <arguments> <argument name="entities" xsi:type="array"> @@ -652,12 +668,14 @@ <item name="mediaGalleryCreate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\CreateHandler</item> <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> + <item name="tierPriceCreator" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler</item> </item> <item name="update" xsi:type="array"> <item name="optionUpdater" xsi:type="string">Magento\Catalog\Model\Product\Option\SaveHandler</item> <item name="mediaGalleryUpdate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\UpdateHandler</item> <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> + <item name="tierPriceUpdater" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler</item> </item> </item> </argument> @@ -809,6 +827,9 @@ <argument name="attributePersistor" xsi:type="object">Magento\Catalog\Model\ResourceModel\AttributePersistor</argument> </arguments> </virtualType> + <type name="Magento\Eav\Model\ResourceModel\ReadSnapshot"> + <plugin name="catalogReadSnapshot" type="Magento\Catalog\Plugin\Model\ResourceModel\ReadSnapshotPlugin" /> + </type> <type name="Magento\Framework\EntityManager\Operation\AttributePool"> <arguments> <argument name="extensionActions" xsi:type="array"> @@ -854,6 +875,7 @@ <argument name="customFilters" xsi:type="array"> <item name="category_id" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductCategoryFilter</item> <item name="store" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter</item> + <item name="store_id" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter</item> <item name="website_id" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductWebsiteFilter</item> </argument> </arguments> @@ -885,6 +907,14 @@ <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> <plugin name="copy_quote_files_to_order" type="Magento\Catalog\Model\Plugin\QuoteItemProductOption"/> </type> + <type name="Magento\Catalog\Model\ResourceModel\Category"> + <plugin name="remove_redundant_image" type="Magento\Catalog\Plugin\Model\ResourceModel\Category\RemoveRedundantImagePlugin"/> + </type> + <type name="Magento\Catalog\Plugin\Model\ResourceModel\Category\RemoveRedundantImagePlugin"> + <arguments> + <argument name="imageUploader" xsi:type="object">Magento\Catalog\CategoryImageUpload</argument> + </arguments> + </type> <preference for="Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface" type="Magento\Catalog\Model\ResourceModel\Product\CompositeWithWebsiteProcessor" /> <type name="Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor"> <arguments> @@ -903,6 +933,7 @@ <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice"> <arguments> <argument name="baseSelectProcessor" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor</argument> + <argument name="priceTableResolver" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver</argument> </arguments> </type> <type name="Magento\Catalog\Model\Product\Price\CostStorage"> @@ -1057,4 +1088,118 @@ <argument name="productRepository" xsi:type="object">Magento\Catalog\Api\ProductRepositoryInterface\Proxy</argument> </arguments> </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice"> + <arguments> + <argument name="connectionName" xsi:type="string">indexer</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\Factory"> + <arguments> + <argument name="eavAttributeConditionBuilder" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\EavAttributeCondition</argument> + <argument name="nativeAttributeConditionBuilder" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ConditionBuilder\NativeAttributeCondition</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory"> + <arguments> + <argument name="dimensionProviders" xsi:type="array"> + <!-- @see \Magento\Store\Model\Indexer\WebsiteDimensionProvider::DIMENSION_NAME --> + <item name="ws" xsi:type="object">Magento\Store\Model\Indexer\WebsiteDimensionProvider</item> + <!-- @see \Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider::DIMENSION_NAME --> + <item name="cg" xsi:type="object">Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"> + <arguments> + <argument name="priceTableResolver" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver\Proxy</argument> + <argument name="storeManager" xsi:type="object">Magento\Store\Model\StoreManagerInterface\Proxy</argument> + <argument name="context" xsi:type="object">Magento\Framework\App\Http\Context\Proxy</argument> + <!-- Unccomment after fix issue with Proxy generation --> + <!--<argument name="dimensionModeConfiguration" xsi:type="object">--> + <!--Magento\Catalog\Model\Indexer\Product\Price\DimensionModeConfiguration\Proxy--> + <!--</argument>--> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Layer\Filter\Price"> + <arguments> + <argument name="priceTableResolver" xsi:type="object"> + Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\CustomOptionPriceModifier"> + <arguments> + <argument name="tableStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer"> + <arguments> + <argument name="connectionName" xsi:type="string">indexer</argument> + <argument name="tableResolver" xsi:type="object">Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier"> + <arguments> + <argument name="priceModifiers" xsi:type="array"> + <item name="customOptionPriceModifier" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\CustomOptionPriceModifier</item> + </argument> + </arguments> + </type> + <virtualType name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\VirtualProductPrice" type="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\SimpleProductPrice"> + <arguments> + <argument name="productType" xsi:type="string">virtual</argument> + </arguments> + </virtualType> + <type name="Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand"> + <arguments> + <argument name="dimensionSwitchers" xsi:type="array"> + <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher</item> + </argument> + </arguments> + </type> + <type name="Magento\Indexer\Console\Command\IndexerShowDimensionsModeCommand"> + <arguments> + <argument name="indexers" xsi:type="array"> + <item name="catalog_product_price" xsi:type="string">catalog_product_price</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Option\Type\Select"> + <arguments> + <argument name="singleSelectionTypes" xsi:type="array"> + <item name="drop_down" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN</item> + <item name="radio" xsi:type="const">Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Category\Attribute\Source\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product\Attribute\Source\LayoutUpdate"> + <arguments> + <argument name="manager" xsi:type="object">Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index 3fdb554e65b62..407890af97c95 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -56,9 +56,11 @@ </event> <event name="catalog_product_save_before"> <observer name="set_special_price_start_date" instance="Magento\Catalog\Observer\SetSpecialPriceStartDate" /> - <observer name="unset_special_price" instance="Magento\Catalog\Observer\UnsetSpecialPrice" /> </event> <event name="store_save_after"> <observer name="synchronize_website_attributes" instance="Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange" /> </event> + <event name="catalog_category_prepare_save"> + <observer name="additional_authorization" instance="Magento\Catalog\Observer\CategoryDesignAuthorization" /> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index 63fc11c08d8bb..4203af383b366 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -79,4 +79,24 @@ <argument name="typeId" xsi:type="string">recently_compared_product</argument> </arguments> </virtualType> + <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> + <arguments> + <argument name="configurationsMap" xsi:type="array"> + <item name="addCompareSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Catalog::messages/addCompareSuccessMessage.phtml</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\ResourceConnection"> + <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> + <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> + </type> + <type name="Magento\Framework\App\Action\AbstractAction"> + <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> + </type> + <preference for="Magento\Catalog\Model\Product\Type\Price" type="Magento\Catalog\Model\Product\Type\FrontSpecialPrice" /> </config> diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml index 18671a32bb4fb..23e130aa8a991 100644 --- a/app/code/Magento/Catalog/etc/module.xml +++ b/app/code/Magento/Catalog/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Catalog" setup_version="2.2.3"> + <module name="Magento_Catalog" setup_version="2.2.6"> <sequence> <module name="Magento_Eav"/> <module name="Magento_Cms"/> diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd index 3bc24a9099262..734c8f378d5d7 100644 --- a/app/code/Magento/Catalog/etc/product_options.xsd +++ b/app/code/Magento/Catalog/etc/product_options.xsd @@ -61,11 +61,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/product_types.xml b/app/code/Magento/Catalog/etc/product_types.xml index fe4922ab8fa1f..fdcb67ae484d2 100644 --- a/app/code/Magento/Catalog/etc/product_types.xml +++ b/app/code/Magento/Catalog/etc/product_types.xml @@ -7,11 +7,13 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_types.xsd"> <type name="simple" label="Simple Product" modelInstance="Magento\Catalog\Model\Product\Type\Simple" indexPriority="10" sortOrder="10"> + <indexerModel instance="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\SimpleProductPrice" /> <customAttributes> <attribute name="refundable" value="true"/> </customAttributes> </type> <type name="virtual" label="Virtual Product" modelInstance="Magento\Catalog\Model\Product\Type\Virtual" indexPriority="20" sortOrder="40"> + <indexerModel instance="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\VirtualProductPrice" /> <customAttributes> <attribute name="is_real_product" value="false"/> <attribute name="refundable" value="false"/> diff --git a/app/code/Magento/Catalog/etc/product_types_base.xsd b/app/code/Magento/Catalog/etc/product_types_base.xsd index 6cc35fd7bee37..dec952bcf492e 100644 --- a/app/code/Magento/Catalog/etc/product_types_base.xsd +++ b/app/code/Magento/Catalog/etc/product_types_base.xsd @@ -92,11 +92,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 1d2b013f2035d..c1eb483c0bc0d 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -16,4 +16,17 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\ResourceConnection"> + <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> + <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> + </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="product_authorization" type="Magento\Catalog\Plugin\ProductAuthorization" /> + </type> + <type name="Magento\Catalog\Api\CategoryRepositoryInterface"> + <plugin name="category_authorization" type="Magento\Catalog\Plugin\CategoryAuthorization" /> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 98a8ef4de8408..bfbc05b12079d 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -15,4 +15,17 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\ResourceConnection"> + <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> + <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> + </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> + <type name="Magento\Catalog\Api\ProductRepositoryInterface"> + <plugin name="product_authorization" type="Magento\Catalog\Plugin\ProductAuthorization" /> + </type> + <type name="Magento\Catalog\Api\CategoryRepositoryInterface"> + <plugin name="category_authorization" type="Magento\Catalog\Plugin\CategoryAuthorization" /> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/widget.xml b/app/code/Magento/Catalog/etc/widget.xml index ff3773981d407..ea53f5215b7e6 100644 --- a/app/code/Magento/Catalog/etc/widget.xml +++ b/app/code/Magento/Catalog/etc/widget.xml @@ -292,7 +292,7 @@ </parameters> <containers> <container name="sidebar.main"> - <template name="default" value="list" /> + <template name="default" value="sidebar" /> </container> <container name="content"> <template name="grid" value="grid" /> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index de9f5e1975870..e19f495a09c85 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -13,8 +13,8 @@ Position,Position Day,Day Month,Month Year,Year -"<label class=""label""><span>from</span></label>","<label class=""label""><span>from</span></label>" -"<label class=""label""><span>to</span></label>","<label class=""label""><span>to</span></label>" +from,from +to,to [GLOBAL],[GLOBAL] [WEBSITE],[WEBSITE] "[STORE VIEW]","[STORE VIEW]" @@ -516,6 +516,9 @@ Groups,Groups "Maximum image width","Maximum image width" "Maximum image height","Maximum image height" "Maximum number of characters:","Maximum number of characters:" +"Maximum %1 characters", "Maximum %1 characters" +"too many", "too many" +"remaining", "remaining" "start typing to search template","start typing to search template" "Product online","Product online" "Product offline","Product offline" @@ -615,7 +618,7 @@ Submit,Submit "We don't recognize or support this file extension type.","We don't recognize or support this file extension type." "Configure Product","Configure Product" OK,OK -"This value does not follow the specified format (for example, 200X300).","This value does not follow the specified format (for example, 200X300)." +"This value does not follow the specified format (for example, 200x300).","This value does not follow the specified format (for example, 200x300)." "Select type of option.","Select type of option." "Please add rows to option.","Please add rows to option." "Please select items.","Please select items." @@ -795,3 +798,6 @@ Details,Details "Add To Compare","Add To Compare" "Learn more","Learn more" "Recently Viewed","Recently Viewed" +"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." +"Edit Product Design","Edit Product Design" +"Edit Category Design","Edit Category Design" diff --git a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js index 5ffc587f65bec..0677b0a5811c2 100644 --- a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js @@ -6,15 +6,29 @@ var config = { map: { '*': { - categoryForm: 'Magento_Catalog/catalog/category/form', - newCategoryDialog: 'Magento_Catalog/js/new-category-dialog', - categoryTree: 'Magento_Catalog/js/category-tree', - productGallery: 'Magento_Catalog/js/product-gallery', - baseImage: 'Magento_Catalog/catalog/base-image-uploader', - productAttributes: 'Magento_Catalog/catalog/product-attributes' + categoryForm: 'Magento_Catalog/catalog/category/form', + newCategoryDialog: 'Magento_Catalog/js/new-category-dialog', + categoryTree: 'Magento_Catalog/js/category-tree', + productGallery: 'Magento_Catalog/js/product-gallery', + baseImage: 'Magento_Catalog/catalog/base-image-uploader', + productAttributes: 'Magento_Catalog/catalog/product-attributes', + categoryCheckboxTree: 'Magento_Catalog/js/category-checkbox-tree' } }, deps: [ 'Magento_Catalog/catalog/product' - ] + ], + config: { + mixins: { + 'Magento_Catalog/js/components/use-parent-settings/select': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + }, + 'Magento_Catalog/js/components/use-parent-settings/textarea': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + }, + 'Magento_Catalog/js/components/use-parent-settings/single-checkbox': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + } + } + } }; diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml index 740d389735974..cea54e883d2aa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml @@ -4,188 +4,33 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> - -<?php $_divId = 'tree-div_' . time() ?> -<div id="<?= /* @escapeNotVerified */ $_divId ?>" class="tree"></div> -<script id="ie-deferred-loader" defer="defer" src="//:"></script> -<script> - require([ - 'jquery', - "prototype", - "extjs/ext-tree-checkbox", - "mage/adminhtml/form" - ], function(jQuery){ - -//<![CDATA[ - -// TODO: cleanup this script. It was copypasted from catalog/category/tree - -var tree; - /** - * Fix ext compatibility with prototype 1.6 + * @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree */ -Ext.lib.Event.getTarget = function(e) { - var ee = e.browserEvent || e; - return ee.target ? Event.element(ee) : null; -}; - -Ext.tree.TreePanel.Enhanced = function(el, config) -{ - Ext.tree.TreePanel.Enhanced.superclass.constructor.call(this, el, config); -}; - -Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { - - loadTree : function(config, firstLoad) - { - var parameters = config['parameters']; - var data = config['data']; - - if ((typeof parameters['root_visible']) != 'undefined') { - this.rootVisible = parameters['root_visible']*1; - } - - var root = new Ext.tree.TreeNode(parameters); - - this.nodeHash = {}; - this.setRootNode(root); - - if (firstLoad) { - this.addListener('click', this.categoryClick.createDelegate(this)); - } - - this.loader.buildCategoryTree(root, data); - this.el.dom.innerHTML = ''; - // render the tree - this.render(); - }, - - categoryClick : function(node, e) - { - node.getUI().check(!node.getUI().checked()); - } -}); - -jQuery(function() -{ - var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl() ?>' - }); - - categoryLoader.createNode = function(config) { - config.uiProvider = Ext.tree.CheckboxNodeUI; - var node; - var _node = Object.clone(config); - if (config.children && !config.children.length) { - delete(config.children); - node = new Ext.tree.AsyncTreeNode(config); - } else { - node = new Ext.tree.TreeNode(config); - } - - return node; - }; - - categoryLoader.buildCategoryTree = function(parent, config) - { - if (!config) return null; +?> - if (parent && config && config.length){ - for (var i = 0; i < config.length; i++) { - config[i].uiProvider = Ext.tree.CheckboxNodeUI; - var node; - var _node = Object.clone(config[i]); - if (_node.children && !_node.children.length) { - delete(_node.children); - node = new Ext.tree.AsyncTreeNode(_node); - } else { - node = new Ext.tree.TreeNode(config[i]); - } - parent.appendChild(node); - node.loader = node.getOwnerTree().loader; - if (_node.children) { - this.buildCategoryTree(node, _node.children); - } - } - } - }; +<?php $divId = $block->escapeHtml('tree-div_' . time()) ?> +<div id="<?= /* @noEscape */ $divId ?>" class="tree"></div> +<script id="ie-deferred-loader" defer="defer" src="//:"></script> - categoryLoader.buildHash = function(node) +<script type="text/x-magento-init"> { - var hash = {}; - - hash = this.toArray(node.attributes); - - if (node.childNodes.length>0 || (node.loaded==false && node.loading==false)) { - hash['children'] = new Array; - - for (var i = 0, len = node.childNodes.length; i < len; i++) { - if (!hash['children']) { - hash['children'] = new Array; - } - hash['children'].push(this.buildHash(node.childNodes[i])); + "*": { + "categoryCheckboxTree": { + "dataUrl": "<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>", + "divId": "<?= /* @noEscape */ $divId ?>", + "rootVisible": false, + "useAjax": <?= $block->escapeHtml($block->getUseAjax()) ?>, + "currentNodeId": <?= (int)$block->getCategoryId() ?>, + "jsFormObject": "<?= /* @noEscape */ $block->getJsFormObject() ?>", + "name": "<?= $block->escapeHtml($block->getRoot()->getName()) ?>", + "checked": "<?= $block->escapeHtml($block->getRoot()->getChecked()) ?>", + "allowdDrop": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, + "rootId": <?= (int)$block->getRoot()->getId() ?>, + "expanded": true, + "categoryId": <?= (int)$block->getCategoryId() ?>, + "treeJson": <?= /* @noEscape */ $block->getTreeJson() ?> } } - - return hash; - }; - - categoryLoader.toArray = function(attributes) { - var data = {}; - for (var key in attributes) { - var value = attributes[key]; - data[key] = value; - } - - return data; - }; - - categoryLoader.on("beforeload", function(treeLoader, node) { - treeLoader.baseParams.id = node.attributes.id; - }); - - categoryLoader.on("load", function(treeLoader, node, config) { - varienWindowOnload(); - }); - - tree = new Ext.tree.TreePanel.Enhanced('<?= /* @escapeNotVerified */ $_divId ?>', { - animate: false, - loader: categoryLoader, - enableDD: false, - containerScroll: true, - selModel: new Ext.tree.CheckNodeMultiSelectionModel(), - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', - useAjax: <?= /* @escapeNotVerified */ $block->getUseAjax() ?>, - currentNodeId: <?= (int) $block->getCategoryId() ?>, - addNodeTo: false, - rootUIProvider: Ext.tree.CheckboxNodeUI - }); - - tree.on('check', function(node, checked) { - <?= /* @escapeNotVerified */ $block->getJsFormObject() ?>.updateElement.value = this.getChecked().join(', '); - varienElementMethods.setHasChanges(node.getUI().checkbox); - }, tree); - - // set the root node - var parameters = { - text: '<?= /* @escapeNotVerified */ htmlentities($block->getRoot()->getName()) ?>', - draggable: false, - checked:'<?= /* @escapeNotVerified */ $block->getRoot()->getChecked() ?>', - uiProvider: Ext.tree.CheckboxNodeUI, - allowDrop: <?php if ($block->getRoot()->getIsVisible()): ?>true<?php else : ?>false<?php endif; ?>, - id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, - category_id: <?= (int) $block->getCategoryId() ?> - }; - - tree.loadTree({parameters:parameters, data:<?= /* @escapeNotVerified */ $block->getTreeJson() ?>},true); - -}); -//]]> - -}); + } </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml index f58b39a819a0c..c77b66733afc4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml @@ -5,18 +5,18 @@ */ /** - * Template for \Magento\Catalog\Block\Adminhtml\Category\Edit + * @var $block \Magento\Catalog\Block\Adminhtml\Category\Edit */ ?> <div data-id="information-dialog-category" class="messages" style="display: none;"> <div class="message message-notice"> - <div><?= /* @escapeNotVerified */ __('This operation can take a long time') ?></div> + <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> <script type="text/x-magento-init"> { "*": { - "categoryForm": {"refreshUrl": "<?= /* @escapeNotVerified */ $block->getRefreshPathUrl() ?>"} + "categoryForm": {"refreshUrl": "<?= $block->escapeJs($block->escapeUrl($block->getRefreshPathUrl())) ?>"} } } </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml index 4691a709cadeb..af7aec12a57ed 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml @@ -16,8 +16,8 @@ $gridJsObjectName = $blockGrid->getJsObjectName(); { "*": { "Magento_Catalog/catalog/category/assign-products": { - "selectedProducts": <?= /* @escapeNotVerified */ $block->getProductsJson() ?>, - "gridJsObjectName": <?= /* @escapeNotVerified */ '"' . $gridJsObjectName . '"' ?: '{}' ?> + "selectedProducts": <?= /* @noEscape */ $block->getProductsJson() ?>, + "gridJsObjectName": <?= /* @noEscape */ '"' . $gridJsObjectName . '"' ?: '{}' ?> } } } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 9865589556e7b..53025ba3dfb71 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -4,27 +4,26 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree */ ?> <div class="categories-side-col"> <div class="sidebar-actions"> - <?php if ($block->getRoot()): ?> + <?php if ($block->getRoot()) :?> <?= $block->getAddRootButtonHtml() ?><br/> <?= $block->getAddSubButtonHtml() ?> <?php endif; ?> </div> <div class="tree-actions"> - <?php if ($block->getRoot()): ?> + <?php if ($block->getRoot()) :?> <?php //echo $block->getCollapseButtonHtml() ?> <?php //echo $block->getExpandButtonHtml() ?> <a href="#" - onclick="tree.collapseTree(); return false;"><?= /* @escapeNotVerified */ __('Collapse All') ?></a> + onclick="tree.collapseTree(); return false;"><?= $block->escapeHtml(__('Collapse All')) ?></a> <span class="separator">|</span> <a href="#" - onclick="tree.expandTree(); return false;"><?= /* @escapeNotVerified */ __('Expand All') ?></a> + onclick="tree.expandTree(); return false;"><?= $block->escapeHtml(__('Expand All')) ?></a> <?php endif; ?> </div> - <?php if ($block->getRoot()): ?> + <?php if ($block->getRoot()) :?> <div class="tree-holder"> <div id="tree-div" class="tree-wrapper"></div> </div> @@ -32,7 +31,7 @@ <div data-id="information-dialog-tree" class="messages" style="display: none;"> <div class="message message-notice"> - <div><?= /* @escapeNotVerified */ __('This operation can take a long time') ?></div> + <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> <script> @@ -172,7 +171,7 @@ if (!this.collapsed) { this.collapsed = true; - this.loader.dataUrl = '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl(false) ?>'; + this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(false))) ?>'; this.request(this.loader.dataUrl, false); } }, @@ -181,7 +180,7 @@ this.expandAll(); if (this.collapsed) { this.collapsed = false; - this.loader.dataUrl = '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl(true) ?>'; + this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(true))) ?>'; this.request(this.loader.dataUrl, false); } }, @@ -216,7 +215,7 @@ if (tree && switcherParams) { var url; if (switcherParams.useConfirm) { - if (!confirm("<?= /* @escapeNotVerified */ __('Please confirm site switching. All data that hasn\'t been saved will be lost.') ?>")) { + if (!confirm("<?= $block->escapeJs(__('Please confirm site switching. All data that hasn\'t been saved will be lost.')) ?>")) { return false; } } @@ -259,7 +258,7 @@ } }); } else { - var baseUrl = '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>'; + var baseUrl = '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>'; var urlExt = switcherParams.scopeParams + 'id/' + tree.currentNodeId + '/'; url = parseSidUrl(baseUrl, urlExt); setLocation(url); @@ -296,17 +295,18 @@ if (scopeParams) { url = url + scopeParams; } - <?php if ($block->isClearEdit()): ?> + <?php if ($block->isClearEdit()) :?> if (selectedNode) { url = url + 'id/' + config.parameters.category_id; } <?php endif;?> //updateContent(url); //commented since ajax requests replaced with http ones to load a category + jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl() ?>' + dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' }); categoryLoader.processResponse = function (response, parent, callback) { @@ -388,26 +388,26 @@ enableDD: true, containerScroll: true, selModel: new Ext.tree.CheckNodeMultiSelectionModel(), - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', - useAjax: <?= /* @escapeNotVerified */ $block->getUseAjax() ?>, - switchTreeUrl: '<?= /* @escapeNotVerified */ $block->getSwitchTreeUrl() ?>', - editUrl: '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>', - currentNodeId: <?= /* @escapeNotVerified */ (int)$block->getCategoryId() ?>, - baseUrl: '<?= /* @escapeNotVerified */ $block->getEditUrl() ?>' + rootVisible: '<?= (bool)$block->getRoot()->getIsVisible() ?>', + useAjax: <?= $block->escapeJs($block->getUseAjax()) ?>, + switchTreeUrl: '<?= $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) ?>', + editUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>', + currentNodeId: <?= (int)$block->getCategoryId() ?>, + baseUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>' }; defaultLoadTreeParams = { parameters: { - text: <?= /* @escapeNotVerified */ json_encode(htmlentities($block->getRoot()->getName())) ?>, + text: <?= /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) ?>, draggable: false, - allowDrop: <?php if ($block->getRoot()->getIsVisible()): ?>true<?php else : ?>false<?php endif; ?>, + allowDrop: <?php if ($block->getRoot()->getIsVisible()) :?>true<?php else :?>false<?php endif; ?>, id: <?= (int)$block->getRoot()->getId() ?>, expanded: <?= (int)$block->getIsWasExpanded() ?>, store_id: <?= (int)$block->getStore()->getId() ?>, category_id: <?= (int)$block->getCategoryId() ?>, parent: <?= (int)$block->getRequest()->getParam('parent') ?> }, - data: <?= /* @escapeNotVerified */ $block->getTreeJson() ?> + data: <?= /* @noEscape */ $block->getTreeJson() ?> }; reRenderTree(); @@ -485,7 +485,7 @@ click: function () { (function ($) { $.ajax({ - url: '<?= /* @escapeNotVerified */ $block->getMoveUrl() ?>', + url: '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', method: 'POST', data: registry.get('pd'), showLoader: true diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index dbe66ef1aecd3..a556b87e037ef 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -3,24 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_divId = 'tree' . $block->getId() ?> -<div id="<?= /* @escapeNotVerified */ $_divId ?>" class="tree"></div> +<div id="<?= $block->escapeHtmlAttr($_divId) ?>" class="tree"></div> <!--[if IE]> <script id="ie-deferred-loader" defer="defer" src="//:"></script> <![endif]--> <script> require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ -var tree<?= /* @escapeNotVerified */ $block->getId() ?>; +var tree<?= $block->escapeJs($block->getId()) ?>; -var useMassaction = <?= /* @escapeNotVerified */ $block->getUseMassaction() ? 1 : 0 ?>; +var useMassaction = <?= /* @noEscape */ $block->getUseMassaction() ? 1 : 0 ?>; -var isAnchorOnly = <?= /* @escapeNotVerified */ $block->getIsAnchorOnly() ? 1 : 0 ?>; +var isAnchorOnly = <?= /* @noEscape */ $block->getIsAnchorOnly() ? 1 : 0 ?>; Ext.tree.TreePanel.Enhanced = function(el, config) { @@ -44,8 +41,8 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { this.setRootNode(root); if (firstLoad) { - <?php if ($block->getNodeClickListener()): ?> - this.addListener('click', <?= /* @escapeNotVerified */ $block->getNodeClickListener() ?>.createDelegate(this)); + <?php if ($block->getNodeClickListener()) :?> + this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); <?php endif; ?> } @@ -58,10 +55,10 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { jQuery(function() { - var emptyNodeAdded = <?= /* @escapeNotVerified */ ($block->getWithEmptyNode() ? 'false' : 'true') ?>; + var emptyNodeAdded = <?= /* @noEscape */ ($block->getWithEmptyNode() ? 'false' : 'true') ?>; var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= /* @escapeNotVerified */ $block->getLoadTreeUrl() ?>' + dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' }); categoryLoader.buildCategoryTree = function(parent, config) @@ -80,7 +77,7 @@ jQuery(function() // Add empty node to reset category filter if(!emptyNodeAdded) { var empty = Object.clone(_node); - empty.text = '<?= /* @escapeNotVerified */ __('None') ?>'; + empty.text = '<?= $block->escapeJs(__('None')) ?>'; empty.children = []; empty.id = 'none'; empty.path = '1/none'; @@ -151,25 +148,25 @@ jQuery(function() }; categoryLoader.on("beforeload", function(treeLoader, node) { - $('<?= /* @escapeNotVerified */ $_divId ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); + $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); treeLoader.baseParams.id = node.attributes.id; }); - tree<?= /* @escapeNotVerified */ $block->getId() ?> = new Ext.tree.TreePanel.Enhanced('<?= /* @escapeNotVerified */ $_divId ?>', { + tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { animate: false, loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', + rootVisible: false, useAjax: true, currentNodeId: <?= (int) $block->getCategoryId() ?>, addNodeTo: false }); if (useMassaction) { - tree<?= /* @escapeNotVerified */ $block->getId() ?>.on('check', function(node) { - $('<?= /* @escapeNotVerified */ $_divId ?>').fire('node:changed', {node:node}); - }, tree<?= /* @escapeNotVerified */ $block->getId() ?>); + tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { + $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); + }, tree<?= $block->escapeJs($block->getId()) ?>); } // set the root node @@ -177,11 +174,11 @@ jQuery(function() text: 'Psw', draggable: false, id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, + expanded: true, category_id: <?= (int) $block->getCategoryId() ?> }; - tree<?= /* @escapeNotVerified */ $block->getId() ?>.loadTree({parameters:parameters, data:<?= /* @escapeNotVerified */ $block->getTreeJson() ?>},true); + tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); }); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml index 680361eae448e..ea086c3a9e371 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml @@ -3,19 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php -/** - * @see \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element - */ +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var $block \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element */ ?> <?php /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ $element = $block->getElement(); -$note = $element->getNote() ? '<div class="note admin__field-note">' . $element->getNote() . '</div>' : ''; +$note = $element->getNote() ? '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; $elementBeforeLabel = $element->getExtType() == 'checkbox' || $element->getExtType() == 'radio'; $addOn = $element->getBeforeElementHtml() || $element->getAfterElementHtml(); $fieldId = ($element->getHtmlId()) ? ' id="attribute-' . $element->getHtmlId() . '-container"' : ''; @@ -27,8 +24,8 @@ $fieldClass .= ($element->getRequired()) ? ' required' : ''; $fieldClass .= ($note) ? ' with-note' : ''; $fieldClass .= ($entity && $entity->getIsUserDefined()) ? ' user-defined type-' . $entity->getFrontendInput() : ''; -$fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' - . $block->getUiId('form-field', $element->getId()); +$fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . '" ' + . $block->getUiId('form-field', $block->escapeHtmlAttr($element->getId())); ?> <?php $block->checkFieldDisable() ?> @@ -36,38 +33,38 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> -<?php if (!$element->getNoDisplay()): ?> - <?php if ($element->getType() == 'hidden'): ?> +<?php if (!$element->getNoDisplay()) :?> + <?php if ($element->getType() == 'hidden') :?> <?= $element->getElementHtml() ?> - <?php else: ?> - <div<?= /* @escapeNotVerified */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" - data-apply-to="<?= $block->escapeHtml($this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( + <?php else :?> + <div<?= /* @noEscape */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" + data-apply-to="<?= $block->escapeHtmlAttr($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] ))?>" > - <?php if ($elementBeforeLabel): ?> + <?php if ($elementBeforeLabel) :?> <?= $block->getElementHtml() ?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> - <?= /* @escapeNotVerified */ $note ?> - <?php else: ?> + <?= /* @noEscape */ $note ?> + <?php else :?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> <div class="admin__field-control control"> - <?= /* @escapeNotVerified */ ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> - <?= /* @escapeNotVerified */ $note ?> + <?= /* @noEscape */ ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> + <?= /* @noEscape */ $note ?> </div> <?php endif; ?> <div class="field-service"> - <?php if ($block->canDisplayUseDefault()): ?> + <?php if ($block->canDisplayUseDefault()) :?> <label for="<?= $element->getHtmlId() ?>_default" class="choice use-default"> - <input <?php if ($element->getReadonly()):?> disabled="disabled"<?php endif; ?> + <input <?php if ($element->getReadonly()) :?> disabled="disabled"<?php endif; ?> type="checkbox" name="use_default[]" class="use-default-control" id="<?= $element->getHtmlId() ?>_default" - <?php if ($block->usedDefault()): ?> checked="checked"<?php endif; ?> - onclick="<?= /* @escapeNotVerified */ $elementToggleCode ?>" - value="<?= /* @escapeNotVerified */ $block->getAttributeCode() ?>"/> - <span class="use-default-label"><?= /* @escapeNotVerified */ __('Use Default Value') ?></span> + <?php if ($block->usedDefault()) :?> checked="checked"<?php endif; ?> + onclick="<?= $block->escapeHtmlAttr($elementToggleCode) ?>" + value="<?= $block->escapeHtmlAttr($block->getAttributeCode()) ?>"/> + <span class="use-default-label"><?= $block->escapeHtml(__('Use Default Value')) ?></span> </label> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml index ce4d8450f5e63..57eacb8e84a93 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product.phtml @@ -3,14 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php -/** - * @see \Magento\Catalog\Block\Adminhtml\Product - */ -?> +// phpcs:disable PSR2.Files.ClosingTag +/** @var $block \Magento\Catalog\Block\Adminhtml\Product */ +?> <?= $block->getGridHtml() ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml index 74cf8f5f3a70b..e30b981ff36a6 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** * @var $block \Magento\Backend\Block\Widget\Form\Container */ ?> -<?= /* @escapeNotVerified */ $block->getFormInitScripts() ?> -<div data-mage-init='{"floatingHeader": {}}' class="page-actions attribute-popup-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> +<?= /* @noEscape */ $block->getFormInitScripts() ?> +<div data-mage-init='{"floatingHeader": {}}' class="page-actions attribute-popup-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>> <?= $block->getButtonsHtml('header') ?> </div> @@ -21,13 +19,13 @@ <input name="form_key" type="hidden" value="<?= $block->escapeHtml($block->getFormKey()) ?>" /> <?= $block->getChildHtml('form') ?> </form> - - -<script> -require(['jquery', "mage/mage"], function(jQuery){ - - jQuery('#edit_form').mage('form').mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); - -}); +<script type="text/x-magento-init"> + { + "#edit_form": { + "Magento_Catalog/catalog/product/edit/attribute": { + "validationUrl": "<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>" + } + } + } </script> -<?= /* @escapeNotVerified */ $block->getFormScripts() ?> +<?= /* @noEscape */ $block->getFormScripts() ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml index 8a5f1919f78be..1d9f70d8ebd6c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml @@ -4,17 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <script> require([ "jquery", 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/modal/prompt', + 'uiRegistry', "collapsable", "prototype" -], function(jQuery, alert, prompt){ +], function(jQuery, alert, prompt, registry){ function toggleApplyVisibility(select) { if ($(select).value == 1) { @@ -40,13 +40,21 @@ function getFrontTab() { function checkOptionsPanelVisibility(){ if($('manage-options-panel')){ - var panel = $('manage-options-panel').up('.fieldset'); + var panelId = 'manage-options-panel', + panel = $(panelId), + panelFieldSet = panel.up('.fieldset'), + activePanelClass = 'selected-type-options'; if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect')){ - panel.show(); + panelFieldSet.show(); + jQuery(panel).addClass(activePanelClass); + registry.get(panelId, function () { + jQuery('#' + panelId).trigger('render'); + }); } else { - panel.hide(); + panelFieldSet.hide(); + jQuery(panel).removeClass(activePanelClass); } } } @@ -55,7 +63,7 @@ function bindAttributeInputType() { checkOptionsPanelVisibility(); switchDefaultValueField(); - if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ + if($('frontend_input') && ($('frontend_input').value=='boolean' || $('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ if($('is_filterable') && !$('is_filterable').getAttribute('readonly')){ $('is_filterable').disabled = false; } @@ -194,22 +202,22 @@ function switchDefaultValueField() setRowVisibility('frontend_class', false); break; - <?php foreach ($this->helper('Magento\Catalog\Helper\Data')->getAttributeHiddenFields() as $type => $fields): ?> - case '<?= /* @escapeNotVerified */ $type ?>': + <?php foreach ($this->helper(Magento\Catalog\Helper\Data::class)->getAttributeHiddenFields() as $type => $fields) :?> + case '<?= $block->escapeJs($type) ?>': var isFrontTabHidden = false; - <?php foreach ($fields as $one): ?> - <?php if ($one == '_front_fieldset'): ?> + <?php foreach ($fields as $one) :?> + <?php if ($one == '_front_fieldset') :?> getFrontTab().hide(); isFrontTabHidden = true; - <?php elseif ($one == '_default_value'): ?> + <?php elseif ($one == '_default_value') :?> defaultValueTextVisibility = defaultValueTextareaVisibility = defaultValueDateVisibility = defaultValueYesnoVisibility = false; - <?php elseif ($one == '_scope'): ?> + <?php elseif ($one == '_scope') :?> scopeVisibility = false; - <?php else: ?> - setRowVisibility('<?= /* @escapeNotVerified */ $one ?>', false); + <?php else :?> + setRowVisibility('<?= $block->escapeJs($one) ?>', false); <?php endif; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml index f3d39257c266c..1d5d251f00de9 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/labels.phtml @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Labels */ ?> <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="manage-titles-wrapper"> <div class="fieldset-wrapper-title"> <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#manage-titles-content"> - <span><?= /* @escapeNotVerified */ __('Manage Titles (Size, Color, etc.)') ?></span> + <span><?= $block->escapeHtml(__('Manage Titles (Size, Color, etc.)')) ?></span> </strong> </div> <div class="fieldset-wrapper-content in collapse" id="manage-titles-content"> @@ -21,17 +19,23 @@ <table class="admin__control-table" id="attribute-labels-table"> <thead> <tr> - <?php foreach ($block->getStores() as $_store): ?> - <th class="col-store-view"><?= /* @escapeNotVerified */ $_store->getName() ?></th> + <?php foreach ($block->getStores() as $_store) :?> + <th class="col-store-view"><?= $block->escapeHtml($_store->getName()) ?></th> <?php endforeach; ?> </tr> </thead> <tbody> <tr> <?php $_labels = $block->getLabelValues() ?> - <?php foreach ($block->getStores() as $_store): ?> + <?php foreach ($block->getStores() as $_store) :?> <td class="col-store-view"> - <input class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option<?php endif; ?>" type="text" name="frontend_label[<?= /* @escapeNotVerified */ $_store->getId() ?>]" value="<?= $block->escapeHtml($_labels[$_store->getId()]) ?>"<?php if ($block->getReadOnly()):?> disabled="disabled"<?php endif;?>/> + <input class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> required-option<?php endif; ?>" + type="text" + name="frontend_label[<?= $block->escapeHtmlAttr($_store->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_labels[$_store->getId()]) ?>" + <?php if ($block->getReadOnly()) :?> + disabled="disabled" + <?php endif;?>/> </td> <?php endforeach; ?> </tr> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml index a0041d2e02988..e5f8a360c334c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options */ $stores = $block->getStoresSortedBySortOrder(); @@ -23,8 +21,8 @@ $stores = $block->getStoresSortedBySortOrder(); <span><?= $block->escapeHtml(__('Is Default')) ?></span> </th> <?php - foreach ($stores as $_store): ?> - <th<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> class="_required"<?php endif; ?>> + foreach ($stores as $_store) :?> + <th<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> class="_required"<?php endif; ?>> <span><?= $block->escapeHtml(__($_store->getName())) ?></span> </th> <?php endforeach; @@ -43,7 +41,7 @@ $stores = $block->getStoresSortedBySortOrder(); </tr> <tr> <th colspan="<?= (int) $storetotal ?>" class="col-actions-add"> - <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()):?> + <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> <button id="add_new_option_button" data-action="add_new_row" title="<?= $block->escapeHtml(__('Add Option')) ?>" type="button" class="action- scalable add"> @@ -57,24 +55,24 @@ $stores = $block->getStoresSortedBySortOrder(); <input type="hidden" id="option-count-check" value="" /> </div> <script id="row-template" type="text/x-magento-template"> - <tr> + <tr <% if (data.rowClasses) { %>class="<%- data.rowClasses %>"<% } %>> <td class="col-draggable"> - <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()): ?> + <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> <div data-role="draggable-handle" class="draggable-handle" title="<?= $block->escapeHtml(__('Sort Option')) ?>"> </div> <?php endif; ?> - <input data-role="order" type="hidden" name="option[order][<%- data.id %>]" value="<%- data.sort_order %>" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()): ?> disabled="disabled"<?php endif; ?>/> + <input data-role="order" type="hidden" name="option[order][<%- data.id %>]" value="<%- data.sort_order %>" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif; ?>/> </td> <td class="col-default control-table-actions-cell"> - <input class="input-radio" type="<%- data.intype %>" name="default[]" value="<%- data.id %>" <%- data.checked %><?php if ($block->getReadOnly()):?>disabled="disabled"<?php endif;?>/> + <input class="input-radio" type="<%- data.intype %>" name="default[]" value="<%- data.id %>" <%- data.checked %><?php if ($block->getReadOnly()) :?>disabled="disabled"<?php endif;?>/> </td> - <?php foreach ($stores as $_store): ?> - <td class="col-<%- data.id %>"><input name="option[value][<%- data.id %>][<?= (int) $_store->getId() ?>]" value="<%- data.store<?= /* @noEscape */ (int) $_store->getId() ?> %>" class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option required-unique<?php endif; ?>" type="text" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()):?> disabled="disabled"<?php endif;?>/></td> + <?php foreach ($stores as $_store) :?> + <td class="col-<%- data.id %>"><input name="option[value][<%- data.id %>][<?= (int) $_store->getId() ?>]" value="<%- data.store<?= /* @noEscape */ (int) $_store->getId() ?> %>" class="input-text<?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID) :?> required-option required-unique<?php endif; ?>" type="text" <?php if ($block->getReadOnly() || $block->canManageOptionDefaultOnly()) :?> disabled="disabled"<?php endif;?>/></td> <?php endforeach; ?> <td id="delete_button_container_<%- data.id %>" class="col-delete"> <input type="hidden" class="delete-flag" name="option[delete][<%- data.id %>]" value="" /> - <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()):?> + <?php if (!$block->getReadOnly() && !$block->canManageOptionDefaultOnly()) :?> <button id="delete_button_<%- data.id %>" title="<?= $block->escapeHtml(__('Delete')) ?>" type="button" class="action- scalable delete delete-option" > @@ -86,9 +84,11 @@ $stores = $block->getStoresSortedBySortOrder(); </script> <?php $values = []; - foreach($block->getOptionValues() as $value) { + foreach ($block->getOptionValues() as $value) { $value = $value->getData(); - $values[] = is_array($value) ? array_map("htmlspecialchars_decode", $value) : $value; + $values[] = is_array($value) ? array_map(function ($str) { + return htmlspecialchars_decode($str, ENT_QUOTES); + }, $value) : $value; } ?> <script type="text/x-magento-init"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml index d5dfe845e54c2..ee713b53a9221 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main */ ?> <div class="attribute-set"> @@ -31,11 +30,11 @@ </div> <div class="attribute-set-col fieldset-wrapper"> <div class="fieldset-wrapper-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Groups') ?></span> + <span class="title"><?= $block->escapeHtml(__('Groups')) ?></span> </div> - <?php if (!$block->getIsReadOnly()): ?> - <?= /* @escapeNotVerified */ $block->getAddGroupButton() ?> <?= /* @escapeNotVerified */ $block->getDeleteGroupButton() ?> - <p class="note-block"><?= /* @escapeNotVerified */ __('Double click on a group to rename it.') ?></p> + <?php if (!$block->getIsReadOnly()) :?> + <?= /* @noEscape */ $block->getAddGroupButton() ?> <?= /* @noEscape */ $block->getDeleteGroupButton() ?> + <p class="note-block"><?= $block->escapeHtml(__('Double click on a group to rename it.')) ?></p> <?php endif; ?> <?= $block->getSetsFilterHtml() ?> @@ -43,7 +42,7 @@ </div> <div class="attribute-set-col fieldset-wrapper"> <div class="fieldset-wrapper-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Unassigned Attributes') ?></span> + <span class="title"><?= $block->escapeHtml(__('Unassigned Attributes')) ?></span> </div> <div id="tree-div2" class="attribute-set-tree"></div> <script id="ie-deferred-loader" defer="defer" src="//:"></script> @@ -58,8 +57,8 @@ ], function(jQuery, prompt, alert){ //<![CDATA[ - var allowDragAndDrop = <?= /* @escapeNotVerified */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; - var canEditGroups = <?= /* @escapeNotVerified */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; + var allowDragAndDrop = <?= /* @noEscape */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; + var canEditGroups = <?= /* @noEscape */ ($block->getIsReadOnly() ? 'false' : 'true') ?>; var TreePanels = function() { // shorthand @@ -80,13 +79,13 @@ // set the root node this.root = new Ext.tree.TreeNode({ text: 'ROOT', - allowDrug:false, + allowDrag:false, allowDrop:true, id:'1' }); tree.setRootNode(this.root); - buildCategoryTree(this.root, <?= /* @escapeNotVerified */ $block->getGroupTreeJson() ?>); + buildCategoryTree(this.root, <?= /* @noEscape */ $block->getGroupTreeJson() ?>); // render the tree tree.render(); this.root.expand(false, false); @@ -94,7 +93,7 @@ this.ge = new Ext.tree.TreeEditor(tree, { allowBlank:false, - blankText:'<?= /* @escapeNotVerified */ __('A name is required.') ?>', + blankText:'<?= $block->escapeJs(__('A name is required.')) ?>', selectOnFocus:true, cls:'folder' }); @@ -125,7 +124,7 @@ id:'free' }); tree2.setRootNode(this.root2); - buildCategoryTree(this.root2, <?= /* @escapeNotVerified */ $block->getAttributeTreeJson() ?>); + buildCategoryTree(this.root2, <?= /* @noEscape */ $block->getAttributeTreeJson() ?>); this.root2.addListener('beforeinsert', editSet.rightBeforeInsert); this.root2.addListener('beforeappend', editSet.rightBeforeAppend); @@ -188,20 +187,36 @@ for( j in config[i].children ) { if(config[i].children[j].id) { newNode = new Ext.tree.TreeNode(config[i].children[j]); - node.appendChild(newNode); - newNode.addListener('click', editSet.unregister); + + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } } + node.appendChild(newNode); + newNode.addListener('click', editSet.unregister); } } } } } - editSet = function() { - return { - register : function(node) { - editSet.currentNode = node; - }, + + editSet = function () { + return { + register: function (node) { + editSet.currentNode = node; + if (typeof node.ui.onTextChange === 'function') { + node.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + }, unregister : function() { editSet.currentNode = false; @@ -264,8 +279,8 @@ addGroup : function() { prompt({ - title: "<?= /* @escapeNotVerified */ __('Add New Group') ?>", - content: "<?= /* @escapeNotVerified */ __('Please enter a new group name.') ?>", + title: "<?= $block->escapeJs($block->escapeHtml(__('Add New Group'))) ?>", + content: "<?= $block->escapeJs($block->escapeHtml(__('Please enter a new group name.'))) ?>", value: "", validation: true, validationRules: ['required-entry'], @@ -293,6 +308,14 @@ allowDrag : true }); + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + TreePanels.root.appendChild(newNode); newNode.addListener('beforemove', editSet.groupBeforeMove); newNode.addListener('beforeinsert', editSet.groupBeforeInsert); @@ -316,13 +339,14 @@ validateGroupName : function(name, exceptNodeId) { name = name.strip(); + name = name.escapeHTML(); var result = true; if (name === '') { result = false; } for (var i=0; i < TreePanels.root.childNodes.length; i++) { if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && TreePanels.root.childNodes[i].id != exceptNodeId) { - errorText = '<?= /* @escapeNotVerified */ __('An attribute group named "/name/" already exists.') ?>'; + errorText = '<?= $block->escapeJs(__('An attribute group named "/name/" already exists.')) ?>'; alert({ content: errorText.replace("/name/",name) }); @@ -350,7 +374,7 @@ editSet.req.form_key = FORM_KEY; } var req = {data : Ext.util.JSON.encode(editSet.req)}; - var con = new Ext.lib.Ajax.request('POST', '<?= /* @escapeNotVerified */ $block->getMoveUrl() ?>', {success:editSet.success,failure:editSet.failure}, req); + var con = new Ext.lib.Ajax.request('POST', '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', {success:editSet.success,failure:editSet.failure}, req); }, success : function(o) { @@ -425,7 +449,7 @@ rightRemove : function(tree, nodeThis, node) { if( nodeThis.firstChild == null && node.id != 'empty' ) { var newNode = new Ext.tree.TreeNode({ - text : '<?= /* @escapeNotVerified */ __('Empty') ?>', + text : '<?= $block->escapeJs(__('Empty')) ?>', id : 'empty', cls : 'folder', is_user_defined : 1, diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml index c1af14389fe59..227ed4be81fae 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml @@ -8,7 +8,7 @@ <script> require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= /* @escapeNotVerified */ $block->getFormId() ?>').mage('form').mage('validation'); + jQuery('#<?= $block->escapeJs($block->getFormId()) ?>').mage('form').mage('validation'); }); </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml index 902c6932f0ae1..754cb0fe576fd 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/main.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml('grid') ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index 75027d5e043fb..32466a1dfa965 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -3,10 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - - ?> +?> <div id="product_composite_configure" class="product-configure-popup" style="display:none;"> <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe" style="width:0; height:0; border:0px solid #fff; position:absolute; top:-1000px; left:-1000px" onload="window.productConfigure && productConfigure.onLoadIFrame()"></iframe> <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" onsubmit="productConfigure.onConfirmBtn(); return false;" target="product_composite_configure_iframe"> @@ -19,7 +16,7 @@ <div id="product_composite_configure_form_confirmed" style="display:none;"></div> </div> <input type="hidden" name="as_js_varname" value="iFrameResponse" /> - <input type="hidden" name="form_key" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /> + <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> </form> <div id="product_composite_configure_confirmed" style="display:none;"></div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml index acc80fa6ea6b0..c58cdd6b36a1b 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options.phtml @@ -3,24 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> - <?php /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Options */ ?> <?php $options = $block->decorateArray($block->getOptions()); ?> -<?php if (count($options)): ?> - -<?= $block->getChildHtml('options_js') ?> - -<fieldset id="product_composite_configure_fields_options" class="fieldset admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> - <legend class="legend admin__legend"> - <span><?= /* @escapeNotVerified */ __('Custom Options') ?></span> - </legend><br> - <?php foreach ($options as $option): ?> - <?= $block->getOptionHtml($option) ?> - <?php endforeach;?> -</fieldset> - +<?php if (count($options)) :?> + <?= $block->getChildHtml('options_js') ?> + <fieldset id="product_composite_configure_fields_options" + class="fieldset admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> + <legend class="legend admin__legend"> + <span><?= $block->escapeHtml(__('Custom Options')) ?></span> + </legend><br> + <?php foreach ($options as $option) :?> + <?= $block->getOptionHtml($option) ?> + <?php endforeach;?> + </fieldset> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 8e5583a6699b7..bd8d8df8dcff0 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -3,82 +3,88 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +?> +<?php +// phpcs:disable Squiz.WhiteSpace.ControlStructureSpacing -// @codingStandardsIgnoreFile - +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> <?php $_option = $block->getOption(); ?> -<?php $_optionId = $_option->getId(); ?> -<div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> - <label class="label admin__field-label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> - </label> +<?php $_optionId = (int) $_option->getId(); ?> +<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> + <label class="label admin__field-label"> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> + </span> + </label> <div class="admin__field-control control"> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE): ?> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> - <?= $block->getDateHtml() ?> + <?= $block->getDateHtml() ?> - <?php if (!$block->useCalendar()): ?> - <script> -require([ - "prototype", - "Magento_Catalog/catalog/product/composite/configure" -], function(){ + <?php if (!$block->useCalendar()) :?> + <script> + require([ + "prototype", + "Magento_Catalog/catalog/product/composite/configure" + ], function(){ - window.dateOption = productConfigure.opConfig.dateOption; - Event.observe('options_<?= /* @escapeNotVerified */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); - Event.observe('options_<?= /* @escapeNotVerified */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); -}); -</script> - <?php endif; ?> + window.dateOption = productConfigure.opConfig.dateOption; + Event.observe('options_<?= /* @noEscape */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); + Event.observe('options_<?= /* @noEscape */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); + }); + </script> + <?php endif; ?> - <?php endif; ?> + <?php endif; ?> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME): ?> - <span class="time-picker"><?= $block->getTimeHtml() ?></span> - <?php endif; ?> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> + <span class="time-picker"><?= $block->getTimeHtml() ?></span> + <?php endif; ?> - <input type="hidden" name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" value="" /> - <script> -require([ - "jquery", - "mage/backend/validation" -], function(jQuery){ + <input type="hidden" + name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" + class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" + value="" /> + <script> + require([ + "jquery", + "mage/backend/validation" + ], function(jQuery){ - //<![CDATA[ -<?php if ($_option->getIsRequire()): ?> - jQuery.validator.addMethod('validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @escapeNotVerified */ $_optionId ?>"]'); - for (var i=0; i < dateTimeParts.length; i++) { - if (dateTimeParts[i].value == "") return false; - } - return true; - }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); -<?php else: ?> - jQuery.validator.addMethod('validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @escapeNotVerified */ $_optionId ?>"]'); - var hasWithValue = false, hasWithNoValue = false; - var pattern = /day_part$/i; - for (var i=0; i < dateTimeParts.length; i++) { - if (! pattern.test(dateTimeParts[i].id)) { - if (dateTimeParts[i].value === "") { - hasWithValue = true; - } else { - hasWithNoValue = true; + //<![CDATA[ + <?php if ($_option->getIsRequire()) :?> + jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); + for (var i=0; i < dateTimeParts.length; i++) { + if (dateTimeParts[i].value == "") return false; } - } - } - return hasWithValue ^ hasWithNoValue; - }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); -<?php endif; ?> - //]]> - -}); -</script> - </div> + return true; + }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); + <?php else :?> + jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); + var hasWithValue = false, hasWithNoValue = false; + var pattern = /day_part$/i; + for (var i=0; i < dateTimeParts.length; i++) { + if (! pattern.test(dateTimeParts[i].id)) { + if (dateTimeParts[i].value === "") { + hasWithValue = true; + } else { + hasWithNoValue = true; + } + } + } + return hasWithValue ^ hasWithNoValue; + }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); + <?php endif; ?> + //]]> + + }); + </script> + </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml index edf4f68afded7..29bf256bd4d87 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml @@ -3,14 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData() ? true : false; ?> -<?php $_fileName = 'options_' . $_option->getId() . '_file'; ?> +<?php $_fileName = 'options_' . (int)$_option->getId() . '_file'; ?> <?php $_fieldNameAction = $_fileName . '_action'; ?> <?php $_fieldValueAction = $_fileExists ? 'save_old' : 'save_new'; ?> <?php $_fileNamed = $_fileName . '_name'; ?> @@ -20,11 +18,11 @@ require(['prototype'], function(){ //<![CDATA[ - opFile<?= /* @escapeNotVerified */ $_rand ?> = { + opFile<?= /* @noEscape */ $_rand ?> = { initializeFile: function(inputBox) { - this.inputFile = inputBox.select('input[name="<?= /* @escapeNotVerified */ $_fileName ?>"]')[0]; - this.inputFileAction = inputBox.select('input[name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>"]')[0]; - this.fileNameBox = inputBox.up('dd').select('.<?= /* @escapeNotVerified */ $_fileNamed ?>')[0]; + this.inputFile = inputBox.select('input[name="<?= /* @noEscape */ $_fileName ?>"]')[0]; + this.inputFileAction = inputBox.select('input[name="<?= /* @noEscape */ $_fieldNameAction ?>"]')[0]; + this.fileNameBox = inputBox.up('dd').select('.<?= /* @noEscape */ $_fileNamed ?>')[0]; }, toggleFileChange: function(inputBox) { @@ -61,42 +59,44 @@ require(['prototype'], function(){ }); </script> -<div class="admin__field <?php if ($_option->getIsRequire()) echo ' required _required' ?>"> +<div class="admin__field <?= $_option->getIsRequire() ? ' required _required' : '' ?>"> <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> + </span> </label> <div class="admin__field-control control"> - <?php if ($_fileExists): ?> + <?php if ($_fileExists) :?> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @escapeNotVerified */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> - <?= /* @escapeNotVerified */ __('Change') ?> + <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> + <?= $block->escapeHtml(__('Change')) ?> </a>  - <?php if (!$_option->getIsRequire()): ?> - <input type="checkbox" onclick="opFile<?= /* @escapeNotVerified */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>"/> - <span class="label"><?= /* @escapeNotVerified */ __('Delete') ?></span> + <?php if (!$_option->getIsRequire()) :?> + <input type="checkbox" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> + <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> <?php endif; ?> <div class="input-box" <?= $_fileExists ? 'style="display:none"' : '' ?>> <!-- ToDo UI: add appropriate file class when z-index issue in ui dialog will be resolved --> - <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> - <input type="hidden" name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>" value="<?= /* @escapeNotVerified */ $_fieldValueAction ?>" /> + <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()): ?> + <?php if ($_option->getFileExtension()) :?> <div class="admin__field-note"> - <span><?= /* @escapeNotVerified */ __('Compatible file extensions to upload') ?>: <strong><?= /* @escapeNotVerified */ $_option->getFileExtension() ?></strong></span> + <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong></span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0): ?> + <?php if ($_option->getImageSizeX() > 0) :?> <div class="admin__field-note"> - <span><?= /* @escapeNotVerified */ __('Maximum image width') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeX() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0): ?> + <?php if ($_option->getImageSizeY() > 0) :?> <div class="admin__field-note"> - <span><?= /* @escapeNotVerified */ __('Maximum image height') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeY() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml index af09bbe0acd9d..2218ce5d29671 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/select.phtml @@ -3,21 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select */ ?> <?php $_option = $block->getOption(); ?> -<div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> +<div class="admin__field field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> <label class="label admin__field-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control admin__field-control"> <?= $block->getValuesHtml() ?> - <?php if ($_option->getIsRequire()): ?> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX): ?> - <span id="options-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></span> + <?php if ($_option->getIsRequire()) :?> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) :?> + <span id="options-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></span> <?php endif; ?> <?php endif;?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml index 14e485c6445e0..5b16c52f099dc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml @@ -3,26 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Text */ ?> <?php $_option = $block->getOption(); ?> -<div class="field admin__field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> +<div class="field admin__field<?= $_option->getIsRequire() ? ' required _required' : '' ?>"> <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> + </span> </label> <div class="control admin__field-control"> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> - <input type="text" id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" class="input-text admin__control-text <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= /* @escapeNotVerified */ $_option->getMaxCharacters() ? ' validate-length maximum-length-' . $_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" value="<?= $block->escapeHtml($block->getDefaultValue()) ?>" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>" /> - <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA): ?> - <textarea id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" class="admin__control-textarea <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= /* @escapeNotVerified */ $_option->getMaxCharacters() ? ' validate-length maximum-length-' . $_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" rows="5" cols="25" price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_option->getPrice(true)) ?>"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD) :?> + <input type="text" + id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" + class="input-text admin__control-text <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= $_option->getMaxCharacters() ? ' validate-length maximum-length-' . (int) $_option->getMaxCharacters() : '' ?> product-custom-option" + name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($block->getDefaultValue()) ?>" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" /> + <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) :?> + <textarea id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" + class="admin__control-textarea <?= $_option->getIsRequire() ? ' required-entry' : '' ?> <?= $_option->getMaxCharacters() ? ' validate-length maximum-length-' . (int) $_option->getMaxCharacters() : '' ?> product-custom-option" + name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + rows="5" + cols="25" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> <?php endif;?> - <?php if ($_option->getMaxCharacters()): ?> - <p class="note"><?= /* @escapeNotVerified */ __('Maximum number of characters:') ?> <strong><?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?></strong></p> + <?php if ($_option->getMaxCharacters()) :?> + <p class="note"><?= $block->escapeHtml(__('Maximum number of characters:')) ?> <strong><?= (int) $_option->getMaxCharacters() ?></strong></p> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml index 487c9b8e8f2b7..4a31c5d9d1d2b 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/qty.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Qty */ ?> @@ -13,9 +10,9 @@ <fieldset id="product_composite_configure_fields_qty" class="fieldset product-composite-qty-block admin__fieldset <?= $block->getIsLastFieldset() ? 'last-fieldset' : '' ?>"> <div class="field admin__field"> - <label class="label admin__field-label"><span><?= /* @escapeNotVerified */ __('Quantity') ?></span></label> + <label class="label admin__field-label"><span><?= $block->escapeHtml(__('Quantity')) ?></span></label> <div class="control admin__field-control"> - <input id="product_composite_configure_input_qty" class="input-text admin__control-text qty" type="text" name="qty" value="<?= /* @escapeNotVerified */ $block->getQtyValue() * 1 ?>"> + <input id="product_composite_configure_input_qty" class="input-text admin__control-text qty" type="text" name="qty" value="<?= /* @noEscape */ $block->getQtyValue() * 1 ?>"> </div> </div> </fieldset> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml index 7c25c3686eadc..66df098a194ae 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml @@ -3,11 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** * @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit */ @@ -17,11 +16,11 @@ <div id="product-template-suggest-container" class="suggest-expandable"> <div class="action-dropdown"> <button type="button" class="action-toggle" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> - <span><?= /* @escapeNotVerified */ $block->getAttributeSetName() ?></span> + <span><?= $block->escapeHtml($block->getAttributeSetName()) ?></span> </button> <ul class="dropdown-menu"> <li><input type="text" id="product-template-suggest" class="search" - placeholder="<?= /* @noEscape */ __('start typing to search template') ?>"/></li> + placeholder="<?= $block->escapeHtmlAttr(__('start typing to search template')) ?>"/></li> </ul> </div> </div> @@ -30,32 +29,32 @@ <input type="checkbox" id="product-online-switcher" name="product-online-switcher" /> <label class="switcher-label" for="product-online-switcher" - data-text-on="<?= /* @escapeNotVerified */ __('Product online') ?>" - data-text-off="<?= /* @escapeNotVerified */ __('Product offline') ?>" - title="<?= /* @escapeNotVerified */ __('Product online status') ?>"></label> + data-text-on="<?= $block->escapeHtmlAttr(__('Product online')) ?>" + data-text-off="<?= $block->escapeHtmlAttr(__('Product offline')) ?>" + title="<?= $block->escapeHtmlAttr(__('Product online status')) ?>"></label> </div> - <?php if ($block->getProductId()): ?> + <?php if ($block->getProductId()) :?> <?= $block->getDeleteButtonHtml() ?> <?php endif; ?> - <?php if ($block->getProductSetId()): ?> + <?php if ($block->getProductSetId()) :?> <?= $block->getChangeAttributeSetButtonHtml() ?> <?= $block->getSaveSplitButtonHtml() ?> <?php endif; ?> <?= $block->getBackButtonHtml() ?> </div> </div> -<?php if ($block->getUseContainer()): ?> -<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" enctype="multipart/form-data" - data-form="edit-product" data-product-id="<?= /* @escapeNotVerified */ $block->getProduct()->getId() ?>"> +<?php if ($block->getUseContainer()) :?> +<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data" + data-form="edit-product" data-product-id="<?= $block->escapeHtmlAttr($block->getProduct()->getId()) ?>"> <?php endif; ?> <?= $block->getBlockHtml('formkey') ?> <div data-role="tabs" id="product-edit-form-tabs"></div> <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> <?= $block->getChildHtml('product-type-tabs') ?> - <input type="hidden" id="product_type_id" value="<?= /* @escapeNotVerified */ $block->getProduct()->getTypeId() ?>"/> - <input type="hidden" id="attribute_set_id" value="<?= /* @escapeNotVerified */ $block->getProduct()->getAttributeSetId() ?>"/> + <input type="hidden" id="product_type_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> + <input type="hidden" id="attribute_set_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> <button type="submit" class="hidden"></button> -<?php if ($block->getUseContainer()): ?> +<?php if ($block->getUseContainer()) :?> </form> <?php endif; ?> <script> @@ -130,10 +129,10 @@ require([ } } }); - $form.mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); + $form.mage('validation', {validationUrl: '<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>'}); - var masks = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; - var availablePlaceholders = <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; + var masks = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; + var availablePlaceholders = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; var Autogenerator = function(masks) { this._masks = masks || {}; this._fieldReverseIndex = this._buildReverseIndex(this._masks); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml index d1591d70945cf..056cf014f769a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml @@ -4,18 +4,22 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute */ ?> -<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="attributes-edit-form" class="attributes-edit-form" enctype="multipart/form-data"> +<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" + method="post" + id="attributes-edit-form" + class="attributes-edit-form" + enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> -require(['jquery', "mage/mage"], function(jQuery){ - - jQuery('#attributes-edit-form').mage('form') - .mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); - -}); +<script type="text/x-magento-init"> + { + "#attributes-edit-form": { + "Magento_Catalog/catalog/product/edit/attribute": { + "validationUrl": "<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>" + } + } + } </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index efc06d675c369..aea5cb7d00333 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** @var Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory $block */ ?> <script> @@ -30,329 +29,373 @@ }); </script> +<?php +$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); +if (!is_numeric($defaultMinSaleQty)) { + $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); + $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; +} +?> <div class="fieldset-wrapper form-inline advanced-inventory-edit"> <div class="fieldset-wrapper-title"> <strong class="title"> - <span><?= /* @escapeNotVerified */ __('Advanced Inventory') ?></span> + <span><?= $block->escapeHtml(__('Advanced Inventory')) ?></span> </strong> </div> <div class="fieldset-wrapper-content"> <fieldset class="fieldset" id="table_cataloginventory"> <div class="field"> <label class="label" for="inventory_manage_stock"> - <span><?= /* @escapeNotVerified */ __('Manage Stock') ?></span> + <span><?= $block->escapeHtml(__('Manage Stock')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> - <select id="inventory_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[manage_stock]" + <select id="inventory_manage_stock" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[manage_stock]" class="select" disabled="disabled"> - <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> - <option - value="0"<?php if ($block->getFieldValue('manage_stock') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> + <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="0" + <?php if ($block->getFieldValue('manage_stock') == 0) :?> + selected="selected" + <?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> </select> </div> <div class="field choice"> - <input name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_manage_stock]" type="checkbox" + <input name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_manage_stock]" type="checkbox" id="inventory_use_config_manage_stock" data-role="toggle-editability" value="1" checked="checked" disabled="disabled"/> <label for="inventory_use_config_manage_stock" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_manage_stock_checkbox" data-role="toggle-editability-all"/> <label for="inventory_manage_stock_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field required"> <label class="label" for="inventory_qty"> - <span><?= /* @escapeNotVerified */ __('Qty') ?></span> + <span><?= $block->escapeHtml(__('Qty')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text required-entry validate-number" id="inventory_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('qty') * 1 ?>" disabled="disabled"/> + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[qty]" + value="<?= /* @noEscape */ $block->getDefaultConfigValue('qty') * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> <input type="checkbox" id="inventory_qty_checkbox" data-role="toggle-editability-all"/> <label for="inventory_qty_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field with-addon"> <label class="label" for="inventory_min_qty"> - <span><?= /* @escapeNotVerified */ __('Out-of-Stock Threshold') ?></span> + <span><?= $block->escapeHtml(__('Out-of-Stock Threshold')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_qty') * 1 ?>" disabled="disabled"/> + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[min_qty]" + value="<?= /* @noEscape */ $block->getDefaultConfigValue('min_qty') * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_min_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_min_qty]" value="1" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_min_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> <label for="inventory_use_config_min_qty" class="label"> - <span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span> + <span><?= $block->escapeHtml(__('Use Config Settings')) ?></span> </label> </div> <div class="field choice"> <input type="checkbox" id="inventory_min_qty_checkbox" data-role="toggle-editability-all"/> <label for="inventory_min_qty_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_min_sale_qty"> - <span><?= /* @escapeNotVerified */ __('Minimum Qty Allowed in Shopping Cart') ?></span> + <span><?= $block->escapeHtml(__('Minimum Qty Allowed in Shopping Cart')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[min_sale_qty]" + value="<?= /* @noEscape */ $defaultMinSaleQty * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_min_sale_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_min_sale_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_min_sale_qty]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_min_sale_qty" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_min_sale_qty_checkbox" data-role="toggle-editability-all"/> <label for="inventory_min_sale_qty_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_max_sale_qty"> - <span><?= /* @escapeNotVerified */ __('Maximum Qty Allowed in Shopping Cart') ?></span> + <span><?= $block->escapeHtml(__('Maximum Qty Allowed in Shopping Cart')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text validate-number" id="inventory_max_sale_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[max_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('max_sale_qty') * 1 ?>" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[max_sale_qty]" + value="<?= /* @noEscape */ $block->getDefaultConfigValue('max_sale_qty') * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> - <input type="checkbox" id="inventory_use_config_max_sale_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_max_sale_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + <input type="checkbox" + id="inventory_use_config_max_sale_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_max_sale_qty]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_max_sale_qty" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_max_sale_checkbox" data-role="toggle-editability-all"/> <label for="inventory_max_sale_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_is_qty_decimal"> - <span><?= /* @escapeNotVerified */ __('Qty Uses Decimals') ?></span> + <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <select id="inventory_is_qty_decimal" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[is_qty_decimal]" class="select" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_qty_decimal]" + class="select" disabled="disabled"> - <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> - <option - value="1"<?php if ($block->getDefaultConfigValue('is_qty_decimal') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> + <option value="0"><?= $block->escapeHtml(__('No')) ?></option> + <option value="1" + <?php if ($block->getDefaultConfigValue('is_qty_decimal') == 1) :?> + selected="selected" + <?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> </select> </div> <div class="field choice"> <input type="checkbox" id="inventory_is_qty_decimal_checkbox" data-role="toggle-editability-all"/> <label for="inventory_is_qty_decimal_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_backorders"> - <span><?= /* @escapeNotVerified */ __('Backorders') ?></span> + <span><?= $block->escapeHtml(__('Backorders')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> - <select id="inventory_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[backorders]" - class="select" disabled="disabled"> - <?php foreach ($block->getBackordersOption() as $option): ?> + <select id="inventory_backorders" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[backorders]" + class="select" + disabled="disabled"> + <?php foreach ($block->getBackordersOption() as $option) :?> <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) ? ' selected="selected"' : '' ?> <option - value="<?= /* @escapeNotVerified */ $option['value'] ?>"<?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> + value="<?= $block->escapeHtmlAttr($option['value']) ?>"<?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> <?php endforeach; ?> </select> </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_backorders" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_backorders]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_backorders]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_backorders" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_backorders_checkbox" data-role="toggle-editability-all"/> - <label for="inventory_backorders_checkbox" class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + <label for="inventory_backorders_checkbox" + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_notify_stock_qty"> - <span><?= /* @escapeNotVerified */ __('Notify for Quantity Below') ?></span> + <span><?= $block->escapeHtml(__('Notify for Quantity Below')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text validate-number" id="inventory_notify_stock_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[notify_stock_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('notify_stock_qty') * 1 ?>" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[notify_stock_qty]" + value="<?= /* @noEscape */ $block->getDefaultConfigValue('notify_stock_qty') * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> - <input type="checkbox" id="inventory_use_config_notify_stock_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_notify_stock_qty]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + <input type="checkbox" + id="inventory_use_config_notify_stock_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_notify_stock_qty]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_notify_stock_qty" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_notify_stock_qty_checkbox" data-role="toggle-editability-all"/> <label for="inventory_notify_stock_qty_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_enable_qty_increments"> - <span><?= /* @escapeNotVerified */ __('Enable Qty Increments') ?></span> + <span><?= $block->escapeHtml(__('Enable Qty Increments')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <select id="inventory_enable_qty_increments" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[enable_qty_increments]" class="select" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[enable_qty_increments]" + class="select" disabled="disabled"> - <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> - <option - value="0"<?php if ($block->getDefaultConfigValue('enable_qty_increments') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> + <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="0" + <?php if ($block->getDefaultConfigValue('enable_qty_increments') == 0) :?> + selected="selected" + <?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> </select> </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_enable_qty_increments" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_enable_qty_increments" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_enable_qty_increments_checkbox" data-role="toggle-editability-all"/> <label for="inventory_enable_qty_increments_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_qty_increments"> - <span><?= /* @escapeNotVerified */ __('Qty Increments') ?></span> + <span><?= $block->escapeHtml(__('Qty Increments')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <input type="text" class="input-text validate-number" id="inventory_qty_increments" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[qty_increments]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('qty_increments') * 1 ?>" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[qty_increments]" + value="<?= /* @noEscape */ $block->getDefaultConfigValue('qty_increments') * 1 ?>" disabled="disabled"/> </div> <div class="field choice"> - <input type="checkbox" id="inventory_use_config_qty_increments" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[use_config_qty_increments]" value="1" data-role="toggle-editability" checked="checked" disabled="disabled"/> + <input type="checkbox" + id="inventory_use_config_qty_increments" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_qty_increments]" + value="1" + data-role="toggle-editability" + checked="checked" + disabled="disabled"/> <label for="inventory_use_config_qty_increments" - class="label"><span><?= /* @escapeNotVerified */ __('Use Config Settings') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Use Config Settings')) ?></span></label> </div> <div class="field choice"> <input type="checkbox" id="inventory_qty_increments_checkbox" data-role="toggle-editability-all"/> <label for="inventory_qty_increments_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> <div class="field"> <label class="label" for="inventory_stock_availability"> - <span><?= /* @escapeNotVerified */ __('Stock Availability') ?></span> + <span><?= $block->escapeHtml(__('Stock Availability')) ?></span> </label> <div class="control"> <div class="fields-group-2"> <div class="field"> <select id="inventory_stock_availability" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" disabled="disabled"> - <option value="1"><?= /* @escapeNotVerified */ __('In Stock') ?></option> - <option - value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0): ?> selected<?php endif; ?>><?= /* @escapeNotVerified */ __('Out of Stock') ?></option> + <option value="1"><?= $block->escapeHtml(__('In Stock')) ?></option> + <option value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0) :?> selected<?php endif; ?>><?= $block->escapeHtml(__('Out of Stock')) ?></option> </select> </div> <div class="field choice"> <input type="checkbox" id="inventory_stock_availability_checkbox" data-role="toggle-editability-all"/> <label for="inventory_stock_availability_checkbox" - class="label"><span><?= /* @escapeNotVerified */ __('Change') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Change')) ?></span></label> </div> </div> </div> - <div class="field-service" value-scope="<?= /* @escapeNotVerified */ __('[GLOBAL]') ?>"></div> + <div class="field-service" value-scope="<?= $block->escapeHtmlAttr(__('[GLOBAL]')) ?>"></div> </div> </fieldset> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml index cd297a7bbf27b..98b06050e0d1d 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml @@ -4,29 +4,35 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Websites */ ?> <div class="fieldset-wrapper" id="add-products-to-website-wrapper"> <fieldset class="fieldset" id="grop_fields"> <legend class="legend"> - <span><?= /* @escapeNotVerified */ __('Add Product To Websites') ?></span> + <span><?= $block->escapeHtml(__('Add Product To Websites')) ?></span> </legend> <br> <div class="store-scope"> <div class="store-tree" id="add-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website): ?> + <?php foreach ($block->getWebsiteCollection() as $_website) :?> <div class="website-name"> - <input name="add_website_ids[]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->getWebsitesReadonly()): ?>disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox" /> - <label for="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <input name="add_website_ids[]" + value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" + <?php if ($block->getWebsitesReadonly()) :?> + disabled="disabled" + <?php endif;?> + class="checkbox website-checkbox" + id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" + type="checkbox" /> + <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> </div> - <dl class="webiste-groups" id="add_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <dl class="webiste-groups" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> + <?php foreach ($block->getGroupCollection($_website) as $_group) :?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) :?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> @@ -44,27 +50,35 @@ <div class="fieldset-wrapper" id="remove-products-to-website-wrapper"> <fieldset class="fieldset" id="grop_fields"> <legend class="legend"> - <span><?= /* @escapeNotVerified */ __('Remove Product From Websites') ?></span> + <span><?= $block->escapeHtml(__('Remove Product From Websites')) ?></span> </legend> <br> <div class="messages"> <div class="message message-notice"> - <div><?= /* @escapeNotVerified */ __('To hide an item in catalog or search results, set the status to "Disabled".') ?></div> + <div><?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?></div> </div> </div> <div class="store-scope"> <div class="store-tree" id="remove-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website): ?> + <?php foreach ($block->getWebsiteCollection() as $_website) :?> <div class="website-name"> - <input name="remove_website_ids[]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->getWebsitesReadonly()): ?>disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox" /> - <label for="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <input name="remove_website_ids[]" + value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" + <?php if ($block->getWebsitesReadonly()) :?> + disabled="disabled" + <?php endif;?> + class="checkbox website-checkbox" + id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" + type="checkbox" /> + <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> </div> - <dl class="webiste-groups" id="remove_product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <dl class="webiste-groups" + id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> + <?php foreach ($block->getGroupCollection($_website) as $_group) :?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) :?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml index 6a5f6c4648494..d073053e2f854 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml @@ -4,9 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ +/* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ ?> <script id="product-template-selector-template" type="text/x-magento-template"> <% if (!data.term && data.items.length && !data.allShown()) { %> @@ -14,7 +14,7 @@ <% } %> <ul data-mage-init='{"menu":[]}'> <% _.each(data.items, function(value) { %> - <li <%- data.optionData(value) %>><a href="#"><%- value.label %></a></li> + <li <%= data.optionData(value) %>><a href="#"><%- value.label %></a></li> <% }); %> </ul> <% if (!data.term && data.items.length && !data.allShown()) { %> @@ -32,7 +32,7 @@ } }); $suggest - .mage('suggest',<?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSelectorOptions()) ?>) + .mage('suggest',<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) .on('suggestselect', function (e, ui) { if (ui.item.id) { $('[data-form=edit-product]').trigger('changeAttributeSet', ui.item); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml index 84c3257840259..f12a99e6c7843 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml @@ -5,7 +5,7 @@ */ /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\NewCategory */ ?> -<div id="<?= /* @escapeNotVerified */ $block->getNameInLayout() ?>" style="display:none"> +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> <?= $block->getFormHtml() ?> <?= $block->getAfterElementHtml() ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml index 2570a5d712675..ad38d250a3345 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml @@ -3,16 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ ?> <div class="fieldset-wrapper" id="product-custom-options-wrapper" data-block="product-custom-options"> <div class="fieldset-wrapper-title"> <strong class="title"> - <span><?= /* @escapeNotVerified */ __('Custom Options') ?></span> + <span><?= $block->escapeHtml(__('Custom Options')) ?></span> </strong> </div> <div class="fieldset-wrapper-content" id="product-custom-options-content" data-role="product-custom-options-content"> @@ -20,7 +17,7 @@ <div class="messages"> <div class="message message-error" id="dynamic-price-warning" style="display: none;"> <div class="message-inner"> - <div class="message-content"><?= /* @escapeNotVerified */ __('We can\'t save custom-defined options for bundles with dynamic pricing.') ?></div> + <div class="message-content"><?= $block->escapeHtml(__('We can\'t save custom-defined options for bundles with dynamic pricing.')) ?></div> </div> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml index d2bca5ce17321..713366e73aba5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ ?> <?= $block->getTemplatesHtml() ?> @@ -19,30 +18,52 @@ <span id="option_<%- data.id %>_header_title"><%- data.title %></span> </strong> <div class="actions"> - <button type="button" title="<?= /* @escapeNotVerified */ __('Delete Custom Option') ?>" class="action-delete" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_delete"> - <span><?= /* @escapeNotVerified */ __('Delete Custom Option') ?></span> + <button type="button" + title="<?= $block->escapeHtmlAttr(__('Delete Custom Option')) ?>" + class="action-delete" + id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_delete"> + <span><?= $block->escapeHtml(__('Delete Custom Option')) ?></span> </button> </div> - <div id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_move" data-role="draggable-handle" class="draggable-handle" - title="<?= /* @escapeNotVerified */ __('Sort Custom Options') ?>"></div> + <div id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_move" + data-role="draggable-handle" + class="draggable-handle" + title="<?= $block->escapeHtmlAttr(__('Sort Custom Options')) ?>"></div> </div> <div class="fieldset-wrapper-content in collapse" id="<%- data.id %>-content"> <fieldset class="fieldset"> - <fieldset class="fieldset-alt" id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>"> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_is_delete" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][is_delete]" type="hidden" value=""/> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_previous_type" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][previous_type]" type="hidden" value="<%- data.type %>"/> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_previous_group" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][previous_group]" type="hidden" value=""/> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_id" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][id]" type="hidden" value="<%- data.id %>"/> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_option_id" name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][option_id]" type="hidden" value="<%- data.option_id %>"/> - <input name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][sort_order]" type="hidden" value="<%- data.sort_order %>"/> + <fieldset class="fieldset-alt" id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>"> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_is_delete" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][is_delete]" + type="hidden" + value=""/> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_previous_type" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][previous_type]" + type="hidden" + value="<%- data.type %>"/> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_previous_group" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][previous_group]" + type="hidden" + value=""/> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_id" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][id]" + type="hidden" + value="<%- data.id %>"/> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_option_id" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][option_id]" + type="hidden" + value="<%- data.option_id %>"/> + <input name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][sort_order]" + type="hidden" + value="<%- data.sort_order %>"/> <div class="field field-option-title required"> - <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title"> - <?= /* @escapeNotVerified */ __('Option Title') ?> + <label class="label" for="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title"> + <?= $block->escapeHtml(__('Option Title')) ?> </label> <div class="control"> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title" - name="<?= /* @escapeNotVerified */ $block->getFieldName() ?>[<%- data.id %>][title]" + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title" + name="<?= /* @noEscape */ $block->getFieldName() ?>[<%- data.id %>][title]" class="required-entry input-text" type="text" value="<%- data.title %>" @@ -54,8 +75,8 @@ </div> <div class="field field-option-input-type required"> - <label class="label" for="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_title"> - <?= /* @escapeNotVerified */ __('Input Type') ?> + <label class="label" for="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_title"> + <?= $block->escapeHtml(__('Input Type')) ?> </label> <div class="control opt-type"> <?= $block->getTypeSelectHtml() ?> @@ -64,9 +85,12 @@ <div class="field field-option-req"> <div class="control"> - <input id="<?= /* @escapeNotVerified */ $block->getFieldId() ?>_<%- data.id %>_required" class="is-required" type="checkbox" checked="checked"/> + <input id="<?= /* @noEscape */ $block->getFieldId() ?>_<%- data.id %>_required" + class="is-required" + type="checkbox" + checked="checked"/> <label for="field-option-req"> - <?= /* @escapeNotVerified */ __('Required') ?> + <?= $block->escapeHtml(__('Required')) ?> </label> <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> </div> @@ -78,7 +102,7 @@ </script> <div id="import-container" style="display: none;"></div> -<?php if (!$block->isReadonly()): ?> +<?php if (!$block->isReadonly()) :?> <div><input type="hidden" name="affect_product_custom_options" value="1"/></div> <?php endif; ?> <script> @@ -89,21 +113,21 @@ require([ jQuery(function ($) { var fieldSet = $('[data-block=product-custom-options]'); - fieldSet.customOptions(<?php /* @escapeNotVerified */ echo $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( + fieldSet.customOptions(<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( [ 'fieldId' => $block->getFieldId(), - 'productGridUrl' => $block->getProductGridUrl(), + 'productGridUrl' => $block->escapeUrl($block->getProductGridUrl()), 'formKey' => $block->getFormKey(), - 'customOptionsUrl' => $block->getCustomOptionsUrl(), - 'isReadonly' => $block->isReadonly(), - 'itemCount' => $block->getItemCount(), - 'currentProductId' => $block->getCurrentProductId(), + 'customOptionsUrl' => $block->escapeUrl($block->getCustomOptionsUrl()), + 'isReadonly' => (bool) $block->isReadonly(), + 'itemCount' => (int) $block->getItemCount(), + 'currentProductId' => (int) $block->getCurrentProductId(), ] )?>); //adding data to templates <?php /** @var $_value \Magento\Framework\DataObject */ ?> - <?php foreach ($block->getOptionValues() as $_value): ?> - fieldSet.customOptions('addOption', <?= /* @escapeNotVerified */ $_value->toJson() ?>); + <?php foreach ($block->getOptionValues() as $_value) :?> + fieldSet.customOptions('addOption', <?= /* @noEscape */ $_value->toJson() ?>); <?php endforeach; ?> }); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml index 07ce6e5d86256..2063609bf0568 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Date */ ?> <script id="custom-option-date-type-template" type="text/x-magento-template"> @@ -14,10 +12,10 @@ <thead> <tr class="headings"> <?php if ($block->getCanReadPrice() !== false) : ?> - <th><?= /* @escapeNotVerified */ __('Price') ?></th> - <th><?= /* @escapeNotVerified */ __('Price Type') ?></th> + <th><?= $block->escapeHtml(__('Price')) ?></th> + <th><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> - <th><?= /* @escapeNotVerified */ __('SKU') ?></th> + <th><?= $block->escapeHtml(__('SKU')) ?></th> </tr> </thead> <tr> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml index 693c98fc02cab..c6682f565e85a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/file.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\File */ ?> <script id="custom-option-file-type-template" type="text/x-magento-template"> @@ -14,38 +12,44 @@ <thead> <tr> <?php if ($block->getCanReadPrice() !== false) : ?> - <th><?= /* @escapeNotVerified */ __('Price') ?></th> - <th><?= /* @escapeNotVerified */ __('Price Type') ?></th> + <th><?= $block->escapeHtml(__('Price')) ?></th> + <th><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> - <th><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th><?= /* @escapeNotVerified */ __('Compatible File Extensions') ?></th> - <th><?= /* @escapeNotVerified */ __('Maximum Image Size') ?></th> + <th><?= $block->escapeHtml(__('SKU')) ?></th> + <th><?= $block->escapeHtml(__('Compatible File Extensions')) ?></th> + <th><?= $block->escapeHtml(__('Maximum Image Size')) ?></th> </tr> </thead> <tr> <?php if ($block->getCanReadPrice() !== false) : ?> - <td class="opt-price"> - <input name="product[options][<%- data.option_id %>][price]" data-store-label="<%- data.price %>" - class="input-text validate-zero-or-greater" type="text" value="<%- data.price %>" - <?php if ($block->getCanEditPrice() === false) : ?> - disabled="disabled" - <?php endif; ?>> - </td> - <td class="opt-price-type"><?= $block->getPriceTypeSelectHtml('data-attr="price-type"') ?><%- data.checkboxScopePrice %></td> + <td class="opt-price"> + <input name="product[options][<%- data.option_id %>][price]" data-store-label="<%- data.price %>" + class="input-text validate-zero-or-greater" type="text" value="<%- data.price %>" + <?php if ($block->getCanEditPrice() === false) : ?> + disabled="disabled" + <?php endif; ?>> + </td> + <td class="opt-price-type"><?= $block->getPriceTypeSelectHtml('data-attr="price-type"') ?><%- data.checkboxScopePrice %></td> <?php else : ?> - <input name="product[options][<%- data.option_id %>][price]" type="hidden"> - <input id="product_option_<%- data.option_id %>_price_type" name="product[options][<%- data.option_id %>][price_type]" type="hidden"> + <input name="product[options][<%- data.option_id %>][price]" type="hidden"> + <input id="product_option_<%- data.option_id %>_price_type" name="product[options][<%- data.option_id %>][price_type]" type="hidden"> <?php endif; ?> <td> <input name="product[options][<%- data.option_id %>][sku]" class="input-text" type="text" value="<%- data.sku %>"> </td> <td> <input name="product[options][<%- data.option_id %>][file_extension]" class="input-text" type="text" value="<%- data.file_extension %>"> + <div class="note"><?= $block->escapeHtml(__('Enter separated extensions, like: png, jpg, gif.')) ?></div> </td> - <td class="col-file"><?php /* @escapeNotVerified */ echo __('%1 <span>x</span> %2 <span>px.</span>', - '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_x]" value="<%- data.image_size_x %>">', - '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_y]" value="<%- data.image_size_y %>">') ?> - <div class="note"><?= /* @escapeNotVerified */ __('Please leave blank if it is not an image.') ?></div> + <td class="col-file"><?= $block->escapeHtml( + __( + '%1 <span>x</span> %2 <span>px.</span>', + '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_x]" value="<%- data.image_size_x %>">', + '<input class="input-text" type="text" name="product[options][<%- data.option_id %>][image_size_y]" value="<%- data.image_size_y %>">' + ), + ['span', 'input'] + ) ?> + <div class="note"><?= $block->escapeHtml(__('Please leave blank if it is not an image.')) ?></div> </td> </tr> </table> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml index e8c398228a469..c7ff03a08d954 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/select.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Select */ ?> <script id="custom-option-select-type-template" type="text/x-magento-template"> @@ -14,12 +12,12 @@ <thead> <tr> <th class="col-draggable"> </th> - <th class="col-name required"><?= /* @escapeNotVerified */ __('Title') ?><span class="required">*</span></th> + <th class="col-name required"><?= $block->escapeHtml(__('Title')) ?><span class="required">*</span></th> <?php if ($block->getCanReadPrice() !== false) : ?> - <th class="col-price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col-price-type"><?= /* @escapeNotVerified */ __('Price Type') ?></th> + <th class="col-price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col-price-type"><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> - <th class="col-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> + <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> <th class="col-actions"> </th> </tr> </thead> @@ -38,7 +36,7 @@ <tr id="product_option_<%- data.id %>_select_<%- data.select_id %>"> <td class="col-draggable"> <div data-role="draggable-handle" class="draggable-handle" - title="<?= /* @escapeNotVerified */ __('Sort Custom Option') ?>"></div> + title="<?= $block->escapeHtmlAttr(__('Sort Custom Option')) ?>"></div> <input name="product[options][<%- data.id %>][values][<%- data.select_id %>][sort_order]" type="hidden" value="<%- data.sort_order %>"> </td> <td class="col-name select-opt-title"> @@ -58,7 +56,7 @@ <?php endif; ?>> </td> <td class="col-price-type select-opt-price-type"> - <?= /* @escapeNotVerified */ $block->getPriceTypeSelectHtml('data-attr="price-type" <% if (typeof data.scopePriceDisabled != "undefined" && data.scopePriceDisabled != null) { %> disabled="disabled" <% } %>') ?><%- data.checkboxScopePrice %> + <?= /* @noEscape */ $block->getPriceTypeSelectHtml('data-attr="price-type" <% if (typeof data.scopePriceDisabled != "undefined" && data.scopePriceDisabled != null) { %> disabled="disabled" <% } %>') ?><%- data.checkboxScopePrice %> </td> <?php else : ?> <input id="product_option_<%- data.id %>_select_<%- data.select_id %>_price" name="product[options][<%- data.id %>][values][<%- data.select_id %>][price]" type="hidden"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml index c9d7190589ff5..89da5d633ef4d 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/text.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Type\Text */ ?> <script id="custom-option-text-type-template" type="text/x-magento-template"> @@ -14,11 +12,11 @@ <thead> <tr> <?php if ($block->getCanReadPrice() !== false) : ?> - <th class="type-price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="type-type"><?= /* @escapeNotVerified */ __('Price Type') ?></th> + <th class="type-price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="type-type"><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> - <th class="type-sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="type-last last"><?= /* @escapeNotVerified */ __('Max Characters') ?></th> + <th class="type-sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="type-last last"><?= $block->escapeHtml(__('Max Characters')) ?></th> </tr> </thead> <tr> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml index 57715744823d6..30a1da9fdb71c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price\Tier */ $element = $block->getElement(); @@ -20,28 +20,27 @@ $element = $block->getElement(); <?php $_showWebsite = $block->isShowWebsiteColumn(); ?> <?php $_showWebsite = $block->isMultiWebsites(); ?> -<div class="field" id="attribute-<?= /* @escapeNotVerified */ $_htmlId ?>-container" data-attribute-code="<?= /* @escapeNotVerified */ $_htmlId ?>" - data-apply-to="<?= $block->escapeHtml( - $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ) - )?>"> - <label class="label"><span><?= /* @escapeNotVerified */ $block->getElement()->getLabel() ?></span></label> +<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" data-apply-to="<?= $block->escapeHtml( + $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( + $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] + ) +)?>"> + <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> <div class="control"> <table class="admin__control-table tiers_table" id="tiers_table"> <thead> <tr> - <th class="col-websites" <?php if (!$_showWebsite): ?>style="display:none"<?php endif; ?>><?= /* @escapeNotVerified */ __('Web Site') ?></th> - <th class="col-customer-group"><?= /* @escapeNotVerified */ __('Customer Group') ?></th> - <th class="col-qty required"><?= /* @escapeNotVerified */ __('Quantity') ?></th> - <th class="col-price required"><?= /* @escapeNotVerified */ $block->getPriceColumnHeader(__('Item Price')) ?></th> - <th class="col-delete"><?= /* @escapeNotVerified */ __('Action') ?></th> + <th class="col-websites" <?php if (!$_showWebsite) :?>style="display:none"<?php endif; ?>><?= $block->escapeHtml(__('Web Site')) ?></th> + <th class="col-customer-group"><?= $block->escapeHtml(__('Customer Group')) ?></th> + <th class="col-qty required"><?= $block->escapeHtml(__('Quantity')) ?></th> + <th class="col-price required"><?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?></th> + <th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> - <tbody id="<?= /* @escapeNotVerified */ $_htmlId ?>_container"></tbody> + <tbody id="<?= /* @noEscape */ $_htmlId ?>_container"></tbody> <tfoot> <tr> - <td colspan="<?php if (!$_showWebsite): ?>4<?php else: ?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> + <td colspan="<?php if (!$_showWebsite) :?>4<?php else :?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> </tr> </tfoot> </table> @@ -55,39 +54,39 @@ require([ //<![CDATA[ var tierPriceRowTemplate = '<tr>' - + '<td class="col-websites"<?php if (!$_showWebsite): ?> style="display:none"<?php endif; ?>>' - + '<select class="<?= /* @escapeNotVerified */ $_htmlClass ?> required-entry" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' - <?php foreach ($block->getWebsites() as $_websiteId => $_info): ?> - + '<option value="<?= /* @escapeNotVerified */ $_websiteId ?>"><?= $block->escapeJs($_info['name']) ?><?php if (!empty($_info['currency'])): ?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' + + '<td class="col-websites"<?php if (!$_showWebsite) :?> style="display:none"<?php endif; ?>>' + + '<select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' + <?php foreach ($block->getWebsites() as $_websiteId => $_info) :?> + + '<option value="<?= $block->escapeHtmlAttr($_websiteId) ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) :?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' <?php endforeach ?> + '</select></td>' - + '<td class="col-customer-group"><select class="<?= /* @escapeNotVerified */ $_htmlClass ?> custgroup required-entry" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' - <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName): ?> - + '<option value="<?= /* @escapeNotVerified */ $_groupId ?>"><?= $block->escapeJs($_groupName) ?></option>' + + '<td class="col-customer-group"><select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> custgroup required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' + <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName) :?> + + '<option value="<?= $block->escapeHtmlAttr($_groupId) ?>"><?= $block->escapeHtml($_groupName) ?></option>' <?php endforeach ?> + '</select></td>' + '<td class="col-qty">' - + '<input class="<?= /* @escapeNotVerified */ $_htmlClass ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' - + '<span><?= /* @escapeNotVerified */ __("and above") ?></span>' + + '<input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' + + '<span><?= $block->escapeHtml(__("and above")) ?></span>' + '</td>' - + '<td class="col-price"><input class="<?= /* @escapeNotVerified */ $_htmlClass ?> required-entry <?= /* @escapeNotVerified */ $_priceValueValidation ?>" type="text" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' - + '<td class="col-delete"><input type="hidden" name="<?= /* @escapeNotVerified */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' - + '<button title="<?= /* @escapeNotVerified */ $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' - + '<span><?= /* @escapeNotVerified */ __("Delete") ?></span></button></td>' + + '<td class="col-price"><input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry <?= $block->escapeHtmlAttr($_priceValueValidation) ?>" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' + + '<td class="col-delete"><input type="hidden" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' + + '<button title="<?= $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' + + '<span><?= $block->escapeHtml(__("Delete")) ?></span></button></td>' + '</tr>'; var tierPriceControl = { template: mageTemplate(tierPriceRowTemplate), itemsCount: 0, addItem : function () { - <?php if ($_readonly): ?> + <?php if ($_readonly) :?> if (arguments.length < 4) { return; } <?php endif; ?> var data = { - website_id: '<?= /* @escapeNotVerified */ $block->getDefaultWebsite() ?>', - group: '<?= /* @escapeNotVerified */ $block->getDefaultCustomerGroup() ?>', + website_id: '<?= (int) $block->getDefaultWebsite() ?>', + group: '<?= (int) $block->getDefaultCustomerGroup() ?>', qty: '', price: '', readOnly: false, @@ -104,7 +103,7 @@ var tierPriceControl = { data.readOnly = arguments[4]; } - Element.insert($('<?= /* @escapeNotVerified */ $_htmlId ?>_container'), { + Element.insert($('<?= $block->escapeJs($_htmlId) ?>_container'), { bottom : this.template({ data: data }) @@ -113,7 +112,7 @@ var tierPriceControl = { $('tier_price_row_' + data.index + '_cust_group').value = data.group; $('tier_price_row_' + data.index + '_website').value = data.website_id; - <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()):?> + <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()) :?> var wss = $('tier_price_row_' + data.index + '_website'); var txt = wss.options[wss.selectedIndex].text; @@ -128,11 +127,11 @@ var tierPriceControl = { $('tier_price_row_'+data.index+'_delete_button').hide(); } - <?php if ($_readonly): ?> - $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').select('input', 'select').each(this.disableElement); - $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').up('table').select('button').each(this.disableElement); - <?php else: ?> - $('<?= /* @escapeNotVerified */ $_htmlId ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); + <?php if ($_readonly) :?> + $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(this.disableElement); + $('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button').each(this.disableElement); + <?php else :?> + $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); <?php endif; ?> }, disableElement: function(el) { @@ -150,11 +149,11 @@ var tierPriceControl = { return false; } }; -<?php foreach ($block->getValues() as $_item): ?> -tierPriceControl.addItem('<?= /* @escapeNotVerified */ $_item['website_id'] ?>', '<?= /* @escapeNotVerified */ $_item['cust_group'] ?>', '<?= /* @escapeNotVerified */ $_item['price_qty']*1 ?>', '<?= /* @escapeNotVerified */ $_item['price'] ?>', <?= (int)!empty($_item['readonly']) ?>); +<?php foreach ($block->getValues() as $_item) :?> +tierPriceControl.addItem('<?= $block->escapeJs($_item['website_id']) ?>', '<?= $block->escapeJs($_item['cust_group']) ?>', '<?= /* @noEscape */ $_item['price_qty']*1 ?>', '<?= $block->escapeJs($_item['price']) ?>', <?= /* @noEscape */ (int)!empty($_item['readonly']) ?>); <?php endforeach; ?> -<?php if ($_readonly): ?> -$('<?= /* @escapeNotVerified */ $_htmlId ?>_container').up('table').select('button') +<?php if ($_readonly) :?> +$('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button') .each(tierPriceControl.disableElement); <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml index 44fdb75cdac21..0c1da98c7d85a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/serializer.phtml @@ -4,9 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Ajax\Serializer */ ?> +// phpcs:disable Magento2.Security.InsecureFunction.DiscouragedWithAlternative <?php $_id = 'id_' . md5(microtime()) ?> -<input type="hidden" name="<?= /* @escapeNotVerified */ $block->getInputElementName() ?>" value="" id="<?= /* @escapeNotVerified */ $_id ?>" /> +<input type="hidden" + name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" + value="" + id="<?= /* @noEscape */ $_id ?>" /> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml index 8f7f20f32d982..0193d7764cbb5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Websites */ ?> <fieldset id="grop_fields" class="fieldset"> - <legend class="legend"><span><?= /* @escapeNotVerified */ __('Product In Websites') ?></span></legend> + <legend class="legend"><span><?= $block->escapeHtml(__('Product In Websites')) ?></span></legend> <br> - <?php if ($block->getProductId()): ?> + <?php if ($block->getProductId()) :?> <div class="messages"> <div class="message message-notice"> - <?= /* @escapeNotVerified */ __('To hide an item in catalog or search results, set the status to "Disabled".') ?> + <?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?> </div> </div> <?php endif; ?> @@ -21,22 +20,36 @@ <?= $block->getHintHtml() ?> <div class="store-tree"> <?php $_websites = $block->getWebsiteCollection() ?> - <?php foreach ($_websites as $_website): ?> + <?php foreach ($_websites as $_website) :?> <div class="website-name"> - <input name="product[website_ids][]" value="<?= /* @escapeNotVerified */ $_website->getId() ?>" <?php if ($block->isReadonly()): ?> disabled="disabled"<?php endif;?> class="checkbox website-checkbox" id="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>" type="checkbox"<?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1): ?> checked="checked"<?php endif; ?> /> - <label for="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <input name="product[website_ids][]" + value="<?= (int) $_website->getId() ?>" + <?php if ($block->isReadonly()) :?> + disabled="disabled" + <?php endif;?> + class="checkbox website-checkbox" + id="product_website_<?= (int) $_website->getId() ?>" + type="checkbox" + <?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1) :?> + checked="checked" + <?php endif; ?> + /> + <label for="product_website_<?= (int) $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> </div> - <dl class="webiste-groups" id="product_website_<?= /* @escapeNotVerified */ $_website->getId() ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <dl class="webiste-groups" id="product_website_<?= (int) $_website->getId() ?>_data"> + <?php foreach ($block->getGroupCollection($_website) as $_group) :?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) :?> <li> <?= $block->escapeHtml($_store->getName()) ?> - <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())): ?> - <span class="website-<?= /* @escapeNotVerified */ $_website->getId() ?>-select" style="display:none"> - <?= __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)) ?> + <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())) :?> + <span class="website-<?= (int) $_website->getId() ?>-select" style="display:none"> + <?= $block->escapeHtml( + __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)), + ['select', 'option', 'optgroup'] + ) ?> </span> <?php endif; ?> </li> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml index 574c9ee81af7d..60dad8f7bc67c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ $elementName = $block->getElement()->getName() . '[images]'; @@ -15,62 +15,59 @@ $formName = $block->getFormName(); data-mage-init='{"productGallery":{"template":"#<?= $block->getHtmlId() ?>-template"}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" - data-types="<?= $block->escapeHtml( - $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) - ) ?>" - > + data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>" +> <?php if (!$block->getElement()->getReadonly()) {?> <div class="image image-placeholder"> <?= $block->getUploaderHtml() ?> <div class="product-image-wrapper"> <p class="image-placeholder-text"> - <?= /* @escapeNotVerified */ __('Browse to find or drag image here') ?> + <?= $block->escapeHtml(__('Browse to find or drag image here')) ?> </p> </div> </div> <?php } ?> <?php foreach ($block->getImageTypes() as $typeData) { - ?> - <input name="<?= $block->escapeHtml($typeData['name']) ?>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" - class="image-<?= $block->escapeHtml($typeData['code']) ?>" + ?> + <input name="<?= $block->escapeHtmlAttr($typeData['name']) ?>" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" + class="image-<?= $block->escapeHtmlAttr($typeData['code']) ?>" type="hidden" - value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php - + value="<?= $block->escapeHtmlAttr($typeData['value']) ?>"/> + <?php } ?> <script id="<?= $block->getHtmlId() ?>-template" type="text/x-magento-template"> <div class="image item<% if (data.disabled == 1) { %> hidden-for-front<% } %>" data-role="image"> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][position]" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][position]" value="<%- data.position %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" class="position"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][file]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][file]" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" value="<%- data.file %>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][value_id]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][value_id]" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" value="<%- data.value_id %>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][label]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" value="<%- data.label %>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][disabled]" + data-form-part="<?= $block->escapeHtmlAttr(formName) ?>" value="<%- data.disabled %>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][media_type]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][media_type]" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" value="image"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][removed]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][removed]" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" value="" class="is-removed"/> @@ -84,21 +81,21 @@ $formName = $block->getFormName(); <button type="button" class="action-remove" data-role="delete-button" - title="<?= /* @escapeNotVerified */ __('Delete image') ?>"> + title="<?= $block->escapeHtmlAttr(__('Delete image')) ?>"> <span> - <?= /* @escapeNotVerified */ __('Delete image') ?> + <?= $block->escapeHtml(__('Delete image')) ?> </span> </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= /* @escapeNotVerified */ __('Hidden') ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span></div> </div> <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> <div class="item-size"> - <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> + <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> </div> </div> @@ -106,12 +103,9 @@ $formName = $block->getFormName(); <?php foreach ($block->getImageTypes() as $typeData) { ?> - <li data-role-code="<?php /* @escapeNotVerified */ echo $block->escapeHtml( - $typeData['code'] - ) ?>" class="item-role item-role-<?php /* @escapeNotVerified */ echo $block->escapeHtml( - $typeData['code'] - ) ?>"> - <?= /* @escapeNotVerified */ $block->escapeHtml($typeData['label']) ?> + <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" + class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> + <?= $block->escapeHtml($typeData['label']) ?> </li> <?php } @@ -121,98 +115,94 @@ $formName = $block->getFormName(); </script> <script data-role="img-dialog-container-tmpl" type="text/x-magento-template"> - <div class="image-panel" data-role="dialog"> - </div> + <div class="image-panel" data-role="dialog"> + </div> </script> <script data-role="img-dialog-tmpl" type="text/x-magento-template"> - <div class="image-panel-preview"> - <img src="<%- data.url %>" alt="<%- data.label %>" /> - </div> - <div class="image-panel-controls"> - <strong class="image-name"><%- data.label %></strong> + <div class="image-panel-preview"> + <img src="<%- data.url %>" alt="<%- data.label %>" /> + </div> + <div class="image-panel-controls"> + <strong class="image-name"><%- data.label %></strong> - <fieldset class="admin__fieldset fieldset-image-panel"> - <div class="admin__field field-image-description"> - <label class="admin__field-label" for="image-description"> - <span><?= /* @escapeNotVerified */ __('Alt Text') ?></span> - </label> + <fieldset class="admin__fieldset fieldset-image-panel"> + <div class="admin__field field-image-description"> + <label class="admin__field-label" for="image-description"> + <span><?= $block->escapeHtml(__('Alt Text')) ?></span> + </label> - <div class="admin__field-control"> + <div class="admin__field-control"> <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?php /* @escapeNotVerified */ - echo $elementName - ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> - </div> - </div> + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + </div> + </div> - <div class="admin__field field-image-role"> - <label class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Role') ?></span> - </label> - <div class="admin__field-control"> - <ul class="multiselect-alt"> - <?php - foreach ($block->getMediaAttributes() as $attribute) : - ?> - <li class="item"> - <label> - <input class="image-type" - data-role="type-selector" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" - type="checkbox" - value="<?php /* @escapeNotVerified */ echo $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" - /> - <?php /* @escapeNotVerified */ echo $block->escapeHtml( - $attribute->getFrontendLabel() - ) ?> - </label> - </li> + <div class="admin__field field-image-role"> + <label class="admin__field-label"> + <span><?= $block->escapeHtml(__('Role')) ?></span> + </label> + <div class="admin__field-control"> + <ul class="multiselect-alt"> + <?php + foreach ($block->getMediaAttributes() as $attribute) : + ?> + <li class="item"> + <label> + <input class="image-type" + data-role="type-selector" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" + type="checkbox" + value="<?= $block->escapeHtmlAttr($attribute->getAttributeCode()) ?>" + /> + <?= $block->escapeHtml( + $attribute->getFrontendLabel() + ) ?> + </label> + </li> <?php endforeach; - ?> - </ul> - </div> + ?> + </ul> </div> + </div> - <div class="admin__field admin__field-inline field-image-size" data-role="size"> - <label class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Image Size') ?></span> - </label> - <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{size}') ?>"></div> - </div> + <div class="admin__field admin__field-inline field-image-size" data-role="size"> + <label class="admin__field-label"> + <span><?= $block->escapeHtml(__('Image Size')) ?></span> + </label> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{size}')) ?>"></div> + </div> - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> - <label class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Image Resolution') ?></span> - </label> - <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{width}^{height} px') ?>"></div> - </div> + <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> + <label class="admin__field-label"> + <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> + </label> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> + </div> - <div class="admin__field field-image-hide"> - <div class="admin__field-control"> - <div class="admin__field admin__field-option"> - <input type="checkbox" - id="hide-from-product-page" - data-role="visibility-trigger" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" - value="1" - class="admin__control-checkbox" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" - <% if (data.disabled == 1) { %>checked="checked"<% } %> /> - - <label for="hide-from-product-page" class="admin__field-label"> - <?= /* @escapeNotVerified */ __('Hide from Product Page') ?> - </label> - </div> + <div class="admin__field field-image-hide"> + <div class="admin__field-control"> + <div class="admin__field admin__field-option"> + <input type="checkbox" + id="hide-from-product-page" + data-role="visibility-trigger" + data-form-part="<?= $block->escapeHtmlAttr($formName) ?>" + value="1" + class="admin__control-checkbox" + name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][disabled]" + <% if (data.disabled == 1) { %>checked="checked"<% } %> /> + + <label for="hide-from-product-page" class="admin__field-label"> + <?= $block->escapeHtml(__('Hide from Product Page')) ?> + </label> </div> </div> - </fieldset> - </div> + </div> + </fieldset> + </div> </script> <?= $block->getChildHtml('new-video') ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml index 4134392c0f52b..0a13aee5930ad 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var \Magento\Catalog\Block\Adminhtml\Product\Edit\Js $block */ ?> @@ -30,8 +30,8 @@ function registerTaxRecalcs() { Event.observe($('tax_class_id'), 'change', recalculateTax); } -var priceFormat = <?= /* @escapeNotVerified */ $this->helper('Magento\Tax\Helper\Data')->getPriceFormat($block->getStore()) ?>; -var taxRates = <?= /* @escapeNotVerified */ $block->getAllRatesByProductClassJson() ?>; +var priceFormat = <?= /* @noEscape */ $this->helper(Magento\Tax\Helper\Data::class)->getPriceFormat($block->getStore()) ?>; +var taxRates = <?= /* @noEscape */ $block->getAllRatesByProductClassJson() ?>; function recalculateTax() { if (typeof dynamicTaxes == 'undefined') { @@ -75,10 +75,10 @@ function bindActiveProductTab(event, ui) { jQuery(document).on('tabsactivate', bindActiveProductTab); // bind active tab -<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')): ?> +<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')) :?> jQuery(function () { - if (jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').length && jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').is(':mage-tabs')) { - var activeAnchor = jQuery('#<?= /* @escapeNotVerified */ $tabsBlock->getId() ?>').tabs('activeAnchor'); + if (jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').length && jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').is(':mage-tabs')) { + var activeAnchor = jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').tabs('activeAnchor'); if (activeAnchor && $('store_switcher')) { $('store_switcher').switchParams = 'active_tab/' + activeAnchor.prop('name') + '/'; } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml index 5b07121de49dc..7c3bee3d4d2fc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/alert.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @@ -14,7 +12,7 @@ ?> <div id="alert_messages_block"><?= $block->getMessageHtml() ?></div> <div> - <h4 class="icon-head head-edit-form"><?= /* @escapeNotVerified */ __('Product Alerts') ?></h4> + <h4 class="icon-head head-edit-form"><?= $block->escapeHtml(__('Product Alerts')) ?></h4> </div> <div class="clear"></div> <?= $block->getAccordionHtml() ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml index 15c33c56e3ac6..c66f31cc809ea 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml @@ -4,382 +4,448 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory */ ?> -<?php if ($block->isReadonly()): ?> -<?php $_readonly = ' disabled="disabled" '; ?> -<?php else: ?> -<?php $_readonly = ''; ?> +<?php if ($block->isReadonly()) :?> + <?php $_readonly = ' disabled="disabled" '; ?> +<?php else :?> + <?php $_readonly = ''; ?> <?php endif; ?> <fieldset class="fieldset form-inline"> -<legend class="legend"><span><?= /* @escapeNotVerified */ __('Advanced Inventory') ?></span></legend> -<br> -<div id="table_cataloginventory"> -<div class="field"> - <label class="label" for="inventory_manage_stock"> - <span><?= /* @escapeNotVerified */ __('Manage Stock') ?></span> - </label> - <div class="control"> - <select id="inventory_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @escapeNotVerified */ $_readonly ?>> - <option value="1"><?= /* @escapeNotVerified */ __('Yes') ?></option> - <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> - </select> - <input type="hidden" id="inventory_manage_stock_default" value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('manage_stock') ?>"> - <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_manage_stock" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_manage_stock"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <legend class="legend"><span><?= $block->escapeHtml(__('Advanced Inventory')) ?></span></legend> + <br> + <div id="table_cataloginventory"> + <div class="field"> + <label class="label" for="inventory_manage_stock"> + <span><?= $block->escapeHtml(__('Manage Stock')) ?></span> + </label> + <div class="control"> + <select id="inventory_manage_stock" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> + <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + </select> + <input type="hidden" + id="inventory_manage_stock_default" + value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('manage_stock')) ?>"> + <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_manage_stock" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> -<?php if (!$block->getProduct()->isComposite()): ?> -<div class="field"> - <label class="label" for="inventory_qty"> - <span><?= /* @escapeNotVerified */ __('Qty') ?></span> - </label> - <div class="control"> - <?php if (!$_readonly): ?> - <input type="hidden" id="original_inventory_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty') * 1 ?>"> - <?php endif;?> - <input type="text" class="input-text validate-number" id="inventory_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <?php if (!$block->getProduct()->isComposite()) :?> + <div class="field"> + <label class="label" for="inventory_qty"> + <span><?= $block->escapeHtml(__('Qty')) ?></span> + </label> + <div class="control"> + <?php if (!$_readonly) :?> + <input type="hidden" + id="original_inventory_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" + value="<?= /* @noEscape */ $block->getFieldValue('qty') * 1 ?>"> + <?php endif;?> + <input type="text" + class="input-text validate-number" + id="inventory_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty]" + value="<?= /* @noEscape */ $block->getFieldValue('qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> -<div class="field"> - <label class="label" for="inventory_min_qty"> - <span><?= /* @escapeNotVerified */ __('Out-of-Stock Threshold') ?></span> - </label> - <div class="control"> - <input type="text" class="input-text validate-number" id="inventory_min_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][min_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('min_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> + <div class="field"> + <label class="label" for="inventory_min_qty"> + <span><?= $block->escapeHtml(__('Out-of-Stock Threshold')) ?></span> + </label> + <div class="control"> + <input type="text" + class="input-text validate-number" + id="inventory_min_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_qty]" + value="<?= /* @noEscape */ $block->getFieldValue('min_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_min_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_min_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - </div> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_min_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(["prototype"], function(){ -toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <?php if (!$block->isReadonly()) :?> + <script> + require(["prototype"], function(){ + toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> -<div class="field"> - <label class="label" for="inventory_min_sale_qty"> - <span><?= /* @escapeNotVerified */ __('Minimum Qty Allowed in Shopping Cart') ?></span> - </label> - <div class="control"> - <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" - name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_min_sale_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <div class="field"> + <label class="label" for="inventory_min_sale_qty"> + <span><?= $block->escapeHtml(__('Minimum Qty Allowed in Shopping Cart')) ?></span> + </label> + <div class="control"> + <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" + value="<?= /* @noEscape */ $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_min_sale_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" + class="checkbox" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_min_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> -<div class="field"> - <label class="label" for="inventory_max_sale_qty"> - <span><?= /* @escapeNotVerified */ __('Maximum Qty Allowed in Shopping Cart') ?></span> - </label> - <div class="control"> - <input type="text" class="input-text validate-number" id="inventory_max_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> - <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <div class="control-inner-wrap"> - <input type="checkbox" id="inventory_use_config_max_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_max_sale_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <div class="field"> + <label class="label" for="inventory_max_sale_qty"> + <span><?= $block->escapeHtml(__('Maximum Qty Allowed in Shopping Cart')) ?></span> + </label> + <div class="control"> + <input type="text" + class="input-text validate-number" + id="inventory_max_sale_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" + value="<?= /* @noEscape */ $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <div class="control-inner-wrap"> + <input type="checkbox" + id="inventory_use_config_max_sale_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" + class="checkbox" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_max_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> - <?php if ($block->canUseQtyDecimals()): ?> - <div class="field"> - <label class="label" for="inventory_is_qty_decimal"> - <span><?= /* @escapeNotVerified */ __('Qty Uses Decimals') ?></span> - </label> - <div class="control"> - <select id="inventory_is_qty_decimal" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @escapeNotVerified */ $_readonly ?>> - <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> - <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> - </select> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> - </div> + <?php if ($block->canUseQtyDecimals()) :?> + <div class="field"> + <label class="label" for="inventory_is_qty_decimal"> + <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> + </label> + <div class="control"> + <select id="inventory_is_qty_decimal" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> + <option value="0"><?= $block->escapeHtml(__('No')) ?></option> + <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> + </select> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> - <?php if (!$block->isVirtual()) : ?> - <div class="field"> - <label class="label" for="inventory_is_decimal_divided"> - <span><?= /* @escapeNotVerified */ __('Allow Multiple Boxes for Shipping') ?></span> - </label> - <div class="control"> - <select id="inventory_is_decimal_divided" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @escapeNotVerified */ $_readonly ?>> - <option value="0"><?= /* @escapeNotVerified */ __('No') ?></option> - <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> - </select> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> + <?php if (!$block->isVirtual()) :?> + <div class="field"> + <label class="label" for="inventory_is_decimal_divided"> + <span><?= $block->escapeHtml(__('Allow Multiple Boxes for Shipping')) ?></span> + </label> + <div class="control"> + <select id="inventory_is_decimal_divided" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> + <option value="0"><?= $block->escapeHtml(__('No')) ?></option> + <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1) :?> + selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> + </select> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> + <?php endif; ?> <?php endif; ?> - </div> - <?php endif; ?> - <?php endif; ?> -<div class="field"> - <label class="label" for="inventory_backorders"> - <span><?= /* @escapeNotVerified */ __('Backorders') ?></span> - </label> - <div class="control"> - <select id="inventory_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @escapeNotVerified */ $_readonly ?>> - <?php foreach ($block->getBackordersOption() as $option): ?> - <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> - <option value="<?= /* @escapeNotVerified */ $option['value'] ?>" <?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> - <?php endforeach; ?> - </select> + <div class="field"> + <label class="label" for="inventory_backorders"> + <span><?= $block->escapeHtml(__('Backorders')) ?></span> + </label> + <div class="control"> + <select id="inventory_backorders" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getBackordersOption() as $option) :?> + <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + <?php endforeach; ?> + </select> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_backorders" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_backorders"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_backorders" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_backorders"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> -<div class="field"> - <label class="label" for="inventory_notify_stock_qty"> - <span><?= /* @escapeNotVerified */ __('Notify for Quantity Below') ?></span> - </label> - <div class="control"> - <input type="text" class="input-text validate-number" id="inventory_notify_stock_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> + <div class="field"> + <label class="label" for="inventory_notify_stock_qty"> + <span><?= $block->escapeHtml(__('Notify for Quantity Below')) ?></span> + </label> + <div class="control"> + <input type="text" + class="input-text validate-number" + id="inventory_notify_stock_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" + value="<?= /* @noEscape */ $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_notify_stock_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_notify_stock_qty"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> - </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_notify_stock_qty" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_notify_stock_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> - <?php endif; ?> -<div class="field"> - <label class="label" for="inventory_enable_qty_increments"> - <span><?= /* @escapeNotVerified */ __('Enable Qty Increments') ?></span> - </label> - <div class="control"> - <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> - <select id="inventory_enable_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @escapeNotVerified */ $_readonly ?>> - <option value="1"<?php if ($qtyIncrementsEnabled): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('Yes') ?></option> - <option value="0"<?php if (!$qtyIncrementsEnabled): ?> selected="selected"<?php endif; ?>><?= /* @escapeNotVerified */ __('No') ?></option> - </select> - <input type="hidden" id="inventory_enable_qty_increments_default" value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('enable_qty_increments') ?>"> + <?php endif; ?> + <div class="field"> + <label class="label" for="inventory_enable_qty_increments"> + <span><?= $block->escapeHtml(__('Enable Qty Increments')) ?></span> + </label> + <div class="control"> + <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> + <select id="inventory_enable_qty_increments" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> + <option value="1"<?php if ($qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="0"<?php if (!$qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + </select> + <input type="hidden" + id="inventory_enable_qty_increments_default" + value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('enable_qty_increments')) ?>"> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_enable_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_enable_qty_increments"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_enable_qty_increments" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_enable_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> -<div class="field"> - <label class="label" for="inventory_qty_increments"> - <span><?= /* @escapeNotVerified */ __('Qty Increments') ?></span> - </label> - <div class="control"> - <input type="text" class="input-text validate-digits" id="inventory_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" value="<?= /* @escapeNotVerified */ $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @escapeNotVerified */ $_readonly ?>> - <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->IsNew()) ? 'checked="checked"' : '' ?> - <input type="checkbox" id="inventory_use_config_qty_increments" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" value="1" <?= /* @escapeNotVerified */ $_checked ?> onclick="toggleValueElements(this, this.parentNode);" <?= /* @escapeNotVerified */ $_readonly ?>> - <label for="inventory_use_config_qty_increments"><?= /* @escapeNotVerified */ __('Use Config Settings') ?></label> + <div class="field"> + <label class="label" for="inventory_qty_increments"> + <span><?= $block->escapeHtml(__('Qty Increments')) ?></span> + </label> + <div class="control"> + <input type="text" + class="input-text validate-digits" + id="inventory_qty_increments" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" + value="<?= /* @noEscape */ $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + <div class="control-inner-wrap"> + <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?> + <input type="checkbox" + id="inventory_use_config_qty_increments" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" + value="1" <?= /* @noEscape */ $_checked ?> + onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> + <label for="inventory_use_config_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + </div> + <?php if (!$block->isReadonly()) :?> + <script> + require(['prototype'], function(){ + toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); + }); + </script> + <?php endif; ?> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> </div> - <?php if (!$block->isReadonly()): ?> - <script> -require(['prototype'], function(){ -toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); -}); -</script> - <?php endif; ?> - </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> -<div class="field"> - <label class="label" for="inventory_stock_availability"> - <span><?= /* @escapeNotVerified */ __('Stock Availability') ?></span> - </label> - <div class="control"> - <select id="inventory_stock_availability" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @escapeNotVerified */ $_readonly ?>> - <?php foreach ($block->getStockOption() as $option): ?> - <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> - <option value="<?= /* @escapeNotVerified */ $option['value'] ?>" <?= /* @escapeNotVerified */ $_selected ?>><?= /* @escapeNotVerified */ $option['label'] ?></option> - <?php endforeach; ?> - </select> + <div class="field"> + <label class="label" for="inventory_stock_availability"> + <span><?= $block->escapeHtml(__('Stock Availability')) ?></span> + </label> + <div class="control"> + <select id="inventory_stock_availability" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getStockOption() as $option) :?> + <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + <?php endforeach; ?> + </select> + </div> + <?php if (!$block->isSingleStoreMode()) :?> + <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> + <?php endif; ?> + </div> </div> - <?php if (!$block->isSingleStoreMode()): ?> - <div class="field-service"><?= /* @escapeNotVerified */ __('[GLOBAL]') ?></div> - <?php endif; ?> -</div> -</div> </fieldset> <script> -require(["jquery","prototype"], function(jQuery){ + require(["jquery","prototype"], function(jQuery){ - //<![CDATA[ - function changeManageStockOption() - { - var manageStock = $('inventory_use_config_manage_stock').checked + //<![CDATA[ + function changeManageStockOption() + { + var manageStock = $('inventory_use_config_manage_stock').checked ? $('inventory_manage_stock_default').value : $('inventory_manage_stock').value; - var catalogInventoryNotManageStockFields = { - inventory_min_sale_qty: true, - inventory_max_sale_qty: true, - inventory_enable_qty_increments : true, - inventory_qty_increments: true - }; - - $$('#table_cataloginventory > div').each(function(el) { - if (el == $('inventory_manage_stock').up(1)) { - return; - } + var catalogInventoryNotManageStockFields = { + inventory_min_sale_qty: true, + inventory_max_sale_qty: true, + inventory_enable_qty_increments : true, + inventory_qty_increments: true + }; - for (field in catalogInventoryNotManageStockFields) { - if ($(field) && ($(field).up(1) == el)) { + $$('#table_cataloginventory > div').each(function(el) { + if (el == $('inventory_manage_stock').up(1)) { return; } - } - el[manageStock == 1 ? 'show' : 'hide'](); - }); + for (field in catalogInventoryNotManageStockFields) { + if ($(field) && ($(field).up(1) == el)) { + return; + } + } - return true; - } + el[manageStock == 1 ? 'show' : 'hide'](); + }); - function applyEnableQtyIncrements() { - var enableQtyIncrements = $('inventory_use_config_enable_qty_increments').checked - ? $('inventory_enable_qty_increments_default').value - : $('inventory_enable_qty_increments').value; + return true; + } - $('inventory_qty_increments').up('.field')[enableQtyIncrements == 1 ? 'show' : 'hide'](); - } + function applyEnableQtyIncrements() { + var enableQtyIncrements = $('inventory_use_config_enable_qty_increments').checked + ? $('inventory_enable_qty_increments_default').value + : $('inventory_enable_qty_increments').value; - function applyEnableDecimalDivided() { - <?php if (!$block->isVirtual()) : ?> - $('inventory_is_decimal_divided').up('.field').hide(); - <?php endif; ?> - $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); - $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); - if ($('inventory_is_qty_decimal').value == 1) { - <?php if (!$block->isVirtual()) : ?> - $('inventory_is_decimal_divided').up('.field').show(); - <?php endif; ?> - $('inventory_qty_increments').addClassName('validate-number'); - $('inventory_min_sale_qty').addClassName('validate-number'); - } else { - $('inventory_qty_increments').addClassName('validate-digits'); - $('inventory_min_sale_qty').addClassName('validate-digits'); + $('inventory_qty_increments').up('.field')[enableQtyIncrements == 1 ? 'show' : 'hide'](); } - } - Event.observe(window, 'load', function() { - if ($('inventory_manage_stock') && $('inventory_use_config_manage_stock')) { - Event.observe($('inventory_manage_stock'), 'change', changeManageStockOption); - Event.observe($('inventory_use_config_manage_stock'), 'change', changeManageStockOption); - changeManageStockOption(); + function applyEnableDecimalDivided() { + <?php if (!$block->isVirtual()) :?> + $('inventory_is_decimal_divided').up('.field').hide(); + <?php endif; ?> + $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); + $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); + if ($('inventory_is_qty_decimal').value == 1) { + <?php if (!$block->isVirtual()) :?> + $('inventory_is_decimal_divided').up('.field').show(); + <?php endif; ?> + $('inventory_qty_increments').addClassName('validate-number'); + $('inventory_min_sale_qty').addClassName('validate-number'); + } else { + $('inventory_qty_increments').addClassName('validate-digits'); + $('inventory_min_sale_qty').addClassName('validate-digits'); + } } - if ($('inventory_enable_qty_increments') && $('inventory_use_config_enable_qty_increments')) { - //Delegation is used because of events, which are not firing while the input is disabled - jQuery('#inventory_enable_qty_increments').parent() + + Event.observe(window, 'load', function() { + if ($('inventory_manage_stock') && $('inventory_use_config_manage_stock')) { + Event.observe($('inventory_manage_stock'), 'change', changeManageStockOption); + Event.observe($('inventory_use_config_manage_stock'), 'change', changeManageStockOption); + changeManageStockOption(); + } + if ($('inventory_enable_qty_increments') && $('inventory_use_config_enable_qty_increments')) { + //Delegation is used because of events, which are not firing while the input is disabled + jQuery('#inventory_enable_qty_increments').parent() .on('change', '#inventory_enable_qty_increments', applyEnableQtyIncrements); - Event.observe($('inventory_use_config_enable_qty_increments'), 'change', applyEnableQtyIncrements); - applyEnableQtyIncrements(); - } - if ($('inventory_is_qty_decimal') && $('inventory_qty_increments') && $('inventory_min_sale_qty')) { - Event.observe($('inventory_is_qty_decimal'), 'change', applyEnableDecimalDivided); - applyEnableDecimalDivided(); - } - }); + Event.observe($('inventory_use_config_enable_qty_increments'), 'change', applyEnableQtyIncrements); + applyEnableQtyIncrements(); + } + if ($('inventory_is_qty_decimal') && $('inventory_qty_increments') && $('inventory_min_sale_qty')) { + Event.observe($('inventory_is_qty_decimal'), 'change', applyEnableDecimalDivided); + applyEnableDecimalDivided(); + } + }); - window.applyEnableDecimalDivided = applyEnableDecimalDivided; - window.applyEnableQtyIncrements = applyEnableQtyIncrements; - window.changeManageStockOption = changeManageStockOption; - //]]> + window.applyEnableDecimalDivided = applyEnableDecimalDivided; + window.applyEnableQtyIncrements = applyEnableQtyIncrements; + window.changeManageStockOption = changeManageStockOption; + //]]> -}); + }); </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml index 9c568cab16d84..d4a7687a0934b 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml @@ -3,8 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <script> require([ @@ -28,7 +26,7 @@ var catalogWysiwygEditor = { url: editorUrl, data: { element_id: elementId + '_editor', - store_id: '<?= /* @escapeNotVerified */ $block->getStoreId() ?>' + store_id: '<?= $block->escapeJs($block->getStoreId()) ?>' }, showLoader: true, dataType: 'html', @@ -43,9 +41,10 @@ var catalogWysiwygEditor = { if (this.modal) { this.modal.html(jQuery(data).html()); + this.modal.modal('option', 'firedElementId', elementId); } else { this.modal = jQuery(data).modal({ - title: '<?= /* @escapeNotVerified */ __('WYSIWYG Editor') ?>', + title: '<?= $block->escapeJs($block->escapeHtml(__('WYSIWYG Editor'))) ?>', modalClass: 'magento', type: 'slide', firedElementId: elementId, diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml index 04ccfb5aee8d0..17fb517b32547 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml @@ -4,24 +4,24 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes\Search */ ?> <div id="product-attribute-search-container" class="suggest-expandable attribute-selector"> <div class="action-dropdown"> <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> - <span><?= /* @escapeNotVerified */ __('Add Attribute') ?></span> + <span><?= $block->escapeHtml(__('Add Attribute')) ?></span> </button> <div class="dropdown-menu"> <input data-role="product-attribute-search" - data-group="<?= $block->escapeHtml($block->getGroupCode()) ?>" + data-group="<?= $block->escapeHtmlAttr($block->getGroupCode()) ?>" class="search" type="text" - placeholder="<?= /* @noEscape */ __('start typing to search attribute') ?>" /> + placeholder="<?= $block->escapeHtmlAttr(__('start typing to search attribute')) ?>" /> </div> </div> -<script data-template-for="product-attribute-search-<?= /* @escapeNotVerified */ $block->getGroupId() ?>" type="text/x-magento-template"> +<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" type="text/x-magento-template"> <ul data-mage-init='{"menu":[]}'> <% if (data.items.length) { %> <% _.each(data.items, function(value){ %> @@ -29,7 +29,7 @@ <% }); %> <% } else { %><span class="mage-suggest-no-records"><%- data.noRecordsText %></span><% } %> </ul> - <div class="actions"><?= /* @escapeNotVerified */ $block->getAttributeCreate() ?></div> + <div class="actions"><?= $block->escapeHtml($block->getAttributeCreate()) ?></div> </script> <script> @@ -51,13 +51,13 @@ }); }); - $suggest.mage('suggest', <?= /* @escapeNotVerified */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSelectorOptions()) ?>) + $suggest.mage('suggest', <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) .on('suggestselect', function (e, ui) { $(this).val(''); var templateId = $('#attribute_set_id').val(); if (ui.item.id) { $.ajax({ - url: '<?= /* @escapeNotVerified */ $block->getAddAttributeUrl() ?>', + url: '<?= $block->escapeJs($block->escapeUrl($block->getAddAttributeUrl())) ?>', type: 'POST', dataType: 'json', data: {attribute_id: ui.item.id, template_id: templateId, group: $(this).data('group')}, diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml index 6a62f01f97b65..6534f7a18cc3b 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs.phtml @@ -4,40 +4,38 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs */ ?> -<?php if (!empty($tabs)): ?> +<?php if (!empty($tabs)) :?> <?php $tabGroups = [ \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::BASIC_TAB_GROUP_CODE, \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::ADVANCED_TAB_GROUP_CODE, ];?> - <div id="<?= /* @escapeNotVerified */ $block->getId() ?>" + <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>" data-mage-init='{"tabs":{ - "active": "<?= /* @escapeNotVerified */ $block->getActiveTabId() ?>", - "destination": "#<?= /* @escapeNotVerified */ $block->getDestElementId() ?>", - "shadowTabs": "<?= /* @escapeNotVerified */ $block->getAllShadowTabs() ?>", - "tabsBlockPrefix": "<?= /* @escapeNotVerified */ $block->getId() ?>_", + "active": "<?= $block->escapeHtmlAttr($block->getActiveTabId()) ?>", + "destination": "#<?= $block->escapeHtmlAttr($block->getDestElementId()) ?>", + "shadowTabs": "<?= /* @noEscape */ $block->getAllShadowTabs() ?>", + "tabsBlockPrefix": "<?= $block->escapeHtmlAttr($block->getId()) ?>_", "tabIdArgument": "active_tab", - "tabPanelClass": "<?= /* @escapeNotVerified */ $block->getPanelsClass() ?>", - "excludedPanel": "<?= /* @escapeNotVerified */ $block->getExcludedPanel() ?>", + "tabPanelClass": "<?= $block->escapeHtmlAttr($block->getPanelsClass()) ?>", + "excludedPanel": "<?= $block->escapeHtmlAttr($block->getExcludedPanel()) ?>", "groups": "ul.tabs" }}'> - <?php foreach ($tabGroups as $tabGroupCode): ?> + <?php foreach ($tabGroups as $tabGroupCode) :?> <?php $tabGroupId = $block->getId() . '-' . $tabGroupCode; $isBasic = $tabGroupCode == \Magento\Catalog\Block\Adminhtml\Product\Edit\Tabs::BASIC_TAB_GROUP_CODE; $activeCollapsible = $block->isAdvancedTabGroupActive() ? true : false; ?> - <div class="admin__page-nav <?php if (!$isBasic): ?> <?= '_collapsed' ?> <?php endif;?>" + <div class="admin__page-nav <?php if (!$isBasic) :?> <?= '_collapsed' ?> <?php endif;?>" data-role="container" - id="<?= /* @escapeNotVerified */ $tabGroupId ?>" - <?php if (!$isBasic): ?> + id="<?= $block->escapeHtmlAttr($tabGroupId) ?>" + <?php if (!$isBasic) :?> data-mage-init='{"collapsible":{ - "active": "<?= /* @escapeNotVerified */ $activeCollapsible ?>", + "active": "<?= /* @noEscape */ $activeCollapsible ?>", "openedState": "_show", "closedState": "_hide", "animate": 200, @@ -45,44 +43,45 @@ }}' <?php endif;?>> - <div class="admin__page-nav-title-wrap" <?= /* @escapeNotVerified */ $block->getUiId('title') ?> data-role="title"> - <div class="admin__page-nav-title <?php if (!$isBasic): ?> <?= '_collapsible' ?><?php endif;?>" + <div class="admin__page-nav-title-wrap" <?= /* @noEscape */ $block->getUiId('title') ?> data-role="title"> + <div class="admin__page-nav-title <?php if (!$isBasic) :?> <?= '_collapsible' ?><?php endif;?>" data-role="trigger"> <strong> - <?= /* @escapeNotVerified */ $isBasic ? __('Basic Settings') : __('Advanced Settings') ?> + <?= $block->escapeHtml($isBasic ? __('Basic Settings') : __('Advanced Settings')) ?> </strong> <span data-role="title-messages" class="admin__page-nav-title-messages"></span> </div> </div> - <ul <?= /* @escapeNotVerified */ $block->getUiId('tab', $tabGroupId) ?> class="tabs admin__page-nav-items" data-role="content"> - <?php foreach ($tabs as $_tab): ?> + <ul <?= /* @noEscape */ $block->getUiId('tab', $tabGroupId) ?> class="tabs admin__page-nav-items" data-role="content"> + <?php foreach ($tabs as $_tab) :?> <?php /** @var $_tab \Magento\Backend\Block\Widget\Tab\TabInterface */ ?> <?php if (!$block->canShowTab($_tab) || $_tab->getParentTab() || ($_tab->getGroupCode() && $_tab->getGroupCode() != $tabGroupCode) - || (!$_tab->getGroupCode() && $isBasic)): continue; endif;?> + || (!$_tab->getGroupCode() && $isBasic)) : continue; + endif;?> <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> - <li class="admin__page-nav-item <?php if ($block->getTabIsHidden($_tab)): ?> <?= "no-display" ?> <?php endif; ?> " <?= /* @escapeNotVerified */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> - <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" - name="<?= /* @escapeNotVerified */ $block->getTabId($_tab, false) ?>" - title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" - class="admin__page-nav-link <?= /* @escapeNotVerified */ $_tabClass ?>" - data-tab-type="<?= /* @escapeNotVerified */ $_tabType ?>" <?= /* @escapeNotVerified */ $block->getUiId('tab', 'link', $_tab->getId()) ?> + <li class="admin__page-nav-item <?php if ($block->getTabIsHidden($_tab)) :?> <?= "no-display" ?> <?php endif; ?> " <?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> + <a href="<?= $block->escapeUrl($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" + title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" + class="admin__page-nav-link <?= $block->escapeHtmlAttr($_tabClass) ?>" + data-tab-type="<?= /* @noEscape */ $_tabType ?>" <?= /* @noEscape */ $block->getUiId('tab', 'link', $_tab->getId()) ?> > <span><?= $block->escapeHtml($block->getTabLabel($_tab)) ?></span> <span class="admin__page-nav-item-messages" data-role="item-messages"> <span class="admin__page-nav-item-message _changed"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= /* @escapeNotVerified */ __('Changes have been made to this section that have not been saved.') ?> + <?= $block->escapeHtml(__('Changes have been made to this section that have not been saved.')) ?> </span> </span> <span class="admin__page-nav-item-message _error"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?> + <?= $block->escapeHtml(__('This tab contains invalid data. Please resolve this before saving.')) ?> </span> </span> <span class="admin__page-nav-item-message-loader"> @@ -93,11 +92,11 @@ </span> </span> </a> - <div id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>_content" class="no-display" - data-tab-panel="<?= /* @escapeNotVerified */ $_tab->getTabId() ?>" - <?= /* @escapeNotVerified */ $block->getUiId('tab', 'content', $_tab->getId()) ?> + <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" class="no-display" + data-tab-panel="<?= $block->escapeHtmlAttr($_tab->getTabId()) ?>" + <?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?> > - <?= /* @escapeNotVerified */ $block->getTabContent($_tab) ?> + <?= /* @noEscape */ $block->getTabContent($_tab) ?> <?= /* @noEscape */ $block->getAccordion($_tab) ?> </div> </li> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml index 842ed17375f77..4d88042e18cc1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/tabs/child_tab.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\ChildTab */ ?> <div class="fieldset-wrapper admin__collapsible-block-wrapper" data-tab="<?= /* @noEscape */ $block->getTabId() ?>" diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml index 8df3e32b0a2c3..c814298d1dbc5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml @@ -4,11 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Grid */ ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true):?> + <?php if ($block->getHideFormElement() !== true) :?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -16,43 +15,43 @@ <select id="<?= $block->getHtmlId() ?>-select" class="local-validation admin__control-select"> - <option class="admin__control-select-placeholder" value="" selected><?= /* @escapeNotVerified */ __('Actions') ?></option> - <?php foreach ($block->getItems() as $_item): ?> - <option value="<?= /* @escapeNotVerified */ $_item->getId() ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= /* @escapeNotVerified */ $_item->getLabel() ?></option> + <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> + <?php foreach ($block->getItems() as $_item) :?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true):?> + <?php if ($block->getHideFormElement() !== true) :?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item): ?> - <div id="<?= $block->getHtmlId() ?>-item-<?= /* @escapeNotVerified */ $_item->getId() ?>-block"> + <?php foreach ($block->getItems() as $_item) :?> + <div id="<?= $block->getHtmlId() ?>-item-<?= $block->escapeHtmlAttr($_item->getId()) ?>-block"> <?= $_item->getAdditionalActionBlockHtml() ?> </div> <?php endforeach; ?> </div> <div class="mass-select-wrap"> - <select id="<?= $block->getHtmlId() ?>-mass-select" data-menu="grid-mass-select"> - <optgroup label="<?= /* @escapeNotVerified */ __('Mass Actions') ?>"> + <select id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-mass-select" data-menu="grid-mass-select"> + <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()):?> + <?php if ($block->getUseSelectAll()) :?> <option value="selectAll"> - <?= /* @escapeNotVerified */ __('Select All') ?> + <?= $block->escapeHtml(__('Select All')) ?> </option> <option value="unselectAll"> - <?= /* @escapeNotVerified */ __('Unselect All') ?> + <?= $block->escapeHtml(__('Unselect All')) ?> </option> <?php endif; ?> <option value="selectVisible"> - <?= /* @escapeNotVerified */ __('Select Visible') ?> + <?= $block->escapeHtml(__('Select Visible')) ?> </option> <option value="unselectVisible"> - <?= /* @escapeNotVerified */ __('Unselect Visible') ?> + <?= $block->escapeHtml(__('Unselect Visible')) ?> </option> </optgroup> </select> @@ -65,19 +64,19 @@ $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { var massAction = $('option:selected', this).val(); switch (massAction) { - <?php if ($block->getUseSelectAll()):?> + <?php if ($block->getUseSelectAll()) :?> case 'selectAll': - return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectAll(); + return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); break case 'unselectAll': - return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectAll(); + return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); break <?php endif; ?> case 'selectVisible': - return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.selectVisible(); + return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); break case 'unselectVisible': - return <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.unselectVisible(); + return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); break } this.blur(); @@ -85,8 +84,8 @@ }); - <?php if (!$block->getParentBlock()->canDisplayContainer()): ?> - <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setGridIds('<?= /* @escapeNotVerified */ $block->getGridIdsJson() ?>'); + <?php if (!$block->getParentBlock()->canDisplayContainer()) :?> + <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); <?php endif; ?> </script> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml index fb450d19312fa..668dc4a28a6d9 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/rss/grid/link.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Adminhtml\Rss\Grid\Link */ ?> -<?php if ($block->isRssAllowed() && $block->getLink()): ?> -<a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="link-feed"><?= /* @escapeNotVerified */ $block->getLabel() ?></a> +<?php if ($block->isRssAllowed() && $block->getLink()) :?> +<a href="<?= $block->escapeUrl($block->getLink()) ?>" class="link-feed"><?= $block->escapeHtml($block->getLabel()) ?></a> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 4eb0b986edfb1..83beca4814e25 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -42,7 +42,7 @@ </settings> </dataProvider> </dataSource> - <fieldset name="general"> + <fieldset name="general" sortOrder="5"> <settings> <collapsible>false</collapsible> <label/> @@ -256,7 +256,6 @@ <item name="type" xsi:type="string">group</item> <item name="config" xsi:type="array"> <item name="breakLine" xsi:type="boolean">true</item> - <item name="required" xsi:type="boolean">true</item> </item> </argument> <field name="available_sort_by" formElement="multiselect"> @@ -297,7 +296,6 @@ <item name="type" xsi:type="string">group</item> <item name="config" xsi:type="array"> <item name="breakLine" xsi:type="boolean">true</item> - <item name="required" xsi:type="boolean">true</item> </item> </argument> <field name="default_sort_by" formElement="select"> @@ -459,34 +457,43 @@ </checkbox> </formElements> </field> - <field name="custom_design" sortOrder="180" formElement="select"> + <field name="custom_design" component="Magento_Catalog/js/components/use-parent-settings/select" sortOrder="180" formElement="select"> <settings> <dataType>string</dataType> <label translate="true">Theme</label> <imports> - <link name="disabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> - <field name="page_layout" sortOrder="190" formElement="select"> + <field name="page_layout" component="Magento_Catalog/js/components/use-parent-settings/select" sortOrder="190" formElement="select"> <settings> <dataType>string</dataType> <label translate="true">Layout</label> <imports> - <link name="disabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> - <field name="custom_layout_update" sortOrder="200" formElement="textarea"> + <field name="custom_layout_update" component="Magento_Catalog/js/components/use-parent-settings/textarea" sortOrder="200" formElement="textarea"> <settings> <dataType>string</dataType> <label translate="true">Layout Update XML</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> + </imports> + </settings> + </field> + <field name="custom_layout_update_file" component="Magento_Catalog/js/components/use-parent-settings/select" sortOrder="205" formElement="select"> + <settings> + <dataType>string</dataType> + <label translate="true">Custom Layout Update</label> + <imports> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> - <field name="custom_apply_to_products" component="Magento_Ui/js/form/element/single-checkbox" sortOrder="210" formElement="checkbox"> + <field name="custom_apply_to_products" component="Magento_Catalog/js/components/use-parent-settings/single-checkbox" sortOrder="210" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="default" xsi:type="number">0</item> @@ -499,7 +506,7 @@ <dataType>boolean</dataType> <label translate="true">Apply Design to Products</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> <formElements> @@ -542,7 +549,7 @@ <dataType>string</dataType> <label translate="true">Schedule Update From</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked</link> + <link name="serviceDisabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked || $.data.serviceDisabled</link> </imports> </settings> </field> @@ -555,7 +562,7 @@ <dataType>string</dataType> <label translate="true">To</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked</link> + <link name="serviceDisabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked || $.data.serviceDisabled</link> </imports> </settings> </field> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml index 772bc1e6ec5d7..f795fcabe701c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_attribute_add_form.xml @@ -147,7 +147,6 @@ <item name="isTemplate" xsi:type="boolean">true</item> <item name="is_collection" xsi:type="boolean">true</item> <item name="componentType" xsi:type="string">container</item> - <item name="positionProvider" xsi:type="string">attribute_options.position</item> </item> </argument> <field name="is_default" component="Magento_Catalog/js/form/element/checkbox" sortOrder="0" formElement="checkbox"> @@ -184,12 +183,8 @@ </item> </argument> <settings> - <additionalClasses> - <class name="_hidden">true</class> - </additionalClasses> <dataType>text</dataType> <visible>false</visible> - <dataScope>position</dataScope> </settings> </field> </container> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 09332d66633f1..578281f44c4cf 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -48,7 +48,9 @@ </settings> </filterSelect> </filters> - <massaction name="listing_massaction" component="Magento_Ui/js/grid/tree-massactions"> + <massaction name="listing_massaction" + component="Magento_Ui/js/grid/tree-massactions" + class="\Magento\Catalog\Ui\Component\Product\MassAction"> <action name="delete"> <settings> <confirm> @@ -188,6 +190,13 @@ <label translate="true">Websites</label> </settings> </column> + <column name="cost" class="Magento\Catalog\Ui\Component\Listing\Columns\Price" sortOrder="120"> + <settings> + <addField>true</addField> + <filter>textRange</filter> + <label translate="true">Cost</label> + </settings> + </column> <actionsColumn name="actions" class="Magento\Catalog\Ui\Component\Listing\Columns\ProductActions" sortOrder="200"> <settings> <indexField>entity_id</indexField> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js index 75ee3019cf4b6..41f7a874c26f3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js @@ -82,7 +82,7 @@ define([ return function (config, element) { config = config || {}; jQuery(element).on('click', function () { - categorySubmit(config.url, config.ajax); + categorySubmit(); }); }; }); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js index 0f6689b88db06..0a04358e41123 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js @@ -14,6 +14,7 @@ define([ options: { categoryIdSelector: 'input[name="id"]', categoryPathSelector: 'input[name="path"]', + categoryParentSelector: 'input[name="parent"]', refreshUrl: config.refreshUrl }, @@ -45,6 +46,7 @@ define([ } else { $(this.options.categoryIdSelector).val(data.id).change(); $(this.options.categoryPathSelector).val(data.path).change(); + $(this.options.categoryParentSelector).val(data.parentId).change(); } } }; diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 6903a17bcdcca..a2804a8723ce0 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -469,26 +469,6 @@ define([ } }, - /** - * toggles Selects states (for IE) except those to be shown in popup - */ - /*_toggleSelectsExceptBlock: function(flag) { - if(Prototype.Browser.IE){ - if (this.blockForm) { - var states = new Array; - var selects = this.blockForm.getElementsByTagName("select"); - for(var i=0; i<selects.length; i++){ - states[i] = selects[i].style.visibility - } - } - if (this.blockForm) { - for(i=0; i<selects.length; i++){ - selects[i].style.visibility = states[i] - } - } - } - },*/ - /** * Close configuration window */ diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js new file mode 100644 index 0000000000000..e1923dc46d68e --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage', + 'validation' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element).mage('form').validation({ + validationUrl: config.validationUrl + }); + }; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js b/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js index 3ebd4bdf9c804..11a1a65cbab47 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/component/image-size-field.js @@ -26,7 +26,7 @@ define([ return !!(m && m[1] > 0 && m[2] > 0); }, - $.mage.__('This value does not follow the specified format (for example, 200X300).') + $.mage.__('This value does not follow the specified format (for example, 200x300).') ); return Abstract.extend({ diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js new file mode 100644 index 0000000000000..1d64418e36ff5 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js @@ -0,0 +1,279 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'prototype', + 'extjs/ext-tree-checkbox', + 'mage/adminhtml/form' +], function (jQuery) { + 'use strict'; + + return function (config) { + var tree, + options = { + dataUrl: config.dataUrl, + divId: config.divId, + rootVisible: config.rootVisible, + useAjax: config.useAjax, + currentNodeId: config.currentNodeId, + jsFormObject: window[config.jsFormObject], + name: config.name, + checked: config.checked, + allowDrop: config.allowDrop, + rootId: config.rootId, + expanded: config.expanded, + categoryId: config.categoryId, + treeJson: config.treeJson + }, + data = {}, + parameters = {}, + root = {}, + key = ''; + + /* eslint-disable */ + /** + * Fix ext compatibility with prototype 1.6 + */ + Ext.lib.Event.getTarget = function (e) {// eslint-disable-line no-undef + var ee = e.browserEvent || e; + + return ee.target ? Event.element(ee) : null; + }; + + /** + * @param {Object} el + * @param {Object} config + */ + Ext.tree.TreePanel.Enhanced = function (el, config) {// eslint-disable-line no-undef + Ext.tree.TreePanel.Enhanced.superclass.constructor.call(this, el, config);// eslint-disable-line no-undef + }; + + Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, {// eslint-disable-line no-undef + /* eslint-enable */ + /** + * @param {Object} config + * @param {Boolean} firstLoad + */ + loadTree: function (config, firstLoad) {// eslint-disable-line no-shadow + parameters = config.parameters, + data = config.data, + root = new Ext.tree.TreeNode(parameters);// eslint-disable-line no-undef + + if (typeof parameters.rootVisible != 'undefined') { + this.rootVisible = parameters.rootVisible * 1; + } + + this.nodeHash = {}; + this.setRootNode(root); + + if (firstLoad) { + this.addListener('click', this.categoryClick.createDelegate(this)); + } + + this.loader.buildCategoryTree(root, data); + this.el.dom.innerHTML = ''; + // render the tree + this.render(); + }, + + /** + * @param {Object} node + */ + categoryClick: function (node) { + node.getUI().check(!node.getUI().checked()); + } + }); + + jQuery(function () { + var categoryLoader = new Ext.tree.TreeLoader({// eslint-disable-line no-undef + dataUrl: config.dataUrl + }); + + /** + * @param {Object} response + * @param {Object} parent + * @param {Function} callback + */ + categoryLoader.processResponse = function (response, parent, callback) { + config = JSON.parse(response.responseText); + + this.buildCategoryTree(parent, config); + + if (typeof callback === 'function') { + callback(this, parent); + } + }; + + /** + * @param {Object} config + * @returns {Object} + */ + categoryLoader.createNode = function (config) {// eslint-disable-line no-shadow + var node; + + config.uiProvider = Ext.tree.CheckboxNodeUI;// eslint-disable-line no-undef + + if (config.children && !config.children.length) { + delete config.children; + node = new Ext.tree.AsyncTreeNode(config);// eslint-disable-line no-undef + } else { + node = new Ext.tree.TreeNode(config);// eslint-disable-line no-undef + } + + return node; + }; + + /** + * @param {Object} parent + * @param {Object} config + * @param {Integer} i + */ + categoryLoader.processCategoryTree = function (parent, config, i) {// eslint-disable-line no-shadow + var node, + _node = {}; + + config[i].uiProvider = Ext.tree.CheckboxNodeUI;// eslint-disable-line no-undef + + _node = Object.clone(config[i]); + + if (_node.children && !_node.children.length) { + delete _node.children; + node = new Ext.tree.AsyncTreeNode(_node);// eslint-disable-line no-undef + } else { + node = new Ext.tree.TreeNode(config[i]);// eslint-disable-line no-undef + } + parent.appendChild(node); + node.loader = node.getOwnerTree().loader; + + if (_node.children) { + categoryLoader.buildCategoryTree(node, _node.children); + } + }; + + /** + * @param {Object} parent + * @param {Object} config + * @returns {void} + */ + categoryLoader.buildCategoryTree = function (parent, config) {// eslint-disable-line no-shadow + var i = 0; + + if (!config) { + return null; + } + + if (parent && config && config.length) { + for (i; i < config.length; i++) { + categoryLoader.processCategoryTree(parent, config, i); + } + } + }; + + /** + * + * @param {Object} hash + * @param {Object} node + * @returns {Object} + */ + categoryLoader.buildHashChildren = function (hash, node) {// eslint-disable-line no-shadow + var i = 0, + len; + + // eslint-disable-next-line no-extra-parens + if ((node.childNodes.length > 0) || (node.loaded === false && node.loading === false)) { + hash.children = []; + + for (i, len = node.childNodes.length; i < len; i++) { + /* eslint-disable */ + if (!hash.children) { + hash.children = []; + } + /* eslint-enable */ + hash.children.push(this.buildHash(node.childNodes[i])); + } + } + + return hash; + }; + + /** + * @param {Object} node + * @returns {Object} + */ + categoryLoader.buildHash = function (node) { + var hash = {}; + + hash = this.toArray(node.attributes); + + return categoryLoader.buildHashChildren(hash, node); + }; + + /** + * @param {Object} attributes + * @returns {Object} + */ + categoryLoader.toArray = function (attributes) { + data = {}; + + for (key in attributes) { + + if (attributes[key]) { + data[key] = attributes[key]; + } + } + + return data; + }; + + categoryLoader.on('beforeload', function (treeLoader, node) { + treeLoader.baseParams.id = node.attributes.id; + treeLoader.baseParams.selected = options.jsFormObject.updateElement.value; + }); + + /* eslint-disable */ + categoryLoader.on('load', function () { + varienWindowOnload(); + }); + + tree = new Ext.tree.TreePanel.Enhanced(options.divId, { + animate: false, + loader: categoryLoader, + enableDD: false, + containerScroll: true, + selModel: new Ext.tree.CheckNodeMultiSelectionModel(), + rootVisible: options.rootVisible, + useAjax: options.useAjax, + currentNodeId: options.currentNodeId, + addNodeTo: false, + rootUIProvider: Ext.tree.CheckboxNodeUI + }); + + tree.on('check', function (node) { + options.jsFormObject.updateElement.value = this.getChecked().join(', '); + varienElementMethods.setHasChanges(node.getUI().checkbox); + }, tree); + + // set the root node + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + parameters = { + text: options.name, + draggable: false, + checked: options.checked, + uiProvider: Ext.tree.CheckboxNodeUI, + allowDrop: options.allowDrop, + id: options.rootId, + expanded: options.expanded, + category_id: options.categoryId + }; + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + + tree.loadTree({ + parameters: parameters, data: options.treeJson + }, true); + /* eslint-enable */ + }); + }; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js index 99b1252b8f781..157879041cd66 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js @@ -5,9 +5,10 @@ define([ 'jquery', + 'mageUtils', 'jquery/ui', 'jquery/jstree/jquery.jstree' -], function ($) { +], function ($, utils) { 'use strict'; $.widget('mage.categoryTree', { @@ -36,8 +37,8 @@ define([ ajax: { url: options.url, type: 'POST', - success: $.proxy(function (node) { - return this._convertData(node[0]); + success: $.proxy(function (nodes) { + return this._convertDataNodes(nodes); }, this), /** @@ -76,6 +77,21 @@ define([ } }, + /** + * @param {Array} nodes + * @returns {Array} + * @private + */ + _convertDataNodes: function (nodes) { + var nodesData = []; + + nodes.forEach(function (node) { + nodesData.push(this._convertData(node)); + }, this); + + return nodesData; + }, + /** * @param {Object} node * @return {*} @@ -88,9 +104,10 @@ define([ if (!node) { return result; } + result = { data: { - title: node.name + ' (' + node['product_count'] + ')' + title: utils.unescape(node.name) + ' (' + node['product_count'] + ')' }, attr: { 'class': node.cls + (!!node.disabled ? ' disabled' : '') //eslint-disable-line no-extra-boolean-cast diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js index 9201c1c8e0fb4..b5c0e7a95d401 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/dynamic-rows-tier-price.js @@ -9,6 +9,10 @@ define([ ], function (_, DynamicRows) { 'use strict'; + /** + * @deprecated Parent method contains labels sorting. + * @see Magento_Ui/js/dynamic-rows/dynamic-rows + */ return DynamicRows.extend({ /** diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js new file mode 100644 index 0000000000000..1ddb24f3eefbb --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/select' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js new file mode 100644 index 0000000000000..0f166d3b45582 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/single-checkbox' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js new file mode 100644 index 0000000000000..3ef2bb21241a7 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/textarea' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js new file mode 100644 index 0000000000000..d140cc0fad74e --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js @@ -0,0 +1,62 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore' +], function (_) { + 'use strict'; + + var mixin = { + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.custom_use_parent_settings:checked' + }, + useParent: false, + useDefaults: false + }, + + /** + * Disable form input if settings for parent section is used + * or default value is applied. + * + * @param {Boolean} isUseParent + */ + toggleDisabled: function (isUseParent) { + var disabled = this.useParent = isUseParent; + + if (!disabled && !_.isUndefined(this.service)) { + disabled = !!this.isUseDefault(); + } + + this.saveUseDefaults(); + this.disabled(disabled); + }, + + /** + * Stores original state of the field. + */ + saveUseDefaults: function () { + this.useDefaults = this.disabled(); + }, + + /** @inheritdoc */ + setInitialValue: function () { + this._super(); + this.isUseDefault(this.useDefaults); + + return this; + }, + + /** @inheritdoc */ + toggleUseDefault: function (state) { + this._super(); + this.disabled(state || this.useParent); + } + }; + + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js index 51ffeaea0fc0c..2f6703cc92eac 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js @@ -54,9 +54,16 @@ define([ if (!_.isEmpty(this.suffixName) || _.isNumber(this.suffixName)) { suffixName = '.' + this.suffixName; } - this.dataScope = 'data.' + this.prefixName + '.' + this.elementName + suffixName; - this.links.value = this.provider + ':' + this.dataScope; + this.exportDataLink = 'data.' + this.prefixName + '.' + this.elementName + suffixName; + this.exports.value = this.provider + ':' + this.exportDataLink; + }, + + /** @inheritdoc */ + destroy: function () { + this._super(); + + this.source.remove(this.exportDataLink); }, /** diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js index 5239eb207efca..7adc0dcfdf408 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js @@ -13,17 +13,21 @@ define([ 'jquery/ui', 'prototype', 'form', - 'validation' + 'validation', + 'mage/translate' ], function (jQuery, mageTemplate, rg) { 'use strict'; return function (config) { - var attributeOption = { + var optionPanel = jQuery('#manage-options-panel'), + editForm = jQuery('#edit_form'), + attributeOption = { table: $('attribute-options-table'), itemCount: 0, totalItems: 0, rendered: 0, template: mageTemplate('#row-template'), + newOptionClass: 'new-option', isReadOnly: config.isReadOnly, add: function (data, render) { var isNewOption = false, @@ -32,7 +36,8 @@ define([ if (typeof data.id == 'undefined') { data = { 'id': 'option_' + this.itemCount, - 'sort_order': this.itemCount + 1 + 'sort_order': this.itemCount + 1, + 'rowClasses': this.newOptionClass }; isNewOption = true; } @@ -84,6 +89,10 @@ define([ this.totalItems--; this.updateItemsCountField(); } + + if (element.hasClassName(this.newOptionClass)) { + element.remove(); + } }, updateItemsCountField: function () { $('option-count-check').value = this.totalItems > 0 ? '1' : ''; @@ -135,7 +144,9 @@ define([ return optionDefaultInputType; } - }; + }, + tableBody = jQuery(), + activePanelClass = 'selected-type-options'; if ($('add_new_option_button')) { Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption, {}, true)); @@ -144,7 +155,7 @@ define([ attributeOption.remove(event); }); - jQuery('#manage-options-panel').on('render', function () { + optionPanel.on('render', function () { attributeOption.ignoreValidate(); if (attributeOption.rendered) { @@ -170,7 +181,33 @@ define([ }); }); } - + editForm.on('beforeSubmit', function () { + var optionContainer = optionPanel.find('table tbody'), + optionsValues; + + if (optionPanel.hasClass(activePanelClass)) { + optionsValues = jQuery.map( + optionContainer.find('tr'), + function (row) { + return jQuery(row).find('input, select, textarea').serialize(); + } + ); + jQuery('<input>') + .attr({ + type: 'hidden', + name: 'serialized_options' + }) + .val(JSON.stringify(optionsValues)) + .prependTo(editForm); + } + tableBody = optionContainer.detach(); + }); + editForm.on('afterValidate.error highlight.validate', function () { + if (optionPanel.hasClass(activePanelClass)) { + optionPanel.find('table').append(tableBody); + jQuery('input[name="serialized_options"]').remove(); + } + }); window.attributeOption = attributeOption; window.optionDefaultInputType = attributeOption.getOptionInputType(); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js b/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js index 475c9d2dc0601..1c79331253251 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/product/weight-handler.js @@ -67,7 +67,7 @@ define([ }, /** - * Has weight swither + * Has weight switcher * @returns {*} */ hasWeightSwither: function () { diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html index d4cfb02611416..9a52dcefa3042 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html +++ b/app/code/Magento/Catalog/view/adminhtml/web/template/form/element/action-delete.html @@ -7,7 +7,7 @@ <button class="action-delete" attr="{'data-action': 'remove_row'}" data-bind=" - click: function(){ $data.processingDeleteRecord($parents); }, + click: function(){ $parent.processingDeleteRecord($record().index, $record.recordId); }, attr: { title: $parent.deleteButtonLabel } diff --git a/app/code/Magento/Catalog/view/adminhtml/web/template/image-preview.html b/app/code/Magento/Catalog/view/adminhtml/web/template/image-preview.html index a6a1b3e5b05e8..a319576195f75 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/template/image-preview.html +++ b/app/code/Magento/Catalog/view/adminhtml/web/template/image-preview.html @@ -13,7 +13,8 @@ event="load: $parent.onPreviewLoad.bind($parent)" attr=" src: $parent.getFilePreview($file), - alt: $file.name"> + alt: $file.name, + title: $file.name"> </a> <div class="actions"> diff --git a/app/code/Magento/Catalog/view/base/templates/js/components.phtml b/app/code/Magento/Catalog/view/base/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Catalog/view/base/templates/js/components.phtml +++ b/app/code/Magento/Catalog/view/base/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml new file mode 100644 index 0000000000000..55c8a8fcb4edd --- /dev/null +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product\Option; + +/** + * @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select\Checkable + */ +$option = $block->getOption(); +if ($option) : ?> + <?php + $configValue = $block->getPreconfiguredValue($option); + $optionType = $option->getType(); + $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; + $count = 1; + ?> + +<div class="options-list nested" id="options-<?= $block->escapeHtmlAttr($option->getId()) ?>-list"> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()) :?> + <div class="field choice admin__field admin__field-option"> + <input type="radio" + id="options_<?= $block->escapeHtmlAttr($option->getId()) ?>" + class="radio admin__control-radio product-custom-option" + name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" + data-selector="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" + onclick="<?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + value="" + checked="checked" + /> + <label class="label admin__field-label" for="options_<?= $block->escapeHtmlAttr($option->getId()) ?>"> + <span> + <?= $block->escapeHtml(__('None')) ?> + </span> + </label> + </div> + <?php endif; ?> + + <?php foreach ($option->getValues() as $value) : ?> + <?php + $checked = ''; + $count++; + if ($arraySign) { + $checked = is_array($configValue) && in_array($value->getOptionTypeId(), $configValue) ? 'checked' : ''; + } else { + $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; + } + $dataSelector = 'options[' . $option->getId() . ']'; + if ($arraySign) { + $dataSelector .= '[' . $value->getOptionTypeId() . ']'; + } + ?> + + <div class="field choice admin__field admin__field-option <?= /* @noEscape */ $option->getIsRequire() ? 'required': '' ?>"> + <input type="<?= $block->escapeHtmlAttr($optionType) ?>" + class="<?= /* @noEscape */ $optionType === Option::OPTION_TYPE_RADIO + ? 'radio admin__control-radio' + : 'checkbox admin__control-checkbox' ?> <?= $option->getIsRequire() + ? 'required': '' ?> + product-custom-option + <?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]<?= /* @noEscape */ $arraySign ?>" + id="options_<?= $block->escapeHtmlAttr($option->getId() . '_' . $count) ?>" + value="<?= $block->escapeHtmlAttr($value->getOptionTypeId()) ?>" + <?= $block->escapeHtml($checked) ?> + data-selector="<?= $block->escapeHtmlAttr($dataSelector) ?>" + price="<?= $block->escapeHtmlAttr($block->getCurrencyByStore($value)) ?>" + /> + <label class="label admin__field-label" + for="options_<?= $block->escapeHtmlAttr($option->getId() . '_' . $count) ?>"> + <span> + <?= $block->escapeHtml($value->getTitle()) ?> + </span> + <?= /* @noEscape */ $block->formatPrice($value) ?> + </label> + </div> + <?php endforeach; ?> + </div> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml index 86168c742c0f1..b2c2acb7419bd 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/amount/default.phtml @@ -3,30 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php /** @var \Magento\Framework\Pricing\Render\Amount $block */ ?> +<?php /** @var $block \Magento\Framework\Pricing\Render\Amount */ ?> -<span class="price-container <?= /* @escapeNotVerified */ $block->getAdjustmentCssClasses() ?>" +<span class="price-container <?= $block->escapeHtmlAttr($block->getAdjustmentCssClasses()) ?>" <?= $block->getSchema() ? ' itemprop="offers" itemscope itemtype="http://schema.org/Offer"' : '' ?>> - <?php if ($block->getDisplayLabel()): ?> - <span class="price-label"><?= /* @escapeNotVerified */ $block->getDisplayLabel() ?></span> + <?php if ($block->getDisplayLabel()) :?> + <span class="price-label"><?= $block->escapeHtml($block->getDisplayLabel()) ?></span> <?php endif; ?> - <span <?php if ($block->getPriceId()): ?> id="<?= /* @escapeNotVerified */ $block->getPriceId() ?>"<?php endif;?> - <?= ($block->getPriceDisplayLabel()) ? 'data-label="' . $block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes() . '"' : '' ?> - data-price-amount="<?= /* @escapeNotVerified */ $block->getDisplayValue() ?>" - data-price-type="<?= /* @escapeNotVerified */ $block->getPriceType() ?>" - class="price-wrapper <?= /* @escapeNotVerified */ $block->getPriceWrapperCss() ?>"> - <?= /* @escapeNotVerified */ $block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()) ?> - </span> - <?php if ($block->hasAdjustmentsHtml()): ?> + <span <?php if ($block->getPriceId()) :?> id="<?= $block->escapeHtmlAttr($block->getPriceId()) ?>"<?php endif;?> + <?= ($block->getPriceDisplayLabel()) ? 'data-label="' . $block->escapeHtmlAttr($block->getPriceDisplayLabel() . $block->getPriceDisplayInclExclTaxes()) . '"' : '' ?> + data-price-amount="<?= $block->escapeHtmlAttr($block->getDisplayValue()) ?>" + data-price-type="<?= $block->escapeHtmlAttr($block->getPriceType()) ?>" + class="price-wrapper <?= $block->escapeHtmlAttr($block->getPriceWrapperCss()) ?>" + ><?= $block->escapeHtml($block->formatCurrency($block->getDisplayValue(), (bool)$block->getIncludeContainer()), ['span']) ?></span> + <?php if ($block->hasAdjustmentsHtml()) :?> <?= $block->getAdjustmentsHtml() ?> <?php endif; ?> - <?php if ($block->getSchema()): ?> - <meta itemprop="price" content="<?= /* @escapeNotVerified */ $block->getDisplayValue() ?>" /> - <meta itemprop="priceCurrency" content="<?= /* @escapeNotVerified */ $block->getDisplayCurrencyCode() ?>" /> + <?php if ($block->getSchema()) :?> + <meta itemprop="price" content="<?= $block->escapeHtmlAttr($block->getDisplayValue()) ?>" /> + <meta itemprop="priceCurrency" content="<?= $block->escapeHtmlAttr($block->getDisplayCurrencyCode()) ?>" /> <?php endif; ?> </span> diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml index 98b713be685d0..33cd071a27f84 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/configured_price.phtml @@ -6,19 +6,58 @@ ?> <?php /** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ +$schema = ($block->getZone() == 'item_view') ? true : false; +$idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; /** @var \Magento\Catalog\Pricing\Price\ConfiguredPrice $configuredPrice */ $configuredPrice = $block->getPrice(); -$schema = ($block->getZone() == 'item_view') ? true : false; -$priceLabel = ($block->getPriceLabel() !== null) - ? $block->getPriceLabel() - : ''; +/** @var \Magento\Catalog\Pricing\Price\ConfiguredRegularPrice $configuredRegularPrice */ +$configuredRegularPrice = $block->getPriceType( + \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface::CONFIGURED_REGULAR_PRICE_CODE +); ?> -<p class="price-as-configured"> - <?= /* @escapeNotVerified */ $block->renderAmount($configuredPrice->getAmount(), [ - 'display_label' => $priceLabel, - 'price_id' => $block->getPriceId('product-price-'), - 'price_type' => 'finalPrice', - 'include_container' => true, - 'schema' => $schema - ]); ?> -</p> +<?php if ($configuredPrice->getAmount()->getValue() < $configuredRegularPrice->getAmount()->getValue()) : ?> + <p class="price-as-configured"> + <span class="special-price"> + <?= /* @noEscape */ $block->renderAmount( + $configuredPrice->getAmount(), + [ + 'display_label' => $block->escapeHtml(__('Special Price')), + 'price_id' => $block->escapeHtml($block->getPriceId('product-price-' . $idSuffix)), + 'price_type' => 'finalPrice', + 'include_container' => true, + 'schema' => $schema + ] + ); ?> + </span> + <span class="old-price"> + <?= /* @noEscape */ $block->renderAmount( + $configuredRegularPrice->getAmount(), + [ + 'display_label' => $block->escapeHtml(__('Regular Price')), + 'price_id' => $block->escapeHtml($block->getPriceId('old-price-' . $idSuffix)), + 'price_type' => 'oldPrice', + 'include_container' => true, + 'skip_adjustments' => true + ] + ); ?> + </span> + </p> +<?php else : ?> + <?php + $priceLabel = ($block->getPriceLabel() !== null) + ? $block->getPriceLabel() + : ''; + ?> + <p class="price-as-configured"> + <?= /* @noEscape */ $block->renderAmount( + $configuredPrice->getAmount(), + [ + 'display_label' => $block->escapeHtml($priceLabel), + 'price_id' => $block->escapeHtml($block->getPriceId('product-price-' . $idSuffix)), + 'price_type' => 'finalPrice', + 'include_container' => true, + 'schema' => $schema + ] + ); ?> + </p> +<?php endif; ?> diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml index 065472c686129..84a1153bea67a 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/default.phtml @@ -3,21 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php -/** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ +// phpcs:disable PSR2.Files.ClosingTag -$productId = $block->getSaleableItem()->getId(); +/** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ /** ex: \Magento\Catalog\Pricing\Price\RegularPrice */ /** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ $priceModel = $block->getPriceType('regular_price'); -/* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ +/* @noEscape */ echo $block->renderAmount($priceModel->getAmount(), [ 'price_id' => $block->getPriceId('product-price-'), 'include_container' => true ]); diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml index 72d9124173898..e56804a06de22 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml @@ -3,16 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ -$productId = $block->getSaleableItem()->getId(); - /** ex: \Magento\Catalog\Pricing\Price\RegularPrice */ /** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ $priceModel = $block->getPriceType('regular_price'); @@ -23,9 +18,9 @@ $finalPriceModel = $block->getPriceType('final_price'); $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; $schema = ($block->getZone() == 'item_view') ? true : false; ?> -<?php if ($block->hasSpecialPrice()): ?> +<?php if ($block->hasSpecialPrice()) :?> <span class="special-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ + <?= /* @noEscape */ $block->renderAmount($finalPriceModel->getAmount(), [ 'display_label' => __('Special Price'), 'price_id' => $block->getPriceId('product-price-' . $idSuffix), 'price_type' => 'finalPrice', @@ -34,7 +29,7 @@ $schema = ($block->getZone() == 'item_view') ? true : false; ]); ?> </span> <span class="old-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ + <?= /* @noEscape */ $block->renderAmount($priceModel->getAmount(), [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'price_type' => 'oldPrice', @@ -42,8 +37,8 @@ $schema = ($block->getZone() == 'item_view') ? true : false; 'skip_adjustments' => true ]); ?> </span> -<?php else: ?> - <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ +<?php else :?> + <?= /* @noEscape */ $block->renderAmount($finalPriceModel->getAmount(), [ 'price_id' => $block->getPriceId('product-price-' . $idSuffix), 'price_type' => 'finalPrice', 'include_container' => true, @@ -51,14 +46,14 @@ $schema = ($block->getZone() == 'item_view') ? true : false; ]); ?> <?php endif; ?> -<?php if ($block->showMinimalPrice()): ?> - <?php if ($block->getUseLinkForAsLowAs()):?> - <a href="<?= /* @escapeNotVerified */ $block->getSaleableItem()->getProductUrl() ?>" class="minimal-price-link"> - <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> +<?php if ($block->showMinimalPrice()) :?> + <?php if ($block->getUseLinkForAsLowAs()) :?> + <a href="<?= $block->escapeUrl($block->getSaleableItem()->getProductUrl()) ?>" class="minimal-price-link"> + <?= /* @noEscape */ $block->renderAmountMinimal() ?> </a> - <?php else:?> + <?php else :?> <span class="minimal-price-link"> - <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> + <?= /* @noEscape */ $block->renderAmountMinimal() ?> </span> <?php endif?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml index f5cffb99d75dd..5949b54268a62 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Generic.WhiteSpace.ScopeIndent + /** @var \Magento\Catalog\Pricing\Render\PriceBox $block */ /** @var \Magento\Catalog\Pricing\Price\TierPrice $tierPriceModel */ @@ -18,17 +18,17 @@ $msrpShowOnGesture = $block->getPriceType('msrp_price')->isShowPriceOnGesture(); $product = $block->getSaleableItem(); ?> <?php if (count($tierPrices)) : ?> - <ul class="<?= /* @escapeNotVerified */ ($block->hasListClass() ? $block->getListClass() : 'prices-tier items') ?>"> - <?php foreach ($tierPrices as $index => $price) : ?> - <li class="item"> - <?php + <ul class="<?= $block->escapeHtmlAttr(($block->hasListClass() ? $block->getListClass() : 'prices-tier items')) ?>"> + <?php foreach ($tierPrices as $index => $price) : ?> + <li class="item"> + <?php $productId = $product->getId(); $isSaleable = $product->isSaleable(); $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - if ($msrpShowOnGesture && $price['price']->getValue() < $product->getMsrp()): + if ($msrpShowOnGesture && $price['price']->getValue() < $product->getMsrp()) : $addToCartUrl = ''; if ($isSaleable) { - $addToCartUrl = $this->helper('\Magento\Checkout\Helper\Cart') + $addToCartUrl = $this->helper(\Magento\Checkout\Helper\Cart::class) ->getAddUrl($product, ['qty' => $price['price_qty']]); } $tierPriceData = [ @@ -54,13 +54,13 @@ $product = $block->getSaleableItem(); if ($block->getCanDisplayQty($product)) { $tierPriceData['qty'] = $price['price_qty']; } - ?> - <?= /* @escapeNotVerified */ __('Buy %1 for: ', $price['price_qty']) ?> - <a href="javascript:void(0);" - id="<?= /* @escapeNotVerified */ ($popupId) ?>" - data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> - <?= /* @escapeNotVerified */ __('Click for price') ?></a> - <?php else: + ?> + <?= $block->escapeHtml(__('Buy %1 for: ', $price['price_qty'])) ?> + <a href="javascript:void(0);" + id="<?= $block->escapeHtmlAttr($popupId) ?>" + data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> + <?= $block->escapeHtml(__('Click for price')) ?></a> + <?php else : $priceAmountBlock = $block->renderAmount( $price['price'], [ @@ -70,22 +70,22 @@ $product = $block->getSaleableItem(); 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_OPTION ] ); - ?> - <?php /* @escapeNotVerified */ echo ($block->getShowDetailedPrice() !== false) - ? __( - 'Buy %1 for %2 each and <strong class="benefit">save<span class="percent tier-%3"> %4</span>%</strong>', - $price['price_qty'], - $priceAmountBlock, - $index, - $tierPriceModel->getSavePercent($price['price']) - ) - : __('Buy %1 for %2 each', $price['price_qty'], $priceAmountBlock); - ?> - <?php endif; ?> - </li> - <?php endforeach; ?> + ?> + <?= /* @noEscape */ ($block->getShowDetailedPrice() !== false) + ? __( + 'Buy %1 for %2 each and <strong class="benefit">save<span class="percent tier-%3"> %4</span>%</strong>', + $price['price_qty'], + $priceAmountBlock, + $index, + $block->formatPercent($price['percentage_value'] ?? $tierPriceModel->getSavePercent($price['price'])) + ) + : __('Buy %1 for %2 each', $price['price_qty'], $priceAmountBlock); + ?> + <?php endif; ?> + </li> + <?php endforeach; ?> </ul> - <?php if ($msrpShowOnGesture):?> + <?php if ($msrpShowOnGesture) :?> <script type="text/x-magento-init"> { ".product-info-main": { @@ -95,9 +95,9 @@ $product = $block->getSaleableItem(); "inputQty": "#qty", "attr": "[data-tier-price]", "productForm": "#product_addtocart_form", - "productId": "<?= /* @escapeNotVerified */ $productId ?>", + "productId": "<?= (int) $productId ?>", "productIdInput": "input[type=hidden][name=product]", - "isSaleable": "<?= /* @escapeNotVerified */ $isSaleable ?>" + "isSaleable": "<?= (bool) $isSaleable ?>" } } } diff --git a/app/code/Magento/Catalog/view/base/web/js/price-options.js b/app/code/Magento/Catalog/view/base/web/js/price-options.js index ceeea4c878622..e18abe3af38a6 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-options.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-options.js @@ -20,8 +20,10 @@ define([ optionConfig: {}, optionHandlers: {}, optionTemplate: '<%= data.label %>' + - '<% if (data.finalPrice.value) { %>' + + '<% if (data.finalPrice.value > 0) { %>' + ' +<%- data.finalPrice.formatted %>' + + '<% } else if (data.finalPrice.value < 0) { %>' + + ' <%- data.finalPrice.formatted %>' + '<% } %>', controlContainer: 'dd' }; diff --git a/app/code/Magento/Catalog/view/base/web/js/price-utils.js b/app/code/Magento/Catalog/view/base/web/js/price-utils.js index 7c4280e64930f..e2ea42f7d5fe3 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-utils.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-utils.js @@ -79,7 +79,7 @@ define([ am = Number(Math.round(Math.abs(amount - i) + 'e+' + precision) + ('e-' + precision)); r = (j ? i.substr(0, j) + groupSymbol : '') + i.substr(j).replace(re, '$1' + groupSymbol) + - (precision ? decimalSymbol + am.toFixed(2).replace(/-/, 0).slice(2) : ''); + (precision ? decimalSymbol + am.toFixed(precision).replace(/-/, 0).slice(2) : ''); return pattern.replace('%s', r).replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } diff --git a/app/code/Magento/Catalog/view/base/web/template/product/link.html b/app/code/Magento/Catalog/view/base/web/template/product/link.html index 98255e1b8a9e2..2c70300f7aec3 100644 --- a/app/code/Magento/Catalog/view/base/web/template/product/link.html +++ b/app/code/Magento/Catalog/view/base/web/template/product/link.html @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ --> -<a class="product-item-link" +<a if="isAllowed()" + class="product-item-link" attr="href: $row().url" text="label"/> diff --git a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html index 318a6ceed69d1..cf76762b1ff58 100644 --- a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html +++ b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image.html @@ -11,6 +11,7 @@ class="product-image-photo" attr="src: getImageUrl($row()), alt: getLabel($row()), + title: getLabel($row()), width: getResizedImageWidth($row()), height: getResizedImageHeight($row())"/> </a> diff --git a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html index 2baa9926df5f1..68b7f4e386896 100644 --- a/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html +++ b/app/code/Magento/Catalog/view/base/web/template/product/list/columns/image_with_borders.html @@ -14,7 +14,7 @@ data-bind="style: {'padding-bottom': getHeight($row())/getWidth($row()) * 100 + '%'}"> <img class="product-image-photo" data-bind="attr: {src: getImageUrl($row()), - alt: getLabel($row())}" /> + alt: getLabel($row()), title: getLabel($row())}" /> </span> </span> </a> diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml index c3d0406740dcc..609fe79041b4f 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml @@ -28,6 +28,13 @@ <argument name="add_attribute" xsi:type="string">itemscope itemtype="http://schema.org/Product"</argument> </arguments> </referenceBlock> + + <referenceBlock name="breadcrumbs" template="Magento_Catalog::product/breadcrumbs.phtml"> + <arguments> + <argument name="viewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\Breadcrumbs</argument> + </arguments> + </referenceBlock> + <referenceContainer name="content"> <container name="product.info.main" htmlTag="div" htmlClass="product-info-main" before="-"> <container name="product.info.price" label="Product info auxiliary container" htmlTag="div" htmlClass="product-info-price"> @@ -84,7 +91,11 @@ <container name="product.info.social" label="Product social links container" htmlTag="div" htmlClass="product-social-links"> <block class="Magento\Catalog\Block\Product\View" name="product.info.addto" as="addto" template="Magento_Catalog::product/view/addto.phtml"> <block class="Magento\Catalog\Block\Product\View\AddTo\Compare" name="view.addto.compare" after="view.addto.wishlist" - template="Magento_Catalog::product/view/addto/compare.phtml" /> + template="Magento_Catalog::product/view/addto/compare.phtml" > + <arguments> + <argument name="addToCompareViewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability</argument> + </arguments> + </block> </block> <block class="Magento\Catalog\Block\Product\View" name="product.info.mailto" template="Magento_Catalog::product/view/mailto.phtml"/> </container> @@ -114,7 +125,11 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"/> + <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"> + <arguments> + <argument name="gallery_options" xsi:type="object">Magento\Catalog\Block\Product\View\GalleryOptions</argument> + </arguments> + </block> <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> <arguments> @@ -130,7 +145,7 @@ </block> </container> <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> - <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" template="Magento_Catalog::product/view/attribute.phtml" group="detailed_info"> + <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" as="description" template="Magento_Catalog::product/view/attribute.phtml" group="detailed_info"> <arguments> <argument name="at_call" xsi:type="string">getDescription</argument> <argument name="at_code" xsi:type="string">description</argument> diff --git a/app/code/Magento/Catalog/view/frontend/requirejs-config.js b/app/code/Magento/Catalog/view/frontend/requirejs-config.js index b588600b7db87..55df18afeb024 100644 --- a/app/code/Magento/Catalog/view/frontend/requirejs-config.js +++ b/app/code/Magento/Catalog/view/frontend/requirejs-config.js @@ -18,5 +18,12 @@ var config = { priceUtils: 'Magento_Catalog/js/price-utils', catalogAddToCart: 'Magento_Catalog/js/catalog-add-to-cart' } + }, + config: { + mixins: { + 'Magento_Theme/js/view/breadcrumbs': { + 'Magento_Catalog/js/product/breadcrumbs': true + } + } } }; diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml index 3619ce94031c2..b50095e91d999 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/cms.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -14,7 +11,7 @@ * @var $block \Magento\Catalog\Block\Category\View */ ?> -<?php if ($block->isContentMode() || $block->isMixedMode()): ?> +<?php if ($block->isContentMode() || $block->isMixedMode()) :?> <div class="category-cms"> <?= $block->getCmsBlockHtml() ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml index 0efce1014f9c2..2f5b852575c78 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/description.phtml @@ -3,19 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** * Category view template * * @var $block \Magento\Catalog\Block\Category\View */ ?> -<?php if ($_description = $block->getCurrentCategory()->getDescription()): ?> +<?php if ($_description = $block->getCurrentCategory()->getDescription()) :?> <div class="category-description"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->categoryAttribute($block->getCurrentCategory(), $_description, 'description') ?> + <?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->categoryAttribute( + $block->getCurrentCategory(), + $_description, + 'description' + ) ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml index edff2147ad14b..0711d085ae6af 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/image.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,14 +10,25 @@ * * @var $block \Magento\Catalog\Block\Category\View */ + +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact +// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput +// phpcs:disable PSR2.Files.ClosingTag ?> <?php - $_helper = $this->helper('Magento\Catalog\Helper\Output'); + $_helper = $this->helper(Magento\Catalog\Helper\Output::class); $_category = $block->getCurrentCategory(); $_imgHtml = ''; if ($_imgUrl = $_category->getImageUrl()) { - $_imgHtml = '<div class="category-image"><img src="' . $_imgUrl . '" alt="' . $block->escapeHtml($_category->getName()) . '" title="' . $block->escapeHtml($_category->getName()) . '" class="image" /></div>'; + $_imgHtml = '<div class="category-image"><img src="' + . $block->escapeUrl($_imgUrl) + . '" alt="' + . $block->escapeHtmlAttr($_category->getName()) + . '" title="' + . $block->escapeHtmlAttr($_category->getName()) + . '" class="image" /></div>'; $_imgHtml = $_helper->categoryAttribute($_category, $_imgHtml, 'image'); - /* @escapeNotVerified */ echo $_imgHtml; + /* @noEscape */ echo $_imgHtml; } ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml index c521cf03ad156..9454fdf5de2b8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/products.phtml @@ -3,17 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable PSR2.Files.ClosingTag + /** * Category view template * * @var $block \Magento\Catalog\Block\Category\View */ ?> -<?php if (!$block->isContentMode() || $block->isMixedMode()): ?> +<?php if (!$block->isContentMode() || $block->isMixedMode()) :?> <?= $block->getProductListHtml() ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml index 774aa8d839e87..65ee7ea789e46 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/rss.phtml @@ -4,9 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Category\Rss\Link */ ?> -<?php if ($block->isRssAllowed() && $block->getLink() && $block->isTopCategory()): ?> - <a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="action link rss"><span><?= /* @escapeNotVerified */ $block->getLabel() ?></span></a> +<?php if ($block->isRssAllowed() && $block->getLink() && $block->isTopCategory()) :?> + <a href="<?= $block->escapeUrl($block->getLink()) ?>" + class="action link rss"><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml index 2b0098be6545b..15fdd30c2d93f 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_block.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <div class="widget block block-category-link"> - <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml index f53c1c1ed90d7..8f3b2ae613731 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/category/widget/link/link_inline.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <span class="widget block block-category-link-inline"> - <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> </span> diff --git a/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml b/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml index 4c103b40ba28c..52bec7858a919 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/frontend_storage_manager.phtml @@ -3,7 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile + +/** @var $block Magento\Catalog\Block\FrontendStorageManager */ ?> <script type="text/x-magento-init"> { @@ -12,9 +13,8 @@ "components": { "storage-manager": { "component": "Magento_Catalog/js/storage-manager", - "appendTo": "<?= /* @escapeNotVerified */ $block->getParentComponentName() ?>", - "storagesConfiguration" : - <?= /* @escapeNotVerified */ $block->getConfigurationJson() ?> + "appendTo": "<?= $block->escapeJs($block->getParentComponentName()) ?>", + "storagesConfiguration" : <?= /* @noEscape */ $block->getConfigurationJson() ?> } } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml new file mode 100644 index 0000000000000..f5dca566abfed --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/templates/messages/addCompareSuccessMessage.phtml @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> +<?= $block->escapeHtml( + __( + 'You added product %1 to the <a href="%2">comparison list</a>.', + $block->getData('product_name'), + $block->getData('compare_list_url') + ), + ['a'] +); diff --git a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml index fa70e15135578..6d5ddb95ab178 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Category left navigation * @@ -15,24 +13,29 @@ <?php if (!$block->getCategory()) { return; } ?> -<?php $_categories = $block->getCurrentChildCategories(); ?> +<?php $_categories = $block->getCurrentChildCategories() ;?> <?php $_count = is_array($_categories) ? count($_categories) : $_categories->count(); ?> -<?php if ($_count): ?> +<?php if ($_count) :?> <div class="block filter"> <div class="title"> - <strong><?= /* @escapeNotVerified */ __('Shop By') ?></strong> + <strong><?= $block->escapeHtml(__('Shop By')) ?></strong> </div> <div class="content"> - <strong class="subtitle"><?= /* @escapeNotVerified */ __('Shopping Options') ?></strong> + <strong class="subtitle"><?= $block->escapeHtml(__('Shopping Options')) ?></strong> <dl class="options" id="narrow-by-list2"> - <dt><?= /* @escapeNotVerified */ __('Category') ?></dt> + <dt><?= $block->escapeHtml(__('Category')) ?></dt> <dd> <ol class="items"> - <?php foreach ($_categories as $_category): ?> - <?php if ($_category->getIsActive()): ?> + <?php /** @var \Magento\Catalog\Model\Category $_category */ ?> + <?php foreach ($_categories as $_category) :?> + <?php if ($_category->getIsActive()) :?> <li class="item"> - <a href="<?= /* @escapeNotVerified */ $block->getCategoryUrl($_category) ?>"<?php if ($block->isCategoryActive($_category)): ?> class="current"<?php endif; ?>><?= $block->escapeHtml($_category->getName()) ?></a> - <span class="count"><?= /* @escapeNotVerified */ $_category->getProductCount() ?></span> + <a href="<?= $block->escapeUrl($block->getCategoryUrl($_category)) ?>" + <?php if ($block->isCategoryActive($_category)) :?> + class="current" + <?php endif; ?> + ><?= $block->escapeHtml($_category->getName()) ?></a> + <span class="count"><?= $block->escapeHtml($_category->getProductCount()) ?></span> </li> <?php endif; ?> <?php endforeach ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml new file mode 100644 index 0000000000000..fd18db94fdc88 --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\Theme\Block\Html\Breadcrumbs $block */ +/** @var \Magento\Catalog\ViewModel\Product\Breadcrumbs $viewModel */ +$viewModel = $block->getData('viewModel'); +?> +<div class="breadcrumbs"></div> +<?php +$widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($viewModel->getJsonConfigurationHtmlEscaped()); +$widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['breadcrumbs']); +?> +<script type="text/x-magento-init"> + { + ".breadcrumbs": { + "breadcrumbs": <?= /* @noEscape */ $widgetOptions ?> + } + } +</script> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml index b8595aae9d993..05a5649135ef5 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/link.phtml @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var $block Magento\Framework\View\Element\Template */ ?> <li class="item link compare" data-bind="scope: 'compareProducts'" data-role="compare-products-link"> - <a class="action compare no-display" title="<?= /* @escapeNotVerified */ __('Compare Products') ?>" + <a class="action compare no-display" title="<?= $block->escapeHtmlAttr(__('Compare Products')) ?>" data-bind="attr: {'href': compareProducts().listUrl}, css: {'no-display': !compareProducts().count}" > - <?= /* @escapeNotVerified */ __('Compare Products') ?> + <?= $block->escapeHtml(__('Compare Products')) ?> <span class="counter qty" data-bind="text: compareProducts().countCaption"></span> </a> </li> <script type="text/x-magento-init"> -{"[data-role=compare-products-link]": {"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>}} +{"[data-role=compare-products-link]": {"Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>}} </script> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 6a2dd1f27d4a9..2cf2399e38af0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -4,14 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable PSR2.ControlStructures.SwitchDeclaration +// phpcs:disable Generic.WhiteSpace.ScopeIndent /* @var $block \Magento\Catalog\Block\Product\Compare\ListCompare */ ?> -<?php $_total = $block->getItems()->getSize() ?> -<?php if ($_total): ?> - <a href="#" class="action print" title="<?= /* @escapeNotVerified */ __('Print This Page') ?>"> - <span><?= /* @escapeNotVerified */ __('Print This Page') ?></span> +<?php $total = $block->getItems()->getSize() ?> +<?php if ($total) :?> + <a href="#" class="action print hidden-print" title="<?= $block->escapeHtmlAttr(__('Print This Page')) ?>"> + <span><?= $block->escapeHtml(__('Print This Page')) ?></span> </a> <div class="table-wrapper comparison"> <table class="data table table-comparison" id="product-comparison" @@ -21,19 +23,19 @@ "selectors":{ "productAddToCartSelector":"button.action.tocart"} }}'> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Compare Products') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('Compare Products')) ?></caption> <thead> <tr> - <?php $_i = 0 ?> - <?php foreach ($block->getItems() as $_item): ?> - <?php if ($_i++ == 0): ?> - <th scope="row" class="cell label remove"><span><?= /* @escapeNotVerified */ __('Remove Product') ?></span></th> + <?php $index = 0 ?> + <?php foreach ($block->getItems() as $item) :?> + <?php if ($index++ == 0) :?> + <th scope="row" class="cell label remove"><span><?= $block->escapeHtml(__('Remove Product')) ?></span></th> <?php endif; ?> - <td class="cell remove product"> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataRemove($_item) ?>' - class="action delete" title="<?= /* @escapeNotVerified */ __('Remove Product') ?>"> - <span><?= /* @escapeNotVerified */ __('Remove Product') ?></span> + <td class="cell remove product hidden-print"> + <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class);?> + <a href="#" data-post='<?= /* @noEscape */ $compareHelper->getPostDataRemove($item) ?>' + class="action delete" title="<?= $block->escapeHtmlAttr(__('Remove Product')) ?>"> + <span><?= $block->escapeHtml(__('Remove Product')) ?></span> </a> </td> <?php endforeach; ?> @@ -41,45 +43,55 @@ </thead> <tbody> <tr> - <?php $_i = 0; ?> - <?php $_helper = $this->helper('Magento\Catalog\Helper\Output'); ?> - <?php /** @var $_item \Magento\Catalog\Model\Product */ ?> - <?php foreach ($block->getItems() as $_item): ?> - <?php if ($_i++ == 0): ?> - <th scope="row" class="cell label product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> + <?php $index = 0; ?> + <?php $helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> + <?php /** @var $item \Magento\Catalog\Model\Product */ ?> + <?php foreach ($block->getItems() as $item) :?> + <?php if ($index++ == 0) :?> + <th scope="row" class="cell label product"> + <span><?= $block->escapeHtml(__('Product')) ?></span> + </th> <?php endif; ?> - <td data-th="<?= $block->escapeHtml(__('Product')) ?>" class="cell product info"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" title="<?= /* @escapeNotVerified */ $block->stripTags($_item->getName(), null, true) ?>"> - <?= $block->getImage($_item, 'product_comparison_list')->toHtml() ?> + <td data-th="<?= $block->escapeHtmlAttr(__('Product')) ?>" class="cell product info"> + <a class="product-item-photo" + href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" + title="<?= /* @noEscape */ $block->stripTags($item->getName(), null, true) ?>"> + <?= $block->getImage($item, 'product_comparison_list')->toHtml() ?> </a> <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" title="<?= /* @escapeNotVerified */ $block->stripTags($_item->getName(), null, true) ?>"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getName(), 'name') ?> + <a href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" + title="<?= /* @noEscape */ $block->stripTags($item->getName(), null, true) ?>"> + <?= /* @noEscape */ $helper->productAttribute($item, $item->getName(), 'name') ?> </a> </strong> - <?= $block->getReviewsSummaryHtml($_item, 'short') ?> - <?= /* @escapeNotVerified */ $block->getProductPrice($_item, '-compare-list-top') ?> - <div class="product-item-actions"> + <?= $block->getReviewsSummaryHtml($item, 'short') ?> + <?= /* @noEscape */ $block->getProductPrice($item, '-compare-list-top') ?> + <div class="product-item-actions hidden-print"> <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <form data-role="tocart-form" action="<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Product\Compare')->getAddToCartUrl($_item) ?>" method="post"> + <?php if ($item->isSaleable()) :?> + <form data-role="tocart-form" + action="<?= $block->escapeUrl($this->helper(Magento\Catalog\Helper\Product\Compare::class)->getAddToCartUrl($item)) ?>" + method="post"> <?= $block->getBlockHtml('formkey') ?> <button type="submit" class="action tocart primary"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </form> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php else :?> + <?php if ($item->getIsSalable()) :?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else :?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?> + <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow()) :?> <div class="secondary-addto-links actions-secondary" data-role="add-to-links"> - <a href="#" data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' + class="action towishlist" + data-action="add-to-wishlist"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> </div> <?php endif; ?> @@ -89,44 +101,46 @@ </tr> </tbody> <tbody> - <?php foreach ($block->getAttributes() as $_attribute): ?> - <tr> - <?php $_i = 0 ?> - <?php foreach ($block->getItems() as $_item): ?> - <?php if ($_i++ == 0): ?> - <th scope="row" class="cell label"> - <span class="attribute label"> - <?= $block->escapeHtml($_attribute->getStoreLabel() ? $_attribute->getStoreLabel() : __($_attribute->getFrontendLabel())) ?> - </span> - </th> - <?php endif; ?> - <td class="cell product attribute"> - <div class="attribute value"> - <?php switch ($_attribute->getAttributeCode()) { - case "price": ?> - <?php - /* @escapeNotVerified */ echo $block->getProductPrice( - $_item, - '-compare-list-' . $_attribute->getCode() + <?php foreach ($block->getAttributes() as $attribute) :?> + <?php $index = 0; ?> + <?php if ($block->hasAttributeValueForProducts($attribute)) :?> + <tr> + <?php foreach ($block->getItems() as $item) :?> + <?php if ($index++ == 0) :?> + <th scope="row" class="cell label"> + <span class="attribute label"> + <?= $block->escapeHtml($attribute->getStoreLabel() ? $attribute->getStoreLabel() : __($attribute->getFrontendLabel())) ?> + </span> + </th> + <?php endif; ?> + <td class="cell product attribute"> + <div class="attribute value"> + <?php switch ($attribute->getAttributeCode()) { + case "price": ?> + <?= + /* @noEscape */ $block->getProductPrice( + $item, + '-compare-list-' . $attribute->getCode() ) - ?> - <?php break; - case "small_image": ?> - <?php $block->getImage($_item, 'product_small_image')->toHtml(); ?> + ?> + <?php break; + case "small_image": ?> + <?php $block->getImage($item, 'product_small_image')->toHtml(); ?> + <?php break; + default: ?> + <?= /* @noEscape */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> <?php break; - default: ?> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $block->getProductAttributeValue($_item, $_attribute), $_attribute->getAttributeCode()) ?> - <?php break; - } ?> - </div> - </td> - <?php endforeach; ?> - </tr> + } ?> + </div> + </td> + <?php endforeach; ?> + </tr> + <?php endif; ?> <?php endforeach; ?> </tbody> </table> </div> - <?php if (!$block->isRedirectToCartEnabled()) : ?> + <?php if (!$block->isRedirectToCartEnabled()) :?> <script type="text/x-magento-init"> { "[data-role=tocart-form]": { @@ -135,6 +149,6 @@ } </script> <?php endif; ?> -<?php else: ?> - <div class="message info empty"><div><?= /* @escapeNotVerified */ __('You have no items to compare.') ?></div></div> +<?php else :?> + <div class="message info empty"><div><?= $block->escapeHtml(__('You have no items to compare.')) ?></div></div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml index 8daa342454445..809ddc5c61701 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/sidebar.phtml @@ -4,12 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /* @var $block \Magento\Framework\View\Element\Template */ ?> <div class="block block-compare" data-bind="scope: 'compareProducts'" data-role="compare-products-sidebar"> <div class="block-title"> - <strong id="block-compare-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Compare Products') ?></strong> + <strong id="block-compare-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Compare Products')) ?></strong> <span class="counter qty no-display" data-bind="text: compareProducts().countCaption, css: {'no-display': !compareProducts().count}"></span> </div> <!-- ko if: compareProducts().count --> @@ -20,29 +21,32 @@ <strong class="product-item-name"> <a data-bind="attr: {href: product_url}, html: name" class="product-item-link"></a> </strong> - <a href="#" data-bind="attr: {'data-post': remove_url}" title="<?= /* @escapeNotVerified */ __('Remove This Item') ?>" class="action delete"> - <span><?= /* @escapeNotVerified */ __('Remove This Item') ?></span> + <a href="#" + data-bind="attr: {'data-post': remove_url}" + title="<?= $block->escapeHtmlAttr(__('Remove This Item')) ?>" + class="action delete"> + <span><?= $block->escapeHtml(__('Remove This Item')) ?></span> </a> </li> </ol> <div class="actions-toolbar"> <div class="primary"> - <a data-bind="attr: {'href': compareProducts().listUrl}" class="action compare primary"><span><?= /* @escapeNotVerified */ __('Compare') ?></span></a> + <a data-bind="attr: {'href': compareProducts().listUrl}" class="action compare primary"><span><?= $block->escapeHtml(__('Compare')) ?></span></a> </div> <div class="secondary"> <a id="compare-clear-all" href="#" class="action clear" data-post="<?=$block->escapeHtml( - $this->helper('Magento\Catalog\Helper\Product\Compare')->getPostDataClearList() + $this->helper(Magento\Catalog\Helper\Product\Compare::class)->getPostDataClearList() ) ?>"> - <span><?= /* @escapeNotVerified */ __('Clear All') ?></span> + <span><?= $block->escapeHtml(__('Clear All')) ?></span> </a> </div> </div> </div> <!-- /ko --> <!-- ko ifnot: compareProducts().count --> - <div class="empty"><?= /* @escapeNotVerified */ __('You have no items to compare.') ?></div> + <div class="empty"><?= $block->escapeHtml(__('You have no items to compare.')) ?></div> <!-- /ko --> </div> <script type="text/x-magento-init"> -{"[data-role=compare-products-sidebar]": {"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>}} +{"[data-role=compare-products-sidebar]": {"Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>}} </script> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml index 6133d55d676c3..c23b6c3404a9e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml @@ -4,42 +4,48 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Catalog\Block\Product\Gallery $block */ ?> <?php $_width = $block->getImageWidth(); ?> -<div class="product-image-popup" style="width:<?= /* @escapeNotVerified */ $_width ?>px;"> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @escapeNotVerified */ __('Close Window') ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()): ?> +<div class="product-image-popup" style="width:<?= /* @noEscape */ $_width ?>px;"> + <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()): ?> - <a href="<?= /* @escapeNotVerified */ $_prevUrl ?>" class="prev">« <?= /* @escapeNotVerified */ __('Prev') ?></a> - <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()): ?> - <a href="<?= /* @escapeNotVerified */ $_nextUrl ?>" class="next"><?= /* @escapeNotVerified */ __('Next') ?> »</a> - <?php endif; ?> + <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php endif; ?> + <?php if ($_nextUrl = $block->getNextImageUrl()) :?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php endif; ?> </div> <?php endif; ?> - <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())): ?> - <h1 class="image-label"><?= /* @escapeNotVerified */ $_imageTitle ?></h1> + <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())) :?> + <h1 class="image-label"><?= /* @noEscape */ $_imageTitle ?></h1> <?php endif; ?> <?php - $imageUrl = $this->helper('Magento\Catalog\Helper\Image') + $imageUrl = $this->helper(Magento\Catalog\Helper\Image::class) ->init($block->getProduct(), 'product_page_image_large') ->setImageFile($block->getImageFile()) ->getUrl(); ?> - <img src="<?= /* @escapeNotVerified */ $imageUrl ?>"<?php if ($_width): ?> width="<?= /* @escapeNotVerified */ $_width ?>"<?php endif; ?> alt="<?= $block->escapeHtml($block->getCurrentImage()->getLabel()) ?>" title="<?= $block->escapeHtml($block->getCurrentImage()->getLabel()) ?>" id="product-gallery-image" class="image" data-mage-init='{"catalogGallery":{}}'/> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= /* @escapeNotVerified */ __('Close Window') ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()): ?> + <img src="<?= $block->escapeUrl($imageUrl) ?>" + <?php if ($_width) :?> + width="<?= /* @noEscape */ $_width ?>" + <?php endif; ?> + alt="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" + title="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" + id="product-gallery-image" + class="image" + data-mage-init='{"catalogGallery":{}}'/> + <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()): ?> - <a href="<?= /* @escapeNotVerified */ $_prevUrl ?>" class="prev">« <?= /* @escapeNotVerified */ __('Prev') ?></a> - <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()): ?> - <a href="<?= /* @escapeNotVerified */ $_nextUrl ?>" class="next"><?= /* @escapeNotVerified */ __('Next') ?> »</a> - <?php endif; ?> + <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php endif; ?> + <?php if ($_nextUrl = $block->getNextImageUrl()) :?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php endif; ?> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml index 94b829eb92137..5a1b102ff6362 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml @@ -7,8 +7,8 @@ <?php /** @var $block \Magento\Catalog\Block\Product\Image */ ?> <img class="photo image" - <?= /* @escapeNotVerified */ $block->getCustomAttributes() ?> - src="<?= /* @escapeNotVerified */ $block->getImageUrl() ?>" - width="<?= /* @escapeNotVerified */ $block->getWidth() ?>" - height="<?= /* @escapeNotVerified */ $block->getHeight() ?>" - alt="<?= /* @escapeNotVerified */ $block->stripTags($block->getLabel(), null, true) ?>" /> + <?= $block->escapeHtml($block->getCustomAttributes()) ?> + src="<?= $block->escapeUrl($block->getImageUrl()) ?>" + width="<?= $block->escapeHtmlAttr($block->getWidth()) ?>" + height="<?= $block->escapeHtmlAttr($block->getHeight()) ?>" + alt="<?= /* @noEscape */ $block->stripTags($block->getLabel(), null, true) ?>" /> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 0352f7f276630..917bea9fbcadb 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -7,13 +7,13 @@ <?php /** @var $block \Magento\Catalog\Block\Product\Image */ ?> <span class="product-image-container" - style="width:<?= /* @escapeNotVerified */ $block->getWidth() ?>px;"> + style="width:<?= $block->escapeHtmlAttr($block->getWidth()) ?>px;"> <span class="product-image-wrapper" - style="padding-bottom: <?= /* @escapeNotVerified */ ($block->getRatio() * 100) ?>%;"> + style="padding-bottom: <?= /* @noEscape */ ($block->getRatio() * 100) ?>%;"> <img class="product-image-photo" - <?= /* @escapeNotVerified */ $block->getCustomAttributes() ?> - src="<?= /* @escapeNotVerified */ $block->getImageUrl() ?>" - width="<?= /* @escapeNotVerified */ $block->getResizedImageWidth() ?>" - height="<?= /* @escapeNotVerified */ $block->getResizedImageHeight() ?>" - alt="<?= /* @escapeNotVerified */ $block->stripTags($block->getLabel(), null, true) ?>"/></span> + <?= $block->escapeHtmlAttr($block->getCustomAttributes()) ?> + src="<?= $block->escapeUrl($block->getImageUrl()) ?>" + width="<?= $block->escapeHtmlAttr($block->getResizedImageWidth()) ?>" + height="<?= $block->escapeHtmlAttr($block->getResizedImageHeight()) ?>" + alt="<?= /* @noEscape */ $block->stripTags($block->getLabel(), null, true) ?>"/></span> </span> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index baf849a295e0b..cdcaaabe5d343 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -5,10 +5,10 @@ */ use Magento\Framework\App\Action\Action; -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** * Product list template * @@ -17,11 +17,11 @@ use Magento\Framework\App\Action\Action; ?> <?php $_productCollection = $block->getLoadedProductCollection(); -$_helper = $this->helper('Magento\Catalog\Helper\Output'); +$_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> -<?php if (!$_productCollection->count()): ?> - <div class="message info empty"><div><?= /* @escapeNotVerified */ __('We can\'t find products matching the selection.') ?></div></div> -<?php else: ?> +<?php if (!$_productCollection->count()) :?> + <div class="message info empty"><div><?= $block->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> +<?php else :?> <?= $block->getToolbarHtml() ?> <?= $block->getAdditionalHtml() ?> <?php @@ -41,13 +41,13 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); */ $pos = $block->getPositioned(); ?> - <div class="products wrapper <?= /* @escapeNotVerified */ $viewMode ?> products-<?= /* @escapeNotVerified */ $viewMode ?>"> + <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>"> <?php $iterator = 1; ?> <ol class="products list items product-items"> <?php /** @var $_product \Magento\Catalog\Model\Product */ ?> - <?php foreach ($_productCollection as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="item product product-item">' : '</li><li class="item product product-item">' ?> - <div class="product-item-info" data-container="product-grid"> + <?php foreach ($_productCollection as $_product) :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="item product product-item">' : '</li><li class="item product product-item">' ?> + <div class="product-item-info" data-container="product-<?= /* @noEscape */ $viewMode ?>"> <?php $productImage = $block->getImage($_product, $image); if ($pos != null) { @@ -56,7 +56,9 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); } ?> <?php // Product Image ?> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product photo product-item-photo" tabindex="-1"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + class="product photo product-item-photo" + tabindex="-1"> <?= $productImage->toHtml() ?> </a> <div class="product details product-item-details"> @@ -65,48 +67,55 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); ?> <strong class="product name product-item-name"> <a class="product-item-link" - href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> + href="<?= $block->escapeUrl($_product->getProductUrl()) ?>"> + <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> </a> </strong> <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> - <?= /* @escapeNotVerified */ $block->getProductPrice($_product) ?> + <?= /* @noEscape */ $block->getProductPrice($_product) ?> <?= $block->getProductDetailsHtml($_product) ?> <div class="product-item-inner"> - <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $position : '' ?>> - <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $position : '' ?>> - <?php if ($_product->isSaleable()): ?> + <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $block->escapeHtmlAttr($position) : '' ?>> + <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $block->escapeHtmlAttr($position) : '' ?>> + <?php if ($_product->isSaleable()) :?> <?php $postParams = $block->getAddToCartPostParams($_product); ?> - <form data-role="tocart-form" data-product-sku="<?= /* @NoEscape */ $_product->getSku() ?>" action="<?= /* @NoEscape */ $postParams['action'] ?>" method="post"> - <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $postParams['data']['product'] ?>"> - <input type="hidden" name="<?= /* @escapeNotVerified */ Action::PARAM_NAME_URL_ENCODED ?>" value="<?= /* @escapeNotVerified */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <form data-role="tocart-form" + data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" + action="<?= $block->escapeUrl($postParams['action']) ?>" + method="post"> + <input type="hidden" + name="product" + value="<?= /* @noEscape */ $postParams['data']['product'] ?>"> + <input type="hidden" name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" + value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> <?= $block->getBlockHtml('formkey') ?> <button type="submit" - title="<?= $block->escapeHtml(__('Add to Cart')) ?>" - class="action tocart primary"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" + class="action tocart primary" disabled> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </form> - <?php else: ?> - <?php if ($_product->isAvailable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php else :?> + <?php if ($_product->isAvailable()) :?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else :?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> - <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $position : '' ?>> - <?php if ($addToBlock = $block->getChildBlock('addto')): ?> + <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $block->escapeHtmlAttr($position) : '' ?>> + <?php if ($addToBlock = $block->getChildBlock('addto')) :?> <?= $addToBlock->setProduct($_product)->getChildHtml() ?> <?php endif; ?> </div> </div> - <?php if ($showDescription):?> + <?php if ($showDescription) :?> <div class="product description product-item-description"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" title="<?= /* @escapeNotVerified */ $_productNameStripped ?>" - class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> + <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= /* @noEscape */ $_productNameStripped ?>" + class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> </div> <?php endif; ?> </div> @@ -117,12 +126,12 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); </ol> </div> <?= $block->getToolbarHtml() ?> - <?php if (!$block->isRedirectToCartEnabled()) : ?> + <?php if (!$block->isRedirectToCartEnabled()) :?> <script type="text/x-magento-init"> { "[data-role=tocart-form], .form.map.checkout": { "catalogAddToCart": { - "product_sku": "<?= /* @NoEscape */ $_product->getSku() ?>" + "product_sku": "<?= $block->escapeJs($_product->getSku()) ?>" } } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml index 8798170e8c0b0..c23ee021ca3a8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/addto/compare.phtml @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** @var $block Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare */ ?> <a href="#" class="action tocompare" title="<?= $block->escapeHtml(__('Add to Compare')) ?>" aria-label="<?= $block->escapeHtml(__('Add to Compare')) ?>" - data-post='<?= /* @escapeNotVerified */ $block->getCompareHelper()->getPostDataParams($block->getProduct()) ?>' + data-post='<?= /* @noEscape */ $block->getCompareHelper()->getPostDataParams($block->getProduct()) ?>' role="button"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> </a> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index fc0967ca60d2d..f71494748c6c5 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect +// phpcs:disable Squiz.WhiteSpace.ControlStructureSpacing /* @var $block \Magento\Catalog\Block\Product\AbstractProduct */ ?> <?php + switch ($type = $block->getType()) { case 'related-rule': @@ -29,7 +32,7 @@ switch ($type = $block->getType()) { $templateType = null; $description = false; } - break; + break; case 'related': /** @var \Magento\Catalog\Block\Product\ProductList\Related $block */ @@ -49,7 +52,7 @@ switch ($type = $block->getType()) { $templateType = null; $description = false; } - break; + break; case 'upsell-rule': if ($exist = $block->hasItems()) { @@ -68,7 +71,7 @@ switch ($type = $block->getType()) { $description = false; $canItemsAddToCart = false; } - break; + break; case 'upsell': /** @var \Magento\Catalog\Block\Product\ProductList\Upsell $block */ @@ -88,7 +91,7 @@ switch ($type = $block->getType()) { $description = false; $canItemsAddToCart = false; } - break; + break; case 'crosssell-rule': /** @var \Magento\Catalog\Block\Product\ProductList\Crosssell $block */ @@ -106,7 +109,7 @@ switch ($type = $block->getType()) { $description = false; $canItemsAddToCart = false; } - break; + break; case 'crosssell': /** @var \Magento\Catalog\Block\Product\ProductList\Crosssell $block */ @@ -124,7 +127,7 @@ switch ($type = $block->getType()) { $description = false; $canItemsAddToCart = false; } - break; + break; case 'new': if ($exist = $block->getProductCollection()) { @@ -144,106 +147,104 @@ switch ($type = $block->getType()) { $description = ($mode == 'list') ? true : false; $canItemsAddToCart = false; } - break; + break; - case 'other': - break; + default: + $exist = null; } ?> - -<?php if ($exist):?> - - <?php if ($type == 'related' || $type == 'upsell'): ?> - <?php if ($type == 'related'): ?> - <div class="block <?= /* @escapeNotVerified */ $class ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= /* @escapeNotVerified */ $limit ?>" data-shuffle="<?= /* @escapeNotVerified */ $shuffle ?>"> - <?php else: ?> - <div class="block <?= /* @escapeNotVerified */ $class ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= /* @escapeNotVerified */ $limit ?>" data-shuffle="<?= /* @escapeNotVerified */ $shuffle ?>"> +<?php if ($exist) :?> +<?php if ($type == 'related' || $type == 'upsell') :?> +<?php if ($type == 'related') :?> +<div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>"> + <?php else :?> + <div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>"> <?php endif; ?> - <?php else: ?> - <div class="block <?= /* @escapeNotVerified */ $class ?>"> + <?php else :?> + <div class="block <?= $block->escapeHtmlAttr($class) ?>"> <?php endif; ?> <div class="block-title title"> - <strong id="block-<?= /* @escapeNotVerified */ $class ?>-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong id="block-<?= $block->escapeHtmlAttr($class) ?>-heading" role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> - <div class="block-content content" aria-labelledby="block-<?= /* @escapeNotVerified */ $class ?>-heading"> - <?php if ($type == 'related' && $canItemsAddToCart): ?> + <div class="block-content content" aria-labelledby="block-<?= $block->escapeHtmlAttr($class) ?>-heading"> + <?php if ($type == 'related' && $canItemsAddToCart) :?> <div class="block-actions"> - <?= /* @escapeNotVerified */ __('Check items to add to the cart or') ?> - <button type="button" class="action select" role="select-all"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> + <?= $block->escapeHtml(__('Check items to add to the cart or')) ?> + <button type="button" class="action select" role="select-all"><span><?= $block->escapeHtml(__('select all')) ?></span></button> </div> <?php endif; ?> - <div class="products wrapper grid products-grid products-<?= /* @escapeNotVerified */ $type ?>"> + <div class="products wrapper grid products-grid products-<?= $block->escapeHtmlAttr($type) ?>"> <ol class="products list items product-items"> <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> + <?php foreach ($items as $_item) :?> <?php $available = ''; ?> - <?php if (!$_item->isComposite() && $_item->isSaleable() && $type == 'related'): ?> - <?php if (!$_item->getRequiredOptions()): ?> + <?php if (!$_item->isComposite() && $_item->isSaleable() && $type == 'related') :?> + <?php if (!$_item->getRequiredOptions()) :?> <?php $available = 'related-available'; ?> <?php endif; ?> <?php endif; ?> - <?php if ($type == 'related' || $type == 'upsell'): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="item product product-item" style="display: none;">' : '</li><li class="item product product-item" style="display: none;">' ?> - <?php else: ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="item product product-item">' : '</li><li class="item product product-item">' ?> + <?php if ($type == 'related' || $type == 'upsell') :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="item product product-item" style="display: none;">' : '</li><li class="item product product-item" style="display: none;">' ?> + <?php else :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="item product product-item">' : '</li><li class="item product product-item">' ?> <?php endif; ?> - <div class="product-item-info <?= /* @escapeNotVerified */ $available ?>"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product photo product-item-photo"> + <div class="product-item-info <?= /* @noEscape */ $available ?>"> + <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product photo product-item-photo"> <?= $block->getImage($_item, $image)->toHtml() ?> </a> <div class="product details product-item-details"> - <strong class="product name product-item-name"><a class="product-item-link" title="<?= $block->escapeHtml($_item->getName()) ?>" href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>"> + <strong class="product name product-item-name"><a class="product-item-link" title="<?= $block->escapeHtml($_item->getName()) ?>" href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>"> <?= $block->escapeHtml($_item->getName()) ?></a> </strong> - <?= /* @escapeNotVerified */ $block->getProductPrice($_item) ?> + <?= /* @noEscape */ $block->getProductPrice($_item) ?> - <?php if ($templateType): ?> - <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> - <?php endif; ?> + <?php if ($templateType) :?> + <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> + <?php endif; ?> - <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related'): ?> - <?php if (!$_item->getRequiredOptions()): ?> - <div class="field choice related"> - <input type="checkbox" class="checkbox related" id="related-checkbox<?= /* @escapeNotVerified */ $_item->getId() ?>" name="related_products[]" value="<?= /* @escapeNotVerified */ $_item->getId() ?>" /> - <label class="label" for="related-checkbox<?= /* @escapeNotVerified */ $_item->getId() ?>"><span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span></label> - </div> - <?php endif; ?> - <?php endif; ?> + <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related') :?> + <?php if (!$_item->getRequiredOptions()) :?> + <div class="field choice related"> + <input type="checkbox" class="checkbox related" id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" name="related_products[]" value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> + <label class="label" for="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span></label> + </div> + <?php endif; ?> + <?php endif; ?> - <?php if ($showAddTo || $showCart): ?> - <div class="product actions product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> - <?php endif; ?> + <?php if ($showAddTo || $showCart) :?> + <div class="product actions product-item-actions"> + <?php if ($showCart) :?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) :?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) :?> + <button class="action tocart primary" data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else :?> + <?php $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->escapeUrl($block->getAddToCartUrl($_item)), ['product' => $_item->getEntityId()]) + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + <?php else :?> + <?php if ($_item->getIsSalable()) :?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else :?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> + <?php endif; ?> + </div> <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showAddTo): ?> + <?php if ($showAddTo) :?> <div class="secondary-addto-links actions-secondary" data-role="add-to-links"> - <?php if ($addToBlock = $block->getChildBlock('addto')): ?> + <?php if ($addToBlock = $block->getChildBlock('addto')) :?> <?= $addToBlock->setProduct($_item)->getChildHtml() ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml index 02a6e999ad51f..76ef6baf4993e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,11 +10,16 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ -use Magento\Catalog\Model\Product\ProductList\Toolbar; + +// phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile +// phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket ?> -<?php if ($block->getCollection()->getSize()): ?> - <div class="toolbar toolbar-products" data-mage-init='<?= /* @escapeNotVerified */ $block->getWidgetOptionsJson() ?>'> - <?php if ($block->isExpanded()): ?> +<?php if ($block->getCollection()->getSize()) :?> + <?php $widget = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getWidgetOptionsJson()); + $widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['productListToolbarForm']); + ?> + <div class="toolbar toolbar-products" data-mage-init='{"productListToolbarForm":<?= /* @noEscape */ $widgetOptions ?>}'> + <?php if ($block->isExpanded()) :?> <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?> <?php endif; ?> @@ -27,7 +29,7 @@ use Magento\Catalog\Model\Product\ProductList\Toolbar; <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?> - <?php if ($block->isExpanded()): ?> + <?php if ($block->isExpanded()) :?> <?php include ($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml index b4ff1afa1c606..a8f504d6a4f17 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/amount.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,19 +10,27 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ -use Magento\Catalog\Model\Product\ProductList\Toolbar; ?> <p class="toolbar-amount" id="toolbar-amount"> - <?php if ($block->getLastPageNum() > 1): ?> - <?php /* @escapeNotVerified */ echo __('Items %1-%2 of %3', - '<span class="toolbar-number">' . $block->getFirstNum() . '</span>', - '<span class="toolbar-number">' . $block->getLastNum() . '</span>', - '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> - <?php elseif ($block->getTotalNum() == 1): ?> - <?php /* @escapeNotVerified */ echo __('%1 Item', - '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> - <?php else: ?> - <?php /* @escapeNotVerified */ echo __('%1 Items', - '<span class="toolbar-number">' . $block->getTotalNum() . '</span>') ?> + <?php if ($block->getLastPageNum() > 1) :?> + <?= $block->escapeHtml( + __( + 'Items %1-%2 of %3', + '<span class="toolbar-number">' . $block->getFirstNum() . '</span>', + '<span class="toolbar-number">' . $block->getLastNum() . '</span>', + '<span class="toolbar-number">' . $block->getTotalNum() . '</span>' + ), + ['span'] + ) ?> + <?php elseif ($block->getTotalNum() == 1) :?> + <?= $block->escapeHtml( + __('%1 Item', '<span class="toolbar-number">' . $block->getTotalNum() . '</span>'), + ['span'] + ) ?> + <?php else :?> + <?= $block->escapeHtml( + __('%1 Items', '<span class="toolbar-number">' . $block->getTotalNum() . '</span>'), + ['span'] + ) ?> <?php endif; ?> </p> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml index ec4541bde5ca6..4ded219748c64 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/limiter.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,21 +10,22 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ -use Magento\Catalog\Model\Product\ProductList\Toolbar; ?> <div class="field limiter"> <label class="label" for="limiter"> - <span><?= /* @escapeNotVerified */ __('Show') ?></span> + <span><?= $block->escapeHtml(__('Show')) ?></span> </label> <div class="control"> <select id="limiter" data-role="limiter" class="limiter-options"> - <?php foreach ($block->getAvailableLimit() as $_key => $_limit): ?> - <option value="<?= /* @escapeNotVerified */ $_key ?>"<?php if ($block->isLimitCurrent($_key)): ?> - selected="selected"<?php endif ?>> - <?= /* @escapeNotVerified */ $_limit ?> + <?php foreach ($block->getAvailableLimit() as $_key => $_limit) :?> + <option value="<?= $block->escapeHtmlAttr($_key) ?>" + <?php if ($block->isLimitCurrent($_key)) :?> + selected="selected" + <?php endif ?>> + <?= $block->escapeHtml($_limit) ?> </option> <?php endforeach; ?> </select> </div> - <span class="limiter-text"><?= /* @escapeNotVerified */ __('per page') ?></span> + <span class="limiter-text"><?= $block->escapeHtml(__('per page')) ?></span> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml index 92514c5b8ea50..58dde199998bc 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/sorter.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,14 +10,13 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ -use Magento\Catalog\Model\Product\ProductList\Toolbar; ?> <div class="toolbar-sorter sorter"> - <label class="sorter-label" for="sorter"><?= /* @escapeNotVerified */ __('Sort By') ?></label> + <label class="sorter-label" for="sorter"><?= $block->escapeHtml(__('Sort By')) ?></label> <select id="sorter" data-role="sorter" class="sorter-options"> - <?php foreach ($block->getAvailableOrders() as $_key => $_order): ?> - <option value="<?= /* @escapeNotVerified */ $_key ?>" - <?php if ($block->isOrderCurrent($_key)): ?> + <?php foreach ($block->getAvailableOrders() as $_key => $_order) :?> + <option value="<?= $block->escapeHtmlAttr($_key) ?>" + <?php if ($block->isOrderCurrent($_key)) :?> selected="selected" <?php endif; ?> > @@ -28,13 +24,21 @@ use Magento\Catalog\Model\Product\ProductList\Toolbar; </option> <?php endforeach; ?> </select> - <?php if ($block->getCurrentDirection() == 'desc'): ?> - <a title="<?= /* @escapeNotVerified */ __('Set Ascending Direction') ?>" href="#" class="action sorter-action sort-desc" data-role="direction-switcher" data-value="asc"> - <span><?= /* @escapeNotVerified */ __('Set Ascending Direction') ?></span> + <?php if ($block->getCurrentDirection() == 'desc') :?> + <a title="<?= $block->escapeHtmlAttr(__('Set Ascending Direction')) ?>" + href="#" + class="action sorter-action sort-desc" + data-role="direction-switcher" + data-value="asc"> + <span><?= $block->escapeHtml(__('Set Ascending Direction')) ?></span> </a> - <?php else: ?> - <a title="<?= /* @escapeNotVerified */ __('Set Descending Direction') ?>" href="#" class="action sorter-action sort-asc" data-role="direction-switcher" data-value="desc"> - <span><?= /* @escapeNotVerified */ __('Set Descending Direction') ?></span> + <?php else :?> + <a title="<?= $block->escapeHtmlAttr(__('Set Descending Direction')) ?>" + href="#" + class="action sorter-action sort-asc" + data-role="direction-switcher" + data-value="desc"> + <span><?= $block->escapeHtml(__('Set Descending Direction')) ?></span> </a> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml index 366dfba71b0d1..955897f315d6f 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar/viewmode.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,32 +10,31 @@ * * @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar */ -use Magento\Catalog\Model\Product\ProductList\Toolbar; ?> -<?php if ($block->isEnabledViewSwitcher()): ?> -<div class="modes"> - <?php $_modes = $block->getModes(); ?> - <?php if ($_modes && count($_modes) > 1): ?> - <strong class="modes-label" id="modes-label"><?= /* @escapeNotVerified */ __('View as') ?></strong> - <?php foreach ($block->getModes() as $_code => $_label): ?> - <?php if ($block->isModeActive($_code)): ?> - <strong title="<?= /* @escapeNotVerified */ $_label ?>" - class="modes-mode active mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" - data-value="<?= /* @escapeNotVerified */ strtolower($_code) ?>"> - <span><?= /* @escapeNotVerified */ $_label ?></span> - </strong> - <?php else: ?> - <a class="modes-mode mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" - title="<?= /* @escapeNotVerified */ $_label ?>" - href="#" - data-role="mode-switcher" - data-value="<?= /* @escapeNotVerified */ strtolower($_code) ?>" - id="mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>" - aria-labelledby="modes-label mode-<?= /* @escapeNotVerified */ strtolower($_code) ?>"> - <span><?= /* @escapeNotVerified */ $_label ?></span> - </a> - <?php endif; ?> - <?php endforeach; ?> - <?php endif; ?> -</div> +<?php if ($block->isEnabledViewSwitcher()) :?> + <div class="modes"> + <?php $_modes = $block->getModes(); ?> + <?php if ($_modes && count($_modes) > 1) :?> + <strong class="modes-label" id="modes-label"><?= $block->escapeHtml(__('View as')) ?></strong> + <?php foreach ($block->getModes() as $_code => $_label) :?> + <?php if ($block->isModeActive($_code)) :?> + <strong title="<?= $block->escapeHtmlAttr($_label) ?>" + class="modes-mode active mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" + data-value="<?= $block->escapeHtmlAttr(strtolower($_code)) ?>"> + <span><?= $block->escapeHtml($_label) ?></span> + </strong> + <?php else :?> + <a class="modes-mode mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" + title="<?= $block->escapeHtmlAttr($_label) ?>" + href="#" + data-role="mode-switcher" + data-value="<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" + id="mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>" + aria-labelledby="modes-label mode-<?= $block->escapeHtmlAttr(strtolower($_code)) ?>"> + <span><?= $block->escapeHtml($_label) ?></span> + </a> + <?php endif; ?> + <?php endforeach; ?> + <?php endif; ?> + </div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml index 5f437cc484ed4..bddaa3f3d0b0b 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml @@ -3,28 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable Magento2.Files.LineLength.MaxExceeded +// phpcs:disable Magento2.Security.LanguageConstruct.DirectOutput + /** * Product list template * - * @see \Magento\Catalog\Block\Product\ListProduct + * @var $block \Magento\Catalog\Block\Product\ListProduct */ ?> <?php $start = microtime(true); $_productCollection = $block->getLoadedProductCollection(); -$_helper = $this->helper('Magento\Catalog\Helper\Output'); +$_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> -<?php if (!$_productCollection->count()): ?> -<p class="message note"><?= /* @escapeNotVerified */ __('We can\'t find products matching the selection.') ?></p> -<?php else: ?> -<?= $block->getToolbarHtml() ?> -<?= $block->getAdditionalHtml() ?> -<?php +<?php if (!$_productCollection->count()) :?> + <p class="message note"><?= $block->escapeHtml(__('We can\'t find products matching the selection.')) ?></p> +<?php else :?> + <?= $block->getToolbarHtml() ?> + <?= $block->getAdditionalHtml() ?> + <?php if ($block->getMode() == 'grid') { $viewMode = 'grid'; $image = 'category_page_grid'; @@ -37,56 +38,56 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW; } ?> -<div class="products wrapper <?= /* @escapeNotVerified */ $viewMode ?>"> +<div class="products wrapper <?= /* @noEscape */ $viewMode ?>"> <?php $iterator = 1; ?> <ol class="products list items"> - <?php foreach ($_productCollection as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="item product">' : '</li><li class="item product">' ?> + <?php foreach ($_productCollection as $_product) :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="item product">' : '</li><li class="item product">' ?> <div class="product"> <?php // Product Image ?> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product photo"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" class="product photo"> <?= $block->getImage($_product, $image)->toHtml() ?> </a> <div class="product details"> - <?php + <?php $info = []; $info['name'] = '<strong class="product name">' - . ' <a href="' . $_product->getProductUrl() . '" title="' - . $block->stripTags($_product->getName(), null, true) . '">' - . $_helper->productAttribute($_product, $_product->getName(), 'name') - . '</a></strong>'; + . ' <a href="' . $block->escapeUrl($_product->getProductUrl()) . '" title="' + . $block->stripTags($_product->getName(), null, true) . '">' + . $_helper->productAttribute($_product, $_product->getName(), 'name') + . '</a></strong>'; $info['price'] = $block->getProductPrice($_product); $info['review'] = $block->getReviewsSummaryHtml($_product, $templateType); if ($_product->isSaleable()) { - $info['button'] = '<button type="button" title="' . __('Add to Cart') . '" class="action tocart"' - . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->getAddToCartUrl($_product) . '"} }\'>' - . '<span>' . __('Add to Cart') . '</span></button>'; + $info['button'] = '<button type="button" title="' . $block->escapeHtmlAttr(__('Add to Cart')) . '" class="action tocart"' + . ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->escapeUrl($block->getAddToCartUrl($_product)) . '"} }\'>' + . '<span>' . $block->escapeHtml(__('Add to Cart')) . '</span></button>'; } else { - $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . __('In stock') . '</span></div>' : - '<div class="stock unavailable"><span>' . __('Out of stock') . '</span></div>'; + $info['button'] = $_product->getIsSalable() ? '<div class="stock available"><span>' . $block->escapeHtml(__('In stock')) . '</span></div>' : + '<div class="stock unavailable"><span>' . $block->escapeHtml(__('Out of stock')) . '</span></div>'; } $info['links'] = '<div class="product links" data-role="add-to-links">' - . '<a href="#" data-post=\'' . $this->helper('Magento\Wishlist\Helper\Data')->getAddParams($_product) . '\' class="action towishlist" data-action="add-to-wishlist">' - . '<span>' . __('Add to Wish List') . '</span></a>' - . '<a href="' . $block->getAddToCompareUrl($_product) . '" class="action tocompare">' - . '<span>' . __('Add to Compare') . '</span></a></div>'; + . '<a href="#" data-post=\'' . $this->helper(Magento\Wishlist\Helper\Data::class)->getAddParams($_product) . '\' class="action towishlist" data-action="add-to-wishlist">' + . '<span>' . $block->escapeHtml(__('Add to Wish List')) . '</span></a>' + . '<a href="' . $block->escapeUrl($block->getAddToCompareUrl($_product)) . '" class="action tocompare">' + . '<span>' . $block->escapeHtml(__('Add to Compare')) . '</span></a></div>'; $info['actions'] = '<div class="product action">' . $info['button'] . $info['links'] . '</div>'; if ($showDescription) { $info['description'] = '<div class="product description">' - . $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') - . ' <a href="' . $_product->getProductUrl() . '" class="action more">' - . __('Learn More') . '</a></div>'; + . $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') + . ' <a href="' . $block->escapeUrl($_product->getProductUrl()) . '" class="action more">' + . $block->escapeHtml(__('Learn More')) . '</a></div>'; } else { $info['description'] = ''; } $details = $block->getInfoOrder() ?: ['name','price','review','description','actions']; foreach ($details as $detail) { - /* @escapeNotVerified */ echo $info[$detail]; + /* @noEscape */ echo $info[$detail]; } ?> @@ -98,4 +99,4 @@ $_helper = $this->helper('Magento\Catalog\Helper\Output'); </div> <?= $block->getToolbarHtml() ?> <?php endif; ?> -<?= /* @escapeNotVerified */ $time_taken = microtime(true) - $start ?> +<?= /* @noEscape */ $time_taken = microtime(true) - $start ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml index 2d89e24cc7aac..14e27124d51c6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/additional.phtml @@ -4,9 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag +/** @var $block \Magento\Catalog\Block\Product\View\Additional */ ?> -<?php foreach ($block->getChildHtmlList() as $_html): ?> - <?= /* @escapeNotVerified */ $_html ?> +<?php foreach ($block->getChildHtmlList() as $_html) :?> + <?= /* @noEscape */ $_html ?> <?php endforeach; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml index 0893cfab0bbf8..1924175764555 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Product\View*/ ?> <div class="product-addto-links" data-role="add-to-links"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml index 16a5147e13458..78fd641466c6e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -/** @var $block \Magento\Catalog\Block\Catalog\Product\View\Addto\Compare */ +/** @var $block \Magento\Catalog\Block\Product\View\Addto\Compare */ ?> - -<a href="#" data-post='<?= /* @escapeNotVerified */ $block->getPostDataParams() ?>' +<?php $viewModel = $block->getData('addToCompareViewModel'); ?> +<?php if ($viewModel->isAvailableForCompare($block->getProduct())) : ?> +<a href="#" data-post='<?= /* @noEscape */ $block->getPostDataParams() ?>' data-role="add-to-links" - class="action tocompare"><span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span></a> + class="action tocompare"><span><?= $block->escapeHtml(__('Add to Compare')) ?></span></a> +<?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml index c7f577107095f..c094ff3a70a8b 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml @@ -4,24 +4,23 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Product\View */ ?> <?php $_product = $block->getProduct(); ?> <?php $buttonTitle = __('Add to Cart'); ?> -<?php if ($_product->isSaleable()): ?> +<?php if ($_product->isSaleable()) :?> <div class="box-tocart"> <div class="fieldset"> - <?php if ($block->shouldRenderQuantity()): ?> + <?php if ($block->shouldRenderQuantity()) :?> <div class="field qty"> - <label class="label" for="qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></label> + <label class="label" for="qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> <input type="number" name="qty" id="qty" - value="<?= /* @escapeNotVerified */ $block->getProductDefaultQty() * 1 ?>" - title="<?= /* @escapeNotVerified */ __('Qty') ?>" + min="0" + value="<?= /* @noEscape */ $block->getProductDefaultQty() * 1 ?>" + title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" class="input-text qty" data-validate="<?= $block->escapeHtml(json_encode($block->getQuantityValidators())) ?>" /> @@ -30,27 +29,16 @@ <?php endif; ?> <div class="actions"> <button type="submit" - title="<?= /* @escapeNotVerified */ $buttonTitle ?>" + title="<?= $block->escapeHtmlAttr($buttonTitle) ?>" class="action primary tocart" - id="product-addtocart-button"> - <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> + id="product-addtocart-button" disabled> + <span><?= $block->escapeHtml($buttonTitle) ?></span> </button> <?= $block->getChildHtml('', true) ?> </div> </div> </div> <?php endif; ?> -<?php if ($block->isRedirectToCartEnabled()) : ?> -<script type="text/x-magento-init"> - { - "#product_addtocart_form": { - "Magento_Catalog/product/view/validation": { - "radioCheckboxClosest": ".nested" - } - } - } -</script> -<?php else : ?> <script type="text/x-magento-init"> { "#product_addtocart_form": { @@ -58,4 +46,3 @@ } } </script> -<?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml index 4ea207dffae7f..f06c2d8df66b6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml @@ -4,17 +4,22 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** * Product view template * - * @see \Magento\Catalog\Block\Product\View\Description + * @var $block \Magento\Catalog\Block\Product\View\Description */ ?> <?php -$_helper = $this->helper('Magento\Catalog\Helper\Output'); +$_helper = $this->helper(Magento\Catalog\Helper\Output::class); $_product = $block->getProduct(); + +if (!$_product instanceof \Magento\Catalog\Model\Product) { + return; +} + $_call = $block->getAtCall(); $_code = $block->getAtCode(); $_className = $block->getCssClass(); @@ -32,14 +37,18 @@ if ($_attributeLabel && $_attributeLabel == 'default') { $_attributeLabel = $_product->getResource()->getAttribute($_code)->getStoreLabel(); } if ($_attributeType && $_attributeType == 'text') { - $_attributeValue = ($_helper->productAttribute($_product, $_product->$_call(), $_code)) ? $_product->getAttributeText($_code) : ''; + $_attributeValue = ($_helper->productAttribute($_product, $_product->$_call(), $_code)) + ? $_product->getAttributeText($_code) + : ''; } else { $_attributeValue = $_helper->productAttribute($_product, $_product->$_call(), $_code); } ?> -<?php if ($_attributeValue): ?> -<div class="product attribute <?= /* @escapeNotVerified */ $_className ?>"> - <?php if ($renderLabel): ?><strong class="type"><?= /* @escapeNotVerified */ $_attributeLabel ?></strong><?php endif; ?> - <div class="value" <?= /* @escapeNotVerified */ $_attributeAddAttribute ?>><?= /* @escapeNotVerified */ $_attributeValue ?></div> +<?php if ($_attributeValue) :?> +<div class="product attribute <?= $block->escapeHtmlAttr($_className) ?>"> + <?php if ($renderLabel) :?> + <strong class="type"><?= $block->escapeHtml($_attributeLabel) ?></strong> + <?php endif; ?> + <div class="value" <?= /* @noEscape */ $_attributeAddAttribute ?>><?= /* @noEscape */ $_attributeValue ?></div> </div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml index c930d2195a01b..a4f0fb3efab9e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** * Product additional attributes template @@ -13,18 +13,18 @@ */ ?> <?php - $_helper = $this->helper('Magento\Catalog\Helper\Output'); + $_helper = $this->helper(Magento\Catalog\Helper\Output::class); $_product = $block->getProduct(); ?> -<?php if ($_additional = $block->getAdditionalData()): ?> +<?php if ($_additional = $block->getAdditionalData()) :?> <div class="additional-attributes-wrapper table-wrapper"> <table class="data table additional-attributes" id="product-attribute-specs-table"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('More Information') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('More Information')) ?></caption> <tbody> - <?php foreach ($_additional as $_data): ?> + <?php foreach ($_additional as $_data) :?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> - <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> + <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> + <td class="col data" data-th="<?= $block->escapeHtmlAttr($_data['label']) ?>"><?= /* @noEscape */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> </tr> <?php endforeach; ?> </tbody> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml index 4414214f99a6e..a4aa675b2c346 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/counter.phtml @@ -13,7 +13,7 @@ { "*": { "Magento_Catalog/js/product/view/provider": { - "data": <?= /* @escapeNotVerified */ $block->getCurrentProductData() ?> + "data": <?= /* @noEscape */ $block->getCurrentProductData() ?> } } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml index b5cdd1a2a31ba..36dbe7fbc35fe 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/description.phtml @@ -4,7 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable PSR2.Files.ClosingTag /** * Product description template @@ -12,4 +13,8 @@ * @var $block \Magento\Catalog\Block\Product\View\Description */ ?> -<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($block->getProduct(), $block->getProduct()->getDescription(), 'description') ?> +<?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->productAttribute( + $block->getProduct(), + $block->getProduct()->getDescription(), + 'description' +) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index b1af46b80552d..8887d0074d52f 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml @@ -4,35 +4,34 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Catalog\Block\Product\View\Details $block */ ?> -<?php if ($detailedInfoGroup = $block->getGroupChildNames('detailed_info', 'getChildHtml')):?> +<?php if ($detailedInfoGroup = $block->getGroupChildNames('detailed_info', 'getChildHtml')) :?> <div class="product info detailed"> <?php $layout = $block->getLayout(); ?> <div class="product data items" data-mage-init='{"tabs":{"openedState":"active"}}'> - <?php foreach ($detailedInfoGroup as $name):?> + <?php foreach ($detailedInfoGroup as $name) :?> <?php - $html = $layout->renderElement($name); - if (!trim($html)) { - continue; - } - $alias = $layout->getElementAlias($name); - $label = $block->getChildData($alias, 'title'); + $html = $layout->renderElement($name); + if (!trim($html)) { + continue; + } + $alias = $layout->getElementAlias($name); + $label = $block->getChildData($alias, 'title'); ?> <div class="data item title" - aria-labeledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" - data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> + data-role="collapsible" id="tab-label-<?= $block->escapeHtmlAttr($alias) ?>"> <a class="data switch" tabindex="-1" - data-toggle="switch" - href="#<?= /* @escapeNotVerified */ $alias ?>" - id="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title"> - <?= /* @escapeNotVerified */ $label ?> + data-toggle="trigger" + href="#<?= $block->escapeUrl($alias) ?>" + id="tab-label-<?= $block->escapeHtmlAttr($alias) ?>-title"> + <?= /* @noEscape */ $label ?> </a> </div> - <div class="data item content" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> - <?= /* @escapeNotVerified */ $html ?> + <div class="data item content" + aria-labelledby="tab-label-<?= $block->escapeHtmlAttr($alias) ?>-title" id="<?= $block->escapeHtmlAttr($alias) ?>" data-role="content"> + <?= /* @noEscape */ $html ?> </div> <?php endforeach;?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml index c8c915a3140da..8d298aec9f1cb 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** * Product view template @@ -12,27 +12,28 @@ * @var $block \Magento\Catalog\Block\Product\View */ ?> -<?php $_helper = $this->helper('Magento\Catalog\Helper\Output'); ?> +<?php $_helper = $this->helper(Magento\Catalog\Helper\Output::class); ?> <?php $_product = $block->getProduct(); ?> <div class="product-add-form"> - <form data-product-sku="<?= /* @NoEscape */ $_product->getSku() ?>" - action="<?= /* @NoEscape */ $block->getSubmitUrl($_product) ?>" method="post" - id="product_addtocart_form"<?php if ($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>> - <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $_product->getId() ?>" /> + <form data-product-sku="<?= $block->escapeHtml($_product->getSku()) ?>" + action="<?= $block->escapeUrl($block->getSubmitUrl($_product)) ?>" method="post" + id="product_addtocart_form"<?php if ($_product->getOptions()) :?> enctype="multipart/form-data"<?php endif; ?>> + <input type="hidden" name="product" value="<?= (int)$_product->getId() ?>" /> <input type="hidden" name="selected_configurable_option" value="" /> <input type="hidden" name="related_product" id="related-products-field" value="" /> + <input type="hidden" name="item" value="<?= $block->escapeHtmlAttr($block->getRequest()->getParam('id')) ?>" /> <?= $block->getBlockHtml('formkey') ?> <?= $block->getChildHtml('form_top') ?> - <?php if (!$block->hasOptions()):?> + <?php if (!$block->hasOptions()) :?> <?= $block->getChildHtml('product_info_form_content') ?> - <?php else:?> - <?php if ($_product->isSaleable() && $block->getOptionsContainer() == 'container1'):?> + <?php else :?> + <?php if ($_product->isSaleable() && $block->getOptionsContainer() == 'container1') :?> <?= $block->getChildChildHtml('options_container') ?> <?php endif;?> <?php endif; ?> - <?php if ($_product->isSaleable() && $block->hasOptions() && $block->getOptionsContainer() == 'container2'):?> + <?php if ($_product->isSaleable() && $block->hasOptions() && $block->getOptionsContainer() == 'container2') :?> <?= $block->getChildChildHtml('options_container') ?> <?php endif;?> <?= $block->getChildHtml('form_bottom') ?> @@ -51,6 +52,6 @@ return !$(elem).find('.price-from').length; }); - priceBoxes.priceBox({'priceConfig': <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>}); + priceBoxes.priceBox({'priceConfig': <?= /* @noEscape */ $block->getJsonConfig() ?>}); }); </script> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml index 5a064b33355a4..0f65e39284392 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Product media data template * @@ -15,19 +13,19 @@ <div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> <div data-role="loader" class="loading-mask"> <div class="loader"> - <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" - alt="<?= /* @escapeNotVerified */ __('Loading...') ?>"> + <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) ?>" + alt="<?= $block->escapeHtml(__('Loading...')) ?>"> </div> </div> </div> <!--Fix for jumping content. Loader must be the same size as gallery.--> <script> var config = { - "width": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>, - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "height": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'height') ?> + "width": <?= $block->escapeHtml($block->getImageAttribute('product_page_image_medium', 'width')) ?>, + "thumbheight": <?= $block->escapeHtml($block->getImageAttribute('product_page_image_small', 'height') + ?: $block->getImageAttribute('product_page_image_small', 'width')); ?>, + "navtype": "<?= $block->escapeHtml($block->getVar("gallery/navtype")) ?>", + "height": <?= $block->escapeHtml($block->getImageAttribute('product_page_image_medium', 'height')) ?> }, thumbBarHeight = 0, loader = document.querySelectorAll('[data-gallery-role="gallery-placeholder"] [data-role="loader"]')[0]; @@ -43,67 +41,11 @@ "[data-gallery-role=gallery-placeholder]": { "mage/gallery/gallery": { "mixins":["magnifier/magnify"], - "magnifierOpts": <?= /* @escapeNotVerified */ $block->getMagnifier() ?>, - "data": <?= /* @escapeNotVerified */ $block->getGalleryImagesJson() ?>, - "options": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/nav") ?>", - <?php if (($block->getVar("gallery/loop"))): ?> - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/loop") ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/keyboard"))): ?> - "keyboard": <?= /* @escapeNotVerified */ $block->getVar("gallery/keyboard") ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/arrows"))): ?> - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/arrows") ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/allowfullscreen"))): ?> - "allowfullscreen": <?= /* @escapeNotVerified */ $block->getVar("gallery/allowfullscreen") ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/caption"))): ?> - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/caption") ?>, - <?php endif; ?> - "width": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>", - "thumbwidth": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_small', 'width') ?>", - <?php if ($block->getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?> - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getImageAttribute('product_page_image_medium', 'height') || $block->getImageAttribute('product_page_image_medium', 'width')): ?> - "height": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_medium', 'height') - ?: $block->getImageAttribute('product_page_image_medium', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/transition/effect") ?>", - <?php if (($block->getVar("gallery/navarrows"))): ?> - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/navarrows") ?>, - <?php endif; ?> - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navdir") ?>" - }, - "fullscreen": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/nav") ?>", - <?php if ($block->getVar("gallery/fullscreen/loop")): ?> - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/loop") ?>, - <?php endif; ?> - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navdir") ?>", - <?php if ($block->getVar("gallery/transition/navarrows")): ?> - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navarrows") ?>, - <?php endif; ?> - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navtype") ?>", - <?php if ($block->getVar("gallery/fullscreen/arrows")): ?> - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/arrows") ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/fullscreen/caption")): ?> - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/caption") ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/fullscreen/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/effect") ?>" - }, - "breakpoints": <?= /* @escapeNotVerified */ $block->getBreakpoints() ?> + "magnifierOpts": <?= /* @noEscape */ $block->getMagnifier() ?>, + "data": <?= /* @noEscape */ $block->getGalleryImagesJson() ?>, + "options": <?= /* @noEscape */ $block->getGalleryOptions()->getOptionsJson() ?>, + "fullscreen": <?= /* @noEscape */ $block->getGalleryOptions()->getFSOptionsJson() ?>, + "breakpoints": <?= /* @noEscape */ $block->getBreakpoints() ?> } } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml index d52b594ededdf..f57c9b68ddbd2 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/mailto.phtml @@ -4,11 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php $_product = $block->getProduct() ?> -<?php if ($block->canEmailToFriend()): ?> - <a href="<?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Product')->getEmailToFriendUrl($_product) ?>" - class="action mailto friend"><span><?= /* @escapeNotVerified */ __('Email') ?></span></a> +<?php if ($block->canEmailToFriend()) :?> + <a href="<?= $block->escapeUrl($this->helper(Magento\Catalog\Helper\Product::class)->getEmailToFriendUrl($_product)) ?>" + class="action mailto friend"><span><?= $block->escapeHtml(__('Email')) ?></span></a> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml index 87655797f40e5..7f14b71a60c7a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/currency.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Directory\Block\Currency */ ?> -<meta property="product:price:currency" content="<?= /* @escapeNotVerified */ $block->stripTags($block->getCurrentCurrencyCode()) ?>"/> +<meta property="product:price:currency" + content="<?= /* @noEscape */ $block->stripTags($block->getCurrentCurrencyCode()) ?>"/> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index b1e46776af465..eb2bde647f9b1 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml @@ -4,17 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Product\View */ ?> -<meta property="og:type" content="og:product" /> -<meta property="og:title" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getName())) ?>" /> -<meta property="og:image" content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> -<meta property="og:description" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" /> +<meta property="og:type" content="product" /> +<meta property="og:title" + content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getName()) ?>" /> +<meta property="og:image" + content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> +<meta property="og:description" + content="<?= /* @noEscape */ $block->stripTags($block->getProduct()->getShortDescription()) ?>" /> <meta property="og:url" content="<?= $block->escapeUrl($block->getProduct()->getProductUrl()) ?>" /> -<?php if ($priceAmount = $block->getProduct()->getFinalPrice()):?> - <meta property="product:price:amount" content="<?= /* @escapeNotVerified */ $priceAmount ?>"/> +<?php if ($priceAmount = $block->getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()) :?> + <meta property="product:price:amount" content="<?= $block->escapeHtmlAttr($priceAmount) ?>"/> <?= $block->getChildHtml('meta.currency') ?> <?php endif;?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml index 3ebfa76860950..d9a0c845b9f83 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options.phtml @@ -4,26 +4,24 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\Catalog\Block\Product\View\Options */ ?> <?php $_options = $block->decorateArray($block->getOptions()) ?> <?php $_productId = $block->getProduct()->getId() ?> -<?php if (count($_options)):?> +<?php if (count($_options)) :?> <script type="text/x-magento-init"> { "#product_addtocart_form": { "priceOptions": { - "optionConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, + "optionConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, "controlContainer": ".field", "priceHolderSelector": "[data-product-id='<?= $block->escapeHtml($_productId) ?>'][data-role=priceBox]" } } } </script> - <?php foreach ($_options as $_option): ?> + <?php foreach ($_options as $_option) :?> <?= $block->getOptionHtml($_option) ?> <?php endforeach; ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml index 3420512977aad..d101700997d34 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml @@ -3,45 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> <?php $_option = $block->getOption() ?> -<?php $_optionId = $_option->getId() ?> +<?php $_optionId = $block->escapeHtmlAttr($_option->getId()) ?> <?php $class = ($_option->getIsRequire()) ? ' required' : ''; ?> -<div class="field date<?= /* @escapeNotVerified */ $class ?>" +<div class="field date<?= /* @noEscape */ $class ?>" data-mage-init='{"priceOptionDate":{"fromSelector":"#product_addtocart_form"}}'> - <fieldset class="fieldset fieldset-product-options-inner<?= /* @escapeNotVerified */ $class ?>"> + <fieldset class="fieldset fieldset-product-options-inner<?= /* @noEscape */ $class ?>"> <legend class="legend"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> </legend> <div class="control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE): ?> - + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> <?= $block->getDateHtml() ?> - <?php endif; ?> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME): ?> + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> <?= $block->getTimeHtml() ?> <?php endif; ?> - - <?php if ($_option->getIsRequire()): ?> + <?php if ($_option->getIsRequire()) :?> <input type="hidden" - name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" - class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" + name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" + class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" value="" - data-validate="{'validate-required-datetime':<?= /* @escapeNotVerified */ $_optionId ?>}"/> - <?php else: ?> + data-validate="{'validate-required-datetime':<?= /* @noEscape */ $_optionId ?>}"/> + <?php else :?> <input type="hidden" - name="validate_datetime_<?= /* @escapeNotVerified */ $_optionId ?>" - class="validate-datetime-<?= /* @escapeNotVerified */ $_optionId ?>" + name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" + class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" value="" - data-validate="{'validate-optional-datetime':<?= /* @escapeNotVerified */ $_optionId ?>}"/> + data-validate="{'validate-optional-datetime':<?= /* @noEscape */ $_optionId ?>}"/> <?php endif; ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml index 2006bf6e9f414..c25dab8b70a5c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_option = $block->getOption() ?> <div class="field"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml index 3ceba2eebd214..e83e55ad2a03c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml @@ -3,64 +3,62 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData(); ?> -<?php $_fileName = 'options_' . $_option->getId() . '_file'; ?> +<?php $_fileName = 'options_' . $block->escapeHtmlAttr($_option->getId()) . '_file'; ?> <?php $_fieldNameAction = $_fileName . '_action'; ?> <?php $_fieldValueAction = $_fileExists ? 'save_old' : 'save_new'; ?> <?php $_fileNamed = $_fileName . '_name'; ?> <?php $class = ($_option->getIsRequire()) ? ' required' : ''; ?> -<div class="field file<?= /* @escapeNotVerified */ $class ?>"> +<div class="field file<?= /* @noEscape */ $class ?>"> <label class="label" for="<?= /* @noEscape */ $_fileName ?>" id="<?= /* @noEscape */ $_fileName ?>-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> - <?php if ($_fileExists): ?> + <?php if ($_fileExists) :?> <div class="control"> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> <a href="javascript:void(0)" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > - <?= /* @escapeNotVerified */ __('Change') ?> + <?= $block->escapeHtml(__('Change')) ?> </a> - <?php if (!$_option->getIsRequire()): ?> - <input type="checkbox" id="delete-<?= /* @escapeNotVerified */ $_fileName ?>" /> - <span class="label"><?= /* @escapeNotVerified */ __('Delete') ?></span> + <?php if (!$_option->getIsRequire()) :?> + <input type="checkbox" id="delete-<?= /* @noEscape */ $_fileName ?>" /> + <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> </div> <?php endif; ?> - <div class="control" id="input-box-<?= /* @escapeNotVerified */ $_fileName ?>" + <div class="control" id="input-box-<?= /* @noEscape */ $_fileName ?>" data-mage-init='{"priceOptionFile":{ "fileName":"<?= /* @noEscape */ $_fileName ?>", "fileNamed":"<?= /* @noEscape */ $_fileNamed ?>", - "fieldNameAction":"<?= /* @escapeNotVerified */ $_fieldNameAction ?>", - "changeFileSelector":"#change-<?= /* @escapeNotVerified */ $_fileName ?>", - "deleteFileSelector":"#delete-<?= /* @escapeNotVerified */ $_fileName ?>"} + "fieldNameAction":"<?= /* @noEscape */ $_fieldNameAction ?>", + "changeFileSelector":"#change-<?= /* @noEscape */ $_fileName ?>", + "deleteFileSelector":"#delete-<?= /* @noEscape */ $_fileName ?>"} }' <?= $_fileExists ? 'style="display:none"' : '' ?>> <input type="file" - name="<?= /* @escapeNotVerified */ $_fileName ?>" - id="<?= /* @escapeNotVerified */ $_fileName ?>" + name="<?= /* @noEscape */ $_fileName ?>" + id="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required' : '' ?>" - <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> - <input type="hidden" name="<?= /* @escapeNotVerified */ $_fieldNameAction ?>" value="<?= /* @escapeNotVerified */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()): ?> + <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> + <?php if ($_option->getFileExtension()) :?> <p class="note"> - <?= /* @escapeNotVerified */ __('Compatible file extensions to upload') ?>: <strong><?= /* @escapeNotVerified */ $_option->getFileExtension() ?></strong> + <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0): ?> + <?php if ($_option->getImageSizeX() > 0) :?> <p class="note"> - <?= /* @escapeNotVerified */ __('Maximum image width') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeX() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong> + <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0): ?> + <?php if ($_option->getImageSizeY() > 0) :?> <p class="note"> - <?= /* @escapeNotVerified */ __('Maximum image height') ?>: <strong><?= /* @escapeNotVerified */ $_option->getImageSizeY() ?> <?= /* @escapeNotVerified */ __('px.') ?></strong> + <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml index 980b78f917cf2..c4c1d24423bb0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/select.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select */ ?> @@ -13,15 +10,15 @@ $_option = $block->getOption(); $class = ($_option->getIsRequire()) ? ' required' : ''; ?> -<div class="field<?= /* @escapeNotVerified */ $class ?>"> - <label class="label" for="select_<?= /* @escapeNotVerified */ $_option->getId() ?>"> +<div class="field<?= /* @noEscape */ $class ?>"> + <label class="label" for="select_<?= $block->escapeHtmlAttr($_option->getId()) ?>"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> <?= $block->getValuesHtml() ?> - <?php if ($_option->getIsRequire()): ?> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX): ?> - <span id="options-<?= /* @escapeNotVerified */ $_option->getId() ?>-container"></span> + <?php if ($_option->getIsRequire()) :?> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) :?> + <span id="options-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></span> <?php endif; ?> <?php endif;?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml index 79dc8591fd724..dd4c000d1f338 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml @@ -3,10 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Text */ ?> <?php $_option = $block->getOption(); $class = ($_option->getIsRequire()) ? ' required' : ''; @@ -14,14 +12,14 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; <div class="field<?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) { echo ' textarea'; -} ?><?= /* @escapeNotVerified */ $class ?>"> - <label class="label" for="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text"> +} ?><?= /* @noEscape */ $class ?>"> + <label class="label" for="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> <div class="control"> - <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> + <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD) :?> <?php $_textValidate = null; if ($_option->getIsRequire()) { $_textValidate['required'] = true; @@ -29,17 +27,18 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textValidate['validate-no-utf8mb4-characters'] = true; ?> <input type="text" - id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" + id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" class="input-text product-custom-option" - <?php if (!empty($_textValidate)) {?> - data-validate="<?= $block->escapeHtml(json_encode($_textValidate)) ?>" - <?php } ?> - name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" + <?php if (!empty($_textValidate)) {?> + data-validate="<?= $block->escapeHtml(json_encode($_textValidate)) ?>" + <?php } ?> + name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" value="<?= $block->escapeHtml($block->getDefaultValue()) ?>"/> - <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA): ?> + <?php elseif ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_AREA) :?> <?php $_textAreaValidate = null; if ($_option->getIsRequire()) { $_textAreaValidate['required'] = true; @@ -47,20 +46,36 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textAreaValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textAreaValidate['validate-no-utf8mb4-characters'] = true; ?> - <textarea id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" + <textarea id="options_<?= $block->escapeHtmlAttr($_option->getId()) ?>_text" class="product-custom-option" <?php if (!empty($_textAreaValidate)) {?> data-validate="<?= $block->escapeHtml(json_encode($_textAreaValidate)) ?>" <?php } ?> - name="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" - data-selector="options[<?= /* @escapeNotVerified */ $_option->getId() ?>]" + name="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="options[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" rows="5" cols="25"><?= $block->escapeHtml($block->getDefaultValue()) ?></textarea> <?php endif; ?> - <?php if ($_option->getMaxCharacters()): ?> - <p class="note"><?= /* @escapeNotVerified */ __('Maximum number of characters:') ?> - <strong><?= /* @escapeNotVerified */ $_option->getMaxCharacters() ?></strong></p> + <?php if ($_option->getMaxCharacters()) :?> + <p class="note note_<?= $block->escapeHtmlAttr($_option->getId()) ?>"> + <?= $block->escapeHtml(__('Maximum %1 characters', $_option->getMaxCharacters())) ?> + <span class="character-counter no-display"></span> + </p> <?php endif; ?> </div> + <?php if ($_option->getMaxCharacters()) :?> + <script type="text/x-magento-init"> + { + "[data-selector='options[<?= $block->escapeJs($_option->getId()) ?>]']": { + "Magento_Catalog/js/product/remaining-characters": { + "maxLength": "<?= (int)$_option->getMaxCharacters() ?>", + "noteSelector": ".note_<?= $block->escapeJs($_option->getId()) ?>", + "counterSelector": ".note_<?= $block->escapeJs($_option->getId()) ?> .character-counter" + } + } + } + </script> + <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml index ca6960a215a7a..88ee45bafe731 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/wrapper.phtml @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var $block Magento\Catalog\Block\Product\View */ ?> <?php $required = ''; if ($block->hasRequiredOptions()) { - $required = ' data-hasrequired="' . __('* Required Fields') . '"'; + $required = ' data-hasrequired="' . $block->escapeHtmlAttr(__('* Required Fields')) . '"'; } ?> -<div class="product-options-wrapper" id="product-options-wrapper"<?= /* @escapeNotVerified */ $required ?>> +<div class="product-options-wrapper" id="product-options-wrapper"<?= /* @noEscape */ $required ?>> <div class="fieldset" tabindex="0"> <?= $block->getChildHtml('', true) ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml index e8c0b32fd7692..bad67f98c81d3 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/price_clone.phtml @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +?> +<?php +// phpcs:disable PSR2.Files.ClosingTag -// @codingStandardsIgnoreFile - +/** @var \Magento\Catalog\Block\Product\AbstractProduct $block */ ?> -<?php /** @var \Magento\Catalog\Block\Product\AbstractProduct $block */ ?> <?php $_product = $block->getProduct() ?> <?= $block->getProductPriceHtml( $_product, diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml index 5575d00df7457..62d6cd202f644 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/review.phtml @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +?> +<?php +// phpcs:disable PSR2.Files.ClosingTag -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Catalog\Block\Product\AbstractProduct */ ?> -<?php /** @var $block \Magento\Catalog\Block\Product\AbstractProduct */ ?> <?= $block->getReviewsSummaryHtml($block->getProduct(), false, true) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml index 7e522b4f88306..30edb2df03754 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/type/default.phtml @@ -3,21 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Catalog\Block\Product\View\AbstractView */?> <?php $_product = $block->getProduct() ?> -<?php if ($block->displayProductStockStatus()): ?> - <?php if ($_product->isAvailable()): ?> - <div class="stock available" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('In stock') ?></span> +<?php if ($block->displayProductStockStatus()) :?> + <?php if ($_product->isAvailable()) :?> + <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> - <?php else: ?> - <div class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> + <?php else :?> + <div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('Out of stock')) ?></span> </div> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml index e0550cc7d4414..85d88f31f9d6a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/grid.phtml @@ -1,18 +1,16 @@ <?php /** - * Copyright © Magento, Inc. All rights reserved. + * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - //@codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?php /* @escapeNotVerified */ echo $block->renderApp( +<?= /* @noEscape */ $block->renderApp( [ 'widget_columns' => [ 'displayMode' => 'grid' @@ -21,4 +19,4 @@ 'imageCode' => 'recently_compared_products_grid_content_widget' ] ] -); +); ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml index 3a4f81d946bfd..2cef2aea57df7 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/list.phtml @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ - //@codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?php /* @escapeNotVerified */ echo $block->renderApp( +<?= /* @noEscape */ $block->renderApp( [ 'widget_columns' => [ 'displayMode' => 'list' @@ -21,4 +19,4 @@ 'imageCode' => 'recently_compared_products_list_content_widget' ] ] -); +); ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml index f1b2f1a214945..6e363bc159605 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/compared/sidebar.phtml @@ -4,23 +4,19 @@ * See COPYING.txt for license details. */ - //@codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?php /* @escapeNotVerified */ echo $block->renderApp( +<?= /* @noEscape */ $block->renderApp( [ 'listing' => [ 'displayMode' => 'grid' ], - 'column' => [ - 'image' => [ - 'imageCode' => 'recently_compared_products_images_names_widget' - ] + 'image' => [ + 'imageCode' => 'recently_compared_products_images_names_widget' ] ] -); +); ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml index 2ec671b8de3ab..69f0319134ea0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_block.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <div class="widget block block-product-link"> - <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml index 373eda1117455..8d9f6500894b4 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/link/link_inline.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <span class="widget block block-product-link-inline"> - <a <?= /* @escapeNotVerified */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><span><?= $block->escapeHtml($block->getLabel()) ?></span></a> </span> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml index 9f6576f61ee7f..9e5da250065cd 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml @@ -4,61 +4,61 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable Magento2.Files.LineLength.MaxExceeded ?> -<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> +<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> <div class="block widget block-new-products-list"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> + <strong><?= $block->escapeHtml(__('New Products')) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> <?php $iterator = 1; ?> - <ol class="product-items" id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>"> - <?php foreach ($_products->getItems() as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <ol class="product-items" id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>"> + <?php foreach ($_products->getItems() as $_product) :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> <div class="product-item-info"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>"> <?= $block->getImage($_product, 'side_column_widget_product_thumbnail')->toHtml() ?> </a> <div class="product-item-details"> <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>)" class="product-item-link"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>)" class="product-item-link"> + <?= /* @noEscape */ $this->helper(Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name') ?> </a> </strong> - <?= /* @escapeNotVerified */ $block->getProductPriceHtml($_product, '-widget-new-' . $suffix) ?> + <?= /* @noEscape */ $block->getProductPriceHtml($_product, '-widget-new-' . $suffix) ?> <div class="product-item-actions"> <div class="actions-primary"> - <?php if ($_product->isSaleable()): ?> - <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)): ?> - <button type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" + <?php if ($_product->isSaleable()) :?> + <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)) :?> + <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="action tocart primary" - data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_product) ?>"}}'> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_product)) ?>"}}'> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> - <?php else: ?> + <?php else :?> <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]); + $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->escapeUrl($block->getAddToCartUrl($_product)), ['product' => $_product->getEntityId()]); ?> - <button type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" + <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>'> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-post='<?= /* @noEscape */ $postData ?>'> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> <?php endif; ?> - <?php else: ?> - <?php if ($_product->getIsSalable()): ?> - <div class="stock available" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('In stock') ?></span> + <?php else :?> + <?php if ($_product->getIsSalable()) :?> + <div class="stock available" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('In stock')) ?></span> </div> - <?php else: ?> - <div class="stock unavailable" title="<?= /* @escapeNotVerified */ __('Availability') ?>"> - <span><?= /* @escapeNotVerified */ __('Out of stock') ?></span> + <?php else :?> + <div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> + <span><?= $block->escapeHtml(__('Out of stock')) ?></span> </div> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml index 2c40f9f7d63dc..8a776adc95018 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_images_list.phtml @@ -3,22 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> +<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> <div class="block widget block-new-products-images"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> + <strong><?= $block->escapeHtml(__('New Products')) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-images"> - <?php foreach ($_products->getItems() as $_product): ?> + <ol id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>" + class="product-items product-items-images"> + <?php foreach ($_products->getItems() as $_product) :?> <li class="product-item"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>"> <?php /* new_products_images_only_widget */ ?> <?= $block->getImage($_product, 'new_products_images_only_widget')->toHtml() ?> </a> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml index c0fb12df91137..371d4df7c0206 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_names_list.phtml @@ -4,24 +4,26 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> -<?php if (($_products = $block->getProductCollection()) && $_products->getSize()): ?> +<?php if (($_products = $block->getProductCollection()) && $_products->getSize()) :?> <div class="block widget block-new-products-names"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('New Products') ?></strong> + <strong><?= $block->escapeHtml(__('New Products')) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-new-products-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-names"> - <?php foreach ($_products->getItems() as $_product): ?> + <ol id="widget-new-products-<?= $block->escapeHtmlAttr($suffix) ?>" + class="product-items product-items-names"> + <?php foreach ($_products->getItems() as $_product) :?> <li class="product-item"> <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>)" + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= /* @noEscape */ $block->stripTags($_product->getName(), null, true) ?>)" class="product-item-link"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> + <?= /* @noEscape */ $this->helper( + Magento\Catalog\Helper\Output::class + )->productAttribute($_product, $_product->getName(), 'name') ?> </a> </strong> </li> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml index e600a571b5b30..7d875d43c4ec6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,6 +10,10 @@ * * @var $block \Magento\Catalog\Block\Product\Widget\NewWidget */ + +// phpcs:disable Magento2.Files.LineLength.MaxExceeded +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())) { $type = 'widget-new-grid'; @@ -30,85 +31,94 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> } ?> -<?php if ($exist):?> - <div class="block widget block-new-products <?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) :?> + <div class="block widget block-new-products <?= /* @noEscape */ $mode ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> + <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> + <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> + <ol class="product-items <?= /* @noEscape */ $type ?>"> <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <?php foreach ($items as $_item) :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" + class="product-item-photo"> <?= $block->getImage($_item, $image)->toHtml() ?> </a> <div class="product-item-details"> <strong class="product-item-name"> <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> - <?php - echo $block->getProductPriceHtml($_item, $type); - ?> + <?= $block->getProductPriceHtml($_item, $type); ?> - <?php if ($templateType): ?> + <?php if ($templateType) :?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> + <?php if ($showWishlist || $showCompare || $showCart) :?> <div class="product-item-actions"> - <?php if ($showCart): ?> + <?php if ($showCart) :?> <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> + <?php if ($_item->isSaleable()) :?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) :?> <button class="action tocart primary" - data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' + type="button" + title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> - <?php else: ?> + <?php else :?> <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) + $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData( + $block->escapeUrl($block->getAddToCartUrl($_item)), + ['product' => (int) $_item->getEntityId()] + ) ?> <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-post='<?= /* @noEscape */ $postData ?>' + type="button" + title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php else :?> + <?php if ($_item->getIsSalable()) :?> + <div class="stock available"> + <span><?= $block->escapeHtml(__('In stock')) ?></span> + </div> + <?php else :?> + <div class="stock unavailable"> + <span><?= $block->escapeHtml(__('Out of stock')) ?></span> + </div> <?php endif; ?> <?php endif; ?> </div> <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> + <?php if ($showWishlist || $showCompare) :?> <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> + <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) :?> <a href="#" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' - class="action towishlist" data-action="add-to-wishlist" - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' + class="action towishlist" + data-action="add-to-wishlist" + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> + <?php if ($block->getAddToCompareUrl() && $showCompare) :?> + <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class);?> <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> </a> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml index c83140d9342ce..b963a7594a763 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php + +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +// phpcs:disable Magento2.Files.LineLength.MaxExceeded + /** * Template for displaying new products widget * @@ -21,7 +22,8 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> $image = 'new_products_content_widget_list'; $title = __('New Products'); $items = $block->getProductCollection()->getItems(); - $_helper = $this->helper('Magento\Catalog\Helper\Output'); + /** @var Magento\Catalog\Helper\Output $_helper */ + $_helper = $this->helper(Magento\Catalog\Helper\Output::class); $showWishlist = true; $showCompare = true; @@ -31,95 +33,95 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> } ?> -<?php if ($exist):?> - <div class="block widget block-new-products <?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) :?> + <div class="block widget block-new-products <?= /* @noEscape */ $mode ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> + <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> + <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> + <ol class="product-items <?= /* @noEscape */ $type ?>"> <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <?php foreach ($items as $_item) :?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> <?= $block->getImage($_item, $image)->toHtml() ?> </a> <div class="product-item-details"> <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> <?= $block->getProductPriceHtml($_item, $type) ?> - <?php if ($templateType): ?> + <?php if ($templateType) :?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> + <?php if ($showWishlist || $showCompare || $showCart) :?> <div class="product-item-actions"> - <?php if ($showCart): ?> + <?php if ($showCart) :?> <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> + <?php if ($_item->isSaleable()) :?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) :?> <button class="action tocart primary" - data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> - <?php else: ?> + <?php else :?> <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); + $postDataHelper = $this->helper(Magento\Framework\Data\Helper\PostHelper::class); $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) ?> <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php else :?> + <?php if ($_item->getIsSalable()) :?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else :?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> + <?php if ($showWishlist || $showCompare) :?> <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> + <?php if ($this->helper(Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) :?> <a href="#" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare'); ?> + <?php if ($block->getAddToCompareUrl() && $showCompare) :?> + <?php $compareHelper = $this->helper(Magento\Catalog\Helper\Product\Compare::class); ?> <a href="#" class="action tocompare" - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>'> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>" + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>'> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> </a> <?php endif; ?> </div> <?php endif; ?> </div> <?php endif; ?> - <?php if ($description):?> + <?php if ($description) :?> <div class="product-item-description"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description') ?> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" - class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> + <?= /* @noEscape */ $_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description') ?> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" + class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml index 578630a11e930..62d875af3cadd 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/grid.phtml @@ -3,18 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?= /* @escapeNotVerified */ $block->renderApp([ +<?= /* @noEscape */ $block->renderApp([ 'widget_columns' => [ 'displayMode' => 'grid' ], 'image' => [ 'imageCode' => 'recently_viewed_products_grid_content_widget' ] -]); +]); ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml index 3770c330ad73e..f42effbb3163b 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/list.phtml @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ + +// phpcs:disable PSR2.Files.ClosingTag + +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?= /* @escapeNotVerified */ $block->renderApp([ +<?= /* @noEscape */ $block->renderApp([ 'widget_columns' => [ 'displayMode' => 'list' ], 'image' => [ 'imageCode' => 'recently_viewed_products_list_content_widget' ] -]); +]); ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml index 1c4ad3105a2b5..a9320985957f6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/viewed/sidebar.phtml @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ - //@codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/** - * @var $block \Magento\Ui\Block\Wrapper - */ +/** @var $block \Magento\Ui\Block\Wrapper */ ?> -<?php /* @escapeNotVerified */ echo $block->renderApp( +<?= /* @noEscape */ $block->renderApp( [ 'listing' => [ 'displayMode' => 'grid' @@ -21,4 +19,4 @@ 'imageCode' => 'recently_viewed_products_images_names_widget' ] ] -); +); ?> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index 7686de1d45c5d..dffe4a1f64665 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -6,8 +6,10 @@ define([ 'jquery', 'mage/translate', + 'underscore', + 'Magento_Catalog/js/product/view/product-ids-resolver', 'jquery/ui' -], function ($, $t) { +], function ($, $t, _, idsResolver) { 'use strict'; $.widget('mage.catalogAddToCart', { @@ -27,9 +29,13 @@ define([ /** @inheritdoc */ _create: function () { + var addToCartButton = $(this.element).find(this.options.addToCartButtonSelector); + if (this.options.bindSubmit) { this._bindSubmit(); } + + addToCartButton.attr('disabled', false); }, /** @@ -38,12 +44,33 @@ define([ _bindSubmit: function () { var self = this; + if (this.element.data('catalog-addtocart-initialized')) { + return; + } + + this.element.data('catalog-addtocart-initialized', 1); this.element.on('submit', function (e) { e.preventDefault(); self.submitForm($(this)); }); }, + /** + * @private + * @param {String} url + */ + _redirect: function (url) { + var urlParts = url.split('#'), + locationParts = window.location.href.split('#'), + forceReload = urlParts[0] === locationParts[0]; + + window.location.assign(url); + + if (forceReload) { + window.location.reload(); + } + }, + /** * @return {Boolean} */ @@ -54,37 +81,31 @@ define([ /** * Handler for the form 'submit' event * - * @param {Object} form + * @param {jQuery} form */ submitForm: function (form) { - var addToCartButton, self = this; - - if (form.has('input[type="file"]').length && form.find('input[type="file"]').val() !== '') { - self.element.off('submit'); - // disable 'Add to Cart' button - addToCartButton = $(form).find(this.options.addToCartButtonSelector); - addToCartButton.prop('disabled', true); - addToCartButton.addClass(this.options.addToCartButtonDisabledClass); - form.submit(); - } else { - self.ajaxSubmit(form); - } + this.ajaxSubmit(form); }, /** - * @param {String} form + * @param {jQuery} form */ ajaxSubmit: function (form) { - var self = this; + var self = this, + productIds = idsResolver(form), + formData = new FormData(form[0]); $(self.options.minicartSelector).trigger('contentLoading'); self.disableAddToCartButton(form); $.ajax({ url: form.attr('action'), - data: form.serialize(), + data: formData, type: 'post', dataType: 'json', + cache: false, + contentType: false, + processData: false, /** @inheritdoc */ beforeSend: function () { @@ -97,7 +118,12 @@ define([ success: function (res) { var eventData, parameters; - $(document).trigger('ajax:addToCart', form.data().productSku); + $(document).trigger('ajax:addToCart', { + 'sku': form.data().productSku, + 'productIds': productIds, + 'form': form, + 'response': res + }); if (self.isLoaderEnabled()) { $('body').trigger(self.options.processStop); @@ -116,7 +142,8 @@ define([ parameters.push(eventData.redirectParameters.join('&')); res.backUrl = parameters.join('#'); } - window.location = res.backUrl; + + self._redirect(res.backUrl); return; } @@ -138,6 +165,13 @@ define([ .html(res.product.statusText); } self.enableAddToCartButton(form); + }, + + /** @inheritdoc */ + complete: function (res) { + if (res.state() === 'rejected') { + location.reload(); + } } }); }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js b/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js new file mode 100644 index 0000000000000..032b8541939c3 --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js @@ -0,0 +1,184 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Theme/js/model/breadcrumb-list' +], function ($, breadcrumbList) { + 'use strict'; + + return function (widget) { + + $.widget('mage.breadcrumbs', widget, { + options: { + categoryUrlSuffix: '', + useCategoryPathInUrl: false, + product: '', + categoryItemSelector: '.category-item', + menuContainer: '[data-action="navigation"] > ul' + }, + + /** @inheritdoc */ + _render: function () { + this._appendCatalogCrumbs(); + this._super(); + }, + + /** + * Append category and product crumbs. + * + * @private + */ + _appendCatalogCrumbs: function () { + var categoryCrumbs = this._resolveCategoryCrumbs(); + + categoryCrumbs.forEach(function (crumbInfo) { + breadcrumbList.push(crumbInfo); + }); + + if (this.options.product) { + breadcrumbList.push(this._getProductCrumb()); + } + }, + + /** + * Resolve categories crumbs. + * + * @return Array + * @private + */ + _resolveCategoryCrumbs: function () { + var menuItem = this._resolveCategoryMenuItem(), + categoryCrumbs = []; + + if (menuItem !== null && menuItem.length) { + categoryCrumbs.unshift(this._getCategoryCrumb(menuItem)); + + while ((menuItem = this._getParentMenuItem(menuItem)) !== null) { + categoryCrumbs.unshift(this._getCategoryCrumb(menuItem)); + } + } + + return categoryCrumbs; + }, + + /** + * Returns crumb data. + * + * @param {Object} menuItem + * @return {Object} + * @private + */ + _getCategoryCrumb: function (menuItem) { + return { + 'name': 'category', + 'label': menuItem.text(), + 'link': menuItem.attr('href'), + 'title': '' + }; + }, + + /** + * Returns product crumb. + * + * @return {Object} + * @private + */ + _getProductCrumb: function () { + return { + 'name': 'product', + 'label': this.options.product, + 'link': '', + 'title': '' + }; + }, + + /** + * Find parent menu item for current. + * + * @param {Object} menuItem + * @return {Object|null} + * @private + */ + _getParentMenuItem: function (menuItem) { + var classes, + classNav, + parentClass, + parentMenuItem = null; + + if (!menuItem) { + return null; + } + + classes = menuItem.parent().attr('class'); + classNav = classes.match(/(nav\-)[0-9]+(\-[0-9]+)+/gi); + + if (classNav) { + classNav = classNav[0]; + parentClass = classNav.substr(0, classNav.lastIndexOf('-')); + + if (parentClass.lastIndexOf('-') !== -1) { + parentMenuItem = $(this.options.menuContainer).find('.' + parentClass + ' > a'); + parentMenuItem = parentMenuItem.length ? parentMenuItem : null; + } + } + + return parentMenuItem; + }, + + /** + * Returns category menu item. + * + * Tries to resolve category from url or from referrer as fallback and + * find menu item from navigation menu by category url. + * + * @return {Object|null} + * @private + */ + _resolveCategoryMenuItem: function () { + var categoryUrl = this._resolveCategoryUrl(), + menu = $(this.options.menuContainer), + categoryMenuItem = null; + + if (categoryUrl && menu.length) { + categoryMenuItem = menu.find( + this.options.categoryItemSelector + + ' > a[href="' + categoryUrl + '"]' + ); + } + + return categoryMenuItem; + }, + + /** + * Returns category url. + * + * @return {String} + * @private + */ + _resolveCategoryUrl: function () { + var categoryUrl; + + if (this.options.useCategoryPathInUrl) { + // In case category path is used in product url - resolve category url from current url. + categoryUrl = window.location.href.split('?')[0]; + categoryUrl = categoryUrl.substring(0, categoryUrl.lastIndexOf('/')) + + this.options.categoryUrlSuffix; + } else { + // In other case - try to resolve it from referrer (without parameters). + categoryUrl = document.referrer; + + if (categoryUrl.indexOf('?') > 0) { + categoryUrl = categoryUrl.substr(0, categoryUrl.indexOf('?')); + } + } + + return categoryUrl; + } + }); + + return $.mage.breadcrumbs; + }; +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js index 88be03a04e71a..b8b6ff65be2b4 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -27,7 +27,9 @@ define([ directionDefault: 'asc', orderDefault: 'position', limitDefault: '9', - url: '' + url: '', + formKey: '', + post: false }, /** @inheritdoc */ @@ -89,7 +91,7 @@ define([ baseUrl = urlPaths[0], urlParams = urlPaths[1] ? urlPaths[1].split('&') : [], paramData = {}, - parameters, i; + parameters, i, form, params, key, input, formKey; for (i = 0; i < urlParams.length; i++) { parameters = urlParams[i].split('='); @@ -99,12 +101,38 @@ define([ } paramData[paramName] = paramValue; - if (paramValue == defaultValue) { //eslint-disable-line eqeqeq - delete paramData[paramName]; - } - paramData = $.param(paramData); + if (this.options.post) { + form = document.createElement('form'); + params = [this.options.mode, this.options.direction, this.options.order, this.options.limit]; + + for (key in paramData) { + if (params.indexOf(key) !== -1) { //eslint-disable-line max-depth + input = document.createElement('input'); + input.name = key; + input.value = paramData[key]; + form.appendChild(input); + delete paramData[key]; + } + } + formKey = document.createElement('input'); + formKey.name = 'form_key'; + formKey.value = this.options.formKey; + form.appendChild(formKey); + + paramData = $.param(paramData); + baseUrl += paramData.length ? '?' + paramData : ''; - location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + form.action = baseUrl; + form.method = 'POST'; + document.body.appendChild(form); + form.submit(); + } else { + if (paramValue == defaultValue) { //eslint-disable-line eqeqeq + delete paramData[paramName]; + } + paramData = $.param(paramData); + location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + } } }); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js index c53b2fa6e2a7a..b29ebe7d57d1c 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js @@ -5,11 +5,13 @@ define([ 'underscore', + 'jquery', 'mageUtils', 'uiElement', 'Magento_Catalog/js/product/storage/storage-service', - 'Magento_Customer/js/customer-data' -], function (_, utils, Element, storage, customerData) { + 'Magento_Customer/js/customer-data', + 'Magento_Catalog/js/product/view/product-ids-resolver' +], function (_, $, utils, Element, storage, customerData, productResolver) { 'use strict'; return Element.extend({ @@ -135,11 +137,16 @@ define([ */ filterIds: function (ids) { var _ids = {}, - currentTime = new Date().getTime() / 1000; + currentTime = new Date().getTime() / 1000, + currentProductIds = productResolver($('#product_addtocart_form')); _.each(ids, function (id) { - if (currentTime - id['added_at'] < ~~this.idsStorage.lifetime) { + if ( + currentTime - id['added_at'] < ~~this.idsStorage.lifetime && + !_.contains(currentProductIds, id['product_id']) + ) { _ids[id['product_id']] = id; + } }, this); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js new file mode 100644 index 0000000000000..3e29e1ebd4d9c --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/remaining-characters.js @@ -0,0 +1,62 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate', + 'jquery/ui' +], function ($, $t) { + 'use strict'; + + $.widget('mage.remainingCharacters', { + options: { + remainingText: $t('remaining'), + tooManyText: $t('too many'), + errorClass: 'mage-error', + noDisplayClass: 'no-display' + }, + + /** + * Initializes custom option component + * + * @private + */ + _create: function () { + this.note = $(this.options.noteSelector); + this.counter = $(this.options.counterSelector); + + this.updateCharacterCount(); + this.element.on('change keyup paste', this.updateCharacterCount.bind(this)); + }, + + /** + * Updates counter message + */ + updateCharacterCount: function () { + var length = this.element.val().length, + diff = this.options.maxLength - length; + + this.counter.text(this._formatMessage(diff)); + this.counter.toggleClass(this.options.noDisplayClass, length === 0); + this.note.toggleClass(this.options.errorClass, diff < 0); + }, + + /** + * Format remaining characters message + * + * @param {int} diff + * @returns {String} + * @private + */ + _formatMessage: function (diff) { + var count = Math.abs(diff), + qualifier = diff < 0 ? this.options.tooManyText : this.options.remainingText; + + return '(' + count + ' ' + qualifier + ')'; + } + }); + + return $.mage.remainingCharacters; +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js index 00cb7d2db6885..873f0b4e7a6f5 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js @@ -34,6 +34,21 @@ define([ }; } + /** + * Set data to localStorage with support check. + * + * @param {String} namespace + * @param {Object} data + */ + function setLocalStorageItem(namespace, data) { + try { + window.localStorage.setItem(namespace, JSON.stringify(data)); + } catch (e) { + console.warn('localStorage is unavailable - skipping local caching of product data'); + console.error(e); + } + } + return { /** @@ -118,7 +133,7 @@ define([ if (_.isEmpty(data)) { this.localStorage.removeAll(); } else { - this.localStorage.set(data); + setLocalStorageItem(this.namespace, data); } }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js index 7eafbad8299d8..ec07c19a2c1b1 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js @@ -11,6 +11,21 @@ define([ ], function ($, _, ko, utils) { 'use strict'; + /** + * Set data to localStorage with support check. + * + * @param {String} namespace + * @param {Object} data + */ + function setLocalStorageItem(namespace, data) { + try { + window.localStorage.setItem(namespace, JSON.stringify(data)); + } catch (e) { + console.warn('localStorage is unavailable - skipping local caching of product data'); + console.error(e); + } + } + return { /** @@ -94,11 +109,7 @@ define([ * Initializes handler to "data" property update */ internalDataHandler: function (data) { - var localStorage = this.localStorage.get(); - - if (!utils.compare(data, localStorage).equal) { - this.localStorage.set(data); - } + setLocalStorageItem(this.namespace, data); }, /** diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js index 014002bdc4af9..b35ab867bb0e7 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js @@ -47,7 +47,7 @@ define([ * @param {*} data */ add: function (data) { - if (!utils.compare(data, this.data()).equal) { + if (!_.isEmpty(data)) { this.data(_.extend(utils.copy(this.data()), data)); } }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js new file mode 100644 index 0000000000000..f13e8f84a1b13 --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'Magento_Catalog/js/product/view/product-ids' +], function (_, productIds) { + 'use strict'; + + /** + * Returns id's of products in form. + * + * @param {jQuery} $form + * @return {Array} + */ + return function ($form) { + var idSet = productIds(), + product = _.findWhere($form.serializeArray(), { + name: 'product' + }); + + if (!_.isUndefined(product)) { + idSet.push(product.value); + } + + return _.uniq(idSet); + }; +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js new file mode 100644 index 0000000000000..2198b7b8e48b0 --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'ko' +], function (ko) { + 'use strict'; + + return ko.observableArray([]); +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/storage-manager.js b/app/code/Magento/Catalog/view/frontend/web/js/storage-manager.js index fcba9e8dcf468..3b0e9c5e8e260 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/storage-manager.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/storage-manager.js @@ -129,7 +129,7 @@ define([ }, /** - * Prepare storages congfig. + * Prepare storages config. * * @returns {Object} Chainable. */ diff --git a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js index c0637cb672dc6..755e777a01f77 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js @@ -13,7 +13,8 @@ define([ $.widget('mage.productValidate', { options: { bindSubmit: false, - radioCheckboxClosest: '.nested' + radioCheckboxClosest: '.nested', + addToCartButtonSelector: '.action.tocart' }, /** @@ -41,6 +42,7 @@ define([ return false; } }); + $(this.options.addToCartButtonSelector).attr('disabled', false); } }); diff --git a/app/code/Magento/CatalogAnalytics/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogAnalytics/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogAnalytics/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 <insert your license name here>" 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/app/code/Magento/CatalogAnalytics/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogAnalytics/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogAnalytics/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 <insert your license name here>" 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/app/code/Magento/CatalogAnalytics/Test/Mftf/README.md b/app/code/Magento/CatalogAnalytics/Test/Mftf/README.md new file mode 100644 index 0000000000000..1d9f8f781e4b9 --- /dev/null +++ b/app/code/Magento/CatalogAnalytics/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Analytics Functional Tests + +The Functional Test Module for **Magento Catalog Analytics** module. diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json index bc3c8a1010449..847c69dc3a387 100644 --- a/app/code/Magento/CatalogAnalytics/composer.json +++ b/app/code/Magento/CatalogAnalytics/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-catalog-analytics", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-catalog": "102.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 8ff71faaacd98..dc172bacb32f9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogImportExport\Model\Export; +use Magento\Catalog\Model\ResourceModel\Product\Option\Collection; use Magento\ImportExport\Model\Import; use \Magento\Store\Model\Store; use \Magento\CatalogImportExport\Model\Import\Product as ImportProduct; @@ -202,7 +203,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity protected $_itemFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Option\Collection + * @var Collection */ protected $_optionColFactory; @@ -352,7 +353,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig * @param \Magento\Catalog\Model\ResourceModel\ProductFactory $productFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFactory @@ -361,9 +362,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeColFactory * @param Product\Type\Factory $_typeFactory - * @param \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider - * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer + * @param ProductEntity\LinkTypeProvider $linkTypeProvider + * @param RowCustomizerInterface $rowCustomizer * @param array $dateAttrCodes + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -517,10 +519,13 @@ protected function getMediaGallery(array $productIds) if (empty($productIds)) { return []; } + + $productEntityJoinField = $this->getProductEntityLinkField(); + $select = $this->_connection->select()->from( ['mgvte' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value_to_entity')], [ - "mgvte.{$this->getProductEntityLinkField()}", + "mgvte.$productEntityJoinField", 'mgvte.value_id' ] )->joinLeft( @@ -532,26 +537,28 @@ protected function getMediaGallery(array $productIds) ] )->joinLeft( ['mgv' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value')], - '(mg.value_id = mgv.value_id AND mgv.store_id = 0)', + "(mg.value_id = mgv.value_id) and (mgvte.$productEntityJoinField = mgv.$productEntityJoinField)", [ 'mgv.label', 'mgv.position', - 'mgv.disabled' + 'mgv.disabled', + 'mgv.store_id' ] )->where( - "mgvte.{$this->getProductEntityLinkField()} IN (?)", + "mgvte.$productEntityJoinField IN (?)", $productIds ); $rowMediaGallery = []; $stmt = $this->_connection->query($select); while ($mediaRow = $stmt->fetch()) { - $rowMediaGallery[$mediaRow[$this->getProductEntityLinkField()]][] = [ + $rowMediaGallery[$mediaRow[$productEntityJoinField]][] = [ '_media_attribute_id' => $mediaRow['attribute_id'], '_media_image' => $mediaRow['filename'], '_media_label' => $mediaRow['label'], '_media_position' => $mediaRow['position'], '_media_is_disabled' => $mediaRow['disabled'], + '_media_store_id' => $mediaRow['store_id'], ]; } @@ -687,6 +694,8 @@ protected function updateDataWithCategoryColumns(&$dataRow, &$rowCategories, $pr } /** + * Get header columns + * * {@inheritdoc} */ public function _getHeaderColumns() @@ -746,6 +755,8 @@ protected function _getExportMainAttrCodes() } /** + * Get entity collection + * * {@inheritdoc} */ protected function _getEntityCollection($resetCollection = false) @@ -791,9 +802,8 @@ protected function getItemsPerPage() // Maximal Products limit $maxProductsLimit = 5000; - $this->_itemsPerPage = intval( - ($memoryLimit * $memoryUsagePercent - memory_get_usage(true)) / $memoryPerProduct - ); + $this->_itemsPerPage = (int) + ($memoryLimit * $memoryUsagePercent - memory_get_usage(true)) / $memoryPerProduct; if ($this->_itemsPerPage < $minProductsLimit) { $this->_itemsPerPage = $minProductsLimit; } @@ -817,9 +827,8 @@ protected function paginateCollection($page, $pageSize) } /** - * Export process - * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function export() { @@ -853,7 +862,11 @@ public function export() } /** - * {@inheritdoc} + * Apply filter to collection and add not skipped attributes to select. + * + * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection + * * @since 100.2.0 */ protected function _prepareEntityCollection(\Magento\Eav\Model\Entity\Collection\AbstractCollection $collection) @@ -915,11 +928,10 @@ protected function getExportData() } /** - * Load products' data from the collection - * and filter it (if needed). + * Load products' data from the collection and filter it (if needed). * - * @return array Keys are product IDs, values arrays with keys as store IDs - * and values as store-specific versions of Product entity. + * @return array Keys are product IDs, values arrays with keys as store ID + * and values as store-specific versions of Product entity. */ protected function loadCollection(): array { @@ -931,8 +943,8 @@ protected function loadCollection(): array foreach ($collection as $itemId => $item) { $data[$itemId][$storeId] = $item; } + $collection->clear(); } - $collection->clear(); return $data; } @@ -1024,12 +1036,10 @@ protected function collectRawData() unset($data[$itemId][$storeId][self::COL_ADDITIONAL_ATTRIBUTES]); } - if (!empty($data[$itemId][$storeId]) || $this->hasMultiselectData($item, $storeId)) { - $attrSetId = $item->getAttributeSetId(); - $data[$itemId][$storeId][self::COL_STORE] = $storeCode; - $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; - $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); - } + $attrSetId = $item->getAttributeSetId(); + $data[$itemId][$storeId][self::COL_STORE] = $storeCode; + $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; + $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); $data[$itemId][$storeId][self::COL_SKU] = htmlspecialchars_decode($item->getSku()); $data[$itemId][$storeId]['store_id'] = $storeId; $data[$itemId][$storeId]['product_id'] = $itemId; @@ -1104,6 +1114,7 @@ protected function collectMultirawData() * @param \Magento\Catalog\Model\Product $item * @param int $storeId * @return bool + * @deprecated */ protected function hasMultiselectData($item, $storeId) { @@ -1151,6 +1162,10 @@ protected function isValidAttributeValue($code, $value) $isValid = false; } + if (is_array($value)) { + $isValid = false; + } + return $isValid; } @@ -1162,20 +1177,23 @@ protected function isValidAttributeValue($code, $value) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - private function appendMultirowData(&$dataRow, &$multiRawData) + private function appendMultirowData(&$dataRow, $multiRawData) { $productId = $dataRow['product_id']; $productLinkId = $dataRow['product_link_id']; $storeId = $dataRow['store_id']; $sku = $dataRow[self::COL_SKU]; + $type = $dataRow[self::COL_TYPE]; + $attributeSet = $dataRow[self::COL_ATTR_SET]; unset($dataRow['product_id']); unset($dataRow['product_link_id']); unset($dataRow['store_id']); unset($dataRow[self::COL_SKU]); - + unset($dataRow[self::COL_STORE]); + unset($dataRow[self::COL_ATTR_SET]); + unset($dataRow[self::COL_TYPE]); if (Store::DEFAULT_STORE_ID == $storeId) { - unset($dataRow[self::COL_STORE]); $this->updateDataWithCategoryColumns($dataRow, $multiRawData['rowCategories'], $productId); if (!empty($multiRawData['rowWebsites'][$productId])) { $websiteCodes = []; @@ -1191,11 +1209,13 @@ private function appendMultirowData(&$dataRow, &$multiRawData) $additionalImageLabels = []; $additionalImageIsDisabled = []; foreach ($multiRawData['mediaGalery'][$productLinkId] as $mediaItem) { - $additionalImages[] = $mediaItem['_media_image']; - $additionalImageLabels[] = $mediaItem['_media_label']; + if ((int)$mediaItem['_media_store_id'] === Store::DEFAULT_STORE_ID) { + $additionalImages[] = $mediaItem['_media_image']; + $additionalImageLabels[] = $mediaItem['_media_label']; - if ($mediaItem['_media_is_disabled'] == true) { - $additionalImageIsDisabled[] = $mediaItem['_media_image']; + if ($mediaItem['_media_is_disabled'] == true) { + $additionalImageIsDisabled[] = $mediaItem['_media_image']; + } } } $dataRow['additional_images'] = @@ -1229,6 +1249,21 @@ private function appendMultirowData(&$dataRow, &$multiRawData) } } $dataRow = $this->rowCustomizer->addData($dataRow, $productId); + } else { + $additionalImageIsDisabled = []; + if (!empty($multiRawData['mediaGalery'][$productLinkId])) { + foreach ($multiRawData['mediaGalery'][$productLinkId] as $mediaItem) { + if ((int)$mediaItem['_media_store_id'] === $storeId) { + if ($mediaItem['_media_is_disabled'] == true) { + $additionalImageIsDisabled[] = $mediaItem['_media_image']; + } + } + } + } + if ($additionalImageIsDisabled) { + $dataRow['hide_from_product_page'] = + implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $additionalImageIsDisabled); + } } if (!empty($this->collectedMultiselectsData[$storeId][$productId])) { @@ -1243,11 +1278,23 @@ private function appendMultirowData(&$dataRow, &$multiRawData) } if (!empty($multiRawData['customOptionsData'][$productLinkId][$storeId])) { + $shouldBeMerged = true; $customOptionsRows = $multiRawData['customOptionsData'][$productLinkId][$storeId]; - $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; - $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); - $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + if ($storeId != Store::DEFAULT_STORE_ID + && !empty($multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]) + ) { + $defaultCustomOptions = $multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]; + if (!array_diff($defaultCustomOptions, $customOptionsRows)) { + $shouldBeMerged = false; + } + } + + if ($shouldBeMerged) { + $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; + $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); + $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + } } if (empty($dataRow)) { @@ -1256,6 +1303,9 @@ private function appendMultirowData(&$dataRow, &$multiRawData) $dataRow[self::COL_STORE] = $this->_storeIdToCode[$storeId]; } $dataRow[self::COL_SKU] = $sku; + $dataRow[self::COL_ATTR_SET] = $attributeSet; + $dataRow[self::COL_TYPE] = $type; + return $dataRow; } @@ -1322,6 +1372,12 @@ protected function optionRowToCellString($option) } /** + * Collect custom options data for products that will be exported. + * + * Option name and type will be collected for all store views, all other data (which can't be changed on store view + * level will be collected for DEFAULT_STORE_ID only. + * Store view specified data will be saved to the additional store view row. + * * @param int[] $productIds * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1330,54 +1386,54 @@ protected function optionRowToCellString($option) protected function getCustomOptionsData($productIds) { $customOptionsData = []; + $defaultOptionsData = []; foreach (array_keys($this->_storeIdToCode) as $storeId) { - if (Store::DEFAULT_STORE_ID != $storeId) { - continue; - } $options = $this->_optionColFactory->create(); - /* @var \Magento\Catalog\Model\ResourceModel\Product\Option\Collection $options*/ - $options->addOrder('sort_order'); - $options->reset()->addOrder('sort_order')->addTitleToResult( - $storeId - )->addPriceToResult( - $storeId - )->addProductToFilter( - $productIds - )->addValuesToResult( - $storeId - ); + /* @var Collection $options*/ + $options->reset() + ->addOrder('sort_order', Collection::SORT_ORDER_ASC) + ->addTitleToResult($storeId) + ->addPriceToResult($storeId) + ->addProductToFilter($productIds) + ->addValuesToResult($storeId); foreach ($options as $option) { + $optionData = $option->toArray(); $row = []; $productId = $option['product_id']; - $row['name'] = $option['title']; $row['type'] = $option['type']; - $row['required'] = $option['is_require']; - $row['price'] = $option['price']; - $row['price_type'] = ($option['price_type'] == 'percent') ? $option['price_type'] : 'fixed'; - $row['sku'] = $option['sku']; - if ($option['max_characters']) { - $row['max_characters'] = $option['max_characters']; - } + $row['required'] = $this->getOptionValue('is_require', $defaultOptionsData, $optionData); + $row['price'] = $this->getOptionValue('price', $defaultOptionsData, $optionData); + $row['sku'] = $this->getOptionValue('sku', $defaultOptionsData, $optionData); + if (array_key_exists('max_characters', $optionData) + || array_key_exists('max_characters', $defaultOptionsData) + ) { + $row['max_characters'] = $this->getOptionValue('max_characters', $defaultOptionsData, $optionData); + } foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { - if (!isset($option[$fileOptionKey])) { - continue; + if (isset($option[$fileOptionKey]) || isset($defaultOptionsData[$fileOptionKey])) { + $row[$fileOptionKey] = $this->getOptionValue($fileOptionKey, $defaultOptionsData, $optionData); } + } + $percentType = $this->getOptionValue('price_type', $defaultOptionsData, $optionData); + $row['price_type'] = ($percentType === 'percent') ? 'percent' : 'fixed'; - $row[$fileOptionKey] = $option[$fileOptionKey]; + if (Store::DEFAULT_STORE_ID === $storeId) { + $optionId = $option['option_id']; + $defaultOptionsData[$optionId] = $option->toArray(); } $values = $option->getValues(); if ($values) { foreach ($values as $value) { - $valuePriceType = ($value['price_type'] == 'percent') ? $value['price_type'] : 'fixed'; + $row['option_title'] = $value['title']; $row['option_title'] = $value['title']; $row['price'] = $value['price']; - $row['price_type'] = $valuePriceType; + $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; $row['sku'] = $value['sku']; $customOptionsData[$productId][$storeId][] = $this->optionRowToCellString($row); } @@ -1392,6 +1448,29 @@ protected function getCustomOptionsData($productIds) return $customOptionsData; } + /** + * Get value for custom option according to store or default value + * + * @param string $optionName + * @param array $defaultOptionsData + * @param array $optionData + * @return mixed + */ + private function getOptionValue(string $optionName, array $defaultOptionsData, array $optionData) + { + $optionId = $optionData['option_id']; + + if (isset($optionData[$optionName])) { + return $optionData[$optionName]; + } + + if (isset($defaultOptionsData[$optionId][$optionName])) { + return $defaultOptionsData[$optionId][$optionName]; + } + + return null; + } + /** * Clean up already loaded attribute collection. * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 5f4a512ec34de..c390a230ce96b 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Model\Import; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; +use Magento\CatalogImportExport\Model\StockItemImporterInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; @@ -17,6 +22,9 @@ use Magento\ImportExport\Model\Import\Entity\AbstractEntity; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; /** * Import entity product model @@ -26,7 +34,6 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @codingStandardsIgnoreFile * @since 100.0.2 */ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity @@ -123,6 +130,16 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ const COL_NAME = 'name'; + /** + * Column new_from_date. + */ + const COL_NEW_FROM_DATE = 'new_from_date'; + + /** + * Column new_to_date. + */ + const COL_NEW_TO_DATE = 'new_to_date'; + /** * Column product website. */ @@ -251,6 +268,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * Validation failure message template definitions * * @var array + * @codingStandardsIgnoreStart */ protected $_messageTemplates = [ ValidatorInterface::ERROR_INVALID_SCOPE => 'Invalid value in Scope column', @@ -286,8 +304,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage', ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', - ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually' + ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', + ValidatorInterface::ERROR_NEW_TO_DATE => 'Make sure new_to_date is later than or the same as new_from_date', + // Can't add new translated strings in patch release + 'invalidLayoutUpdate' => 'Invalid format.', + 'insufficientPermissions' => 'Invalid format.', ]; + //@codingStandardsIgnoreEnd /** * Map between import file fields and system fields/attributes. @@ -307,8 +330,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity Product::COL_TYPE => 'product_type', Product::COL_PRODUCT_WEBSITES => 'product_websites', 'status' => 'product_online', - 'news_from_date' => 'new_from_date', - 'news_to_date' => 'new_to_date', + 'news_from_date' => self::COL_NEW_FROM_DATE, + 'news_to_date' => self::COL_NEW_TO_DATE, 'options_container' => 'display_product_options_in', 'minimal_price' => 'map_price', 'msrp' => 'msrp_price', @@ -621,6 +644,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $_logger; + /** + * "Duplicate multiselect values" error array key + * + * @var string + */ + private static $errorDuplicateMultiselectValues = 'duplicatedMultiselectValues'; + /** * {@inheritdoc} */ @@ -697,6 +727,18 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $catalogConfig; + /** + * Provide ability to process and save images during import. + * + * @var MediaGalleryProcessor + */ + private $mediaProcessor; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -712,7 +754,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider * @param \Magento\Catalog\Helper\Data $catalogData * @param \Magento\ImportExport\Model\Import\Config $importConfig - * @param Proxy\Product\ResourceFactory $resourceFactory + * @param Proxy\Product\ResourceModelFactory $resourceFactory * @param Product\OptionFactory $optionFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory * @param Product\Type\Factory $productTypeFactory @@ -736,7 +778,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param array $data * @param array $dateAttrCodes * @param CatalogConfig $catalogConfig - * @throws \Magento\Framework\Exception\LocalizedException + * @param MediaGalleryProcessor $mediaProcessor + * @param ProductRepositoryInterface|null $productRepository * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -779,7 +822,9 @@ public function __construct( \Magento\Catalog\Model\Product\Url $productUrl, array $data = [], array $dateAttrCodes = [], - CatalogConfig $catalogConfig = null + CatalogConfig $catalogConfig = null, + MediaGalleryProcessor $mediaProcessor = null, + ProductRepositoryInterface $productRepository = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -812,7 +857,8 @@ public function __construct( $this->dateAttrCodes = array_merge($this->dateAttrCodes, $dateAttrCodes); $this->catalogConfig = $catalogConfig ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(CatalogConfig::class); - + $this->mediaProcessor = $mediaProcessor ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(MediaGalleryProcessor::class); parent::__construct( $jsonHelper, $importExportData, @@ -823,16 +869,18 @@ public function __construct( $string, $errorAggregator ); - $this->_optionEntity = isset( - $data['option_entity'] - ) ? $data['option_entity'] : $optionFactory->create( - ['data' => ['product_entity' => $this]] - ); + $this->_optionEntity = $data['option_entity'] + ?? $optionFactory->create(['data' => ['product_entity' => $this]]); $this->_initAttributeSets() ->_initTypeModels() ->_initSkus(); $this->validator->init($this); + $this->productRepository = $productRepository ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(ProductRepositoryInterface::class); + + $errorMessageText = __('Value for multiselect attribute %s contains duplicated values'); + $this->_messageTemplates[self::$errorDuplicateMultiselectValues] = $errorMessageText; } /** @@ -848,7 +896,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ { if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $attrCode); + $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); } return false; } @@ -977,7 +1025,10 @@ protected function _deleteProducts() $this->transactionManager->rollBack(); throw $e; } - $this->_eventManager->dispatch('catalog_product_import_bunch_delete_after', ['adapter' => $this, 'bunch' => $bunch]); + $this->_eventManager->dispatch( + 'catalog_product_import_bunch_delete_after', + ['adapter' => $this, 'bunch' => $bunch] + ); } } return $this; @@ -1084,12 +1135,12 @@ protected function _initTypeModels() $params = [$this, $productTypeName]; if (!($model = $this->_productTypeFactory->create($productTypeConfig['model'], ['params' => $params])) ) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Entity type model \'%1\' is not found', $productTypeConfig['model']) ); } if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __( 'Entity type model must be an instance of ' . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class @@ -1210,7 +1261,7 @@ protected function _saveLinks() : []; foreach ($linkSkus as $linkedKey => $linkedSku) { $linkedSku = trim($linkedSku); - if ((!is_null($this->skuProcessor->getNewSku($linkedSku)) || $this->isSkuExist($linkedSku)) + if (($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku)) && strcasecmp($linkedSku, $sku) !== 0 ) { $newSku = $this->skuProcessor->getNewSku($linkedSku); @@ -1277,7 +1328,8 @@ protected function _saveLinks() // process linked product positions $this->_connection->insertOnDuplicate( $resource->getAttributeTypeTable('int'), - $positionRows, ['value'] + $positionRows, + ['value'] ); } } @@ -1336,7 +1388,7 @@ protected function _saveProductCategories(array $categoriesData) $delProductId[] = $productId; foreach (array_keys($categories) as $categoryId) { - $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 1]; + $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0]; } } if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { @@ -1446,6 +1498,7 @@ private function getNewSkuFieldsForSelect() * Init media gallery resources * @return void * @since 100.0.4 + * @deprecated */ protected function initMediaGalleryResources() { @@ -1469,48 +1522,7 @@ protected function initMediaGalleryResources() */ protected function getExistingImages($bunch) { - $result = []; - if ($this->getErrorAggregator()->hasToBeTerminated()) { - return $result; - } - - $this->initMediaGalleryResources(); - $productSKUs = array_map('strval', array_column($bunch, self::COL_SKU)); - $select = $this->_connection->select()->from( - ['mg' => $this->mediaGalleryTableName], - ['value' => 'mg.value'] - )->joinInner( - ['mgvte' => $this->mediaGalleryEntityToValueTableName], - '(mg.value_id = mgvte.value_id)', - [ - $this->getProductEntityLinkField() => 'mgvte.' . $this->getProductEntityLinkField(), - 'value_id' => 'mgvte.value_id' - ] - )->joinLeft( - ['mgv' => $this->mediaGalleryValueTableName], - sprintf( - '(mg.value_id = mgv.value_id AND mgv.%s = mgvte.%s AND mgv.store_id = %d)', - $this->getProductEntityLinkField(), - $this->getProductEntityLinkField(), - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ), - [ - 'label' => 'mgv.label' - ] - )->joinInner( - ['pe' => $this->productEntityTableName], - "(mgvte.{$this->getProductEntityLinkField()} = pe.{$this->getProductEntityLinkField()})", - ['sku' => 'pe.sku'] - )->where( - 'pe.sku IN (?)', - $productSKUs - ); - - foreach ($this->_connection->fetchAll($select) as $image) { - $result[$image['sku']][$image['value']] = $image; - } - - return $result; + return $this->mediaProcessor->getExistingImages($bunch); } /** @@ -1551,6 +1563,7 @@ public function getImagesFromRow(array $rowData) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @throws LocalizedException */ protected function _saveProducts() { @@ -1568,6 +1581,7 @@ protected function _saveProducts() $tierPrices = []; $mediaGallery = []; $labelsForUpdate = []; + $imagesForChangeVisibility = []; $uploadedImages = []; $previousType = null; $prevAttributeSet = null; @@ -1581,19 +1595,34 @@ protected function _saveProducts() continue; } if ($this->getErrorAggregator()->hasToBeTerminated()) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; + if (ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS + !== $this->_parameters[Import::FIELD_NAME_VALIDATION_STRATEGY] + ) { + $this->getErrorAggregator()->addRowToSkip($rowNum); + continue; + } } $rowScope = $this->getRowScope($rowData); - $rowData[self::URL_KEY] = $this->getUrlKey($rowData); + $urlKey = $this->getUrlKey($rowData); + if (!empty($rowData[self::URL_KEY])) { + // If url_key column and its value were in the CSV file + $rowData[self::URL_KEY] = $urlKey; + } elseif ($this->isNeedToChangeUrlKey($rowData)) { + // If url_key column was empty or even not declared in the CSV file but by the rules it is need to + // be setteed. In case when url_key is generating from name column we have to ensure that the bunch + // of products will pass for the event with url_key column. + $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; + } $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); continue; - } elseif (self::SCOPE_STORE == $rowScope) { + } + + if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; @@ -1611,7 +1640,7 @@ protected function _saveProducts() // wrong attribute_set_code was received if (!$attributeSetId) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __( 'Wrong attribute set code "%1", please correct it and try again.', $rowData['attribute_set_code'] @@ -1656,6 +1685,14 @@ protected function _saveProducts() $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); $this->websitesCache[$rowSku][$websiteId] = true; } + } else { + $product = $this->retrieveProductBySku($rowSku); + if ($product) { + $websiteIds = $product->getWebsiteIds(); + foreach ($websiteIds as $websiteId) { + $this->websitesCache[$rowSku][$websiteId] = true; + } + } } // 3. Categories phase @@ -1687,13 +1724,24 @@ protected function _saveProducts() } // 5. Media gallery phase - $disabledImages = []; list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); - if (isset($rowData['_media_is_disabled'])) { - $disabledImages = array_flip( - explode($this->getMultipleValueSeparator(), $rowData['_media_is_disabled']) - ); + $storeId = !empty($rowData[self::COL_STORE]) + ? $this->getStoreIdByCode($rowData[self::COL_STORE]) + : Store::DEFAULT_STORE_ID; + $imageHiddenStates = $this->getImagesHiddenStates($rowData); + foreach (array_keys($imageHiddenStates) as $image) { + if (array_key_exists($rowSku, $existingImages) + && array_key_exists($image, $existingImages[$rowSku]) + ) { + $rowImages[self::COL_MEDIA_IMAGE][] = $image; + $uploadedImages[$image] = $image; + } + + if (empty($rowImages)) { + $rowImages[self::COL_MEDIA_IMAGE][] = $image; + } } + $rowData[self::COL_MEDIA_IMAGE] = []; /* @@ -1709,13 +1757,8 @@ protected function _saveProducts() if ($uploadedFile) { $uploadedImages[$columnImage] = $uploadedFile; } else { - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, - null, - null, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL - ); + unset($rowData[$column]); + $this->skipRow($rowNum, ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE); } } else { $uploadedFile = $uploadedImages[$columnImage]; @@ -1725,25 +1768,37 @@ protected function _saveProducts() $rowData[$column] = $uploadedFile; } - if ($uploadedFile && !isset($mediaGallery[$rowSku][$uploadedFile])) { + if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { if (isset($existingImages[$rowSku][$uploadedFile])) { + $currentFileData = $existingImages[$rowSku][$uploadedFile]; if (isset($rowLabels[$column][$columnImageKey]) - && $rowLabels[$column][$columnImageKey] != $existingImages[$rowSku][$uploadedFile]['label'] + && $rowLabels[$column][$columnImageKey] != $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], - 'imageData' => $existingImages[$rowSku][$uploadedFile] + 'imageData' => $currentFileData, + ]; + } + + if (array_key_exists($uploadedFile, $imageHiddenStates) + && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] + ) { + $imagesForChangeVisibility[] = [ + 'disabled' => $imageHiddenStates[$uploadedFile], + 'imageData' => $currentFileData, ]; } } else { if ($column == self::COL_MEDIA_IMAGE) { $rowData[$column][] = $uploadedFile; } - $mediaGallery[$rowSku][$uploadedFile] = [ + $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ 'attribute_id' => $this->getMediaGalleryAttributeId(), - 'label' => isset($rowLabels[$column][$columnImageKey]) ? $rowLabels[$column][$columnImageKey] : '', + 'label' => isset($rowLabels[$column][$columnImageKey]) + ? $rowLabels[$column][$columnImageKey] : '', 'position' => ++$position, - 'disabled' => isset($disabledImages[$columnImage]) ? '1' : '0', + 'disabled' => isset($imageHiddenStates[$columnImage]) + ? $imageHiddenStates[$columnImage] : '0', 'value' => $uploadedFile, ]; } @@ -1756,7 +1811,7 @@ protected function _saveProducts() ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) : 0; $productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null; - if (!is_null($productType)) { + if ($productType !== null) { $previousType = $productType; } if (isset($rowData[self::COL_ATTR_SET])) { @@ -1764,13 +1819,13 @@ protected function _saveProducts() } if (self::SCOPE_NULL == $rowScope) { // for multiselect attributes only - if (!is_null($prevAttributeSet)) { + if ($prevAttributeSet !== null) { $rowData[self::COL_ATTR_SET] = $prevAttributeSet; } - if (is_null($productType) && !is_null($previousType)) { + if ($productType === null && $previousType !== null) { $productType = $previousType; } - if (is_null($productType)) { + if ($productType === null) { continue; } } @@ -1805,8 +1860,7 @@ protected function _saveProducts() $attrTable = $attribute->getBackend()->getTable(); $storeIds = [0]; - if ( - 'datetime' == $attribute->getBackendType() + if ('datetime' == $attribute->getBackendType() && ( in_array($attribute->getAttributeCode(), $this->dateAttrCodes) || $attribute->getIsUserDefined() @@ -1864,6 +1918,8 @@ protected function _saveProducts() $mediaGallery )->_saveProductAttributes( $attributes + )->updateMediaGalleryVisibility( + $imagesForChangeVisibility )->updateMediaGalleryLabels( $labelsForUpdate ); @@ -1878,6 +1934,34 @@ protected function _saveProducts() } /** + * Prepare array with image states (visible or hidden from product page) + * + * @param array $rowData + * @return array + */ + private function getImagesHiddenStates(array $rowData): array + { + $statesArray = []; + $mappingArray = [ + '_media_is_disabled' => '1', + ]; + + foreach ($mappingArray as $key => $value) { + if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) { + $items = explode($this->getMultipleValueSeparator(), $rowData[$key]); + + foreach ($items as $item) { + $statesArray[$item] = $value; + } + } + } + + return $statesArray; + } + + /** + * Resolve valid category ids from provided row data. + * * @param array $rowData * @return array */ @@ -1900,7 +1984,13 @@ protected function processRowCategories($rowData) . ' ' . $error['exception']->getMessage() ); } + } else { + $product = $this->retrieveProductBySku($rowData['sku']); + if ($product) { + $categoryIds = $product->getCategoryIds(); + } } + return $categoryIds; } @@ -1981,7 +2071,7 @@ protected function _saveProductTierPrices(array $tierPriceData) */ protected function _getUploader() { - if (is_null($this->_fileUploader)) { + if ($this->_fileUploader === null) { $this->_fileUploader = $this->_uploaderFactory->create(); $this->_fileUploader->init(); @@ -1998,7 +2088,7 @@ protected function _getUploader() } if (!$this->_fileUploader->setTmpDir($tmpPath)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('File directory \'%1\' is not readable.', $tmpPath) ); } @@ -2007,7 +2097,7 @@ protected function _getUploader() $this->_mediaDirectory->create($destinationPath); if (!$this->_fileUploader->setDestDir($destinationPath)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('File directory \'%1\' is not writable.', $destinationPath) ); } @@ -2029,6 +2119,7 @@ public function getUploader() * Return a new file name if the same file is already exists. * * @param string $fileName + * @param bool $renameFileOff * @return string */ protected function uploadMediaFiles($fileName, $renameFileOff = false) @@ -2062,95 +2153,14 @@ private function getSystemFile($fileName) * * @param array $mediaGalleryData * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _saveMediaGallery(array $mediaGalleryData) { if (empty($mediaGalleryData)) { return $this; } - $this->initMediaGalleryResources(); - $productIds = []; - $imageNames = []; - $multiInsertData = []; - $valueToProductId = []; - foreach ($mediaGalleryData as $productSku => $mediaGalleryRows) { - $productId = $this->skuProcessor->getNewSku($productSku)[$this->getProductEntityLinkField()]; - $productIds[] = $productId; - $insertedGalleryImgs = []; - foreach ($mediaGalleryRows as $insertValue) { - if (!in_array($insertValue['value'], $insertedGalleryImgs)) { - $valueArr = [ - 'attribute_id' => $insertValue['attribute_id'], - 'value' => $insertValue['value'], - ]; - $valueToProductId[$insertValue['value']][] = $productId; - $imageNames[] = $insertValue['value']; - $multiInsertData[] = $valueArr; - $insertedGalleryImgs[] = $insertValue['value']; - } - } - } - $oldMediaValues = $this->_connection->fetchAssoc( - $this->_connection->select()->from($this->mediaGalleryTableName, ['value_id', 'value']) - ->where('value IN (?)', $imageNames) - ); - $this->_connection->insertOnDuplicate($this->mediaGalleryTableName, $multiInsertData, []); - $multiInsertData = []; - $newMediaSelect = $this->_connection->select()->from($this->mediaGalleryTableName, ['value_id', 'value']) - ->where('value IN (?)', $imageNames); - if (array_keys($oldMediaValues)) { - $newMediaSelect->where('value_id NOT IN (?)', array_keys($oldMediaValues)); - } - - $dataForSkinnyTable = []; - $newMediaValues = $this->_connection->fetchAssoc($newMediaSelect); - foreach ($mediaGalleryData as $productSku => $mediaGalleryRows) { - foreach ($mediaGalleryRows as $insertValue) { - foreach ($newMediaValues as $value_id => $values) { - if ($values['value'] == $insertValue['value']) { - $insertValue['value_id'] = $value_id; - $insertValue[$this->getProductEntityLinkField()] - = array_shift($valueToProductId[$values['value']]); - unset($newMediaValues[$value_id]); - break; - } - } - if (isset($insertValue['value_id'])) { - $valueArr = [ - 'value_id' => $insertValue['value_id'], - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - $this->getProductEntityLinkField() => $insertValue[$this->getProductEntityLinkField()], - 'label' => $insertValue['label'], - 'position' => $insertValue['position'], - 'disabled' => $insertValue['disabled'], - ]; - $multiInsertData[] = $valueArr; - $dataForSkinnyTable[] = [ - 'value_id' => $insertValue['value_id'], - $this->getProductEntityLinkField() => $insertValue[$this->getProductEntityLinkField()], - ]; - } - } - } - try { - $this->_connection->insertOnDuplicate( - $this->mediaGalleryValueTableName, - $multiInsertData, - ['value_id', 'store_id', $this->getProductEntityLinkField(), 'label', 'position', 'disabled'] - ); - $this->_connection->insertOnDuplicate( - $this->mediaGalleryEntityToValueTableName, - $dataForSkinnyTable, - ['value_id'] - ); - } catch (\Exception $e) { - $this->_connection->delete( - $this->mediaGalleryTableName, - $this->_connection->quoteInto('value_id IN (?)', $newMediaValues) - ); - } + $this->mediaProcessor->saveMediaGallery($mediaGalleryData); + return $this; } @@ -2214,39 +2224,8 @@ protected function _saveStockItem() $row = []; $sku = $rowData[self::COL_SKU]; if ($this->skuProcessor->getNewSku($sku) !== null) { - $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id']; + $row = $this->formatStockDataForRow($rowData); $productIdsToReindex[] = $row['product_id']; - - $row['website_id'] = $this->stockConfiguration->getDefaultScopeId(); - $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId(); - - $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); - $existStockData = $stockItemDo->getData(); - - $row = array_merge( - $this->defaultStockData, - array_intersect_key($existStockData, $this->defaultStockData), - array_intersect_key($rowData, $this->defaultStockData), - $row - ); - - if ($this->stockConfiguration->isQty( - $this->skuProcessor->getNewSku($sku)['type_id'] - ) - ) { - $stockItemDo->setData($row); - $row['is_in_stock'] = $this->stockStateProvider->verifyStock($stockItemDo); - if ($this->stockStateProvider->verifyNotification($stockItemDo)) { - $row['low_stock_date'] = $this->dateTime->gmDate( - 'Y-m-d H:i:s', - (new \DateTime())->getTimestamp() - ); - } - $row['stock_status_changed_auto'] = - (int)!$this->stockStateProvider->verifyStock($stockItemDo); - } else { - $row['qty'] = 0; - } } if (!isset($stockData[$sku])) { @@ -2333,7 +2312,7 @@ public function getEntityTypeCode() * Returns array of new products data with SKU as key. All SKU keys are in lowercase for avoiding creation of * new products with the same SKU in different letter cases. * - * @var string $sku + * @param string $sku * @return array */ public function getNewSku($sku = null) @@ -2412,32 +2391,35 @@ public function validateRow(array $rowData, $rowNum) // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } } if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } return true; } + // if product doesn't exist, need to throw critical error else all errors should be not critical. + $errorLevel = $this->getValidationErrorLevel($sku); + if (!$this->validator->isValid($rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute()); + $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); } } if (null === $sku) { - $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); } elseif (false === $sku) { - $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); } elseif (self::SCOPE_STORE == $rowScope && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); } // SKU is specified, row is SCOPE_DEFAULT, new product block begins @@ -2452,20 +2434,16 @@ public function validateRow(array $rowData, $rowNum) $this->prepareNewSkuData($sku) ); } else { - $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); } } else { // validate new product type and attribute set - if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); - } elseif (!isset( - $rowData[self::COL_ATTR_SET] - ) || !isset( - $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]] - ) + if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); + } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); - } elseif (is_null($this->skuProcessor->getNewSku($sku))) { + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); + } elseif ($this->skuProcessor->getNewSku($sku) === null) { $this->skuProcessor->addNewSku( $sku, [ @@ -2520,24 +2498,50 @@ public function validateRow(array $rowData, $rowNum) ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, $rowData[self::COL_NAME], - $message - ); + $message, + $errorLevel + ) + ->getErrorAggregator() + ->addRowToSkip($rowNum); } } } + + if (!empty($rowData[self::COL_NEW_FROM_DATE]) && !empty($rowData[self::COL_NEW_TO_DATE]) + ) { + $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_FROM_DATE], false)); + $newToTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_TO_DATE], false)); + if ($newFromTimestamp > $newToTimestamp) { + $this->skipRow( + $rowNum, + ValidatorInterface::ERROR_NEW_TO_DATE, + $errorLevel, + $rowData[self::COL_NEW_TO_DATE] + ); + } + } + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } /** + * Check if need to validate url key. + * * @param array $rowData * @return bool */ private function isNeedToValidateUrlKey($rowData) { + if (!empty($rowData[self::COL_SKU]) && empty($rowData[self::URL_KEY]) + && $this->getBehavior() === Import::BEHAVIOR_APPEND + && $this->isSkuExist($rowData[self::COL_SKU])) { + return false; + } + return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) && (empty($rowData[self::COL_VISIBILITY]) - || $rowData[self::COL_VISIBILITY] - !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); + || $rowData[self::COL_VISIBILITY] + !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); } /** @@ -2648,7 +2652,8 @@ private function parseAttributesWithoutWrappedValues($attributesData) private function parseAttributesWithWrappedValues($attributesData) { $attributes = []; - preg_match_all('~((?:[a-zA-Z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~', + preg_match_all( + '~((?:[a-zA-Z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~', $attributesData, $matches ); @@ -2709,7 +2714,10 @@ private function _setStockUseConfigFieldsValues($rowData) { $useConfigFields = []; foreach ($rowData as $key => $value) { - $useConfigName = self::INVENTORY_USE_CONFIG_PREFIX . $key; + $useConfigName = $key === StockItemInterface::ENABLE_QTY_INCREMENTS + ? StockItemInterface::USE_CONFIG_ENABLE_QTY_INC + : self::INVENTORY_USE_CONFIG_PREFIX . $key; + if (isset($this->defaultStockData[$key]) && isset($this->defaultStockData[$useConfigName]) && !empty($value) @@ -2753,9 +2761,7 @@ private function _customFieldsMapping($rowData) } /** - * Validate data rows and save bunches to DB - * - * @return $this + * {@inheritdoc} */ protected function _saveValidatedBunches() { @@ -2806,7 +2812,12 @@ protected function checkUrlKeyDuplicates() ); foreach ($urlKeyDuplicates as $entityData) { $rowNum = $this->rowNumbers[$entityData['store_id']][$entityData['request_path']]; - $this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum); + $message = sprintf( + $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY), + $entityData['request_path'], + $entityData['sku'] + ); + $this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, 'url_key', $message); } } } @@ -2831,17 +2842,22 @@ protected function getProductUrlSuffix($storeId = null) } /** + * Retrieve url key from provided row data. + * * @param array $rowData * @return string + * * @since 100.0.3 */ protected function getUrlKey($rowData) { if (!empty($rowData[self::URL_KEY])) { - return strtolower($rowData[self::URL_KEY]); + $urlKey = (string) $rowData[self::URL_KEY]; + return trim(strtolower($urlKey)); } - if (!empty($rowData[self::COL_NAME])) { + if (!empty($rowData[self::COL_NAME]) + && (array_key_exists(self::URL_KEY, $rowData) || !$this->isSkuExist($rowData[self::COL_SKU]))) { return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); } @@ -2860,6 +2876,26 @@ protected function getResource() return $this->_resource; } + /** + * Whether a url key is needed to be change. + * + * @param array $rowData + * @return bool + */ + private function isNeedToChangeUrlKey(array $rowData): bool + { + $urlKey = $this->getUrlKey($rowData); + $productExists = $this->isSkuExist($rowData[self::COL_SKU]); + $markedToEraseUrlKey = isset($rowData[self::URL_KEY]); + // The product isn't new and the url key index wasn't marked for change. + if (!$urlKey && $productExists && !$markedToEraseUrlKey) { + // Seems there is no need to change the url key + return false; + } + + return true; + } + /** * Get product entity link field * @@ -2901,39 +2937,22 @@ private function updateMediaGalleryLabels(array $labels) if (empty($labels)) { return; } + $this->mediaProcessor->updateMediaGalleryLabels($labels); + } - $insertData = []; - foreach ($labels as $label) { - $imageData = $label['imageData']; - - if ($imageData['label'] === null) { - $insertData[] = [ - 'label' => $label['label'], - $this->getProductEntityLinkField() => $imageData[$this->getProductEntityLinkField()], - 'value_id' => $imageData['value_id'], - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID - ]; - } else { - $this->_connection->update( - $this->mediaGalleryValueTableName, - [ - 'label' => $label['label'] - ], - [ - $this->getProductEntityLinkField() . ' = ?' => $imageData[$this->getProductEntityLinkField()], - 'value_id = ?' => $imageData['value_id'], - 'store_id = ?' => \Magento\Store\Model\Store::DEFAULT_STORE_ID - ] - ); - } + /** + * Update 'disabled' field for media gallery entity + * + * @param array $images + * @return $this + */ + private function updateMediaGalleryVisibility(array $images): Product + { + if (!empty($images)) { + $this->mediaProcessor->updateMediaGalleryVisibility($images); } - if (!empty($insertData)) { - $this->_connection->insertMultiple( - $this->mediaGalleryValueTableName, - $insertData - ); - } + return $this; } /** @@ -2972,4 +2991,100 @@ private function getExistingSku($sku) { return $this->_oldSku[strtolower($sku)]; } + + /** + * Format row data to DB compatible values + * + * @param array $rowData + * @return array + */ + private function formatStockDataForRow(array $rowData) + { + $sku = $rowData[self::COL_SKU]; + $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id']; + $row['website_id'] = $this->stockConfiguration->getDefaultScopeId(); + $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId(); + + $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); + $existStockData = $stockItemDo->getData(); + + $row = array_merge( + $this->defaultStockData, + array_intersect_key($existStockData, $this->defaultStockData), + array_intersect_key($rowData, $this->defaultStockData), + $row + ); + + if ($this->stockConfiguration->isQty( + $this->skuProcessor->getNewSku($sku)['type_id'] + ) + ) { + $stockItemDo->setData($row); + $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); + if ($this->stockStateProvider->verifyNotification($stockItemDo)) { + $row['low_stock_date'] = $this->dateTime->gmDate( + 'Y-m-d H:i:s', + (new \DateTime())->getTimestamp() + ); + } + $row['stock_status_changed_auto'] = + (int)!$this->stockStateProvider->verifyStock($stockItemDo); + } else { + $row['qty'] = 0; + } + + return $row; + } + + /** + * Retrieve product by sku. + * + * @param string $sku + * @return \Magento\Catalog\Api\Data\ProductInterface|null + */ + private function retrieveProductBySku(string $sku) + { + try { + $product = $this->productRepository->get($sku); + } catch (NoSuchEntityException $e) { + return null; + } + + return $product; + } + + /** + * Add row as skipped + * + * @param int $rowNum + * @param string $errorCode Error code or simply column name + * @param string $errorLevel error level + * @param string|null $colName optional column name + * @return $this + */ + private function skipRow( + int $rowNum, + string $errorCode, + string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, + $colName = null + ): self { + $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); + $this->getErrorAggregator() + ->addRowToSkip($rowNum); + + return $this; + } + + /** + * Returns errorLevel for validation + * + * @param string|bool|null $sku + * @return string + */ + private function getValidationErrorLevel($sku): string + { + return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) + ? ProcessingError::ERROR_LEVEL_CRITICAL + : ProcessingError::ERROR_LEVEL_NOT_CRITICAL; + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index 54fdecbaaf967..f9872c0b2acd6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -66,6 +66,8 @@ public function __construct( } /** + * Initialize categories to be processed + * * @return $this */ protected function initCategories() @@ -112,6 +114,9 @@ protected function createCategory($name, $parentId) if (!($parentCategory = $this->getCategoryById($parentId))) { $parentCategory = $this->categoryFactory->create()->load($parentId); } + + // Set StoreId to 0 to generate URL Keys global and prevent generating url rewrites just for default website + $category->setStoreId(0); $category->setPath($parentCategory->getPath()); $category->setParentId($parentId); $category->setName($name); @@ -232,7 +237,7 @@ public function clearFailedCategories() */ public function getCategoryById($categoryId) { - return isset($this->categoriesCache[$categoryId]) ? $this->categoriesCache[$categoryId] : null; + return $this->categoriesCache[$categoryId] ?? null; } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php new file mode 100644 index 0000000000000..25ae265784bef --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -0,0 +1,418 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogImportExport\Model\Import\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Store\Model\Store; + +/** + * Process and saves images during import. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MediaGalleryProcessor +{ + /** + * @var SkuProcessor + */ + private $skuProcessor; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * DB connection. + * + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * @var ResourceModelFactory + */ + private $resourceFactory; + + /** + * @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel + */ + private $resourceModel; + + /** + * @var ProcessingErrorAggregatorInterface + */ + private $errorAggregator; + + /** + * @var string + */ + private $productEntityLinkField; + + /** + * @var string + */ + private $mediaGalleryTableName; + + /** + * @var string + */ + private $mediaGalleryValueTableName; + + /** + * @var string + */ + private $mediaGalleryEntityToValueTableName; + + /** + * @var string + */ + private $productEntityTableName; + + /** + * MediaProcessor constructor. + * + * @param SkuProcessor $skuProcessor + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param ResourceModelFactory $resourceModelFactory + * @param ProcessingErrorAggregatorInterface $errorAggregator + */ + public function __construct( + SkuProcessor $skuProcessor, + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + ResourceModelFactory $resourceModelFactory, + ProcessingErrorAggregatorInterface $errorAggregator + ) { + $this->skuProcessor = $skuProcessor; + $this->metadataPool = $metadataPool; + $this->connection = $resourceConnection->getConnection(); + $this->resourceFactory = $resourceModelFactory; + $this->errorAggregator = $errorAggregator; + } + + /** + * Save product media gallery. + * + * @param array $mediaGalleryData + * @return void + */ + public function saveMediaGallery(array $mediaGalleryData) + { + $this->initMediaGalleryResources(); + $mediaGalleryDataGlobal = array_replace_recursive(...$mediaGalleryData); + $imageNames = []; + $multiInsertData = []; + $valueToProductId = []; + foreach ($mediaGalleryDataGlobal as $productSku => $mediaGalleryRows) { + $productId = $this->skuProcessor->getNewSku($productSku)[$this->getProductEntityLinkField()]; + $insertedGalleryImgs = []; + foreach ($mediaGalleryRows as $insertValue) { + if (!in_array($insertValue['value'], $insertedGalleryImgs)) { + $valueArr = [ + 'attribute_id' => $insertValue['attribute_id'], + 'value' => $insertValue['value'], + ]; + $valueToProductId[$insertValue['value']][] = $productId; + $imageNames[] = $insertValue['value']; + $multiInsertData[] = $valueArr; + $insertedGalleryImgs[] = $insertValue['value']; + } + } + } + $oldMediaValues = $this->connection->fetchAssoc( + $this->connection->select()->from($this->mediaGalleryTableName, ['value_id', 'value']) + ->where('value IN (?)', $imageNames) + ); + $this->connection->insertOnDuplicate($this->mediaGalleryTableName, $multiInsertData); + $newMediaSelect = $this->connection->select()->from($this->mediaGalleryTableName, ['value_id', 'value']) + ->where('value IN (?)', $imageNames); + if (array_keys($oldMediaValues)) { + $newMediaSelect->where('value_id NOT IN (?)', array_keys($oldMediaValues)); + } + $newMediaValues = $this->connection->fetchAssoc($newMediaSelect); + foreach ($mediaGalleryData as $storeId => $storeMediaGalleryData) { + $this->processMediaPerStore((int)$storeId, $storeMediaGalleryData, $newMediaValues, $valueToProductId); + } + } + + /** + * Update media gallery labels. + * + * @param array $labels + * @return void + */ + public function updateMediaGalleryLabels(array $labels) + { + $this->updateMediaGalleryField($labels, 'label'); + } + + /** + * Update 'disabled' field for media gallery entity + * + * @param array $images + * @return void + */ + public function updateMediaGalleryVisibility(array $images) + { + $this->updateMediaGalleryField($images, 'disabled'); + } + + /** + * Update value for requested field in media gallery entities + * + * @param array $data + * @param string $field + * @return void + */ + private function updateMediaGalleryField(array $data, string $field) + { + $insertData = []; + foreach ($data as $datum) { + $imageData = $datum['imageData']; + + if ($imageData[$field] === null) { + $insertData[] = [ + $field => $datum[$field], + $this->getProductEntityLinkField() => $imageData[$this->getProductEntityLinkField()], + 'value_id' => $imageData['value_id'], + 'store_id' => Store::DEFAULT_STORE_ID, + ]; + } else { + $this->connection->update( + $this->mediaGalleryValueTableName, + [ + $field => $datum[$field], + ], + [ + $this->getProductEntityLinkField() . ' = ?' => $imageData[$this->getProductEntityLinkField()], + 'value_id = ?' => $imageData['value_id'], + 'store_id = ?' => Store::DEFAULT_STORE_ID, + ] + ); + } + } + + if (!empty($insertData)) { + $this->connection->insertMultiple( + $this->mediaGalleryValueTableName, + $insertData + ); + } + } + + /** + * Get existing images for current bunch. + * + * @param array $bunch + * @return array + */ + public function getExistingImages(array $bunch) + { + $result = []; + if ($this->errorAggregator->hasToBeTerminated()) { + return $result; + } + $this->initMediaGalleryResources(); + $productSKUs = array_map( + 'strval', + array_column($bunch, Product::COL_SKU) + ); + $select = $this->connection->select()->from( + ['mg' => $this->mediaGalleryTableName], + ['value' => 'mg.value'] + )->joinInner( + ['mgvte' => $this->mediaGalleryEntityToValueTableName], + '(mg.value_id = mgvte.value_id)', + [ + $this->getProductEntityLinkField() => 'mgvte.' . $this->getProductEntityLinkField(), + 'value_id' => 'mgvte.value_id', + ] + )->joinLeft( + ['mgv' => $this->mediaGalleryValueTableName], + sprintf( + '(mg.value_id = mgv.value_id AND mgv.%s = mgvte.%s AND mgv.store_id = %d)', + $this->getProductEntityLinkField(), + $this->getProductEntityLinkField(), + Store::DEFAULT_STORE_ID + ), + [ + 'label' => 'mgv.label', + 'disabled' => 'mgv.disabled', + ] + )->joinInner( + ['pe' => $this->productEntityTableName], + "(mgvte.{$this->getProductEntityLinkField()} = pe.{$this->getProductEntityLinkField()})", + ['sku' => 'pe.sku'] + )->where( + 'pe.sku IN (?)', + $productSKUs + ); + + foreach ($this->connection->fetchAll($select) as $image) { + $result[$image['sku']][$image['value']] = $image; + } + + return $result; + } + + /** + * Init media gallery resources. + * + * @return void + */ + private function initMediaGalleryResources() + { + if (null == $this->mediaGalleryTableName) { + $this->productEntityTableName = $this->getResource()->getTable('catalog_product_entity'); + $this->mediaGalleryTableName = $this->getResource()->getTable('catalog_product_entity_media_gallery'); + $this->mediaGalleryValueTableName = $this->getResource()->getTable( + 'catalog_product_entity_media_gallery_value' + ); + $this->mediaGalleryEntityToValueTableName = $this->getResource()->getTable( + 'catalog_product_entity_media_gallery_value_to_entity' + ); + } + } + + /** + * Get the last media position for each product from the given list + * + * @param int $storeId + * @param array $productIds + * @return array + */ + private function getLastMediaPositionPerProduct(int $storeId, array $productIds): array + { + $result = []; + if ($productIds) { + $productKeyName = $this->getProductEntityLinkField(); + // this result could be achieved by using GROUP BY. But there is no index on position column, therefore + // it can be slower than the implementation below + $positions = $this->connection->fetchAll( + $this->connection + ->select() + ->from($this->mediaGalleryValueTableName, [$productKeyName, 'position']) + ->where("$productKeyName IN (?)", $productIds) + ->where('value_id is not null') + ->where('store_id = ?', $storeId) + ); + // Make sure the result contains all product ids even if the product has no media files + $result = array_fill_keys($productIds, 0); + // Find the largest position for each product + foreach ($positions as $record) { + $productId = $record[$productKeyName]; + $result[$productId] = $result[$productId] < $record['position'] + ? $record['position'] + : $result[$productId]; + } + } + + return $result; + } + + /** + * Save media gallery data per store. + * + * @param int $storeId + * @param array $mediaGalleryData + * @param array $newMediaValues + * @param array $valueToProductId + * @return void + */ + private function processMediaPerStore( + int $storeId, + array $mediaGalleryData, + array $newMediaValues, + array $valueToProductId + ) { + $multiInsertData = []; + $dataForSkinnyTable = []; + $lastMediaPositionPerProduct = $this->getLastMediaPositionPerProduct( + $storeId, + array_unique(array_merge(...array_values($valueToProductId))) + ); + + foreach ($mediaGalleryData as $mediaGalleryRows) { + foreach ($mediaGalleryRows as $insertValue) { + foreach ($newMediaValues as $valueId => $values) { + if ($values['value'] == $insertValue['value']) { + $insertValue['value_id'] = $valueId; + $insertValue[$this->getProductEntityLinkField()] + = array_shift($valueToProductId[$values['value']]); + unset($newMediaValues[$valueId]); + break; + } + } + if (isset($insertValue['value_id'])) { + $productId = $insertValue[$this->getProductEntityLinkField()]; + $valueArr = [ + 'value_id' => $insertValue['value_id'], + 'store_id' => $storeId, + $this->getProductEntityLinkField() => $productId, + 'label' => $insertValue['label'], + 'position' => $lastMediaPositionPerProduct[$productId] + $insertValue['position'], + 'disabled' => $insertValue['disabled'], + ]; + $multiInsertData[] = $valueArr; + $dataForSkinnyTable[] = [ + 'value_id' => $insertValue['value_id'], + $this->getProductEntityLinkField() => $insertValue[$this->getProductEntityLinkField()], + ]; + } + } + } + try { + $this->connection->insertOnDuplicate( + $this->mediaGalleryValueTableName, + $multiInsertData, + ['value_id', 'store_id', $this->getProductEntityLinkField(), 'label', 'position', 'disabled'] + ); + $this->connection->insertOnDuplicate( + $this->mediaGalleryEntityToValueTableName, + $dataForSkinnyTable, + ['value_id'] + ); + } catch (\Exception $e) { + $this->connection->delete( + $this->mediaGalleryTableName, + $this->connection->quoteInto('value_id IN (?)', $newMediaValues) + ); + } + } + + /** + * Get product entity link field. + * + * @return string + */ + private function getProductEntityLinkField() + { + if (!$this->productEntityLinkField) { + $this->productEntityLinkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + } + + return $this->productEntityLinkField; + } + + /** + * @return \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel + */ + private function getResource() + { + if (!$this->resourceModel) { + $this->resourceModel = $this->resourceFactory->create(); + } + + return $this->resourceModel; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index aa3f46a433a4d..f6e6af10f3f87 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product; @@ -14,6 +12,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection as ProductOptionValueCollection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory as ProductOptionValueCollectionFactory; +use Magento\Store\Model\Store; /** * Entity class which provide possibility to import product custom options @@ -23,6 +22,8 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * @since 100.0.2 */ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity @@ -333,6 +334,11 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $optionTypeTitles; + /** + * @var array + */ + private $lastOptionTitle; + /** * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData * @param ResourceConnection $resource @@ -675,7 +681,12 @@ protected function _getNewOptionsWithTheSameTitlesErrorRows(array $sourceProduct ksort($outerTitles); ksort($innerTitles); if ($outerTitles === $innerTitles) { - $errorRows = array_merge($errorRows, $innerData['rows'], $outerData['rows']); + foreach ($innerData['rows'] as $innerDataRow) { + $errorRows[] = $innerDataRow; + } + foreach ($outerData['rows'] as $outerDataRow) { + $errorRows[] = $outerDataRow; + } } } } @@ -709,7 +720,9 @@ protected function _findOldOptionsWithTheSameTitles() } } if ($optionsCount > 1) { - $errorRows = array_merge($errorRows, $outerData['rows']); + foreach ($outerData['rows'] as $dataRow) { + $errorRows[] = $dataRow; + } } } } @@ -736,7 +749,9 @@ protected function _findNewOldOptionsTypeMismatch() ksort($outerTitles); ksort($innerTitles); if ($outerTitles === $innerTitles && $outerData['type'] != $innerData['type']) { - $errorRows = array_merge($errorRows, $outerData['rows']); + foreach ($outerData['rows'] as $dataRow) { + $errorRows[] = $dataRow; + } } } } @@ -761,7 +776,7 @@ protected function _findExistingOptionId(array $newOptionData, array $newOptionT ksort($newOptionTitles); $existingOptions = $this->_oldCustomOptions[$productId]; foreach ($existingOptions as $optionId => $optionData) { - if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'] == $newOptionTitles) { + if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'][0] == $newOptionTitles[0]) { return $optionId; } } @@ -793,16 +808,18 @@ protected function _addRowsErrors($errorCode, array $errorNumbers) */ protected function _validateMainRow(array $rowData, $rowNumber) { - if (!empty($rowData[self::COLUMN_STORE]) && !array_key_exists( - $rowData[self::COLUMN_STORE], - $this->_storeCodeToId - ) + if (!empty($rowData[self::COLUMN_STORE]) + && !array_key_exists( + $rowData[self::COLUMN_STORE], + $this->_storeCodeToId + ) ) { $this->_productEntity->addRowError(self::ERROR_INVALID_STORE, $rowNumber); - } elseif (!empty($rowData[self::COLUMN_TYPE]) && !array_key_exists( - $rowData[self::COLUMN_TYPE], - $this->_specificTypes - ) + } elseif (!empty($rowData[self::COLUMN_TYPE]) + && !array_key_exists( + $rowData[self::COLUMN_TYPE], + $this->_specificTypes + ) ) { // type $this->_productEntity->addRowError(self::ERROR_INVALID_TYPE, $rowNumber); @@ -906,10 +923,11 @@ protected function _saveNewOptionData(array $rowData, $rowNumber) */ protected function _validateSecondaryRow(array $rowData, $rowNumber) { - if (!empty($rowData[self::COLUMN_STORE]) && !array_key_exists( - $rowData[self::COLUMN_STORE], - $this->_storeCodeToId - ) + if (!empty($rowData[self::COLUMN_STORE]) + && !array_key_exists( + $rowData[self::COLUMN_STORE], + $this->_storeCodeToId + ) ) { $this->_productEntity->addRowError(self::ERROR_INVALID_STORE, $rowNumber); } elseif (!empty($rowData[self::COLUMN_ROW_PRICE]) && !is_numeric(rtrim($rowData[self::COLUMN_ROW_PRICE], '%')) @@ -946,9 +964,10 @@ public function validateRow(array $rowData, $rowNumber) $multiRowData = $this->_getMultiRowFormat($rowData); - foreach ($multiRowData as $optionData) { - - $combinedData = array_merge($rowData, $optionData); + foreach ($multiRowData as $combinedData) { + foreach ($rowData as $key => $field) { + $combinedData[$key] = $field; + } if ($this->_isRowWithCustomOption($combinedData)) { if ($this->_isMainOptionRow($combinedData)) { @@ -1098,15 +1117,15 @@ protected function _getMultiRowFormat($rowData) foreach ($rowData['custom_options'] as $name => $customOption) { $i++; foreach ($customOption as $rowOrder => $optionRow) { - $row = array_merge( - [ - self::COLUMN_STORE => '', - self::COLUMN_TITLE => $name, - self::COLUMN_SORT_ORDER => $i, - self::COLUMN_ROW_SORT => $rowOrder - ], - $this->processOptionRow($name, $optionRow) - ); + $row = [ + self::COLUMN_STORE => '', + self::COLUMN_TITLE => $name, + self::COLUMN_SORT_ORDER => $i, + self::COLUMN_ROW_SORT => $rowOrder + ]; + foreach ($this->processOptionRow($name, $optionRow) as $key => $value) { + $row[$key] = $value; + } $name = ''; $multiRow[] = $row; } @@ -1116,6 +1135,8 @@ protected function _getMultiRowFormat($rowData) } /** + * Process option row. + * * @param string $name * @param array $optionRow * @return array @@ -1124,13 +1145,19 @@ private function processOptionRow($name, $optionRow) { $result = [ self::COLUMN_TYPE => $name ? $optionRow['type'] : '', - self::COLUMN_IS_REQUIRED => $optionRow['required'], - self::COLUMN_ROW_SKU => $optionRow['sku'], - self::COLUMN_PREFIX . 'sku' => $optionRow['sku'], self::COLUMN_ROW_TITLE => '', self::COLUMN_ROW_PRICE => '' ]; - + if (isset($optionRow['_custom_option_store'])) { + $result[self::COLUMN_STORE] = $optionRow['_custom_option_store']; + } + if (isset($optionRow['required'])) { + $result[self::COLUMN_IS_REQUIRED] = $optionRow['required']; + } + if (isset($optionRow['sku'])) { + $result[self::COLUMN_ROW_SKU] = $optionRow['sku']; + $result[self::COLUMN_PREFIX . 'sku'] = $optionRow['sku']; + } if (isset($optionRow['option_title'])) { $result[self::COLUMN_ROW_TITLE] = $optionRow['option_title']; } @@ -1175,10 +1202,14 @@ private function addFileOptions($result, $optionRow) } /** - * Import data rows + * Import data rows. + * + * Additional store view data (option titles) will be sought in store view specified import file rows * * @return boolean * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _importData() { @@ -1189,7 +1220,8 @@ protected function _importData() $this->_tables['catalog_product_option_type_value'] ); $prevOptionId = 0; - + $optionId = null; + $valueId = null; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $products = []; $options = []; @@ -1200,15 +1232,25 @@ protected function _importData() $typeTitles = []; $parentCount = []; $childCount = []; - + $optionsToRemove = []; foreach ($bunch as $rowNumber => $rowData) { - + if (isset($optionId, $valueId) && empty($rowData[Product::COL_STORE_VIEW_CODE])) { + $nextOptionId = $optionId; + $nextValueId = $valueId; + } + $optionId = $nextOptionId; + $valueId = $nextValueId; $multiRowData = $this->_getMultiRowFormat($rowData); - - foreach ($multiRowData as $optionData) { - - $combinedData = array_merge($rowData, $optionData); - + if (!empty($rowData[self::COLUMN_SKU]) && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) { + $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]]; + if (array_key_exists('custom_options', $rowData) && trim($rowData['custom_options']) === "") { + $optionsToRemove[] = $this->_rowProductId; + } + } + foreach ($multiRowData as $combinedData) { + foreach ($rowData as $key => $field) { + $combinedData[$key] = $field; + } if (!$this->isRowAllowedToImport($combinedData, $rowNumber)) { continue; } @@ -1218,7 +1260,7 @@ protected function _importData() $optionData = $this->_collectOptionMainData( $combinedData, $prevOptionId, - $nextOptionId, + $optionId, $products, $prices ); @@ -1228,7 +1270,7 @@ protected function _importData() $this->_collectOptionTypeData( $combinedData, $prevOptionId, - $nextValueId, + $valueId, $typeValues, $typePrices, $typeTitles, @@ -1236,41 +1278,98 @@ protected function _importData() $childCount ); $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); + $this->checkOptionTitles( + $options, + $titles, + $combinedData, + $prevOptionId, + $optionId, + $products, + $prices + ); } } - - // Save prepared custom options data !!! + // Remove all existing options if import behaviour is APPEND + // in other case remove options for products with empty "custom_options" row only if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { $this->_deleteEntities(array_keys($products)); + } elseif (!empty($optionsToRemove)) { + // Remove options for products with empty "custom_options" row + $this->_deleteEntities($optionsToRemove); } - + // Save prepared custom options data if ($this->_isReadyForSaving($options, $titles, $typeValues)) { - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { - $this->_compareOptionsWithExisting($options, $titles, $prices, $typeValues); - $this->restoreOriginalOptionTypeIds($typeValues, $typePrices, $typeTitles); - } - - $this->_saveOptions( - $options - )->_saveTitles( - $titles - )->_savePrices( - $prices - )->_saveSpecificTypeValues( - $typeValues - )->_saveSpecificTypePrices( - $typePrices - )->_saveSpecificTypeTitles( - $typeTitles - )->_updateProducts( - $products - ); + $types = [ + 'values' => $typeValues, + 'prices' => $typePrices, + 'titles' => $typeTitles + ]; + $this->setLastOptionTitle($titles); + $this->savePreparedCustomOptions($products, $options, $titles, $prices, $types); } } return true; } + /** + * Check options titles. + * + * If products were split up between bunches, + * this function will add needed option for option titles. + * + * @param array $options + * @param array $titles + * @param array $combinedData + * @param int $prevOptionId + * @param int $optionId + * @param array $products + * @param array $prices + * @return void + */ + private function checkOptionTitles( + array &$options, + array &$titles, + array $combinedData, + int &$prevOptionId, + int &$optionId, + array $products, + array $prices + ) { + $titlesCount = count($titles); + if ($titlesCount > 0 && count($options) !== $titlesCount) { + $combinedData[Product::COL_STORE_VIEW_CODE] = ''; + $optionId--; + $option = $this->_collectOptionMainData( + $combinedData, + $prevOptionId, + $optionId, + $products, + $prices + ); + if ($option) { + $options[] = $option; + } + } + } + + /** + * Setting last Custom Option Title + * to use it later in _collectOptionTitle + * to set correct title for default store view. + * + * @param array $titles + * @return void + */ + private function setLastOptionTitle(array &$titles) + { + if (count($titles) > 0) { + end($titles); + $key = key($titles); + $this->lastOptionTitle[$key] = $titles[$key]; + } + } + /** * Load data of existed products * @@ -1311,17 +1410,16 @@ protected function _collectOptionMainData( $optionData = null; if ($this->_rowIsMain) { - $optionData = $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType); - - if (!$this->_isRowHasSpecificType( - $this->_rowType - ) && ($priceData = $this->_getPriceData( - $rowData, - $nextOptionId, - $this->_rowType - )) + $optionData = empty($rowData[Product::COL_STORE_VIEW_CODE]) + ? $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType) + : ''; + + if (!$this->_isRowHasSpecificType($this->_rowType) + && ($priceData = $this->_getPriceData($rowData, $nextOptionId, $this->_rowType)) ) { - $prices[$nextOptionId] = $priceData; + if ($this->_isPriceGlobal) { + $prices[$nextOptionId][Store::DEFAULT_STORE_ID] = $priceData; + } } if (!isset($products[$this->_rowProductId])) { @@ -1347,6 +1445,7 @@ protected function _collectOptionMainData( * @param array &$childCount * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _collectOptionTypeData( array $rowData, @@ -1365,39 +1464,30 @@ protected function _collectOptionTypeData( $typeValues[$prevOptionId][] = $specificTypeData['value']; // ensure default title is set - if (!isset($typeTitles[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { - $typeTitles[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['title']; + if (!isset($typeTitles[$nextValueId][Store::DEFAULT_STORE_ID])) { + $typeTitles[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['title']; } if ($specificTypeData['price']) { if ($this->_isPriceGlobal) { - $typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['price']; + $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } else { // ensure default price is set - if (!isset($typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { - $typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['price']; + if (!isset($typePrices[$nextValueId][Store::DEFAULT_STORE_ID])) { + $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; } } - $nextValueId++; - if (isset($parentCount[$prevOptionId])) { - $parentCount[$prevOptionId]++; - } else { - $parentCount[$prevOptionId] = 1; - } } - - if (!isset($childCount[$this->_rowStoreId][$prevOptionId])) { - $childCount[$this->_rowStoreId][$prevOptionId] = 0; - } - $parentValueId = $nextValueId - $parentCount[$prevOptionId] + $childCount[$this->_rowStoreId][$prevOptionId]; - $specificTypeData = $this->_getSpecificTypeData($rowData, $parentValueId, false); + $specificTypeData = $this->_getSpecificTypeData($rowData, 0, false); //For others stores if ($specificTypeData) { - $typeTitles[$parentValueId][$this->_rowStoreId] = $specificTypeData['title']; - $childCount[$this->_rowStoreId][$prevOptionId]++; + if (isset($specificTypeData['price'])) { + $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; + } + $typeTitles[$nextValueId++][$this->_rowStoreId] = $specificTypeData['title']; } } } @@ -1412,11 +1502,15 @@ protected function _collectOptionTypeData( */ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$titles) { - $defaultStoreId = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $defaultStoreId = Store::DEFAULT_STORE_ID; if (!empty($rowData[self::COLUMN_TITLE])) { if (!isset($titles[$prevOptionId][$defaultStoreId])) { - // ensure default title is set - $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; + if (isset($this->lastOptionTitle[$prevOptionId])) { + $titles[$prevOptionId] = $this->lastOptionTitle[$prevOptionId]; + unset($this->lastOptionTitle); + } else { + $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; + } } $titles[$prevOptionId][$this->_rowStoreId] = $rowData[self::COLUMN_TITLE]; } @@ -1430,6 +1524,7 @@ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$ti * @param array &$prices * @param array &$typeValues * @return $this + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _compareOptionsWithExisting(array &$options, array &$titles, array &$prices, array &$typeValues) { @@ -1440,7 +1535,9 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, $titles[$optionId] = $titles[$newOptionId]; unset($titles[$newOptionId]); if (isset($prices[$newOptionId])) { - $prices[$newOptionId]['option_id'] = $optionId; + foreach ($prices[$newOptionId] as $storeId => $priceStoreData) { + $prices[$newOptionId][$storeId]['option_id'] = $optionId; + } } if (isset($typeValues[$newOptionId])) { $typeValues[$optionId] = $typeValues[$newOptionId]; @@ -1473,8 +1570,10 @@ private function restoreOriginalOptionTypeIds(array &$typeValues, array &$typePr $optionType['option_type_id'] = $existingTypeId; $typeTitles[$existingTypeId] = $typeTitles[$optionTypeId]; unset($typeTitles[$optionTypeId]); - $typePrices[$existingTypeId] = $typePrices[$optionTypeId]; - unset($typePrices[$optionTypeId]); + if (isset($typePrices[$optionTypeId])) { + $typePrices[$existingTypeId] = $typePrices[$optionTypeId]; + unset($typePrices[$optionTypeId]); + } // If option type titles match at least in one store, consider current option type as existing break; } @@ -1523,20 +1622,17 @@ private function getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle) */ protected function _parseRequiredData(array $rowData) { - if ($rowData[self::COLUMN_SKU] != '' && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) { - $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]]; - } elseif (!isset($this->_rowProductId)) { + if (!isset($this->_rowProductId)) { return false; } - // Init store if (!empty($rowData[self::COLUMN_STORE])) { if (!isset($this->_storeCodeToId[$rowData[self::COLUMN_STORE]])) { return false; } - $this->_rowStoreId = $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; + $this->_rowStoreId = (int)$this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; } else { - $this->_rowStoreId = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $this->_rowStoreId = Store::DEFAULT_STORE_ID; } // Init option type and set param which tell that row is main if (!empty($rowData[self::COLUMN_TYPE])) { @@ -1644,18 +1740,13 @@ protected function _getOptionData(array $rowData, $productId, $optionId, $type) */ protected function _getPriceData(array $rowData, $optionId, $type) { - if (in_array( - 'price', - $this->_specificTypes[$type] - ) && isset( - $rowData[self::COLUMN_PREFIX . 'price'] - ) && strlen( - $rowData[self::COLUMN_PREFIX . 'price'] - ) > 0 + if (in_array('price', $this->_specificTypes[$type]) + && isset($rowData[self::COLUMN_PREFIX . 'price']) + && strlen($rowData[self::COLUMN_PREFIX . 'price']) > 0 ) { $priceData = [ 'option_id' => $optionId, - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + 'store_id' => $this->_rowStoreId, 'price_type' => 'fixed', ]; @@ -1682,28 +1773,30 @@ protected function _getPriceData(array $rowData, $optionId, $type) */ protected function _getSpecificTypeData(array $rowData, $optionTypeId, $defaultStore = true) { + $data = []; + $priceData = []; + $customOptionRowPrice = $rowData[self::COLUMN_ROW_PRICE]; + if (!empty($customOptionRowPrice) || $customOptionRowPrice === '0') { + $priceData['price'] = (double)rtrim($rowData[self::COLUMN_ROW_PRICE], '%'); + $priceData['price_type'] = ('%' == substr($rowData[self::COLUMN_ROW_PRICE], -1)) ? 'percent' : 'fixed'; + } if (!empty($rowData[self::COLUMN_ROW_TITLE]) && $defaultStore && empty($rowData[self::COLUMN_STORE])) { $valueData = [ 'option_type_id' => $optionTypeId, 'sort_order' => empty($rowData[self::COLUMN_ROW_SORT]) ? 0 : abs($rowData[self::COLUMN_ROW_SORT]), 'sku' => !empty($rowData[self::COLUMN_ROW_SKU]) ? $rowData[self::COLUMN_ROW_SKU] : '', ]; - - $priceData = false; - if (!empty($rowData[self::COLUMN_ROW_PRICE])) { - $priceData = [ - 'price' => (double)rtrim($rowData[self::COLUMN_ROW_PRICE], '%'), - 'price_type' => 'fixed', - ]; - if ('%' == substr($rowData[self::COLUMN_ROW_PRICE], -1)) { - $priceData['price_type'] = 'percent'; - } - } - return ['value' => $valueData, 'title' => $rowData[self::COLUMN_ROW_TITLE], 'price' => $priceData]; + $data['value'] = $valueData; + $data['title'] = $rowData[self::COLUMN_ROW_TITLE]; + $data['price'] = $priceData; } elseif (!empty($rowData[self::COLUMN_ROW_TITLE]) && !$defaultStore && !empty($rowData[self::COLUMN_STORE])) { - return ['title' => $rowData[self::COLUMN_ROW_TITLE]]; + if ($priceData) { + $data['price'] = $priceData; + } + $data['title'] = $rowData[self::COLUMN_ROW_TITLE]; } - return false; + + return $data ?: false; } /** @@ -1761,7 +1854,9 @@ protected function _saveTitles(array $titles) { $titleRows = []; foreach ($titles as $optionId => $storeInfo) { - foreach ($storeInfo as $storeId => $title) { + //for use default + $uniqStoreInfo = array_unique($storeInfo); + foreach ($uniqStoreInfo as $storeId => $title) { $titleRows[] = ['option_id' => $optionId, 'store_id' => $storeId, 'title' => $title]; } } @@ -1785,11 +1880,19 @@ protected function _saveTitles(array $titles) protected function _savePrices(array $prices) { if ($prices) { - $this->_connection->insertOnDuplicate( - $this->_tables['catalog_product_option_price'], - $prices, - ['price', 'price_type'] - ); + $optionPriceRows = []; + foreach ($prices as $storesData) { + foreach ($storesData as $row) { + $optionPriceRows[] = $row; + } + } + if ($optionPriceRows) { + $this->_connection->insertOnDuplicate( + $this->_tables['catalog_product_option_price'], + $optionPriceRows, + ['price', 'price_type'] + ); + } } return $this; @@ -1856,7 +1959,9 @@ protected function _saveSpecificTypeTitles(array $typeTitles) { $optionTypeTitleRows = []; foreach ($typeTitles as $optionTypeId => $storesData) { - foreach ($storesData as $storeId => $title) { + //for use default + $uniqStoresData = array_unique($storesData); + foreach ($uniqStoresData as $storeId => $title) { $optionTypeTitleRows[] = [ 'option_type_id' => $optionTypeId, 'store_id' => $storeId, @@ -1982,4 +2087,44 @@ private function getProductIdentifierField() } return $this->productEntityIdentifierField; } + + /** + * Save prepared custom options + * + * @param array $products + * @param array $options + * @param array $titles + * @param array $prices + * @param array $types + * + * @return void + */ + private function savePreparedCustomOptions( + array $products, + array $options, + array $titles, + array $prices, + array $types + ) { + if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); + $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); + } + + $this->_saveOptions( + $options + )->_saveTitles( + $titles + )->_savePrices( + $prices + )->_saveSpecificTypeValues( + $types['values'] + )->_saveSpecificTypePrices( + $types['prices'] + )->_saveSpecificTypeTitles( + $types['titles'] + )->_updateProducts( + $products + ); + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php index 17f7fae28ba75..49ffdebe7724d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php @@ -85,6 +85,8 @@ interface RowValidatorInterface extends \Magento\Framework\Validator\ValidatorIn const ERROR_DUPLICATE_URL_KEY = 'duplicatedUrlKey'; + const ERROR_NEW_TO_DATE = 'invalidNewToDateValue'; + /** * Value that means all entities (e.g. websites, groups etc.) */ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php index addd1523f87a0..ea6049ba651a5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php @@ -142,7 +142,7 @@ public function getNewSku($sku = null) { if ($sku !== null) { $sku = strtolower($sku); - return isset($this->newSkus[$sku]) ? $this->newSkus[$sku] : null; + return $this->newSkus[$sku] ?? null; } return $this->newSkus; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php index be5644e22b05b..3dff0188a7dbb 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php @@ -75,7 +75,7 @@ public function getWebsiteCodeToId($code = null) $this->_initWebsites(); } if ($code) { - return isset($this->websiteCodeToId[$code]) ? $this->websiteCodeToId[$code] : null; + return $this->websiteCodeToId[$code] ?? null; } return $this->websiteCodeToId; } @@ -90,7 +90,7 @@ public function getWebsiteCodeToStoreIds($code = null) $this->_initWebsites(); } if ($code) { - return isset($this->websiteCodeToStoreIds[$code]) ? $this->websiteCodeToStoreIds[$code] : null; + return $this->websiteCodeToStoreIds[$code] ?? null; } return $this->websiteCodeToStoreIds; } @@ -119,7 +119,7 @@ public function getStoreCodeToId($code = null) $this->_initStores(); } if ($code) { - return isset($this->storeCodeToId[$code]) ? $this->storeCodeToId[$code] : null; + return $this->storeCodeToId[$code] ?? null; } return $this->storeCodeToId; } @@ -134,7 +134,7 @@ public function getStoreIdToWebsiteStoreIds($code = null) $this->_initStores(); } if ($code) { - return isset($this->storeIdToWebsiteStoreIds[$code]) ? $this->storeIdToWebsiteStoreIds[$code] : null; + return $this->storeIdToWebsiteStoreIds[$code] ?? null; } return $this->storeIdToWebsiteStoreIds; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 5681b1aa6607d..fd6212bb636a5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -28,6 +28,13 @@ abstract class AbstractType */ public static $commonAttributesCache = []; + /** + * Maintain a list of invisible attributes + * + * @var array + */ + public static $invAttributesCache = []; + /** * Attribute Code to Id cache * @@ -188,6 +195,8 @@ public function __construct( } /** + * Initialize template for error message. + * * @param array $templateCollection * @return $this */ @@ -278,7 +287,14 @@ protected function _initAttributes() } } foreach ($absentKeys as $attributeSetName => $attributeIds) { - $this->attachAttributesById($attributeSetName, $attributeIds); + $unknownAttributeIds = array_diff( + $attributeIds, + array_keys(self::$commonAttributesCache), + self::$invAttributesCache + ); + if ($unknownAttributeIds || $this->_forcedAttributesCodes) { + $this->attachAttributesById($attributeSetName, $attributeIds); + } } foreach ($entityAttributes as $attributeRow) { if (isset(self::$commonAttributesCache[$attributeRow['attribute_id']])) { @@ -303,37 +319,45 @@ protected function _initAttributes() protected function attachAttributesById($attributeSetName, $attributeIds) { foreach ($this->_prodAttrColFac->create()->addFieldToFilter( - 'main_table.attribute_id', - ['in' => $attributeIds] + ['main_table.attribute_id', 'main_table.attribute_code'], + [ + ['in' => $attributeIds], + ['in' => $this->_forcedAttributesCodes] + ] ) as $attribute) { $attributeCode = $attribute->getAttributeCode(); $attributeId = $attribute->getId(); if ($attribute->getIsVisible() || in_array($attributeCode, $this->_forcedAttributesCodes)) { - self::$commonAttributesCache[$attributeId] = [ - 'id' => $attributeId, - 'code' => $attributeCode, - 'is_global' => $attribute->getIsGlobal(), - 'is_required' => $attribute->getIsRequired(), - 'is_unique' => $attribute->getIsUnique(), - 'frontend_label' => $attribute->getFrontendLabel(), - 'is_static' => $attribute->isStatic(), - 'apply_to' => $attribute->getApplyTo(), - 'type' => \Magento\ImportExport\Model\Import::getAttributeType($attribute), - 'default_value' => strlen( - $attribute->getDefaultValue() - ) ? $attribute->getDefaultValue() : null, - 'options' => $this->_entityModel->getAttributeOptions( - $attribute, - $this->_indexValueAttributes - ), - ]; + if (!isset(self::$commonAttributesCache[$attributeId])) { + self::$commonAttributesCache[$attributeId] = [ + 'id' => $attributeId, + 'code' => $attributeCode, + 'is_global' => $attribute->getIsGlobal(), + 'is_required' => $attribute->getIsRequired(), + 'is_unique' => $attribute->getIsUnique(), + 'frontend_label' => $attribute->getFrontendLabel(), + 'is_static' => $attribute->isStatic(), + 'apply_to' => $attribute->getApplyTo(), + 'type' => \Magento\ImportExport\Model\Import::getAttributeType($attribute), + 'default_value' => strlen( + $attribute->getDefaultValue() + ) ? $attribute->getDefaultValue() : null, + 'options' => $this->_entityModel->getAttributeOptions( + $attribute, + $this->_indexValueAttributes + ), + ]; + } + self::$attributeCodeToId[$attributeCode] = $attributeId; $this->_addAttributeParams( $attributeSetName, self::$commonAttributesCache[$attributeId], $attribute ); + } else { + self::$invAttributesCache[] = $attributeId; } } } @@ -355,6 +379,8 @@ public function retrieveAttributeFromCache($attributeCode) } /** + * Adding attribute option. + * * In case we've dynamically added new attribute option during import we need to add it to our cache * in order to keep it up to date. * @@ -486,8 +512,10 @@ public function isSuitable() } /** - * Prepare attributes values for save: exclude non-existent, static or with empty values attributes; - * set default values if needed + * Adding default attribute to product before save. + * + * Prepare attributes values for save: exclude non-existent, static or with empty values attributes, + * set default values if needed. * * @param array $rowData * @param bool $withDefaultValue @@ -503,7 +531,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe if ($attrParams['is_static']) { continue; } - if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { + if (isset($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))) { if (in_array($attrParams['type'], ['select', 'boolean'])) { $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; } elseif ('multiselect' == $attrParams['type']) { @@ -515,9 +543,9 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe } else { $resultAttrs[$attrCode] = $rowData[$attrCode]; } - } elseif (array_key_exists($attrCode, $rowData)) { + } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $rowData[$attrCode]; - } elseif ($withDefaultValue && null !== $attrParams['default_value']) { + } elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $attrParams['default_value']; } } @@ -534,7 +562,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe public function clearEmptyData(array $rowData) { foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) { - if (!$attrParams['is_static'] && empty($rowData[$attrCode])) { + if (!$attrParams['is_static'] && !isset($rowData[$attrCode])) { unset($rowData[$attrCode]); } } @@ -583,7 +611,8 @@ protected function getProductEntityLinkField() } /** - * Clean cached values + * Clean cached values. + * * @since 100.2.0 */ public function __destruct() diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 60bfdd56a718e..793ad935363d3 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\Framework\Validator\AbstractValidator; +use Magento\Catalog\Model\Product\Attribute\Backend\Sku; /** * Class Validator @@ -69,6 +70,8 @@ protected function textValidation($attrCode, $type) $val = $this->string->cleanString($this->_rowData[$attrCode]); if ($type == 'text') { $valid = $this->string->strlen($val) < Product::DB_MAX_TEXT_LENGTH; + } else if ($attrCode == Product::COL_SKU) { + $valid = $this->string->strlen($val) <= SKU::SKU_MAX_LENGTH; } else { $valid = $this->string->strlen($val) < Product::DB_MAX_VARCHAR_LENGTH; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php new file mode 100644 index 0000000000000..cd73fac89855a --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\Product\Validator; + +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; + +/** + * Validates layout and custom layout update fields + */ +class LayoutUpdate extends AbstractImportValidator +{ + /** + * @var ValidatorFactory + */ + private $layoutValidatorFactory; + + /** + * @var ValidationStateInterface + */ + private $validationState; + + /** + * @param ValidatorFactory $layoutValidatorFactory + * @param ValidationStateInterface $validationState + */ + public function __construct( + ValidatorFactory $layoutValidatorFactory, + ValidationStateInterface $validationState + ) { + $this->layoutValidatorFactory = $layoutValidatorFactory; + $this->validationState = $validationState; + } + + /** + * @inheritdoc + */ + public function isValid($value): bool + { + if (!empty($value['custom_layout_update']) && !$this->validateXml($value['custom_layout_update'])) { + $this->_addMessages( + [ + $this->context->retrieveMessageTemplate('invalidLayoutUpdate') + ] + ); + return false; + } + + return true; + } + + /** + * Validate XML layout update + * + * @param string $xml + * @return bool + */ + private function validateXml(string $xml): bool + { + /** @var $layoutXmlValidator \Magento\Framework\View\Model\Layout\Update\Validator */ + $layoutXmlValidator = $this->layoutValidatorFactory->create( + [ + 'validationState' => $this->validationState, + ] + ); + + try { + if (!$layoutXmlValidator->isValid($xml)) { + return false; + } + } catch (\Exception $e) { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php new file mode 100644 index 0000000000000..07d9be4e0a919 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\Product\Validator; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; + +/** + * Validator to assert that the current user is allowed to make design updates if a layout is provided in the import + */ +class LayoutUpdatePermissions extends AbstractImportValidator +{ + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var array + */ + private $allowedUserTypes = [ + UserContextInterface::USER_TYPE_ADMIN, + UserContextInterface::USER_TYPE_INTEGRATION + ]; + + /** + * @param UserContextInterface $userContext + * @param AuthorizationInterface $authorization + */ + public function __construct( + UserContextInterface $userContext, + AuthorizationInterface $authorization + ) { + $this->userContext = $userContext; + $this->authorization = $authorization; + } + + /** + * Validate that the current user is allowed to make design updates + * + * @param array $data + * @return boolean + */ + public function isValid($data): bool + { + if (empty($data['custom_layout_update'])) { + return true; + } + + $userType = $this->userContext->getUserType(); + $isValid = in_array($userType, $this->allowedUserTypes) + && $this->authorization->isAllowed('Magento_Catalog::edit_product_design'); + + if (!$isValid) { + $this->_addMessages( + [ + $this->context->retrieveMessageTemplate('insufficientPermissions'), + ] + ); + } + + return $isValid; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php index d1fe1eee80e19..e6d6136498a62 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php @@ -17,7 +17,7 @@ class Media extends AbstractImportValidator implements RowValidatorInterface */ const URL_REGEXP = '|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i'; - const PATH_REGEXP = '#^(?!.*[\\/]\.{2}[\\/])(?!\.{2}[\\/])[-\w.\\/]+$#'; + const PATH_REGEXP = '#^(?!.*[\\/]\.{2}[\\/])(?!\.{2}[\\/])[-\w.\\/()]+$#'; const ADDITIONAL_IMAGES = 'additional_images'; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 6ae553b028a98..d2e79c7575375 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -6,17 +6,20 @@ namespace Magento\CatalogImportExport\Model\Import; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\App\ObjectManager; /** * Import entity product model * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Uploader extends \Magento\MediaStorage\Model\File\Uploader { - /** * HTTP scheme * used to compare against the filename and select the proper DriverPool adapter @@ -31,6 +34,13 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ protected $_tmpDir = ''; + /** + * Download directory for url-based resources. + * + * @var string + */ + private $downloadDir; + /** * Destination directory. * @@ -94,6 +104,28 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ protected $_coreFileStorage; + /** + * @var \Magento\Framework\Filesystem + */ + private $filesystem; + + /** + * Instance of random data generator. + * + * @var \Magento\Framework\Math\Random + */ + private $random; + + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver + */ + private $directoryResolver; + + /** + * @var \Magento\Framework\Filesystem\Directory\ReadFactory + */ + private $directoryReadFactory; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage @@ -101,8 +133,11 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory - * @param null $filePath - * @throws \Magento\Framework\Exception\LocalizedException + * @param string|null $filePath + * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver + * @param \Magento\Framework\Math\Random|null $random + * @param \Magento\Framework\Filesystem\Directory\ReadFactory|null $directoryReadFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb, @@ -111,7 +146,10 @@ public function __construct( \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator, \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Filesystem\File\ReadFactory $readFactory, - $filePath = null + $filePath = null, + \Magento\Framework\App\Filesystem\DirectoryResolver $directoryResolver = null, + \Magento\Framework\Math\Random $random = null, + \Magento\Framework\Filesystem\Directory\ReadFactory $directoryReadFactory = null ) { if ($filePath !== null) { $this->_setUploadFile($filePath); @@ -122,6 +160,14 @@ public function __construct( $this->_validator = $validator; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $this->_readFactory = $readFactory; + $this->filesystem = $filesystem; + $this->directoryResolver = $directoryResolver + ?: ObjectManager::getInstance()->get(\Magento\Framework\App\Filesystem\DirectoryResolver::class); + $this->random = $random + ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); + $this->directoryReadFactory = $directoryReadFactory + ?: ObjectManager::getInstance()->get(\Magento\Framework\Filesystem\Directory\ReadFactory::class); + $this->downloadDir = DirectoryList::getDefaultConfig()[DirectoryList::TMP][DirectoryList::PATH]; } /** @@ -146,37 +192,62 @@ public function init() * @param string $fileName * @param bool $renameFileOff * @return array + * @throws LocalizedException */ public function move($fileName, $renameFileOff = false) { - if ($renameFileOff) { - $this->setAllowRenameFiles(false); - } + $this->setAllowRenameFiles(!$renameFileOff); + if (preg_match('/\bhttps?:\/\//i', $fileName, $matches)) { $url = str_replace($matches[0], '', $fileName); - - if ($matches[0] === $this->httpScheme) { - $read = $this->_readFactory->create($url, DriverPool::HTTP); - } else { - $read = $this->_readFactory->create($url, DriverPool::HTTPS); - } - - $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); - $this->_directory->writeFile( - $this->_directory->getRelativePath($this->getTmpDir() . '/' . $fileName), - $read->readAll() - ); + $driver = ($matches[0] === $this->httpScheme) ? DriverPool::HTTP : DriverPool::HTTPS; + $tmpFilePath = $this->downloadFileFromUrl($url, $driver); + } else { + $tmpDir = $this->getTmpDir() ? ($this->getTmpDir() . '/') : ''; + $tmpFilePath = $this->_directory->getRelativePath($tmpDir . $fileName); } - $filePath = $this->_directory->getRelativePath($this->getTmpDir() . '/' . $fileName); - $this->_setUploadFile($filePath); + $this->_setUploadFile($tmpFilePath); $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); unset($result['path']); $result['name'] = self::getCorrectFileName($result['name']); + return $result; } + /** + * Writes a url-based file to the temp directory. + * + * @param string $url + * @param string $driver + * @return string + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function downloadFileFromUrl($url, $driver) + { + $parsedUrlPath = parse_url($url, PHP_URL_PATH); + if (!$parsedUrlPath) { + throw new \Magento\Framework\Exception\LocalizedException(__('Could not parse resource url.')); + } + $urlPathValues = explode('/', $parsedUrlPath); + $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', end($urlPathValues)); + $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); + if ($fileExtension && !$this->checkAllowedExtension($fileExtension)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); + } + $tmpFileName = str_replace(".$fileExtension", '', $fileName); + $tmpFileName .= '_' . $this->random->getRandomString(16); + $tmpFileName .= $fileExtension ? ".$fileExtension" : ''; + $tmpFilePath = $this->_directory->getRelativePath($this->downloadDir . '/' . $tmpFileName); + $this->_directory->writeFile( + $tmpFilePath, + $this->_readFactory->create($url, $driver)->readAll() + ); + return $tmpFilePath; + } + /** * Prepare information about the file for moving * @@ -186,7 +257,20 @@ public function move($fileName, $renameFileOff = false) */ protected function _setUploadFile($filePath) { - if (!$this->_directory->isReadable($filePath)) { + try { + $fullPath = $this->_directory->getAbsolutePath($filePath); + if ($this->getTmpDir()) { + $tmpDir = $this->getDirectoryReadByPath( + $this->_directory->getAbsolutePath($this->getTmpDir()) + ); + } else { + $tmpDir = $this->_directory; + } + $readable = $tmpDir->isReadable($fullPath); + } catch (ValidatorException $exception) { + $readable = false; + } + if (!$readable) { throw new \Magento\Framework\Exception\LocalizedException( __('File \'%1\' was not found or has read restriction.', $filePath) ); @@ -277,7 +361,10 @@ public function getTmpDir() */ public function setTmpDir($path) { - if (is_string($path) && $this->_directory->isReadable($path)) { + if (is_string($path) + && $this->_directory->isReadable($path) + && $this->directoryResolver->validatePath($this->_directory->getAbsolutePath($path), DirectoryList::ROOT) + ) { $this->_tmpDir = $path; return true; } @@ -338,4 +425,17 @@ protected function chmod($file) { return; } + + /** + * Create an instance of directory with read permissions by path. + * + * @param string $path + * @param string $driverCode + * + * @return ReadInterface + */ + private function getDirectoryReadByPath(string $path, string $driverCode = DriverPool::FILE): ReadInterface + { + return $this->directoryReadFactory->create($path, $driverCode); + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php index eaa08431e8952..d8a926f7cfe31 100644 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php +++ b/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin; class Import @@ -18,8 +16,9 @@ class Import /** * @param \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor */ - public function __construct(\Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor) - { + public function __construct( + \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor + ) { $this->_indexerCategoryProductProcessor = $indexerCategoryProductProcessor; } diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php index 64bcfac85e45f..0d0d4ea80530a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php +++ b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin; class Import @@ -18,8 +16,9 @@ class Import /** * @param \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor */ - public function __construct(\Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor) - { + public function __construct( + \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor + ) { $this->_indexerProductCategoryProcessor = $indexerProductCategoryProcessor; } diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogImportExport/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogImportExport/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 <insert your license name here>" 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/app/code/Magento/CatalogImportExport/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogImportExport/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/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 <insert your license name here>" 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/app/code/Magento/CatalogImportExport/Test/Mftf/README.md b/app/code/Magento/CatalogImportExport/Test/Mftf/README.md new file mode 100644 index 0000000000000..bdf321bbcd4bd --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Import Export Functional Tests + +The Functional Test Module for **Magento Catalog Import Export** module. diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php index bf47ec4f9da13..e56cb62e042bb 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php @@ -135,6 +135,9 @@ public function testGetCategoryById($categoriesCache, $expectedResult) $this->assertEquals($expectedResult, $actualResult); } + /** + * @return array + */ public function getCategoryByIdDataProvider() { return [ diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index e24b4e1948149..bd2fe896b8c0a 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -134,8 +134,28 @@ protected function setUp() ->expects($this->any()) ->method('addFieldToFilter') ->with( - 'main_table.attribute_id', - ['in' => ['attribute_id', 'boolean_attribute']] + ['main_table.attribute_id', 'main_table.attribute_code'], + [ + [ + 'in' => + [ + 'attribute_id', + 'boolean_attribute', + ], + ], + [ + 'in' => + [ + 'related_tgtr_position_behavior', + 'related_tgtr_position_limit', + 'upsell_tgtr_position_behavior', + 'upsell_tgtr_position_limit', + 'thumbnail_label', + 'small_image_label', + 'image_label', + ], + ], + ] ) ->willReturn([$attribute1, $attribute2]); @@ -269,6 +289,9 @@ public function testIsRowValidError() $this->assertFalse($this->simpleType->isRowValid($rowData, $rowNum)); } + /** + * @return array + */ public function addAttributeOptionDataProvider() { return [ diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index 6d16f3da9e703..5ec98c4776d63 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -79,8 +79,8 @@ class OptionTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm * @var array */ protected $_expectedPrices = [ - 2 => ['option_id' => 2, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 0], - 3 => ['option_id' => 3, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 2] + 0 => ['option_id' => 2, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 0], + 1 => ['option_id' => 3, 'store_id' => 0, 'price_type' => 'fixed', 'price' => 2] ]; /** diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php new file mode 100644 index 0000000000000..e018fc0cf5ccf --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Validator; + +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\Authorization\Model\UserContextInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions; +use Magento\Framework\AuthorizationInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Test validation for layout update permissions + */ +class LayoutUpdatePermissionsTest extends TestCase +{ + /** + * @var LayoutUpdatePermissions|MockObject + */ + private $validator; + + /** + * @var UserContextInterface|MockObject + */ + private $userContext; + + /** + * @var AuthorizationInterface|MockObject + */ + private $authorization; + + /** + * @var Product + */ + private $context; + + protected function setUp() + { + $this->userContext = $this->createMock(UserContextInterface::class); + $this->authorization = $this->createMock(AuthorizationInterface::class); + $this->context = $this->createMock(Product::class); + $this->context + ->method('retrieveMessageTemplate') + ->with('insufficientPermissions') + ->willReturn('oh no'); + $this->validator = new LayoutUpdatePermissions( + $this->userContext, + $this->authorization + ); + $this->validator->init($this->context); + } + + /** + * @param $value + * @param $userContext + * @param $isAllowed + * @param $isValid + * @dataProvider configurationsProvider + */ + public function testValidationConfiguration($value, $userContext, $isAllowed, $isValid) + { + $this->userContext + ->method('getUserType') + ->willReturn($userContext); + + $this->authorization + ->method('isAllowed') + ->with('Magento_Catalog::edit_product_design') + ->willReturn($isAllowed); + + $result = $this->validator->isValid(['custom_layout_update' => $value]); + $messages = $this->validator->getMessages(); + + self::assertSame($isValid, $result); + + if ($isValid) { + self::assertSame([], $messages); + } else { + self::assertSame(['oh no'], $messages); + } + } + + public function configurationsProvider() + { + return [ + ['', null, null, true], + [null, null, null, true], + ['foo', UserContextInterface::USER_TYPE_ADMIN, true, true], + ['foo', UserContextInterface::USER_TYPE_INTEGRATION, true, true], + ['foo', UserContextInterface::USER_TYPE_ADMIN, false, false], + ['foo', UserContextInterface::USER_TYPE_INTEGRATION, false, false], + ['foo', 'something', null, false], + ]; + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php new file mode 100644 index 0000000000000..d1e8b879f6a08 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Validator; + +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\View\Model\Layout\Update\Validator; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Test validation for layout update + */ +class LayoutUpdateTest extends TestCase +{ + /** + * @var LayoutUpdate|MockObject + */ + private $validator; + + /** + * @var Validator|MockObject + */ + private $layoutValidator; + + protected function setUp() + { + $validatorFactory = $this->createMock(ValidatorFactory::class); + $validationState = $this->createMock(ValidationStateInterface::class); + $this->layoutValidator = $this->createMock(Validator::class); + $validatorFactory->method('create') + ->with(['validationState' => $validationState]) + ->willReturn($this->layoutValidator); + + $this->validator = new LayoutUpdate( + $validatorFactory, + $validationState + ); + } + + public function testValidationIsSkippedWithDataNotPresent() + { + $this->layoutValidator + ->expects($this->never()) + ->method('isValid'); + + $result = $this->validator->isValid([]); + self::assertTrue($result); + } + + public function testValidationFailsProperly() + { + $this->layoutValidator + ->method('isValid') + ->with('foo') + ->willReturn(false); + + $contextMock = $this->createMock(Product::class); + $contextMock + ->method('retrieveMessageTemplate') + ->with('invalidLayoutUpdate') + ->willReturn('oh no'); + $this->validator->init($contextMock); + + $result = $this->validator->isValid(['custom_layout_update' => 'foo']); + $messages = $this->validator->getMessages(); + self::assertFalse($result); + self::assertSame(['oh no'], $messages); + } + + public function testInvalidDataException() + { + $this->layoutValidator + ->method('isValid') + ->willThrowException(new \Exception('foo')); + + $contextMock = $this->createMock(Product::class); + $contextMock + ->method('retrieveMessageTemplate') + ->with('invalidLayoutUpdate') + ->willReturn('oh no'); + $this->validator->init($contextMock); + + $result = $this->validator->isValid(['custom_layout_update' => 'foo']); + $messages = $this->validator->getMessages(); + self::assertFalse($result); + self::assertSame(['oh no'], $messages); + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php index 4e902024769f7..f1965e3063217 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/TierPriceTest.php @@ -48,6 +48,11 @@ protected function setUp() ); } + /** + * @param $groupId + * + * @return \Magento\CatalogImportExport\Model\Import\Product\Validator\TierPrice + */ protected function processInit($groupId) { $searchResult = $this->createMock(\Magento\Customer\Api\Data\GroupSearchResultsInterface::class); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 6d183fc8e6e20..3f72fcc39f548 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class ProductTest @@ -25,126 +29,126 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI const ENTITY_ID = 13; - /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\DB\Adapter\AdapterInterface| MockObject */ protected $_connection; - /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Json\Helper\Data| MockObject */ protected $jsonHelper; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data| MockObject */ protected $_dataSourceModel; - /** @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\App\ResourceConnection| MockObject */ protected $resource; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper| MockObject */ protected $_resourceHelper; - /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\StringUtils|MockObject */ protected $string; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Event\ManagerInterface|MockObject */ protected $_eventManager; - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|MockObject */ protected $stockRegistry; - /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|MockObject */ protected $optionFactory; - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject */ protected $stockConfiguration; - /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|MockObject */ protected $stockStateProvider; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ protected $optionEntity; - /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime|MockObject */ protected $dateTime; /** @var array */ protected $data; - /** @var \Magento\ImportExport\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Helper\Data|MockObject */ protected $importExportData; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|MockObject */ protected $importData; - /** @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\Config|MockObject */ protected $config; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper|MockObject */ protected $resourceHelper; - /** @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Helper\Data|MockObject */ protected $_catalogData; - /** @var \Magento\ImportExport\Model\Import\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\Import\Config|MockObject */ protected $_importConfig; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var MockObject */ protected $_resourceFactory; // @codingStandardsIgnoreStart - /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|MockObject */ protected $_setColFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|MockObject */ protected $_productTypeFactory; - /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|MockObject */ protected $_linkFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|MockObject */ protected $_proxyProdFactory; - /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|MockObject */ protected $_uploaderFactory; - /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem|MockObject */ protected $_filesystem; - /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|MockObject */ protected $_mediaDirectory; - /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|MockObject */ protected $_stockResItemFac; - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ protected $_localeDate; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Indexer\IndexerRegistry|MockObject */ protected $indexerRegistry; - /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Psr\Log\LoggerInterface|MockObject */ protected $_logger; - /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|MockObject */ protected $storeResolver; - /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|MockObject */ protected $skuProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|MockObject */ protected $categoryProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ protected $validator; - /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|MockObject */ protected $objectRelationProcessor; - /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|MockObject */ protected $transactionManager; - /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|MockObject */ // @codingStandardsIgnoreEnd protected $taxClassProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product */ + /** @var Product */ protected $importProduct; /** @@ -152,10 +156,10 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI */ protected $errorAggregator; - /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|MockObject */ protected $scopeConfig; - /** @var \Magento\Catalog\Model\Product\Url|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Catalog\Model\Product\Url|MockObject */ protected $productUrl; /** @@ -334,7 +338,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->importProduct = $objectManager->getObject( - \Magento\CatalogImportExport\Model\Import\Product::class, + Product::class, [ 'jsonHelper' => $this->jsonHelper, 'importExportData' => $this->importExportData, @@ -375,7 +379,7 @@ protected function setUp() 'data' => $this->data ] ); - $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product::class); + $reflection = new \ReflectionClass(Product::class); $reflectionProperty = $reflection->getProperty('metadataPool'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->importProduct, $metadataPoolMock); @@ -584,7 +588,7 @@ public function testGetMultipleValueSeparatorFromParameters() public function testDeleteProductsForReplacement() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods([ 'setParameters', '_deleteProducts' @@ -650,7 +654,7 @@ public function testValidateRowIsAlreadyValidated() */ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour = Import::BEHAVIOR_DELETE) { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) ->getMock(); @@ -662,7 +666,7 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour ->method('getErrorAggregator') ->willReturn($this->getErrorAggregatorObject()); $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); - $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; + $skuKey = Product::COL_SKU; $rowData = [ $skuKey => 'sku', ]; @@ -674,18 +678,22 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour public function testValidateRowDeleteBehaviourAddRowErrorCall() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope', 'addRowError']) + ->setMethods(['getBehavior', 'getRowScope', 'addRowError', 'getErrorAggregator']) ->getMock(); $importProduct->expects($this->exactly(2))->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); $importProduct->expects($this->once())->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); + ->willReturn(Product::SCOPE_DEFAULT); $importProduct->expects($this->once())->method('addRowError'); + $importProduct->method('getErrorAggregator') + ->willReturn( + $this->getErrorAggregatorObject(['addRowToSkip']) + ); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $importProduct->validateRow($rowData, 0); @@ -696,7 +704,7 @@ public function testValidateRowValidatorCheck() $messages = ['validator message']; $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $rowNum = 0; $this->importProduct->validateRow($rowData, $rowNum); @@ -790,12 +798,15 @@ public function testGetCategoryProcessor() $this->assertEquals($expectedResult, $actualResult); } + /** + * @return array + */ public function getStoreIdByCodeDataProvider() { return [ [ '$storeCode' => null, - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$storeCode' => 'value', @@ -810,17 +821,17 @@ public function getStoreIdByCodeDataProvider() public function testValidateRowCheckSpecifiedSku($sku, $expectedError) { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity', 'getRowScope'], + ['addRowError', 'getOptionEntity', 'getRowScope'], ['isRowInvalid' => true] ); $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_STORE => '', + Product::COL_SKU => $sku, + Product::COL_STORE => '', ]; - $this->storeResolver->expects($this->any())->method('getStoreCodeToId')->willReturn(null); + $this->storeResolver->method('getStoreCodeToId')->willReturn(null); $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); @@ -829,7 +840,7 @@ public function testValidateRowCheckSpecifiedSku($sku, $expectedError) $importProduct ->expects($this->once()) ->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE); + ->willReturn(Product::SCOPE_STORE); $importProduct->expects($this->at(1))->method('addRowError')->with($expectedError, $rowNum)->willReturn(null); $importProduct->validateRow($rowData, $rowNum); @@ -843,7 +854,7 @@ public function testValidateRowProcessEntityIncrement() $errorAggregator->method('isRowInvalid')->willReturn(true); $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); - $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; + $rowData = [Product::COL_SKU => false]; //suppress validator $this->_setValidatorMockInImportProduct($this->importProduct); $this->importProduct->validateRow($rowData, $rowNum); @@ -853,14 +864,14 @@ public function testValidateRowProcessEntityIncrement() public function testValidateRowValidateExistingProductTypeAddNewSku() { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity'], + ['addRowError', 'getOptionEntity'], ['isRowInvalid' => true] ); $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -883,7 +894,7 @@ public function testValidateRowValidateExistingProductTypeAddNewSku() $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $expectedData = [ - 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val + 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val 'type_id' => $oldSku[$sku]['type_id'],// type_id_val 'attr_set_id' => $oldSku[$sku]['attr_set_id'], //attr_set_id_val 'attr_set_code' => $_attrSetIdToName[$oldSku[$sku]['attr_set_id']],//attr_set_id_val @@ -901,7 +912,7 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -926,6 +937,11 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() /** * @dataProvider validateRowValidateNewProductTypeAddRowErrorCallDataProvider + * @param string $colType + * @param string $productTypeModelsColType + * @param string $colAttrSet + * @param string $attrSetNameToIdColAttrSet + * @param string $error */ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $colType, @@ -937,15 +953,15 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => $colType, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $colAttrSet, + Product::COL_SKU => $sku, + Product::COL_TYPE => $colType, + Product::COL_ATTR_SET => $colAttrSet, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, + $rowData[Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => $productTypeModelsColType, + $rowData[Product::COL_TYPE] => $productTypeModelsColType, ]; $oldSku = [ $sku => null, @@ -973,29 +989,25 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', + Product::COL_SKU => $sku, + Product::COL_TYPE => 'value', + Product::COL_ATTR_SET => 'value', ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', + $rowData[Product::COL_TYPE] => 'value', ]; $oldSku = [ $sku => null, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' + $rowData[Product::COL_ATTR_SET] => 'attr_set_code_val', ]; $expectedData = [ 'entity_id' => null, - 'type_id' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE],//value + 'type_id' => $rowData[Product::COL_TYPE], //attr_set_id_val - 'attr_set_id' => $_attrSetNameToId[ - $rowData[ - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET - ] - ], - 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value + 'attr_set_id' => $_attrSetNameToId[$rowData[Product::COL_ATTR_SET]], + 'attr_set_code' => $rowData[Product::COL_ATTR_SET], 'row_id' => null ]; $importProduct = $this->createModelMockWithErrorAggregator( @@ -1031,8 +1043,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $expectedAttrSetCode = 'new_attr_set_code'; $newSku = [ @@ -1040,8 +1052,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() 'type_id' => 'new_type_id_val', ]; $expectedRowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $newSku['attr_set_code'], + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => $newSku['attr_set_code'], ]; $oldSku = [ $sku => [ @@ -1075,8 +1087,8 @@ public function testValidateValidateOptionEntity() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $oldSku = [ $sku => [ @@ -1239,6 +1251,9 @@ public function uploadMediaFilesDataProvider() ]; } + /** + * @return array + */ public function getImagesFromRowDataProvider() { return [ @@ -1265,6 +1280,9 @@ public function getImagesFromRowDataProvider() ]; } + /** + * @return array + */ public function validateRowValidateNewProductTypeAddRowErrorCallDataProvider() { return [ @@ -1299,6 +1317,9 @@ public function validateRowValidateNewProductTypeAddRowErrorCallDataProvider() ]; } + /** + * @return array + */ public function validateRowCheckSpecifiedSkuDataProvider() { return [ @@ -1317,11 +1338,14 @@ public function validateRowCheckSpecifiedSkuDataProvider() ]; } + /** + * @return array + */ public function validateRowDataProvider() { return [ [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, ], @@ -1336,12 +1360,12 @@ public function validateRowDataProvider() '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => true, '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, '$behaviour' => Import::BEHAVIOR_REPLACE @@ -1362,7 +1386,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH - 1 + Product::DB_MAX_VARCHAR_LENGTH - 1 ), ], ], @@ -1415,7 +1439,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH - 1 + Product::DB_MAX_TEXT_LENGTH - 1 ), ], ], @@ -1435,7 +1459,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 + Product::DB_MAX_VARCHAR_LENGTH + 1 ), ], ], @@ -1488,7 +1512,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH + 1 + Product::DB_MAX_TEXT_LENGTH + 1 ), ], ], @@ -1500,8 +1524,8 @@ public function isAttributeValidAssertAttrInvalidDataProvider() */ public function getRowScopeDataProvider() { - $colSku = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; - $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; + $colSku = Product::COL_SKU; + $colStore = Product::COL_STORE; return [ [ @@ -1509,21 +1533,21 @@ public function getRowScopeDataProvider() $colSku => null, $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE, ], [ '$rowData' => [ $colSku => 'sku', $colStore => null, ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$rowData' => [ $colSku => 'sku', $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE, ], ]; } @@ -1600,9 +1624,9 @@ protected function overrideMethod(&$object, $methodName, array $parameters = []) * * @see _rewriteGetOptionEntityInImportProduct() * @see _setValidatorMockInImportProduct() - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) { @@ -1618,8 +1642,8 @@ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) * Used in group of validateRow method's tests. * Set validator mock in importProduct, return true for isValid method. * - * @param \Magento\CatalogImportExport\Model\Import\Product - * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject + * @param Product + * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ private function _setValidatorMockInImportProduct($importProduct) { @@ -1633,9 +1657,9 @@ private function _setValidatorMockInImportProduct($importProduct) * Used in group of validateRow method's tests. * Make getOptionEntity return option mock. * - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _rewriteGetOptionEntityInImportProduct($importProduct) { @@ -1650,12 +1674,12 @@ private function _rewriteGetOptionEntityInImportProduct($importProduct) /** * @param array $methods * @param array $errorAggregatorMethods - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) { $methods[] = 'getErrorAggregator'; - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods($methods) ->getMock(); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index e28c2e1f3c01d..7aec018d753bb 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -6,6 +6,9 @@ */ namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class UploaderTest extends \PHPUnit\Framework\TestCase { /** @@ -39,15 +42,28 @@ class UploaderTest extends \PHPUnit\Framework\TestCase protected $readFactory; /** - * @var \Magento\Framework\Filesystem\Directory\Writer| \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Filesystem\Directory\Writer|\PHPUnit_Framework_MockObject_MockObject */ protected $directoryMock; + /** + * @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject + */ + private $random; + + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $directoryResolver; + /** * @var \Magento\CatalogImportExport\Model\Import\Uploader|\PHPUnit_Framework_MockObject_MockObject */ protected $uploader; + /** + * @inheritdoc + */ protected function setUp() { $this->coreFileStorageDb = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) @@ -71,8 +87,8 @@ protected function setUp() ->setMethods(['create']) ->getMock(); - $this->directoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Writer::class) - ->setMethods(['writeFile', 'getRelativePath', 'isWritable', 'getAbsolutePath']) + $this->directoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Write::class) + ->setMethods(['writeFile', 'getRelativePath', 'isWritable', 'isReadable', 'getAbsolutePath']) ->disableOriginalConstructor() ->getMock(); @@ -80,10 +96,21 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getDirectoryWrite']) ->getMock(); + + $this->directoryResolver = $this->getMockBuilder(\Magento\Framework\App\Filesystem\DirectoryResolver::class) + ->disableOriginalConstructor() + ->setMethods(['validatePath']) + ->getMock(); + $this->filesystem->expects($this->any()) ->method('getDirectoryWrite') ->will($this->returnValue($this->directoryMock)); + $this->random = $this->getMockBuilder(\Magento\Framework\Math\Random::class) + ->disableOriginalConstructor() + ->setMethods(['getRandomString']) + ->getMock(); + $this->uploader = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Uploader::class) ->setConstructorArgs([ $this->coreFileStorageDb, @@ -92,45 +119,81 @@ protected function setUp() $this->validator, $this->filesystem, $this->readFactory, + null, + $this->directoryResolver, + $this->random ]) - ->setMethods(['_setUploadFile', 'save', 'getTmpDir']) + ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) ->getMock(); } /** * @dataProvider moveFileUrlDataProvider + * @param string $fileUrl + * @param string $expectedHost + * @param string $expectedFileName + * @param int $checkAllowedExtension + * @throws \Magento\Framework\Exception\LocalizedException + * @return void */ - public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName) - { + public function testMoveFileUrl( + string $fileUrl, + string $expectedHost, + string $expectedFileName, + int $checkAllowedExtension + ) { + $tmpDir = 'var/tmp'; $destDir = 'var/dest/dir'; - $expectedRelativeFilePath = $this->uploader->getTmpDir() . '/' . $expectedFileName; - $this->directoryMock->expects($this->once())->method('isWritable')->with($destDir)->willReturn(true); - $this->directoryMock->expects($this->any())->method('getRelativePath')->with($expectedRelativeFilePath); - $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($destDir) - ->willReturn($destDir . '/' . $expectedFileName); - // Check writeFile() method invoking. - $this->directoryMock->expects($this->any())->method('writeFile')->will($this->returnValue($expectedFileName)); + + // Expected invocation to validate file extension + $this->uploader->expects($this->exactly($checkAllowedExtension))->method('checkAllowedExtension') + ->willReturn(true); + + // Expected invocation to generate random string for file name postfix + $this->random->expects($this->once())->method('getRandomString') + ->with(16) + ->willReturn('38GcEmPFKXXR8NMj'); + + // Expected invocation to build the temp file path with the correct directory and filename + $this->directoryMock->expects($this->any())->method('getRelativePath') + ->with($tmpDir . '/' . $expectedFileName); // Create adjusted reader which does not validate path. $readMock = $this->getMockBuilder(\Magento\Framework\Filesystem\File\Read::class) ->disableOriginalConstructor() ->setMethods(['readAll']) ->getMock(); - // Check readAll() method invoking. - $readMock->expects($this->once())->method('readAll')->will($this->returnValue(null)); - // Check create() method invoking with expected argument. - $this->readFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($readMock))->with($expectedHost); - //Check invoking of getTmpDir(), _setUploadFile(), save() methods. - $this->uploader->expects($this->any())->method('getTmpDir')->will($this->returnValue('')); - $this->uploader->expects($this->once())->method('_setUploadFile')->will($this->returnSelf()); - $this->uploader->expects($this->once())->method('save')->with($destDir . '/' . $expectedFileName) + // Expected invocations to create reader and read contents from url + $this->readFactory->expects($this->once())->method('create') + ->with($expectedHost) + ->will($this->returnValue($readMock)); + $readMock->expects($this->once())->method('readAll') + ->will($this->returnValue(null)); + + // Expected invocation to write the temp file + $this->directoryMock->expects($this->any())->method('writeFile') + ->will($this->returnValue($expectedFileName)); + + // Expected invocations to move the temp file to the destination directory + $this->directoryMock->expects($this->once())->method('isWritable') + ->with($destDir) + ->willReturn(true); + $this->directoryMock->expects($this->once())->method('getAbsolutePath') + ->with($destDir) + ->willReturn($destDir . '/' . $expectedFileName); + $this->uploader->expects($this->once())->method('_setUploadFile') + ->willReturnSelf(); + $this->uploader->expects($this->once())->method('save') + ->with($destDir . '/' . $expectedFileName) ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); + // Do not use configured temp directory + $this->uploader->expects($this->never())->method('getTmpDir'); + $this->uploader->setDestDir($destDir); $result = $this->uploader->move($fileUrl); + $this->assertEquals(['name' => $expectedFileName], $result); $this->assertArrayNotHasKey('path', $result); } @@ -139,14 +202,14 @@ public function testMoveFileName() { $destDir = 'var/dest/dir'; $fileName = 'test_uploader_file'; - $expectedRelativeFilePath = $this->uploader->getTmpDir() . '/' . $fileName; + $expectedRelativeFilePath = $fileName; $this->directoryMock->expects($this->once())->method('isWritable')->with($destDir)->willReturn(true); $this->directoryMock->expects($this->any())->method('getRelativePath')->with($expectedRelativeFilePath); $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($destDir) ->willReturn($destDir . '/' . $fileName); //Check invoking of getTmpDir(), _setUploadFile(), save() methods. - $this->uploader->expects($this->once())->method('getTmpDir')->will($this->returnValue('')); - $this->uploader->expects($this->once())->method('_setUploadFile')->will($this->returnSelf()); + $this->uploader->expects($this->once())->method('getTmpDir')->willReturn(''); + $this->uploader->expects($this->once())->method('_setUploadFile')->willReturnSelf(); $this->uploader->expects($this->once())->method('save')->with($destDir . '/' . $fileName) ->willReturn(['name' => $fileName]); @@ -193,6 +256,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive $this->assertNull($result); } + /** + * @return array + */ public function moveFileUrlDriverPoolDataProvider() { return [ @@ -211,19 +277,104 @@ public function moveFileUrlDriverPoolDataProvider() ]; } + /** + * @return array + */ public function moveFileUrlDataProvider() { return [ - [ - '$fileUrl' => 'http://test_uploader_file', + 'https_no_file_ext' => [ + '$fileUrl' => 'https://test_uploader_file', '$expectedHost' => 'test_uploader_file', - '$expectedFileName' => 'httptest_uploader_file', + '$expectedFileName' => 'test_uploader_file_38GcEmPFKXXR8NMj', + '$checkAllowedExtension' => 0 ], - [ - '$fileUrl' => 'https://!:^&`;file', - '$expectedHost' => '!:^&`;file', - '$expectedFileName' => 'httpsfile', + 'https_invalid_chars' => [ + '$fileUrl' => 'https://www.google.com/!:^&`;image.jpg', + '$expectedHost' => 'www.google.com/!:^&`;image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_invalid_chars_no_file_ext' => [ + '$fileUrl' => 'https://!:^&`;image', + '$expectedHost' => '!:^&`;image', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj', + '$checkAllowedExtension' => 0 + ], + 'http_jpg' => [ + '$fileUrl' => 'http://www.google.com/image.jpg', + '$expectedHost' => 'www.google.com/image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_jpg' => [ + '$fileUrl' => 'https://www.google.com/image.jpg', + '$expectedHost' => 'www.google.com/image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_jpeg' => [ + '$fileUrl' => 'https://www.google.com/image.jpeg', + '$expectedHost' => 'www.google.com/image.jpeg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpeg', + '$checkAllowedExtension' => 1 + ], + 'https_png' => [ + '$fileUrl' => 'https://www.google.com/image.png', + '$expectedHost' => 'www.google.com/image.png', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.png', + '$checkAllowedExtension' => 1 + ], + 'https_gif' => [ + '$fileUrl' => 'https://www.google.com/image.gif', + '$expectedHost' => 'www.google.com/image.gif', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.gif', + '$checkAllowedExtension' => 1 ], + 'https_one_query_param' => [ + '$fileUrl' => 'https://www.google.com/image.jpg?param=1', + '$expectedHost' => 'www.google.com/image.jpg?param=1', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_two_query_params' => [ + '$fileUrl' => 'https://www.google.com/image.jpg?param=1¶m=2', + '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ] + ]; + } + + /** + * @dataProvider validatePathDataProvider + * + * @param bool $pathIsValid + * @return void + */ + public function testSetTmpDir(bool $pathIsValid) + { + $path = 'path'; + $absolutePath = 'absolute_path'; + $this->directoryMock->expects($this->atLeastOnce())->method('isReadable')->with($path)->willReturn(true); + $this->directoryMock->expects($this->atLeastOnce())->method('getAbsolutePath')->with($path) + ->willReturn($absolutePath); + $this->directoryResolver->expects($this->atLeastOnce())->method('validatePath')->with($absolutePath, 'base') + ->willReturn($pathIsValid); + + $this->assertEquals($pathIsValid, $this->uploader->setTmpDir($path)); + } + + /** + * Data provider for the testSetTmpDir() + * + * @return array + */ + public function validatePathDataProvider(): array + { + return [ + [true], + [false], ]; } } diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index cd2cb8e26f1c2..937866bedded4 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-import-export", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-catalog-url-rewrite": "100.2.*", "magento/module-eav": "101.0.*", @@ -12,11 +12,12 @@ "magento/module-catalog-inventory": "100.2.*", "magento/module-media-storage": "100.2.*", "magento/module-customer": "101.0.*", + "magento/module-authorization": "100.2.*", "magento/framework": "101.0.*", "ext-ctype": "*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml index 53772c3b3360a..f51a8ce8bc956 100644 --- a/app/code/Magento/CatalogImportExport/etc/di.xml +++ b/app/code/Magento/CatalogImportExport/etc/di.xml @@ -24,7 +24,14 @@ <item name="website" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Website</item> <item name="weight" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Weight</item> <item name="quantity" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\Quantity</item> + <item name="layout_update" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate</item> + <item name="layout_update_permissions" xsi:type="object">Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions</item> </argument> </arguments> </type> + <type name="Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate"> + <arguments> + <argument name="validationState" xsi:type="object">Magento\Framework\Config\ValidationState\Required</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index 83defa64df250..c6d2a202018b5 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -14,6 +14,14 @@ */ interface StockStatusInterface extends ExtensibleDataInterface { + /**#@+ + * Stock Status values + */ + const STATUS_OUT_OF_STOCK = 0; + + const STATUS_IN_STOCK = 1; + /**#@-*/ + /**#@+ * Stock status object data keys */ diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php index c9b6abeb72e23..2d011e24f4105 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php @@ -72,7 +72,7 @@ public function getProductStockStatusBySku($productSku, $scopeId = null); * @param float $qty * @param int $currentPage * @param int $pageSize - * @return \Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface + * @return \Magento\CatalogInventory\Api\Data\StockItemCollectionInterface */ public function getLowStockItems($scopeId, $qty, $currentPage = 1, $pageSize = 0); diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php index dc992378d128b..f349e94235a9c 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php @@ -84,7 +84,7 @@ protected function _getCustomerGroups($groupId = null) $this->_customerGroups[$notLoggedInGroup->getId()] = $notLoggedInGroup->getCode(); } if ($groupId !== null) { - return isset($this->_customerGroups[$groupId]) ? $this->_customerGroups[$groupId] : null; + return $this->_customerGroups[$groupId] ?? null; } return $this->_customerGroups; } diff --git a/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php b/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php index cf47f39faac45..8355a96e3d0e8 100644 --- a/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php +++ b/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php @@ -39,8 +39,8 @@ public function afterGetQuantityValidators( $params = []; $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); - if ($stockItem->getQtyMaxAllowed()) { - $params['maxAllowed'] = $stockItem->getQtyMaxAllowed(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); } if ($stockItem->getQtyIncrements() > 0) { $params['qtyIncrements'] = (float)$stockItem->getQtyIncrements(); diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php index 568fa600ec52d..6614b418da920 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php @@ -131,7 +131,8 @@ public function getPlaceholderId() */ public function isMsgVisible() { - return $this->getStockQty() > 0 && $this->getStockQtyLeft() <= $this->getThresholdQty(); + return $this->getStockQty() > 0 && $this->getStockQtyLeft() > 0 + && $this->getStockQtyLeft() <= $this->getThresholdQty(); } /** diff --git a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php index b4f5d8b670fb2..96bf5bd965355 100644 --- a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php @@ -95,7 +95,7 @@ protected function serializeValue($value) } return $this->serializer->serialize($data); } else { - return ''; + return $value; } } diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index 410e35096ee58..99a83753e4379 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -156,7 +156,7 @@ public function addIsInStockFilterToCollection($collection) $resource = $this->getStockStatusResource(); $resource->addStockDataToCollection( $collection, - !$isShowOutOfStock || $collection->getFlag('require_stock_items') + !$isShowOutOfStock ); $collection->setFlag($stockFlag, true); } diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php index 06cfde0b7d247..ac1b764ea3738 100644 --- a/app/code/Magento/CatalogInventory/Model/Configuration.php +++ b/app/code/Magento/CatalogInventory/Model/Configuration.php @@ -264,7 +264,7 @@ public function getNotifyStockQty($store = null) */ public function getEnableQtyIncrements($store = null) { - return (bool) $this->scopeConfig->getValue( + return (bool)$this->scopeConfig->getValue( self::XML_PATH_ENABLE_QTY_INCREMENTS, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php new file mode 100644 index 0000000000000..96f943a6e3400 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model\Indexer; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Query\Generator; + +/** + * Class for filter product price index. + */ +class ProductPriceIndexFilter implements PriceModifierInterface +{ + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var Item + */ + private $stockItem; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var string + */ + private $connectionName; + + /** + * @var Generator + */ + private $batchQueryGenerator; + + /** + * @var int + */ + private $batchSize; + + /** + * @param StockConfigurationInterface $stockConfiguration + * @param Item $stockItem + * @param ResourceConnection $resourceConnection + * @param string $connectionName + * @param Generator $batchQueryGenerator + * @param int $batchSize + */ + public function __construct( + StockConfigurationInterface $stockConfiguration, + Item $stockItem, + ResourceConnection $resourceConnection = null, + $connectionName = 'indexer', + Generator $batchQueryGenerator = null, + $batchSize = 100 + ) { + $this->stockConfiguration = $stockConfiguration; + $this->stockItem = $stockItem; + $this->resourceConnection = $resourceConnection ?: ObjectManager::getInstance()->get(ResourceConnection::class); + $this->connectionName = $connectionName; + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(Generator::class); + $this->batchSize = $batchSize; + } + + /** + * Remove out of stock products data from price index. + * + * @param IndexTableStructure $priceTable + * @param array $entityIds + * @return void + * + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) + { + if ($this->stockConfiguration->isShowOutOfStock()) { + return; + } + + $connection = $this->resourceConnection->getConnection($this->connectionName); + $select = $connection->select(); + + $select->from( + ['stock_item' => $this->stockItem->getMainTable()], + ['stock_item.product_id', 'MAX(stock_item.is_in_stock) as max_is_in_stock'] + ); + + if ($this->stockConfiguration->getManageStock()) { + $select->where('stock_item.use_config_manage_stock = 1 OR stock_item.manage_stock = 1'); + } else { + $select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1'); + } + + $select->group('stock_item.product_id'); + $select->having('max_is_in_stock = 0'); + + $batchSelectIterator = $this->batchQueryGenerator->generate( + 'product_id', + $select, + $this->batchSize, + \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR + ); + + foreach ($batchSelectIterator as $select) { + $productIds = null; + foreach ($connection->query($select)->fetchAll() as $row) { + $productIds[] = $row['product_id']; + } + if ($productIds !== null) { + $where = [$priceTable->getEntityField() .' IN (?)' => $productIds]; + $connection->delete($priceTable->getTableName(), $where); + } + } + } +} diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index bc10d38173b4d..f5c26d4294927 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -6,12 +6,19 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement; +use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock; use Magento\Framework\App\ResourceConnection; use Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory; use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\EntityManager\MetadataPool; @@ -25,7 +32,6 @@ /** * Class Full reindex action * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Full extends AbstractAction @@ -60,6 +66,11 @@ class Full extends AbstractAction */ private $activeTableSwitcher; + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + /** * @param ResourceConnection $resource * @param StockFactory $indexerFactory @@ -71,7 +82,7 @@ class Full extends AbstractAction * @param BatchProviderInterface|null $batchProvider * @param array $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher - * + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,7 +95,8 @@ public function __construct( BatchSizeManagementInterface $batchSizeManagement = null, BatchProviderInterface $batchProvider = null, array $batchRowsCount = [], - ActiveTableSwitcher $activeTableSwitcher = null + ActiveTableSwitcher $activeTableSwitcher = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $resource, @@ -97,11 +109,12 @@ public function __construct( $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get(BatchProviderInterface::class); $this->batchSizeManagement = $batchSizeManagement ?: ObjectManager::getInstance()->get( - \Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement::class + BatchSizeManagement::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance() ->get(ActiveTableSwitcher::class); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -109,9 +122,7 @@ public function __construct( * * @param null|array $ids * @throws LocalizedException - * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($ids = null) @@ -120,11 +131,11 @@ public function execute($ids = null) $this->useIdxTable(false); $this->cleanIndexersTables($this->_getTypeIndexers()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys($this->_getConnection()->describeTable($this->_getIdxTable())); - /** @var \Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock $indexer */ + /** @var DefaultStock $indexer */ foreach ($this->_getTypeIndexers() as $indexer) { $indexer->setActionType(self::ACTION_TYPE); $connection = $indexer->getConnection(); @@ -135,22 +146,21 @@ public function execute($ids = null) : $this->batchRowsCount['default']; $this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $batchRowCount + $select, + $batchRowCount, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { + foreach ($batchQueries as $query) { $this->clearTemporaryIndexTable(); - // Get entity ids from batch - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $select->where('type_id = ?', $indexer->getTypeId()); - - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntity($entityIds); $select = $connection->select()->from($this->_getIdxTable(), $columns); @@ -167,6 +177,7 @@ public function execute($ids = null) /** * Delete all records from index table + * * Used to clean table before re-indexation * * @param array $indexers diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index a32faa4640a86..b3fa07479a712 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -10,8 +10,10 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\CatalogInventory\Model\Stock; use Magento\Catalog\Model\Product; @@ -46,25 +48,35 @@ class CacheCleaner */ private $connection; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param ResourceConnection $resource * @param StockConfigurationInterface $stockConfiguration * @param CacheContext $cacheContext * @param ManagerInterface $eventManager + * @param MetadataPool|null $metadataPool */ public function __construct( ResourceConnection $resource, StockConfigurationInterface $stockConfiguration, CacheContext $cacheContext, - ManagerInterface $eventManager + ManagerInterface $eventManager, + MetadataPool $metadataPool = null ) { $this->resource = $resource; $this->stockConfiguration = $stockConfiguration; $this->cacheContext = $cacheContext; $this->eventManager = $eventManager; + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** + * Clean cache by product ids. + * * @param array $productIds * @param callable $reindex * @return void @@ -76,22 +88,37 @@ public function clean(array $productIds, callable $reindex) $productStatusesAfter = $this->getProductStockStatuses($productIds); $productIds = $this->getProductIdsForCacheClean($productStatusesBefore, $productStatusesAfter); if ($productIds) { - $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); + $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); } } /** + * Get current stock statuses for product ids. + * * @param array $productIds * @return array */ private function getProductStockStatuses(array $productIds) { + $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); $select = $this->getConnection()->select() ->from( - $this->resource->getTableName('cataloginventory_stock_status'), + ['css' => $this->resource->getTableName('cataloginventory_stock_status')], ['product_id', 'stock_status', 'qty'] - )->where('product_id IN (?)', $productIds) + ) + ->joinLeft( + ['cpr' => $this->resource->getTableName('catalog_product_relation')], + 'css.product_id = cpr.child_id', + [] + ) + ->joinLeft( + ['cpe' => $this->resource->getTableName('catalog_product_entity')], + 'cpr.parent_id = cpe.' . $linkField, + ['parent_id' => 'cpe.entity_id'] + ) + ->where('product_id IN (?)', $productIds) ->where('stock_id = ?', Stock::DEFAULT_STOCK_ID) ->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId()); @@ -125,6 +152,9 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array if ($statusBefore['stock_status'] !== $statusAfter['stock_status'] || ($stockThresholdQty && $statusAfter['qty'] <= $stockThresholdQty)) { $productIds[] = $productId; + if (isset($statusAfter['parent_id'])) { + $productIds[] = $statusAfter['parent_id']; + } } } @@ -132,6 +162,8 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array } /** + * Get database connection. + * * @return AdapterInterface */ private function getConnection() diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php b/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php new file mode 100644 index 0000000000000..316f7000ae4af --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Model\Plugin; + +use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; +use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\Framework\Model\AbstractModel; + +/** + * Update product price index after product stock status changed. + */ +class PriceIndexUpdater +{ + /** + * @var Processor + */ + private $priceIndexProcessor; + + /** + * @param Processor $priceIndexProcessor + */ + public function __construct(Processor $priceIndexProcessor) + { + $this->priceIndexProcessor = $priceIndexProcessor; + } + + /** + * @param Item $subject + * @param Item $result + * @param AbstractModel $model + * @return Item + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Item $subject, Item $result, AbstractModel $model) + { + $fields = [ + 'is_in_stock', + 'use_config_manage_stock', + 'manage_stock', + ]; + foreach ($fields as $field) { + if ($model->dataHasChangedFor($field)) { + $this->priceIndexProcessor->reindexRow($model->getProductId()); + break; + } + } + + return $result; + } + + /** + * @param Item $subject + * @param $result + * @param int $websiteId + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateSetOutOfStock(Item $subject, $result, int $websiteId) + { + $this->priceIndexProcessor->markIndexerAsInvalid(); + } + + /** + * @param Item $subject + * @param $result + * @param int $websiteId + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateSetInStock(Item $subject, $result, int $websiteId) + { + $this->priceIndexProcessor->markIndexerAsInvalid(); + } +} diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 3fb0790640ffc..a104f5b098b8e 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -16,10 +16,12 @@ use Magento\CatalogInventory\Model\Stock; use Magento\Framework\Event\Observer; use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Quote\Item; /** * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QuantityValidator { @@ -66,7 +68,7 @@ public function __construct( * Add error information to Quote Item * * @param \Magento\Framework\DataObject $result - * @param \Magento\Quote\Model\Quote\Item $quoteItem + * @param Item $quoteItem * @param bool $removeError * @return void */ @@ -99,12 +101,11 @@ private function addErrorInfoToQuote($result, $quoteItem) */ public function validate(Observer $observer) { - /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ + /* @var $quoteItem Item */ $quoteItem = $observer->getEvent()->getItem(); if (!$quoteItem || !$quoteItem->getProductId() || - !$quoteItem->getQuote() || - $quoteItem->getQuote()->getIsSuperMode() + !$quoteItem->getQuote() ) { return; } @@ -117,6 +118,18 @@ public function validate(Observer $observer) throw new LocalizedException(__('The stock item for Product is not valid.')); } + if (($options = $quoteItem->getQtyOptions()) && $qty > 0) { + foreach ($options as $option) { + $this->optionInitializer->initialize($option, $quoteItem, $qty); + } + } else { + $this->stockItemInitializer->initialize($stockItem, $quoteItem, $qty); + } + + if ($quoteItem->getQuote()->getIsSuperMode()) { + return; + } + /* @var \Magento\CatalogInventory\Api\Data\StockStatusInterface $stockStatus */ $stockStatus = $this->stockRegistry->getStockStatus($product->getId(), $product->getStore()->getWebsiteId()); @@ -159,41 +172,17 @@ public function validate(Observer $observer) /** * Check item for options */ - if (($options = $quoteItem->getQtyOptions()) && $qty > 0) { + if ($options) { $qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product); $quoteItem->setData('qty', $qty); if ($stockStatus) { - $result = $this->stockState->checkQtyIncrements( - $product->getId(), - $qty, - $product->getStore()->getWebsiteId() - ); - if ($result->getHasError()) { - $quoteItem->addErrorInfo( - 'cataloginventory', - Data::ERROR_QTY_INCREMENTS, - $result->getMessage() - ); - - $quoteItem->getQuote()->addErrorInfo( - $result->getQuoteMessageIndex(), - 'cataloginventory', - Data::ERROR_QTY_INCREMENTS, - $result->getQuoteMessage() - ); - } else { - // Delete error from item and its quote, if it was set due to qty problems - $this->_removeErrorsFromQuoteAndItem( - $quoteItem, - Data::ERROR_QTY_INCREMENTS - ); - } + $this->checkOptionsQtyIncrements($quoteItem, $options); } // variable to keep track if we have previously encountered an error in one of the options $removeError = true; foreach ($options as $option) { - $result = $this->optionInitializer->initialize($option, $quoteItem, $qty); + $result = $option->getStockStateResult(); if ($result->getHasError()) { $option->setHasError(true); //Setting this to false, so no error statuses are cleared @@ -206,7 +195,7 @@ public function validate(Observer $observer) } } else { if ($quoteItem->getParentItem() === null) { - $result = $this->stockItemInitializer->initialize($stockItem, $quoteItem, $qty); + $result = $quoteItem->getStockStateResult(); if ($result->getHasError()) { $this->addErrorInfoToQuote($result, $quoteItem); } else { @@ -216,10 +205,44 @@ public function validate(Observer $observer) } } + /** + * Verifies product options quantity increments. + * + * @param Item $quoteItem + * @param array $options + * @return void + */ + private function checkOptionsQtyIncrements(Item $quoteItem, array $options) + { + $removeErrors = true; + foreach ($options as $option) { + $result = $this->stockState->checkQtyIncrements( + $option->getProduct()->getId(), + $quoteItem->getData('qty'), + $option->getProduct()->getStore()->getWebsiteId() + ); + if ($result->getHasError()) { + $quoteItem->getQuote()->addErrorInfo( + $result->getQuoteMessageIndex(), + 'cataloginventory', + Data::ERROR_QTY_INCREMENTS, + $result->getQuoteMessage() + ); + + $removeErrors = false; + } + } + + if ($removeErrors) { + // Delete error from item and its quote, if it was set due to qty problems + $this->_removeErrorsFromQuoteAndItem($quoteItem, Data::ERROR_QTY_INCREMENTS); + } + } + /** * Removes error statuses from quote and item, set by this observer * - * @param \Magento\Quote\Model\Quote\Item $item + * @param Item $item * @param int $code * @return void */ diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php index 3e972a1b84203..c114ed2758657 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php @@ -9,6 +9,9 @@ use Magento\CatalogInventory\Api\StockStateInterface; use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\QuoteItemQtyList; +/** + * Class for initialize quote item options. + */ class Option { /** @@ -67,10 +70,6 @@ public function getStockItem( * define that stock item is child for composite product */ $stockItem->setIsChildItem(true); - /** - * don't check qty increments value for option product - */ - $stockItem->setSuppressCheckQtyIncrements(true); return $stockItem; } @@ -121,7 +120,7 @@ public function initialize( /** * if option's qty was updates we also need to update quote item qty */ - $quoteItem->setData('qty', intval($qty)); + $quoteItem->setData('qty', (int)$qty); } if ($result->getMessage() !== null) { $option->setMessage($result->getMessage()); @@ -133,6 +132,8 @@ public function initialize( $stockItem->unsIsChildItem(); + $option->setStockStateResult($result); + return $result; } } diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php index 6bdc4c67de658..6fb0a949941ec 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php @@ -135,6 +135,8 @@ public function initialize( $quoteItem->setBackorders($result->getItemBackorders()); } + $quoteItem->setStockStateResult($result); + return $result; } } diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index 366cb1c3902a3..317d055d3e481 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -288,6 +288,7 @@ protected function _prepareIndexTable($entityIds = null) */ protected function _updateIndex($entityIds) { + $this->deleteOldRecords($entityIds); $connection = $this->getConnection(); $select = $this->_getStockStatusSelect($entityIds, true); $select = $this->getQueryProcessorComposite()->processQuery($select, $entityIds, true); @@ -310,7 +311,6 @@ protected function _updateIndex($entityIds) } } - $this->deleteOldRecords($entityIds); $this->_updateIndexTable($data); return $this; diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php index 9f89d380a8f96..b80c26c53a76e 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php @@ -56,7 +56,9 @@ public function process(Select $select) ['stock' => $stockStatusTable], sprintf('stock.product_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('stock.stock_status = ?', Stock::STOCK_IN_STOCK); + ) + ->where('stock.stock_status = ?', Stock::STOCK_IN_STOCK) + ->where('stock.website_id = ?', 0); } return $select; diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 9223fd32e3567..53f00529b9bcc 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -113,20 +113,20 @@ protected function _construct() } /** - * Lock Stock Item records + * Lock Stock Item records. * * @param int[] $productIds * @param int $websiteId * @return array */ - public function lockProductsStock($productIds, $websiteId) + public function lockProductsStock(array $productIds, $websiteId) { if (empty($productIds)) { return []; } $itemTable = $this->getTable('cataloginventory_stock_item'); $select = $this->getConnection()->select()->from(['si' => $itemTable]) - ->where('website_id=?', $websiteId) + ->where('website_id = ?', $websiteId) ->where('product_id IN(?)', $productIds) ->forUpdate(true); @@ -136,12 +136,19 @@ public function lockProductsStock($productIds, $websiteId) ->columns( [ 'product_id' => 'entity_id', - 'type_id' => 'type_id' + 'type_id' => 'type_id', ] ); - $this->getConnection()->query($select); + $items = []; - return $this->getConnection()->fetchAll($selectProducts); + foreach ($this->getConnection()->query($select)->fetchAll() as $si) { + $items[$si['product_id']] = $si; + } + foreach ($this->getConnection()->fetchAll($selectProducts) as $p) { + $items[$p['product_id']]['type_id'] = $p['type_id']; + } + + return $items; } /** @@ -199,6 +206,8 @@ protected function _initConfig() /** * Set items out of stock basing on their quantities and config settings * + * @deprecated + * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetOutOfStock * @param string|int $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return void @@ -234,6 +243,8 @@ public function updateSetOutOfStock($website = null) /** * Set items in stock basing on their quantities and config settings * + * @deprecated + * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetInStock * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return void @@ -267,6 +278,8 @@ public function updateSetInStock($website) /** * Update items low stock date basing on their quantities and config settings * + * @deprecated + * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateLowStockDate * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return void diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php index 895fffaa4f80b..ce8930ad4f7a6 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php @@ -6,10 +6,14 @@ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Stock; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; -use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\Model\AbstractModel; -use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\DB\Select; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\DateTime\DateTime; /** * Stock item resource model @@ -29,17 +33,36 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $stockIndexerProcessor; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @param Context $context * @param Processor $processor * @param string $connectionName + * @param StockConfigurationInterface $stockConfiguration + * @param DateTime $dateTime */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, + Context $context, Processor $processor, - $connectionName = null + $connectionName = null, + StockConfigurationInterface $stockConfiguration = null, + DateTime $dateTime = null ) { $this->stockIndexerProcessor = $processor; parent::__construct($context, $connectionName); + + $this->stockConfiguration = $stockConfiguration ?? + ObjectManager::getInstance()->get(StockConfigurationInterface::class); + $this->dateTime = $dateTime ?? + ObjectManager::getInstance()->get(DateTime::class); } /** @@ -139,4 +162,160 @@ public function setProcessIndexEvents($process = true) $this->processIndexEvents = $process; return $this; } + + /** + * Set items out of stock basing on their quantities and config settings + * + * @param int $websiteId + * @return void + */ + public function updateSetOutOfStock(int $websiteId) + { + $connection = $this->getConnection(); + + $values = [ + 'is_in_stock' => Stock::STOCK_OUT_OF_STOCK, + 'stock_status_changed_auto' => 1, + ]; + $select = $this->buildProductsSelectByConfigTypes(); + $where = [ + 'website_id = ' . $websiteId, + 'is_in_stock = ' . Stock::STOCK_IN_STOCK, + '(use_config_manage_stock = 1 AND 1 = ' . $this->stockConfiguration->getManageStock() . ')' + . ' OR (use_config_manage_stock = 0 AND manage_stock = 1)', + '(use_config_min_qty = 1 AND qty <= ' . $this->stockConfiguration->getMinQty() . ')' + . ' OR (use_config_min_qty = 0 AND qty <= min_qty)', + 'product_id IN (' . $select->assemble() . ')', + ]; + $backordersWhere = '(use_config_backorders = 0 AND backorders = ' . Stock::BACKORDERS_NO . ')'; + if (Stock::BACKORDERS_NO == $this->stockConfiguration->getBackorders()) { + $where[] = $backordersWhere . ' OR use_config_backorders = 1'; + } else { + $where[] = $backordersWhere; + } + $connection->update($this->getMainTable(), $values, $where); + + $this->stockIndexerProcessor->markIndexerAsInvalid(); + } + + /** + * Set items in stock basing on their quantities and config settings + * + * @param int $websiteId + * @return void + */ + public function updateSetInStock(int $websiteId) + { + $connection = $this->getConnection(); + + $values = [ + 'is_in_stock' => Stock::STOCK_IN_STOCK, + ]; + $select = $this->buildProductsSelectByConfigTypes(); + $where = [ + 'website_id = ' . $websiteId, + 'stock_status_changed_auto = 1', + '(use_config_min_qty = 1 AND qty > ' . $this->stockConfiguration->getMinQty() . ')' + . ' OR (use_config_min_qty = 0 AND qty > min_qty)', + 'product_id IN (' . $select->assemble() . ')', + ]; + $manageStockWhere = '(use_config_manage_stock = 0 AND manage_stock = 1)'; + if ($this->stockConfiguration->getManageStock()) { + $where[] = $manageStockWhere . ' OR use_config_manage_stock = 1'; + } else { + $where[] = $manageStockWhere; + } + $connection->update($this->getMainTable(), $values, $where); + + $this->stockIndexerProcessor->markIndexerAsInvalid(); + } + + /** + * Update items low stock date basing on their quantities and config settings + * + * @param int $websiteId + * @return void + */ + public function updateLowStockDate(int $websiteId) + { + $connection = $this->getConnection(); + + $condition = $connection->quoteInto( + '(use_config_notify_stock_qty = 1 AND qty < ?)', + $this->stockConfiguration->getNotifyStockQty() + ) . ' OR (use_config_notify_stock_qty = 0 AND qty < notify_stock_qty)'; + $currentDbTime = $connection->quoteInto('?', $this->dateTime->gmtDate()); + $conditionalDate = $connection->getCheckSql($condition, $currentDbTime, 'NULL'); + $value = [ + 'low_stock_date' => new \Zend_Db_Expr($conditionalDate), + ]; + $select = $this->buildProductsSelectByConfigTypes(); + $where = [ + 'website_id = ' . $websiteId, + 'product_id IN (' . $select->assemble() . ')' + ]; + $manageStockWhere = '(use_config_manage_stock = 0 AND manage_stock = 1)'; + if ($this->stockConfiguration->getManageStock()) { + $where[] = $manageStockWhere . ' OR use_config_manage_stock = 1'; + } else { + $where[] = $manageStockWhere; + } + $connection->update($this->getMainTable(), $value, $where); + } + + public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr + { + if ($tableAlias) { + $tableAlias .= '.'; + } + $manageStock = $this->getConnection()->getCheckSql( + $tableAlias . 'use_config_manage_stock = 1', + $this->stockConfiguration->getManageStock(), + $tableAlias . 'manage_stock' + ); + + return $manageStock; + } + + public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr + { + if ($tableAlias) { + $tableAlias .= '.'; + } + $itemBackorders = $this->getConnection()->getCheckSql( + $tableAlias . 'use_config_backorders = 1', + $this->stockConfiguration->getBackorders(), + $tableAlias . 'backorders' + ); + + return $itemBackorders; + } + + public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr + { + if ($tableAlias) { + $tableAlias .= '.'; + } + $itemMinSaleQty = $this->getConnection()->getCheckSql( + $tableAlias . 'use_config_min_sale_qty = 1', + $this->stockConfiguration->getMinSaleQty(), + $tableAlias . 'min_sale_qty' + ); + + return $itemMinSaleQty; + } + + /** + * Build select for products with types from config + * + * @return Select + */ + private function buildProductsSelectByConfigTypes(): Select + { + $select = $this->getConnection()->select() + ->from($this->getTable('catalog_product_entity'), 'entity_id') + ->where('type_id IN (?)', array_keys($this->stockConfiguration->getIsQtyTypeIds(true))); + + return $select; + } } diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php index fd67b0f82a5bd..1b0dcd7ed4167 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item/StockItemCriteria.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogInventory\Model\ResourceModel\Stock\Item; use Magento\Framework\Data\AbstractCriteria; @@ -20,7 +18,8 @@ class StockItemCriteria extends AbstractCriteria implements \Magento\CatalogInve */ public function __construct($mapper = '') { - $this->mapperInterfaceName = $mapper ?: \Magento\CatalogInventory\Model\ResourceModel\Stock\Item\StockItemCriteriaMapper::class; + $this->mapperInterfaceName = $mapper + ?: \Magento\CatalogInventory\Model\ResourceModel\Stock\Item\StockItemCriteriaMapper::class; $this->data['initial_condition'] = true; } diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index e9f3cd59af0bb..4e04ed059c8e2 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -5,8 +5,8 @@ */ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; -use Magento\CatalogInventory\Model\Stock; use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Stock; use Magento\Framework\App\ObjectManager; /** @@ -46,19 +46,23 @@ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Store\Model\WebsiteFactory $websiteFactory * @param \Magento\Eav\Model\Config $eavConfig * @param string $connectionName + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Store\Model\WebsiteFactory $websiteFactory, \Magento\Eav\Model\Config $eavConfig, - $connectionName = null + $connectionName = null, + $stockConfiguration = null ) { parent::__construct($context, $connectionName); $this->_storeManager = $storeManager; $this->_websiteFactory = $websiteFactory; $this->eavConfig = $eavConfig; + $this->stockConfiguration = $stockConfiguration ?: ObjectManager::getInstance() + ->get(StockConfigurationInterface::class); } /** @@ -204,7 +208,7 @@ public function getProductCollection($lastEntityId = 0, $limit = 1000) */ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Magento\Store\Model\Website $website) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId($website->getId()); $select->joinLeft( ['stock_status' => $this->getMainTable()], 'e.entity_id = stock_status.product_id AND stock_status.website_id=' . $websiteId, @@ -221,7 +225,7 @@ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Ma */ public function addStockDataToCollection($collection, $isFilterInStock) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId(); $joinCondition = $this->getConnection()->quoteInto( 'e.entity_id = stock_status_index.product_id' . ' AND stock_status_index.website_id = ?', $websiteId @@ -255,7 +259,7 @@ public function addStockDataToCollection($collection, $isFilterInStock) */ public function addIsInStockFilterToCollection($collection) { - $websiteId = $this->getStockConfiguration()->getDefaultScopeId(); + $websiteId = $this->getWebsiteId(); $joinCondition = $this->getConnection()->quoteInto( 'e.entity_id = stock_status_index.product_id' . ' AND stock_status_index.website_id = ?', $websiteId @@ -277,6 +281,19 @@ public function addIsInStockFilterToCollection($collection) return $this; } + /** + * @param \Magento\Store\Model\Website $websiteId + * @return int + */ + private function getWebsiteId($websiteId = null) + { + if (null === $websiteId) { + $websiteId = $this->stockConfiguration->getDefaultScopeId(); + } + + return $websiteId; + } + /** * Retrieve Product(s) status for store * Return array where key is a product_id, value - status @@ -335,18 +352,4 @@ public function getProductStatus($productIds, $storeId = null) } return $statuses; } - - /** - * @return StockConfigurationInterface - * - * @deprecated 100.1.0 - */ - private function getStockConfiguration() - { - if ($this->stockConfiguration === null) { - $this->stockConfiguration = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class); - } - return $this->stockConfiguration; - } } diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status/StockStatusCriteria.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status/StockStatusCriteria.php index 563a36194c990..2ba75309301cb 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status/StockStatusCriteria.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status/StockStatusCriteria.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogInventory\Model\ResourceModel\Stock\Status; use Magento\Framework\Data\AbstractCriteria; @@ -20,7 +18,8 @@ class StockStatusCriteria extends AbstractCriteria implements \Magento\CatalogIn */ public function __construct($mapper = '') { - $this->mapperInterfaceName = $mapper ?: \Magento\CatalogInventory\Model\ResourceModel\Stock\Status\StockStatusCriteriaMapper::class; + $this->mapperInterfaceName = $mapper + ?: \Magento\CatalogInventory\Model\ResourceModel\Stock\Status\StockStatusCriteriaMapper::class; $this->data['initial_condition'] = true; } diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php index f64026cce23a5..9ed891d1dcc0f 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php @@ -26,4 +26,23 @@ public function getAllOptions() ['value' => \Magento\CatalogInventory\Model\Stock::STOCK_OUT_OF_STOCK, 'label' => __('Out of Stock')] ]; } + + /** + * Add Value Sort To Collection Select. + * + * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @param string $dir + * + * @return $this + */ + public function addValueSortToCollection($collection, $dir = \Magento\Framework\Data\Collection::SORT_ORDER_DESC) + { + $collection->getSelect()->joinLeft( + ['stock_item_table' => 'cataloginventory_stock_item'], + "e.entity_id=stock_item_table.product_id", + [] + ); + $collection->getSelect()->order("stock_item_table.qty $dir"); + return $this; + } } diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Stock/Item.php index b4b70041ce148..2bc351edb2d62 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/Item.php @@ -198,7 +198,7 @@ public function getProductId() */ public function getStockStatusChangedAuto() { - return (bool) $this->_getData(static::STOCK_STATUS_CHANGED_AUTO); + return (bool)$this->_getData(static::STOCK_STATUS_CHANGED_AUTO); } /** @@ -206,7 +206,7 @@ public function getStockStatusChangedAuto() */ public function getQty() { - return null === $this->_getData(static::QTY) ? null : floatval($this->_getData(static::QTY)); + return null === $this->_getData(static::QTY) ? null : (float)$this->_getData(static::QTY); } /** @@ -219,7 +219,7 @@ public function getIsInStock() if (!$this->getManageStock()) { return true; } - return (bool) $this->_getData(static::IS_IN_STOCK); + return (bool)$this->_getData(static::IS_IN_STOCK); } /** @@ -228,7 +228,7 @@ public function getIsInStock() */ public function getIsQtyDecimal() { - return (bool) $this->_getData(static::IS_QTY_DECIMAL); + return (bool)$this->_getData(static::IS_QTY_DECIMAL); } /** @@ -237,7 +237,7 @@ public function getIsQtyDecimal() */ public function getIsDecimalDivided() { - return (bool) $this->_getData(static::IS_DECIMAL_DIVIDED); + return (bool)$this->_getData(static::IS_DECIMAL_DIVIDED); } /** @@ -265,7 +265,7 @@ public function getShowDefaultNotificationMessage() */ public function getUseConfigMinQty() { - return (bool) $this->_getData(static::USE_CONFIG_MIN_QTY); + return (bool)$this->_getData(static::USE_CONFIG_MIN_QTY); } /** @@ -289,7 +289,7 @@ public function getMinQty() */ public function getUseConfigMinSaleQty() { - return (bool) $this->_getData(static::USE_CONFIG_MIN_SALE_QTY); + return (bool)$this->_getData(static::USE_CONFIG_MIN_SALE_QTY); } /** @@ -314,7 +314,7 @@ public function getMinSaleQty() */ public function getUseConfigMaxSaleQty() { - return (bool) $this->_getData(static::USE_CONFIG_MAX_SALE_QTY); + return (bool)$this->_getData(static::USE_CONFIG_MAX_SALE_QTY); } /** @@ -339,7 +339,7 @@ public function getMaxSaleQty() */ public function getUseConfigNotifyStockQty() { - return (bool) $this->_getData(static::USE_CONFIG_NOTIFY_STOCK_QTY); + return (bool)$this->_getData(static::USE_CONFIG_NOTIFY_STOCK_QTY); } /** @@ -361,7 +361,7 @@ public function getNotifyStockQty() */ public function getUseConfigEnableQtyInc() { - return (bool) $this->_getData(static::USE_CONFIG_ENABLE_QTY_INC); + return (bool)$this->_getData(static::USE_CONFIG_ENABLE_QTY_INC); } /** @@ -375,7 +375,7 @@ public function getEnableQtyIncrements() if ($this->getUseConfigEnableQtyInc()) { return $this->stockConfiguration->getEnableQtyIncrements($this->getStoreId()); } - return (bool) $this->getData(static::ENABLE_QTY_INCREMENTS); + return (bool)$this->getData(static::ENABLE_QTY_INCREMENTS); } /** @@ -386,7 +386,7 @@ public function getEnableQtyIncrements() */ public function getUseConfigQtyIncrements() { - return (bool) $this->_getData(static::USE_CONFIG_QTY_INCREMENTS); + return (bool)$this->_getData(static::USE_CONFIG_QTY_INCREMENTS); } /** @@ -401,7 +401,8 @@ public function getQtyIncrements() if ($this->getUseConfigQtyIncrements()) { $this->qtyIncrements = $this->stockConfiguration->getQtyIncrements($this->getStoreId()); } else { - $this->qtyIncrements = (int) $this->getData(static::QTY_INCREMENTS); + $qtyIncrements = $this->getData(static::QTY_INCREMENTS); + $this->qtyIncrements = $this->getIsQtyDecimal() ? (float) $qtyIncrements : (int) $qtyIncrements; } } if ($this->qtyIncrements <= 0) { @@ -417,7 +418,7 @@ public function getQtyIncrements() */ public function getUseConfigBackorders() { - return (bool) $this->_getData(static::USE_CONFIG_BACKORDERS); + return (bool)$this->_getData(static::USE_CONFIG_BACKORDERS); } /** @@ -439,7 +440,7 @@ public function getBackorders() */ public function getUseConfigManageStock() { - return (bool) $this->_getData(static::USE_CONFIG_MANAGE_STOCK); + return (bool)$this->_getData(static::USE_CONFIG_MANAGE_STOCK); } /** diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/Stock/Status.php index 9a56c8e8804ec..8a24d3c46abcb 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/Status.php @@ -17,14 +17,6 @@ */ class Status extends AbstractExtensibleModel implements StockStatusInterface { - /**#@+ - * Stock Status values - */ - const STATUS_OUT_OF_STOCK = 0; - - const STATUS_IN_STOCK = 1; - /**#@-*/ - /**#@+ * Field name */ @@ -114,9 +106,9 @@ public function getQty() /** * @return int */ - public function getStockStatus() + public function getStockStatus(): int { - return $this->getData(self::KEY_STOCK_STATUS); + return (int)$this->getData(self::KEY_STOCK_STATUS); } //@codeCoverageIgnoreEnd diff --git a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php index e5154a10f0a19..ee613ac1db018 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php @@ -10,7 +10,7 @@ use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory; use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface as StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; use Magento\CatalogInventory\Model\ResourceModel\Stock\Item as StockItemResource; use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; @@ -145,7 +145,7 @@ public function __construct( /** * @inheritdoc */ - public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stockItem) + public function save(StockItemInterface $stockItem) { try { /** @var \Magento\Catalog\Model\Product $product */ diff --git a/app/code/Magento/CatalogInventory/Model/StockIndex.php b/app/code/Magento/CatalogInventory/Model/StockIndex.php index 6081ef20efd64..9e89a95367df3 100644 --- a/app/code/Magento/CatalogInventory/Model/StockIndex.php +++ b/app/code/Magento/CatalogInventory/Model/StockIndex.php @@ -4,10 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogInventory\Model; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type as ProductType; use Magento\Catalog\Model\Product\Website as ProductWebsite; use Magento\Catalog\Model\ProductFactory; @@ -176,7 +175,7 @@ protected function processChildren( if (isset($childrenStatus[$childId]) && isset($childrenWebsites[$childId]) && in_array($websiteId, $childrenWebsites[$childId]) - && $childrenStatus[$childId] == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + && $childrenStatus[$childId] == Status::STATUS_ENABLED && isset($childrenStock[$childId]) && $childrenStock[$childId] == \Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK ) { @@ -201,13 +200,13 @@ protected function processChildren( */ protected function getWebsitesWithDefaultStores($websiteId = null) { - if (is_null($this->websites)) { + if ($this->websites === null) { /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Status $resource */ $resource = $this->getStockStatusResource(); $this->websites = $resource->getWebsiteStores(); } $websites = $this->websites; - if (!is_null($websiteId) && isset($this->websites[$websiteId])) { + if ($websiteId !== null && isset($this->websites[$websiteId])) { $websites = [$websiteId => $this->websites[$websiteId]]; } return $websites; diff --git a/app/code/Magento/CatalogInventory/Model/StockManagement.php b/app/code/Magento/CatalogInventory/Model/StockManagement.php index 06599446a9ea9..ed8fcef5dea03 100644 --- a/app/code/Magento/CatalogInventory/Model/StockManagement.php +++ b/app/code/Magento/CatalogInventory/Model/StockManagement.php @@ -48,6 +48,11 @@ class StockManagement implements StockManagementInterface */ private $qtyCounter; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** * @param ResourceStock $stockResource * @param StockRegistryProviderInterface $stockRegistryProvider @@ -55,6 +60,7 @@ class StockManagement implements StockManagementInterface * @param StockConfigurationInterface $stockConfiguration * @param ProductRepositoryInterface $productRepository * @param QtyCounterInterface $qtyCounter + * @param StockRegistryStorage|null $stockRegistryStorage */ public function __construct( ResourceStock $stockResource, @@ -62,7 +68,8 @@ public function __construct( StockState $stockState, StockConfigurationInterface $stockConfiguration, ProductRepositoryInterface $productRepository, - QtyCounterInterface $qtyCounter + QtyCounterInterface $qtyCounter, + StockRegistryStorage $stockRegistryStorage = null ) { $this->stockRegistryProvider = $stockRegistryProvider; $this->stockState = $stockState; @@ -70,11 +77,14 @@ public function __construct( $this->productRepository = $productRepository; $this->qtyCounter = $qtyCounter; $this->resource = $stockResource; + $this->stockRegistryStorage = $stockRegistryStorage ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StockRegistryStorage::class); } /** * Subtract product qtys from stock. - * Return array of items that require full save + * + * Return array of items that require full save. * * @param string[] $items * @param int $websiteId @@ -92,9 +102,12 @@ public function registerProductsSale($items, $websiteId = null) $fullSaveItems = $registeredItems = []; foreach ($lockedItems as $lockedItemRecord) { $productId = $lockedItemRecord['product_id']; + $this->stockRegistryStorage->removeStockItem($productId, $websiteId); + /** @var StockItemInterface $stockItem */ $orderedQty = $items[$productId]; $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + $stockItem->setQty($lockedItemRecord['qty']); // update data from locked item $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); if (!$canSubtractQty || !$this->stockConfiguration->isQty($lockedItemRecord['type_id'])) { continue; @@ -102,7 +115,7 @@ public function registerProductsSale($items, $websiteId = null) if (!$stockItem->hasAdminArea() && !$this->stockState->checkQty($productId, $orderedQty, $stockItem->getWebsiteId()) ) { - $this->getResource()->rollBack(); + $this->getResource()->commit(); throw new \Magento\Framework\Exception\LocalizedException( __('Not all of your products are available in the requested quantity.') ); @@ -122,21 +135,30 @@ public function registerProductsSale($items, $websiteId = null) } $this->qtyCounter->correctItemsQty($registeredItems, $websiteId, '-'); $this->getResource()->commit(); + return $fullSaveItems; } /** - * @param string[] $items - * @param int $websiteId - * @return bool + * @inheritdoc */ public function revertProductsSale($items, $websiteId = null) { //if (!$websiteId) { $websiteId = $this->stockConfiguration->getDefaultScopeId(); //} - $this->qtyCounter->correctItemsQty($items, $websiteId, '+'); - return true; + $revertItems = []; + foreach ($items as $productId => $qty) { + $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); + if (!$canSubtractQty || !$this->stockConfiguration->isQty($stockItem->getTypeId())) { + continue; + } + $revertItems[$productId] = $qty; + } + $this->qtyCounter->correctItemsQty($revertItems, $websiteId, '+'); + + return $revertItems; } /** @@ -180,6 +202,8 @@ protected function getProductType($productId) } /** + * Get stock resource. + * * @return ResourceStock */ protected function getResource() diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistry.php b/app/code/Magento/CatalogInventory/Model/StockRegistry.php index d688132fdb916..30b08ee4b8e7f 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistry.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistry.php @@ -171,6 +171,16 @@ public function updateStockItemBySku($productSku, \Magento\CatalogInventory\Api\ $productId = $this->resolveProductId($productSku); $websiteId = $stockItem->getWebsiteId() ?: null; $origStockItem = $this->getStockItem($productId, $websiteId); + + if ($stockItem->getManageStock() + && !$stockItem->getIsInStock() + && $stockItem->getQty() > 0 + && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) <= 0 + && $stockItem->getOrigData(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) !== null + ) { + $stockItem->setIsInStock(true)->setStockStatusChangedAutomaticallyFlag(true); + } + $data = $stockItem->getData(); if ($origStockItem->getItemId()) { unset($data['item_id']); diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php index af417954a8412..f19479e7a009c 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php @@ -68,7 +68,7 @@ public function removeStock($scopeId = null) */ public function getStockItem($productId, $scopeId) { - return isset($this->stockItems[$productId][$scopeId]) ? $this->stockItems[$productId][$scopeId] : null; + return $this->stockItems[$productId][$scopeId] ?? null; } /** @@ -103,7 +103,7 @@ public function removeStockItem($productId, $scopeId = null) */ public function getStockStatus($productId, $scopeId) { - return isset($this->stockStatuses[$productId][$scopeId]) ? $this->stockStatuses[$productId][$scopeId] : null; + return $this->stockStatuses[$productId][$scopeId] ?? null; } /** diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php index 4cffc678314b2..76c72583eda9b 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php +++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogInventory\Model; use Magento\Catalog\Model\ProductFactory; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; +use Magento\Framework\DataObject\Factory as ObjectFactory; use Magento\Framework\Locale\FormatInterface; use Magento\Framework\Math\Division as MathDivision; -use Magento\Framework\DataObject\Factory as ObjectFactory; /** * Interface StockStateProvider @@ -115,13 +113,13 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal()); if (!$stockItem->getIsQtyDecimal()) { $result->setHasQtyOptionUpdate(true); - $qty = intval($qty); + $qty = (int)$qty; /** * Adding stock data to quote item */ $result->setItemQty($qty); $qty = $this->getNumber($qty); - $origQty = intval($origQty); + $origQty = (int)$origQty; $result->setOrigQty($origQty); } @@ -196,7 +194,8 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ if (!$stockItem->getIsChildItem()) { $result->setMessage( __( - 'We don\'t have as many "%1" as you requested, but we\'ll back order the remaining %2.', + 'We don\'t have as many "%1" as you requested, ' + . 'but we\'ll back order the remaining %2.', $stockItem->getProductName(), $backOrderQty * 1 ) @@ -204,7 +203,8 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ } else { $result->setMessage( __( - 'We don\'t have "%1" in the requested quantity, so we\'ll back order the remaining %2.', + 'We don\'t have "%1" in the requested quantity, ' + . 'so we\'ll back order the remaining %2.', $stockItem->getProductName(), $backOrderQty * 1 ) diff --git a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php new file mode 100644 index 0000000000000..8fa90cf6531c4 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogInventory\Observer; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * Add Stock items to product collection. + */ +class AddStockItemsObserver implements ObserverInterface +{ + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * AddStockItemsObserver constructor. + * + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct( + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration + ) { + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Add stock items to products in collection. + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + /** @var Collection $productCollection */ + $productCollection = $observer->getData('collection'); + $productIds = array_keys($productCollection->getItems()); + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setProductsFilter($productIds); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + foreach ($stockItemCollection->getItems() as $item) { + /** @var Product $product */ + $product = $productCollection->getItemById($item->getProductId()); + $productExtension = $product->getExtensionAttributes(); + $productExtension->setStockItem($item); + $product->setExtensionAttributes($productExtension); + } + } +} diff --git a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php index 1e99794d68a40..098e254d785a5 100644 --- a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php @@ -6,15 +6,21 @@ namespace Magento\CatalogInventory\Observer; -use Magento\Framework\Event\ObserverInterface; use Magento\CatalogInventory\Api\StockManagementInterface; +use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; /** * Catalog inventory module observer */ class CancelOrderItemObserver implements ObserverInterface { + /** + * @var \Magento\CatalogInventory\Model\Configuration + */ + protected $configuration; + /** * @var StockManagementInterface */ @@ -26,13 +32,16 @@ class CancelOrderItemObserver implements ObserverInterface protected $priceIndexer; /** + * @param Configuration $configuration * @param StockManagementInterface $stockManagement * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer */ public function __construct( + Configuration $configuration, StockManagementInterface $stockManagement, \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer ) { + $this->configuration = $configuration; $this->stockManagement = $stockManagement; $this->priceIndexer = $priceIndexer; } @@ -49,7 +58,8 @@ public function execute(EventObserver $observer) $item = $observer->getEvent()->getItem(); $children = $item->getChildrenItems(); $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled(); - if ($item->getId() && $item->getProductId() && empty($children) && $qty) { + if ($item->getId() && $item->getProductId() && empty($children) && $qty && $this->configuration + ->getCanBackInStock()) { $this->stockManagement->backItemQty($item->getProductId(), $qty, $item->getStore()->getWebsiteId()); } $this->priceIndexer->reindexRow($item->getProductId()); diff --git a/app/code/Magento/CatalogInventory/Observer/InvalidatePriceIndexUponConfigChangeObserver.php b/app/code/Magento/CatalogInventory/Observer/InvalidatePriceIndexUponConfigChangeObserver.php new file mode 100644 index 0000000000000..976110ec76cf3 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Observer/InvalidatePriceIndexUponConfigChangeObserver.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Observer; + +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Catalog\Model\Indexer\Product\Price\Processor; + +/** + * Catalog inventory config changes module observer. + */ +class InvalidatePriceIndexUponConfigChangeObserver implements ObserverInterface +{ + /** + * @var Processor + */ + private $priceIndexProcessor; + + /** + * @param Processor $priceIndexProcessor + */ + public function __construct(Processor $priceIndexProcessor) + { + $this->priceIndexProcessor = $priceIndexProcessor; + } + + /** + * Invalidate product price index on catalog inventory config changes. + * + * @param Observer $observer + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute(Observer $observer) + { + $changedPaths = (array) $observer->getEvent()->getChangedPaths(); + + if (\in_array(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $changedPaths, true)) { + $priceIndexer = $this->priceIndexProcessor->getIndexer(); + $priceIndexer->invalidate(); + } + } +} diff --git a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php index e473f714bd21d..abd704c1d0406 100644 --- a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php @@ -74,7 +74,6 @@ private function processStockData(Product $product) $this->setStockDataToProduct($product, $stockItem, $quantityAndStockStatus); } } - $product->unsetData('quantity_and_stock_status'); } /** @@ -97,7 +96,7 @@ private function prepareQuantityAndStockStatus(StockItemInterface $stockItem, ar ) { unset($quantityAndStockStatus['is_in_stock']); } - if (isset($quantityAndStockStatus['qty']) + if (array_key_exists('qty', $quantityAndStockStatus) && $stockItem->getQty() == $quantityAndStockStatus['qty'] ) { unset($quantityAndStockStatus['qty']); diff --git a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php index 93a50cc9a7a4d..ab21f32b3f62c 100644 --- a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php @@ -64,8 +64,8 @@ public function execute(EventObserver $observer) { $quote = $observer->getEvent()->getQuote(); $items = $this->productQty->getProductQty($quote->getAllItems()); - $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); - $productIds = array_keys($items); + $revertedItems = $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); + $productIds = array_keys($revertedItems); if (!empty($productIds)) { $this->stockIndexerProcessor->reindexList($productIds); $this->priceIndexer->reindexList($productIds); diff --git a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php index 03ba58d3f4987..67171d6432e58 100644 --- a/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php @@ -39,6 +39,11 @@ class SaveInventoryDataObserver implements ObserverInterface */ private $stockItemValidator; + /** + * @var array + */ + private $parentItemProcessorPool; + /** * @var array */ @@ -77,15 +82,18 @@ class SaveInventoryDataObserver implements ObserverInterface * @param StockConfigurationInterface $stockConfiguration * @param StockRegistryInterface $stockRegistry * @param StockItemValidator $stockItemValidator + * @param array $parentItemProcessorPool */ public function __construct( StockConfigurationInterface $stockConfiguration, StockRegistryInterface $stockRegistry, - StockItemValidator $stockItemValidator = null + StockItemValidator $stockItemValidator = null, + array $parentItemProcessorPool = [] ) { $this->stockConfiguration = $stockConfiguration; $this->stockRegistry = $stockRegistry; $this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class); + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -99,7 +107,10 @@ public function __construct( */ public function execute(EventObserver $observer) { + /** @var Product $product */ $product = $observer->getEvent()->getProduct(); + + /** @var Item $stockItem */ $stockItem = $this->getStockItemToBeUpdated($product); if ($product->getStockData() !== null) { @@ -108,6 +119,7 @@ public function execute(EventObserver $observer) } $this->stockItemValidator->validate($product, $stockItem); $this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem); + $this->processParents($product); } /** @@ -156,4 +168,17 @@ private function getStockData(Product $product) } return $stockData; } + + /** + * Process stock data for parent products + * + * @param Product $product + * @return void + */ + private function processParents(Product $product) + { + foreach ($this->parentItemProcessorPool as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/CatalogInventory/Observer/SubtractQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/SubtractQuoteInventoryObserver.php index 6fbec08e4805b..5d776a488b65e 100644 --- a/app/code/Magento/CatalogInventory/Observer/SubtractQuoteInventoryObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/SubtractQuoteInventoryObserver.php @@ -6,9 +6,11 @@ namespace Magento\CatalogInventory\Observer; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Framework\Event\ObserverInterface; use Magento\CatalogInventory\Api\StockManagementInterface; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Sales\Api\Data\OrderInterface; /** * Catalog inventory module observer @@ -59,16 +61,20 @@ public function execute(EventObserver $observer) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $observer->getEvent()->getQuote(); + /** @var OrderInterface|null $order */ + $order = $observer->getEvent()->getOrder(); // Maybe we've already processed this quote in some event during order placement // e.g. call in event 'sales_model_service_quote_submit_before' and later in 'checkout_submit_all_after' if ($quote->getInventoryProcessed()) { return $this; } - $items = $this->productQty->getProductQty($quote->getAllItems()); + $items = $this->productQty->getProductQty($quote->getAllItems()); /** * Remember items + * + * @var StockItemInterface[] $itemsForReindex */ $itemsForReindex = $this->stockManagement->registerProductsSale( $items, @@ -76,6 +82,28 @@ public function execute(EventObserver $observer) ); $this->itemsForReindex->setItems($itemsForReindex); + if ($order) { + //Marking items as backordered if order is placed. + /** @var StockItemInterface[] $stockItems */ + $stockItems = []; + foreach ($itemsForReindex as $stockItem) { + $stockItems[$stockItem->getProductId()] = $stockItem; + } + foreach ($order->getItems() as $orderItem) { + if (!empty($stockItems[$orderItem->getProductId()])) { + $stock = $stockItems[$orderItem->getProductId()]; + //Found stock of ordered item, + //checking if the item was backordered. + if (($qty = $stock->getQty()) < 0) { + $orderItem->setQtyBackordered( + $orderItem->getQtyOrdered() > (-$qty) + ? (-$qty) : $orderItem->getQtyOrdered() + ); + } + } + } + } + $quote->setInventoryProcessed(true); return $this; } diff --git a/app/code/Magento/CatalogInventory/Observer/UpdateItemsStockUponConfigChangeObserver.php b/app/code/Magento/CatalogInventory/Observer/UpdateItemsStockUponConfigChangeObserver.php index 47ee6512920d8..21c78eca93695 100644 --- a/app/code/Magento/CatalogInventory/Observer/UpdateItemsStockUponConfigChangeObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/UpdateItemsStockUponConfigChangeObserver.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\CatalogInventory\Observer; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; +use Magento\CatalogInventory\Model\Configuration; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; /** * Catalog inventory module observer @@ -15,16 +16,16 @@ class UpdateItemsStockUponConfigChangeObserver implements ObserverInterface { /** - * @var \Magento\CatalogInventory\Model\ResourceModel\Stock + * @var Item */ - protected $resourceStock; + protected $resourceStockItem; /** - * @param \Magento\CatalogInventory\Model\ResourceModel\Stock $resourceStock + * @param Item $resourceStockItem */ - public function __construct(\Magento\CatalogInventory\Model\ResourceModel\Stock $resourceStock) + public function __construct(Item $resourceStockItem) { - $this->resourceStock = $resourceStock; + $this->resourceStockItem = $resourceStockItem; } /** @@ -35,9 +36,18 @@ public function __construct(\Magento\CatalogInventory\Model\ResourceModel\Stock */ public function execute(EventObserver $observer) { - $website = $observer->getEvent()->getWebsite(); - $this->resourceStock->updateSetOutOfStock($website); - $this->resourceStock->updateSetInStock($website); - $this->resourceStock->updateLowStockDate($website); + $website = (int) $observer->getEvent()->getWebsite(); + $changedPaths = (array) $observer->getEvent()->getChangedPaths(); + + if (\array_intersect([ + Configuration::XML_PATH_MANAGE_STOCK, + Configuration::XML_PATH_MIN_QTY, + Configuration::XML_PATH_BACKORDERS, + Configuration::XML_PATH_NOTIFY_STOCK_QTY, + ], $changedPaths)) { + $this->resourceStockItem->updateSetOutOfStock($website); + $this->resourceStockItem->updateSetInStock($website); + $this->resourceStockItem->updateLowStockDate($website); + } } } diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml new file mode 100644 index 0000000000000..49956473132ec --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup"> + <arguments> + <argument name="qty" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + + <fillField selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQty}}" userInput="{{qty}}" stepKey="setMaxSaleQtyValue"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <waitForElementVisible selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyError}}" stepKey="waitValidationErrorMessageAppears"/> + <see selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyError}}" userInput="{{errorMessage}}" stepKey="checkValidationErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml new file mode 100644 index 0000000000000..84dc6b93c885f --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductSetMaxQtyAllowedInShoppingCart"> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductFormSection.advancedInventoryLink}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" visible="false" stepKey="clickOnAdvancedInventoryLinkIfNeeded"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="waitForAdvancedInventoryModalWindowOpen"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" stepKey="fillMaxAllowedQty"/> + <click selector="{{AdminSlideOutDialogSection.doneButton}}" stepKey="clickDone"/> + </actionGroup> + + <actionGroup name="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" extends="AdminProductSetMaxQtyAllowedInShoppingCart"> + <arguments> + <argument name="qty" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCartError}}" after="clickDone" stepKey="waitProductValidationErrorMessageAppears"/> + <see selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCartError}}" userInput="{{errorMessage}}" after="waitProductValidationErrorMessageAppears" stepKey="checkProductValidationErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml new file mode 100644 index 0000000000000..cd5a8cf5bbac9 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="StockOptionsDisplayOutOfStockProductsEnable"> + <data key="path">cataloginventory/options/show_out_of_stock</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StockOptionsDisplayOutOfStockProductsDisable"> + <data key="path">cataloginventory/options/show_out_of_stock</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableCatalogInventoryConfigData"> + <!--Default Value --> + <data key="path">cataloginventory/options/can_subtract</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableCatalogInventoryConfigData"> + <data key="path">cataloginventory/options/can_subtract</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml new file mode 100644 index 0000000000000..767d65f9facca --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultValueForMaxSaleQty" type="cataloginventory_item_options"> + <requiredEntity type="max_sale_qty">MaxSaleQtyDefaultValue</requiredEntity> + </entity> + <entity name="MaxSaleQtyDefaultValue" type="max_sale_qty"> + <data key="value">10000</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogInventory/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogInventory/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 <insert your license name here>" 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/app/code/Magento/CatalogInventory/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogInventory/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogInventory/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 <insert your license name here>" 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/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml new file mode 100644 index 0000000000000..7672cb7478f1a --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogInventoryProductStockOptionsConfiguration" dataType="cataloginventory_item_options" type="create" + auth="adminFormKey" url="/admin/system_config/save/section/cataloginventory/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="cataloginventory_item_options"> + <object key="item_options" dataType="cataloginventory_item_options"> + <object key="fields" dataType="cataloginventory_item_options"> + <object key="max_sale_qty" dataType="max_sale_qty"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml new file mode 100644 index 0000000000000..3d8c3ef3cf9f8 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminInventoryProductStockOptionsConfigPage" url="admin/system_config/edit/section/cataloginventory/#cataloginventory_item_options-link" area="admin" module="Magento_Config"> + <section name="AdminInventoryProductStockOptionsConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..5835e7564c172 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductFormAdvancedInventorySection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/README.md b/app/code/Magento/CatalogInventory/Test/Mftf/README.md new file mode 100644 index 0000000000000..3903fe316b36c --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Inventory Functional Tests + +The Functional Test Module for **Magento Catalog Inventory** module. diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml new file mode 100644 index 0000000000000..ef7fe30f4970b --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminInventoryProductStockOptionsConfigSection"> + <element name="maxSaleQtyInherit" type="checkbox" selector="#cataloginventory_item_options_max_sale_qty_inherit" timeout="30"/> + <element name="maxSaleQty" type="input" selector="#cataloginventory_item_options_max_sale_qty"/> + <element name="maxSaleQtyError" type="input" selector="#cataloginventory_item_options_max_sale_qty-error"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml new file mode 100644 index 0000000000000..9a3eceadb421b --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormAdvancedInventorySection"> + <element name="maxiQtyConfigSetting" type="checkbox" selector="[name='product[stock_data][use_config_max_sale_qty]']"/> + <element name="maxiQtyAllowedInCart" type="input" selector="[name='product[stock_data][max_sale_qty]']"/> + <element name="maxiQtyAllowedInCartError" type="text" selector="[name='product[stock_data][max_sale_qty]'] + label.admin__field-error"/> + <element name="advancedInventoryModal" type="block" selector=".product_form_product_form_advanced_inventory_modal[data-role=modal]"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml new file mode 100644 index 0000000000000..f4b79b17b3fc3 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormSection"> + <element name="advancedInventoryLink" type="button" selector="button[data-index='advanced_inventory_button']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml new file mode 100644 index 0000000000000..1b82aec0ce898 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="Sales restrictions"/> + <title value="Verify that product maximum qty allowed in shopping cart can't be set to zero or less"/> + <description value="Verify that product maximum qty allowed in shopping cart can't be set to zero or less"/> + <severity value="MAJOR"/> + <useCaseId value="MC-17605"/> + <testCaseId value="MC-17677"/> + <group value="catalog"/> + <group value="catalogInventory"/> + </annotations> + <before> + <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> + <createData entity="SimpleProduct3" stepKey="createdProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> + <deleteData createDataKey="createdProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Inventory configuration page --> + <amOnPage url="{{AdminInventoryProductStockOptionsConfigPage.url}}" stepKey="openInventoryConfigPage"/> + <uncheckOption selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQtyInherit}}" stepKey="uncheckUseDefaultValueForMaxSaleQty"/> + <!-- Validate zero value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateZeroValue"> + <argument name="qty" value="0"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate negative value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateNegativeValue"> + <argument name="qty" value="-1"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate alphabetical value --> + <actionGroup ref="AdminCatalogInventoryConfigurationMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="validateAlphabeticalValue"> + <argument name="qty" value="abc"/> + <argument name="errorMessage" value="Please enter a valid number in this field."/> + </actionGroup> + <!-- Fill correct value --> + <fillField selector="{{AdminInventoryProductStockOptionsConfigSection.maxSaleQty}}" userInput="100" stepKey="setMaxSaleQtyValueToCorrectNumber"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigWithCorrectNumber"/> + + <!-- Go to product page --> + <amOnPage url="{{AdminProductEditPage.url($$createdProduct.id$$)}}" stepKey="openAdminProductEditPage"/> + <!-- Validate zero value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateZeroValue"> + <argument name="qty" value="0"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate negative value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateNegativeValue"> + <argument name="qty" value="-1"/> + <argument name="errorMessage" value="Please enter a number greater than 0 in this field."/> + </actionGroup> + <!-- Validate alphabetical value --> + <actionGroup ref="AdminProductMaxQtyAllowedInShoppingCartValidationActionGroup" stepKey="productValidateAlphabeticalValue"> + <argument name="qty" value="abc"/> + <argument name="errorMessage" value="Please enter a valid number in this field."/> + </actionGroup> + <!-- Fill correct value --> + <actionGroup ref="AdminProductSetMaxQtyAllowedInShoppingCart" stepKey="setProductMaxQtyAllowedInShoppingCartToCorrectNumber"> + <argument name="qty" value="50"/> + </actionGroup> + <waitForElementNotVisible selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryModal}}" stepKey="waitForModalFormToDisappear"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml new file mode 100644 index 0000000000000..735b7fe4f00d0 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AssociatedProductToConfigurableOutOfStockTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="MAGETWO-73528: Out of stock associated products to configurable are not full page cache cleaned"/> + <title value="Checking out of stock associated products to configurable after checkout - full page cache cleaned"/> + <description value="After last configurable product was ordered it becomes out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96031"/> + <group value="CatalogInventory"/> + </annotations> + + <before> + <!--Create Configurable product--> + <actionGroup ref="AdminCreateConfigurableProductChildQty1ActionGroup" stepKey="createConfigurableProduct"> + <argument name="productName" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <field key="group_id">1</field> + </createData> + <!--Update index mode, reindex, flush cache--> + <magentoCLI command="indexer:set-mode schedule" stepKey="setScheduleIndexMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindexBefore"/> + <magentoCLI command="cache:flush" stepKey="cacheFlushBefore"/> + </before> + + <after> + <!--Delete configurable product--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + <!--Delete customer--> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <!--Update index mode, reindex, flush cache--> + <magentoCLI command="indexer:set-mode realtime" stepKey="setRealTimeIndexMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindexAfter"/> + <magentoCLI command="cache:flush" stepKey="cacheFlushAfter"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="signUpNewUser"> + <argument name="customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + + <!-- Go to configurable product page --> + <amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage"/> + + <!-- Order product with single quantity --> + <selectOption userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$" + selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}" + stepKey="configProductFillOption" + /> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectShippingMehod"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + + <!--Run cron to reindex products--> + <magentoCLI command="cron:run --group='index'" stepKey="runCron"/> + <magentoCLI command="cron:run --group='index'" stepKey="runCron1"/> + + <!-- Go to configurable product page --> + <amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage1"/> + + <!-- Assert that ordered product with single quantity is not available for order --> + <dontSee userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$" + selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}" + stepKey="assertOptionNotAvailable" + /> + + <!-- Logout customer on Storefront--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php index 4ef2e78e590fb..4ec795daf86aa 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php @@ -28,7 +28,7 @@ protected function setUp() $this->stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Model\Stock\Item::class) ->disableOriginalConstructor() - ->setMethods(['getMinSaleQty', 'getQtyMaxAllowed', 'getQtyIncrements']) + ->setMethods(['getMinSaleQty', 'getMaxSaleQty', 'getQtyIncrements']) ->getMock(); $this->stockRegistry = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) @@ -48,8 +48,8 @@ public function testAfterGetQuantityValidators() 'validate-item-quantity' => [ 'minAllowed' => 0.5, - 'maxAllowed' => 5, - 'qtyIncrements' => 3 + 'maxAllowed' => 5.0, + 'qtyIncrements' => 3.0 ] ]; $validators = []; @@ -74,7 +74,7 @@ public function testAfterGetQuantityValidators() ->with('productId', 'websiteId') ->willReturn($this->stockItem); $this->stockItem->expects($this->once())->method('getMinSaleQty')->willReturn(0.5); - $this->stockItem->expects($this->any())->method('getQtyMaxAllowed')->willReturn(5); + $this->stockItem->expects($this->any())->method('getMaxSaleQty')->willReturn(5); $this->stockItem->expects($this->any())->method('getQtyIncrements')->willReturn(3); $this->assertEquals($result, $this->block->afterGetQuantityValidators($productViewBlock, $validators)); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php index f008ed7d9d694..051e002b80c3e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Helper/MinsaleqtyTest.php @@ -216,7 +216,7 @@ public function testMakeStorableArrayFieldValue($value, $result, $serializeCallC public function makeStorableArrayFieldValueDataProvider() { return [ - 'invalid bool' => [false, ''], + 'invalid bool' => [false, false], 'invalid empty string' => ['', ''], 'valid numeric' => ['22', '22'], 'valid empty array' => [[], '[]', 1], @@ -240,7 +240,8 @@ public function makeStorableArrayFieldValueDataProvider() '[1]', 1, [0 => 1.0] - ] + ], + 'json value' => ['{"32000":2,"0":1}', '{"32000":2,"0":1}'], ]; } } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Helper/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Helper/StockTest.php index 2fe40c118b06a..bd04df0da0a4a 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Helper/StockTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Helper/StockTest.php @@ -167,6 +167,9 @@ public function testAddInStockFilterToCollection($configMock) $this->assertNull($this->stock->addInStockFilterToCollection($collectionMock)); } + /** + * @return array + */ public function filterProvider() { $configMock = $this->getMockBuilder(\Magento\Framework\App\Config::class) diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php index e1a19bf10ecd4..3590c96bd1532 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php @@ -44,7 +44,8 @@ public function testExecuteWithAdapterErrorThrowsException() ] ); - $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $exceptionMessage); + $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectExceptionMessage($exceptionMessage); $model->execute(); } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 5e4249685f8d3..a1282c45ce1fc 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php @@ -12,6 +12,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\Product; @@ -43,6 +44,11 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase */ private $cacheContextMock; + /** + * @var MetadataPool |\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + /** * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -61,6 +67,8 @@ protected function setUp() ->setMethods(['getStockThresholdQty'])->getMockForAbstractClass(); $this->cacheContextMock = $this->getMockBuilder(CacheContext::class)->disableOriginalConstructor()->getMock(); $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); + $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) + ->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()->getMock(); $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $this->resourceMock->expects($this->any()) @@ -73,7 +81,8 @@ protected function setUp() 'resource' => $this->resourceMock, 'stockConfiguration' => $this->stockConfigurationMock, 'cacheContext' => $this->cacheContextMock, - 'eventManager' => $this->eventManagerMock + 'eventManager' => $this->eventManagerMock, + 'metadataPool' => $this->metadataPoolMock, ] ); } @@ -90,6 +99,7 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -105,7 +115,10 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto ->with(Product::CACHE_TAG, [$productId]); $this->eventManagerMock->expects($this->once())->method('dispatch') ->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]); - + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; $this->unit->clean([], $callback); @@ -136,6 +149,7 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -149,6 +163,10 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft ->willReturn($stockThresholdQty); $this->cacheContextMock->expects($this->never())->method('registerEntities'); $this->eventManagerMock->expects($this->never())->method('dispatch'); + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php index 6f02c27ca1dff..656adf748e832 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogInventory\Test\Unit\Model\Plugin; class AfterProductLoadTest extends \PHPUnit\Framework\TestCase @@ -35,7 +32,8 @@ protected function setUp() { $stockRegistryMock = $this->createMock(\Magento\CatalogInventory\Api\StockRegistryInterface::class); $this->productExtensionFactoryMock = $this->getMockBuilder( - \Magento\Catalog\Api\Data\ProductExtensionFactory::class) + \Magento\Catalog\Api\Data\ProductExtensionFactory::class + ) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/ProductLinksTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/ProductLinksTest.php index ea562da2f01c0..3788b1bc401fe 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/ProductLinksTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/ProductLinksTest.php @@ -48,6 +48,9 @@ public function testAfterGetProductCollectionShow($status, $callCount) $this->assertEquals($collectionMock, $this->model->afterGetProductCollection($subjectMock, $collectionMock)); } + /** + * @return array + */ private function buildMocks() { /** @var \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection $collectionMock */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php index eb32c30ab4f86..9ca4496e53172 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php @@ -5,6 +5,9 @@ */ namespace Magento\CatalogInventory\Test\Unit\Model\Quote\Item\QuantityValidator\Initializer; +/** + * Class OptionTest + */ class OptionTest extends \PHPUnit\Framework\TestCase { /** @@ -67,6 +70,9 @@ class OptionTest extends \PHPUnit\Framework\TestCase */ protected $websiteId = 111; + /** + * @inheritdoc + */ protected function setUp() { $optionMethods = [ @@ -140,6 +146,9 @@ protected function setUp() ); } + /** + * @return void + */ public function testInitializeWhenResultIsDecimalGetBackordersMessageHasOptionQtyUpdate() { $optionValue = 5; @@ -151,7 +160,6 @@ public function testInitializeWhenResultIsDecimalGetBackordersMessageHasOptionQt $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); - $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); $this->stockRegistry @@ -212,6 +220,9 @@ public function testInitializeWhenResultIsDecimalGetBackordersMessageHasOptionQt $this->validator->initialize($this->optionMock, $this->quoteItemMock, $qty); } + /** + * @return void + */ public function testInitializeWhenResultNotDecimalGetBackordersMessageHasOptionQtyUpdate() { $optionValue = 5; @@ -222,7 +233,6 @@ public function testInitializeWhenResultNotDecimalGetBackordersMessageHasOptionQ $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); - $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); $this->stockRegistry @@ -267,6 +277,8 @@ public function testInitializeWhenResultNotDecimalGetBackordersMessageHasOptionQ } /** + * @return void + * * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage The stock item for Product in option is not valid. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php index 86a021768a6b3..884908aa1c378 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php @@ -119,6 +119,9 @@ class QuantityValidatorTest extends \PHPUnit\Framework\TestCase */ private $stockStatusMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManager($this); @@ -278,8 +281,13 @@ public function testValidateWithOptions() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); + $optionMock->expects($this->once()) + ->method('getStockStateResult') + ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') ->willReturn($this->stockItemMock); @@ -316,7 +324,7 @@ public function testValidateWithOptionsAndError() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') @@ -324,6 +332,11 @@ public function testValidateWithOptionsAndError() $this->stockRegistryMock->expects($this->at(1)) ->method('getStockStatus') ->willReturn($this->stockStatusMock); + $optionMock->expects($this->once()) + ->method('getStockStateResult') + ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $options = [$optionMock]; $this->createInitialStub(1); $this->setUpStubForQuantity(1, true); @@ -354,12 +367,17 @@ public function testValidateAndRemoveErrorsFromQuote() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); $quoteItem = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods(['getItemId', 'getErrorInfos']) ->getMock(); + $optionMock->expects($this->once()) + ->method('getStockStateResult') + ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') ->willReturn($this->stockItemMock); @@ -441,6 +459,10 @@ public function testException() $this->quantityValidator->validate($this->observerMock); } + /** + * @param $qty + * @param $hasError + */ private function setUpStubForQuantity($qty, $hasError) { $this->productMock->expects($this->any()) @@ -471,6 +493,9 @@ private function setUpStubForQuantity($qty, $hasError) ->willReturn(''); } + /** + * @param $qty + */ private function createInitialStub($qty) { $this->storeMock->expects($this->any()) @@ -528,6 +553,9 @@ private function createInitialStub($qty) ->willReturn($this->resultMock); } + /** + * @return void + */ private function setUpStubForRemoveError() { $quoteItems = [$this->quoteItemMock]; diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php index d60a1f3e400dd..8c9a1aa7715ec 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php @@ -84,6 +84,7 @@ public function testInitializeWithSubitem() 'setMessage', 'setBackorders', '__wakeup', + 'setStockStateResult' ] ) ->disableOriginalConstructor() @@ -178,6 +179,7 @@ public function testInitializeWithSubitem() $quoteItem->expects($this->once())->method('setMessage')->with('message')->will($this->returnSelf()); $result->expects($this->exactly(2))->method('getItemBackorders')->will($this->returnValue('backorders')); $quoteItem->expects($this->once())->method('setBackorders')->with('backorders')->will($this->returnSelf()); + $quoteItem->expects($this->once())->method('setStockStateResult')->with($result)->will($this->returnSelf()); $this->model->initialize($stockItem, $quoteItem, $qty); } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php index 9a46dc99ee008..0598fe7e9fe71 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php @@ -56,9 +56,13 @@ public function testProcess() [] ) ->willReturnSelf(); - $this->select->expects($this->once()) + + $this->select->expects($this->exactly(2)) ->method('where') - ->with('stock.stock_status = ?', Stock::STOCK_IN_STOCK) + ->withConsecutive( + ['stock.stock_status = ?', Stock::STOCK_IN_STOCK, null], + ['stock.website_id = ?', 0, null] + ) ->willReturnSelf(); $this->stockStatusBaseSelectProcessor->process($this->select); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php new file mode 100644 index 0000000000000..003dffdbd4d38 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php @@ -0,0 +1,207 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Test\Unit\Model\ResourceModel; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\CatalogInventory\Model\Configuration as StockConfiguration; +use Magento\CatalogInventory\Model\ResourceModel\Stock; +use Magento\Framework\App\Config; +use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Test for \Magento\CatalogInventory\Model\ResourceModel\Stock + */ +class StockTest extends \PHPUnit\Framework\TestCase +{ + const PRODUCT_TABLE = 'testProductTable'; + const ITEM_TABLE = 'testItemTableName'; + + /** + * @var Stock|\PHPUnit_Framework_MockObject_MockObject + */ + private $stock; + + /** + * @var Mysql|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var DateTime|\PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeMock; + + /** + * @var StockConfiguration|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockConfigurationMock; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $selectMock; + + /** + * @var \Zend_Db_Statement_Interface|\PHPUnit_Framework_MockObject_MockObject + */ + private $statementMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $objectManager->getObject(Context::class); + $this->scopeConfigMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfiguration::class) + ->setMethods(['getIsQtyTypeIds', 'getDefaultScopeId']) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connectionMock = $this->getMockBuilder(Mysql::class) + ->disableOriginalConstructor() + ->getMock(); + $this->statementMock = $this->getMockForAbstractClass(\Zend_Db_Statement_Interface::class); + $this->stock = $this->getMockBuilder(Stock::class) + ->setMethods(['getTable', 'getConnection']) + ->setConstructorArgs( + [ + 'context' => $this->contextMock, + 'scopeConfig' => $this->scopeConfigMock, + 'dateTime' => $this->dateTimeMock, + 'stockConfiguration' => $this->stockConfigurationMock, + 'storeManager' => $this->storeManagerMock, + ] + )->getMock(); + } + + /** + * Test Save Product Status per website with product ids. + * + * @dataProvider productsDataProvider + * @param int $websiteId + * @param array $productIds + * @param array $products + * @param array $result + * + * @return void + */ + public function testLockProductsStock($websiteId, array $productIds, array $products, array $result) + { + $this->selectMock->expects($this->exactly(2)) + ->method('from') + ->withConsecutive( + [$this->identicalTo(['si' => self::ITEM_TABLE])], + [$this->identicalTo(['p' => self::PRODUCT_TABLE]), $this->identicalTo([])] + ) + ->willReturnSelf(); + $this->selectMock->expects($this->exactly(3)) + ->method('where') + ->withConsecutive( + [$this->identicalTo('website_id = ?'), $this->identicalTo($websiteId)], + [$this->identicalTo('product_id IN(?)'), $this->identicalTo($productIds)], + [$this->identicalTo('entity_id IN (?)'), $this->identicalTo($productIds)] + ) + ->willReturnSelf(); + $this->selectMock->expects($this->once()) + ->method('forUpdate') + ->with($this->identicalTo(true)) + ->willReturnSelf(); + $this->selectMock->expects($this->once()) + ->method('columns') + ->with($this->identicalTo(['product_id' => 'entity_id', 'type_id' => 'type_id'])) + ->willReturnSelf(); + $this->connectionMock->expects($this->exactly(2)) + ->method('select') + ->willReturn($this->selectMock); + $this->connectionMock->expects($this->once()) + ->method('query') + ->with($this->identicalTo($this->selectMock)) + ->willReturn($this->statementMock); + $this->statementMock->expects($this->once()) + ->method('fetchAll') + ->willReturn($products); + $this->connectionMock->expects($this->once()) + ->method('fetchAll') + ->with($this->identicalTo($this->selectMock)) + ->willReturn($result); + $this->stock->expects($this->exactly(2)) + ->method('getTable') + ->withConsecutive( + [$this->identicalTo('cataloginventory_stock_item')], + [$this->identicalTo('catalog_product_entity')] + )->will($this->onConsecutiveCalls( + self::ITEM_TABLE, + self::PRODUCT_TABLE + )); + $this->stock->expects($this->exactly(4)) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $lockResult = $this->stock->lockProductsStock($productIds, $websiteId); + + $this->assertEquals($result, $lockResult); + } + + /** + * @return array + */ + public function productsDataProvider() + { + return [ + [ + 0, + [1, 2, 3], + [ + 1 => ['product_id' => 1], + 2 => ['product_id' => 2], + 3 => ['product_id' => 3], + ], + [ + 1 => [ + 'product_id' => 1, + 'type_id' => 'simple', + ], + 2 => [ + 'product_id' => 2, + 'type_id' => 'simple', + ], + 3 => [ + 'product_id' => 3, + 'type_id' => 'simple', + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Source/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Source/StockTest.php new file mode 100644 index 0000000000000..11f41fcaf6d01 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Source/StockTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogInventory\Test\Unit\Model\Source; + +use PHPUnit\Framework\TestCase; + +class StockTest extends TestCase +{ + /** + * @var \Magento\CatalogInventory\Model\Source\Stock + */ + private $model; + + protected function setUp() + { + $this->model = new \Magento\CatalogInventory\Model\Source\Stock(); + } + + public function testAddValueSortToCollection() + { + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $collectionMock = $this->createMock(\Magento\Eav\Model\Entity\Collection\AbstractCollection::class); + $collectionMock->expects($this->atLeastOnce())->method('getSelect')->willReturn($selectMock); + + $selectMock->expects($this->once()) + ->method('joinLeft') + ->with( + ['stock_item_table' => 'cataloginventory_stock_item'], + "e.entity_id=stock_item_table.product_id", + [] + ) + ->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('order') + ->with("stock_item_table.qty DESC") + ->willReturnSelf(); + + $this->model->addValueSortToCollection($collectionMock); + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php index 5c75249b7cbf8..b542dd219af1d 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php @@ -243,41 +243,67 @@ public function testCheckQuoteItemQty(StockItemInterface $stockItem, $expectedRe ); } + /** + * @return array + */ public function verifyStockDataProvider() { return $this->prepareDataForMethod('verifyStock'); } + /** + * @return array + */ public function verifyNotificationDataProvider() { return $this->prepareDataForMethod('verifyNotification'); } + /** + * @return array + */ public function checkQtyDataProvider() { return $this->prepareDataForMethod('checkQty'); } + /** + * @return array + */ public function suggestQtyDataProvider() { return $this->prepareDataForMethod('suggestQty'); } + /** + * @return array + */ public function getStockQtyDataProvider() { return $this->prepareDataForMethod('getStockQty'); } + /** + * @return array + */ public function checkQtyIncrementsDataProvider() { return $this->prepareDataForMethod('checkQtyIncrements'); } + /** + * @return array + */ public function checkQuoteItemQtyDataProvider() { return $this->prepareDataForMethod('checkQuoteItemQty'); } + /** + * @param $methodName + * + * @return array + */ protected function prepareDataForMethod($methodName) { $variations = []; @@ -318,6 +344,9 @@ protected function prepareDataForMethod($methodName) return $variations; } + /** + * @return array + */ protected function getVariations() { $stockQty = 100; @@ -430,6 +459,9 @@ public function testCheckQtyIncrementsMsg($isChildItem, $expectedMsg) $this->assertEquals($expectedMsg, $result->getMessage()->render()); } + /** + * @return array + */ public function checkQtyIncrementsMsgDataProvider() { return [ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/ItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/ItemTest.php index bbc7823b13e01..359b0e80f1b74 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/ItemTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/ItemTest.php @@ -394,6 +394,7 @@ public function testGetQtyIncrements($config, $expected) $this->setDataArrayValue('qty_increments', $config['qty_increments']); $this->setDataArrayValue('enable_qty_increments', $config['enable_qty_increments']); $this->setDataArrayValue('use_config_qty_increments', $config['use_config_qty_increments']); + $this->setDataArrayValue('is_qty_decimal', $config['is_qty_decimal']); if ($config['use_config_qty_increments']) { $this->stockConfiguration->expects($this->once()) ->method('getQtyIncrements') @@ -415,7 +416,8 @@ public function getQtyIncrementsDataProvider() [ 'qty_increments' => 1, 'enable_qty_increments' => true, - 'use_config_qty_increments' => true + 'use_config_qty_increments' => true, + 'is_qty_decimal' => false ], 1 ], @@ -423,7 +425,8 @@ public function getQtyIncrementsDataProvider() [ 'qty_increments' => -2, 'enable_qty_increments' => true, - 'use_config_qty_increments' => true + 'use_config_qty_increments' => true, + 'is_qty_decimal' => false ], false ], @@ -431,10 +434,20 @@ public function getQtyIncrementsDataProvider() [ 'qty_increments' => 3, 'enable_qty_increments' => true, - 'use_config_qty_increments' => false + 'use_config_qty_increments' => false, + 'is_qty_decimal' => false ], 3 ], + [ + [ + 'qty_increments' => 0.5, + 'enable_qty_increments' => true, + 'use_config_qty_increments' => false, + 'is_qty_decimal' => true + ], + 0.5 + ], ]; } @@ -469,6 +482,9 @@ public function testDispatchEvents($eventName, $methodName, $objectName) ); } + /** + * @return array + */ public function eventsDataProvider() { return [ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockManagementTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockManagementTest.php new file mode 100644 index 0000000000000..8d3f310101d95 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockManagementTest.php @@ -0,0 +1,290 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Test\Unit\Model; + +use Magento\CatalogInventory\Model\StockState; +use Magento\CatalogInventory\Model\StockManagement; +use Magento\CatalogInventory\Model\StockRegistryStorage; +use Magento\CatalogInventory\Model\ResourceModel\QtyCounterInterface; +use Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface; +use Magento\CatalogInventory\Model\ResourceModel\Stock as ResourceStock; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; + +/** + * Test for \Magento\CatalogInventory\Model\StockManagement + */ +class StockManagementTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var StockManagement|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockManagement; + + /** + * @var ResourceStock|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockResourceMock; + + /** + * @var StockRegistryProviderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockRegistryProviderMock; + + /** + * @var StockState|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockStateMock; + + /** + * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockConfigurationMock; + + /** + * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepositoryMock; + + /** + * @var QtyCounterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $qtyCounterMock; + + /** + * @var StockRegistryStorage|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockRegistryStorageMock; + + /** + * @var StockItemInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockItemInterfaceMock; + + /** + * @var int + */ + private $websiteId = 0; + + protected function setUp() + { + $this->stockResourceMock = $this->getMockBuilder(ResourceStock::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockRegistryProviderMock = $this->getMockBuilder(StockRegistryProviderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->stockStateMock = $this->getMockBuilder(StockState::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->qtyCounterMock = $this->getMockBuilder(QtyCounterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->stockRegistryStorageMock = $this->getMockBuilder(StockRegistryStorage::class) + ->disableOriginalConstructor() + ->getMock(); + $this->stockItemInterfaceMock = $this->getMockBuilder(StockItemInterface::class) + ->setMethods(['hasAdminArea','getWebsiteId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->stockManagement = $this->getMockBuilder(StockManagement::class) + ->setMethods(['getResource', 'canSubtractQty']) + ->setConstructorArgs( + [ + 'stockResource' => $this->stockResourceMock, + 'stockRegistryProvider' => $this->stockRegistryProviderMock, + 'stockState' => $this->stockStateMock, + 'stockConfiguration' => $this->stockConfigurationMock, + 'productRepository' => $this->productRepositoryMock, + 'qtyCounter' => $this->qtyCounterMock, + 'stockRegistryStorage' => $this->stockRegistryStorageMock, + ] + )->getMock(); + + $this->stockConfigurationMock + ->expects($this->once()) + ->method('getDefaultScopeId') + ->willReturn($this->websiteId); + $this->stockManagement + ->expects($this->any()) + ->method('getResource') + ->willReturn($this->stockResourceMock); + $this->stockRegistryProviderMock + ->expects($this->any()) + ->method('getStockItem') + ->willReturn($this->stockItemInterfaceMock); + $this->stockItemInterfaceMock + ->expects($this->any()) + ->method('hasAdminArea') + ->willReturn(false); + } + + /** + * @dataProvider productsWithCorrectQtyDataProvider + * + * @param array $items + * @param array $lockedItems + * @param bool $canSubtract + * @param bool $isQty + * @param bool $verifyStock + * + * @return void + */ + public function testRegisterProductsSale( + array $items, + array $lockedItems, + $canSubtract, + $isQty, + $verifyStock = true + ) { + $this->stockResourceMock + ->expects($this->once()) + ->method('beginTransaction'); + $this->stockResourceMock + ->expects($this->once()) + ->method('lockProductsStock') + ->willReturn([$lockedItems]); + $this->stockItemInterfaceMock + ->expects($this->any()) + ->method('getItemId') + ->willReturn($lockedItems['product_id']); + $this->stockManagement + ->expects($this->any()) + ->method('canSubtractQty') + ->willReturn($canSubtract); + $this->stockConfigurationMock + ->expects($this->any()) + ->method('isQty') + ->willReturn($isQty); + $this->stockItemInterfaceMock + ->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($this->websiteId); + $this->stockStateMock + ->expects($this->any()) + ->method('checkQty') + ->willReturn(true); + $this->stockStateMock + ->expects($this->any()) + ->method('verifyStock') + ->willReturn($verifyStock); + $this->stockStateMock + ->expects($this->any()) + ->method('verifyNotification') + ->willReturn(false); + $this->stockResourceMock + ->expects($this->once()) + ->method('commit'); + + $this->stockManagement->registerProductsSale($items, $this->websiteId); + } + + /** + * @dataProvider productsWithIncorrectQtyDataProvider + * + * @param array $items + * @param array $lockedItems + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Not all of your products are available in the requested quantity. + * + * @return void + */ + public function testRegisterProductsSaleException(array $items, array $lockedItems) + { + $this->stockResourceMock + ->expects($this->once()) + ->method('beginTransaction'); + $this->stockResourceMock + ->expects($this->once()) + ->method('lockProductsStock') + ->willReturn([$lockedItems]); + $this->stockItemInterfaceMock + ->expects($this->any()) + ->method('getItemId') + ->willReturn($lockedItems['product_id']); + $this->stockManagement + ->expects($this->any()) + ->method('canSubtractQty') + ->willReturn(true); + $this->stockConfigurationMock + ->expects($this->any()) + ->method('isQty') + ->willReturn(true); + $this->stockStateMock + ->expects($this->any()) + ->method('checkQty') + ->willReturn(false); + $this->stockResourceMock + ->expects($this->once()) + ->method('commit'); + + $this->stockManagement->registerProductsSale($items, $this->websiteId); + } + + /** + * @return array + */ + public function productsWithCorrectQtyDataProvider() + { + return [ + [ + [1 => 3], + [ + 'product_id' => 1, + 'qty' => 10, + 'type_id' => 'simple', + ], + false, + false, + ], + [ + [2 => 4], + [ + 'product_id' => 2, + 'qty' => 10, + 'type_id' => 'simple', + ], + true, + true, + ], + [ + [3 => 5], + [ + 'product_id' => 3, + 'qty' => 10, + 'type_id' => 'simple', + ], + true, + true, + false, + ], + ]; + } + + /** + * @return array + */ + public function productsWithIncorrectQtyDataProvider() + { + return [ + [ + [2 => 4], + [ + 'product_id' => 2, + 'qty' => 2, + 'type_id' => 'simple', + ], + ], + ]; + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php index c04bd1e9e4402..03e60a9d39d5e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogInventory\Test\Unit\Model; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class StockRegistryTest */ @@ -16,37 +18,188 @@ class StockRegistryTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogInventory\Api\StockItemCriteriaInterface|MockObject */ protected $criteria; + /** + * @var \Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory|MockObject + */ + private $criteriaFactory; + + /** + * @var \Magento\CatalogInventory\Api\Data\StockItemInterface|MockObject + */ + private $stockItemMock; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject + */ + private $stockConfigurationMock; + + /** + * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface|MockObject + */ + private $stockItemRepositoryMock; + + /** + * @var \Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface|MockObject + */ + private $stockRegistryProviderMock; + + /** + * @var \Magento\Catalog\Model\ProductFactory|MockObject + */ + private $productFactoryMock; + protected function setUp() { $this->criteria = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockItemCriteriaInterface::class) ->disableOriginalConstructor() ->getMock(); - $criteriaFactory = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory::class) - ->setMethods(['create']) + $this->criteriaFactory = $this->getMockBuilder( + \Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory::class + )->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $criteriaFactory->expects($this->once())->method('create')->willReturn($this->criteria); + + $this->stockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getWebsiteId', + 'getData', + 'addData', + 'getManageStock', + 'getIsInStock', + 'getQty', + 'getOrigData', + 'setIsInStock', + 'setStockStatusChangedAutomaticallyFlag', + ]) + ->getMockForAbstractClass(); + + $this->stockConfigurationMock = $this->createMock( + \Magento\CatalogInventory\Api\StockConfigurationInterface::class + ); + $this->stockRegistryProviderMock = $this->createMock( + \Magento\CatalogInventory\Model\Spi\StockRegistryProviderInterface::class + ); + $this->stockItemRepositoryMock = $this->createMock( + \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class + ); + $this->productFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create']); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( \Magento\CatalogInventory\Model\StockRegistry::class, [ - 'criteriaFactory' => $criteriaFactory + 'stockConfiguration' => $this->stockConfigurationMock, + 'stockRegistryProvider' => $this->stockRegistryProviderMock, + 'stockItemRepository' => $this->stockItemRepositoryMock, + 'criteriaFactory' => $this->criteriaFactory, + 'productFactory' => $this->productFactoryMock, ] ); } public function testGetLowStockItems() { + $this->criteriaFactory->expects($this->once())->method('create')->willReturn($this->criteria); $this->criteria->expects($this->once())->method('setLimit')->with(1, 0); $this->criteria->expects($this->once())->method('setScopeFilter')->with(1); $this->criteria->expects($this->once())->method('setQtyFilter')->with('<='); $this->criteria->expects($this->once())->method('addField')->with('qty'); $this->model->getLowStockItems(1, 100); } + + /** + * @return void + */ + public function testUpdateStockItemBySku() + { + $manageStock = 1; + $isInStock = 0; + $qty = 10; + $origQty = 0; + + $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn($manageStock); + $this->stockItemMock->expects($this->once())->method('getIsInStock')->willReturn($isInStock); + $this->stockItemMock->expects($this->once())->method('getQty')->willReturn($qty); + $this->stockItemMock->expects($this->exactly(2)) + ->method('getOrigData') + ->with(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY) + ->willReturn($origQty); + + $this->stockItemMock->expects($this->once())->method('setIsInStock')->with(true)->willReturnSelf(); + $this->stockItemMock->expects($this->once()) + ->method('setStockStatusChangedAutomaticallyFlag') + ->with(true) + ->willReturnSelf(); + + $this->configureAndCallUpdateStockItemBySku(); + } + + /** + * @return void + */ + public function testUpdateStockItemBySkuWithoutUpdateStockStatus() + { + $manageStock = 0; + + $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn($manageStock); + $this->stockItemMock->expects($this->never())->method('getIsInStock'); + $this->stockItemMock->expects($this->never())->method('getQty'); + $this->stockItemMock->expects($this->never()) + ->method('getOrigData') + ->with(\Magento\CatalogInventory\Api\Data\StockItemInterface::QTY); + + $this->stockItemMock->expects($this->never())->method('setIsInStock')->with(true); + $this->stockItemMock->expects($this->never())->method('setStockStatusChangedAutomaticallyFlag')->with(true); + + $this->configureAndCallUpdateStockItemBySku(); + } + + /** + * @return void + */ + private function configureAndCallUpdateStockItemBySku() + { + $productId = 1; + $productSku = 'Simple'; + $websiteId = 0; + $scopeId = 1; + $data = ['item_id' => 1, 'is_in_stock' => 1]; + + /** @var \Magento\CatalogInventory\Api\Data\StockItemInterface|MockObject $origStockItemMock */ + $origStockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getItemId', 'addData', 'setProductId']) + ->getMockForAbstractClass(); + + /** @var \Magento\Catalog\Model\Product|MockObject $productMock */ + $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getIdBySku']); + $this->productFactoryMock->expects($this->once())->method('create')->willReturn($productMock); + $productMock->expects($this->once())->method('getIdBySku')->with($productSku)->willReturn($productId); + + $this->stockItemMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); + $this->stockItemMock->expects($this->once())->method('getData')->willReturn($data); + + $origStockItemMock->expects($this->exactly(2))->method('getItemId')->willReturn(null); + $origStockItemMock->expects($this->once())->method('addData')->with($data)->willReturnSelf(); + $origStockItemMock->expects($this->once())->method('setProductId')->with($productId)->willReturnSelf(); + + $this->stockConfigurationMock->expects($this->once())->method('getDefaultScopeId')->willReturn($scopeId); + $this->stockRegistryProviderMock->expects($this->once()) + ->method('getStockItem') + ->with($productId, $scopeId) + ->willReturn($origStockItemMock); + + $this->stockItemRepositoryMock->expects($this->once()) + ->method('save') + ->with($origStockItemMock) + ->willReturn($origStockItemMock); + + $this->model->updateStockItemBySku($productSku, $this->stockItemMock); + } } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockTest.php index c3abad50ef9f4..9ecab4dca77e3 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockTest.php @@ -125,7 +125,10 @@ public function testDispatchEvents($eventName, $methodName, $objectName) sprintf('Event "%s" with object name "%s" doesn\'t dispatched properly', $eventName, $objectName) ); } - + + /** + * @return array + */ public function eventsDataProvider() { return [ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php new file mode 100644 index 0000000000000..8de05bd014039 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogInventory\Test\Unit\Observer; + +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\CatalogInventory\Api\Data\StockItemCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Observer\AddStockItemsObserver; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +class AddStockItemsObserverTest extends TestCase +{ + /** + * Test subject. + * + * @var AddStockItemsObserver + */ + private $subject; + /** + * @var StockItemCriteriaInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $criteriaInterfaceFactoryMock; + + /** + * @var StockItemRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockItemRepositoryMock; + + /** + * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $stockConfigurationMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->criteriaInterfaceFactoryMock = $this->getMockBuilder(StockItemCriteriaInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class) + ->setMethods(['getList']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) + ->setMethods(['getDefaultScopeId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->subject = $objectManager->getObject( + AddStockItemsObserver::class, + [ + 'criteriaInterfaceFactory' => $this->criteriaInterfaceFactoryMock, + 'stockItemRepository' => $this->stockItemRepositoryMock, + 'stockConfiguration' => $this->stockConfigurationMock + ] + ); + } + + /** + * Test AddStockItemsObserver::execute() add stock item to product as extension attribute. + */ + public function testExecute() + { + $productId = 1; + $defaultScopeId = 0; + + $criteria = $this->getMockBuilder(StockItemCriteriaInterface::class) + ->setMethods(['setProductsFilter', 'setScopeFilter']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $criteria->expects(self::once()) + ->method('setProductsFilter') + ->with(self::identicalTo([$productId])) + ->willReturn(true); + $criteria->expects(self::once()) + ->method('setScopeFilter') + ->with(self::identicalTo($defaultScopeId)) + ->willReturn(true); + + $this->criteriaInterfaceFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($criteria); + $stockItemCollection = $this->getMockBuilder(StockItemCollectionInterface::class) + ->setMethods(['getItems']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $stockItem = $this->getMockBuilder(StockItemInterface::class) + ->setMethods(['getProductId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $stockItem->expects(self::once()) + ->method('getProductId') + ->willReturn($productId); + + $stockItemCollection->expects(self::once()) + ->method('getItems') + ->willReturn([$stockItem]); + + $this->stockItemRepositoryMock->expects(self::once()) + ->method('getList') + ->with(self::identicalTo($criteria)) + ->willReturn($stockItemCollection); + + $this->stockConfigurationMock->expects(self::once()) + ->method('getDefaultScopeId') + ->willReturn($defaultScopeId); + + $productExtension = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['setStockItem']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $productExtension->expects(self::once()) + ->method('setStockItem') + ->with(self::identicalTo($stockItem)); + + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $product->expects(self::once()) + ->method('getExtensionAttributes') + ->willReturn($productExtension); + $product->expects(self::once()) + ->method('setExtensionAttributes') + ->with(self::identicalTo($productExtension)) + ->willReturnSelf(); + + /** @var ProductCollection|\PHPUnit_Framework_MockObject_MockObject $productCollection */ + $productCollection = $this->getMockBuilder(ProductCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $productCollection->expects(self::once()) + ->method('getItems') + ->willReturn([$productId => $product]); + $productCollection->expects(self::once()) + ->method('getItemById') + ->with(self::identicalTo($productId)) + ->willReturn($product); + + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observer */ + $observer = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->getMock(); + $observer->expects(self::once()) + ->method('getData') + ->with('collection') + ->willReturn($productCollection); + + $this->subject->execute($observer); + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/UpdateItemsStockUponConfigChangeObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/UpdateItemsStockUponConfigChangeObserverTest.php index 70a179b484379..7b82b5927d22c 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Observer/UpdateItemsStockUponConfigChangeObserverTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/UpdateItemsStockUponConfigChangeObserverTest.php @@ -15,9 +15,9 @@ class UpdateItemsStockUponConfigChangeObserverTest extends \PHPUnit\Framework\Te protected $observer; /** - * @var \Magento\CatalogInventory\Model\ResourceModel\Stock|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item|\PHPUnit_Framework_MockObject_MockObject */ - protected $resourceStock; + protected $resourceStockItem; /** * @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject @@ -31,11 +31,11 @@ class UpdateItemsStockUponConfigChangeObserverTest extends \PHPUnit\Framework\Te protected function setUp() { - $this->resourceStock = $this->createMock(\Magento\CatalogInventory\Model\ResourceModel\Stock::class); + $this->resourceStockItem = $this->createMock(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); $this->event = $this->getMockBuilder(\Magento\Framework\Event::class) ->disableOriginalConstructor() - ->setMethods(['getWebsite']) + ->setMethods(['getWebsite', 'getChangedPaths']) ->getMock(); $this->eventObserver = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) @@ -50,7 +50,7 @@ protected function setUp() $this->observer = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( \Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver::class, [ - 'resourceStock' => $this->resourceStock, + 'resourceStockItem' => $this->resourceStockItem, ] ); } @@ -58,13 +58,16 @@ protected function setUp() public function testUpdateItemsStockUponConfigChange() { $websiteId = 1; - $this->resourceStock->expects($this->once())->method('updateSetOutOfStock'); - $this->resourceStock->expects($this->once())->method('updateSetInStock'); - $this->resourceStock->expects($this->once())->method('updateLowStockDate'); + $this->resourceStockItem->expects($this->once())->method('updateSetOutOfStock'); + $this->resourceStockItem->expects($this->once())->method('updateSetInStock'); + $this->resourceStockItem->expects($this->once())->method('updateLowStockDate'); $this->event->expects($this->once()) ->method('getWebsite') ->will($this->returnValue($websiteId)); + $this->event->expects($this->once()) + ->method('getChangedPaths') + ->will($this->returnValue([\Magento\CatalogInventory\Model\Configuration::XML_PATH_MANAGE_STOCK])); $this->observer->execute($this->eventObserver); } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php index db183ae5c0da0..0ce62133d6f9b 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php @@ -106,6 +106,9 @@ public function testPrepareSource( $this->assertEquals($expectedResult, $this->useConfigSettings->getData('config')); } + /** + * @return array + */ public function prepareSourceDataProvider() { return [ diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php new file mode 100644 index 0000000000000..d66a783c6720d --- /dev/null +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Ui\DataProvider\Product; + +use Magento\Framework\Data\Collection; +use Magento\Ui\DataProvider\AddFieldToCollectionInterface; + +/** + * Add quantity_and_stock_status field to collection + */ +class AddQuantityAndStockStatusFieldToCollection implements AddFieldToCollectionInterface +{ + /** + * @inheritdoc + */ + public function addField(Collection $collection, $field, $alias = null) + { + $collection->joinField( + 'quantity_and_stock_status', + 'cataloginventory_stock_item', + 'is_in_stock', + 'product_id=entity_id', + '{{table}}.stock_id=1', + 'left' + ); + } +} diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index c5d18ea727534..1d62d99f48fe4 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -224,6 +224,7 @@ private function prepareMeta() $this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/sortOrder', $this->meta ) - 1, + 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ]; $qty['arguments']['data']['config'] = [ 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer', diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index 71172e26698fe..c70c0d9641130 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-inventory", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", @@ -10,10 +10,11 @@ "magento/module-eav": "101.0.*", "magento/module-quote": "101.0.*", "magento/framework": "101.0.*", - "magento/module-ui": "101.0.*" + "magento/module-ui": "101.0.*", + "magento/module-sales": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml index 803a6dae492a0..601f7ef61973b 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml @@ -23,6 +23,7 @@ <arguments> <argument name="addFieldStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFieldToCollection</item> + <item name="quantity_and_stock_status" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection</item> </argument> <argument name="addFilterStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFilterToCollection</item> diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml index b9332575c96f7..546f838b9b428 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml @@ -41,13 +41,13 @@ <![CDATA[Please note that these settings apply to individual items in the cart, not to the entire cart.]]> </comment> <label>Product Stock Options</label> - <field id="manage_stock" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="manage_stock" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Manage Stock</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\CatalogInventory\Model\Config\Backend\Managestock</backend_model> <comment>Changing can take some time due to processing whole catalog.</comment> </field> - <field id="backorders" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="backorders" translate="label comment" type="select" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Backorders</label> <source_model>Magento\CatalogInventory\Model\Source\Backorders</source_model> <backend_model>Magento\CatalogInventory\Model\Config\Backend\Backorders</backend_model> @@ -55,7 +55,7 @@ </field> <field id="max_sale_qty" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Maximum Qty Allowed in Shopping Cart</label> - <validate>validate-number</validate> + <validate>validate-number validate-greater-than-zero</validate> </field> <field id="min_qty" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Out-of-Stock Threshold</label> diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 65bc277121429..8c47865da6aa7 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -44,7 +44,7 @@ </type> <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> <arguments> - <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> + <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> <type name="Magento\Catalog\Model\Layer"> @@ -111,11 +111,29 @@ <argument name="batchSizeManagement" xsi:type="object">Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement</argument> </arguments> </type> - <type name="\Magento\Framework\Data\CollectionModifier"> + <type name="Magento\Framework\Data\CollectionModifier"> <arguments> <argument name="conditions" xsi:type="array"> <item name="stockStatusCondition" xsi:type="object">Magento\CatalogInventory\Model\ProductCollectionStockCondition</item> </argument> </arguments> </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface"> + <arguments> + <argument name="priceModifiers" xsi:type="array"> + <item name="inventoryProductPriceIndexFilter" xsi:type="object">Magento\CatalogInventory\Model\Indexer\ProductPriceIndexFilter</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier"> + <arguments> + <argument name="priceModifiers" xsi:type="array"> + <item name="inventoryProductPriceIndexFilter" xsi:type="object">Magento\CatalogInventory\Model\Indexer\ProductPriceIndexFilter</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> + <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> + </type> </config> diff --git a/app/code/Magento/CatalogInventory/etc/events.xml b/app/code/Magento/CatalogInventory/etc/events.xml index 3b5f2483ec57e..328edbade6068 100644 --- a/app/code/Magento/CatalogInventory/etc/events.xml +++ b/app/code/Magento/CatalogInventory/etc/events.xml @@ -27,9 +27,6 @@ <event name="sales_model_service_quote_submit_failure"> <observer name="inventory" instance="Magento\CatalogInventory\Observer\RevertQuoteInventoryObserver"/> </event> - <event name="restore_quote"> - <observer name="inventory" instance="Magento\CatalogInventory\Observer\RevertQuoteInventoryObserver"/> - </event> <event name="sales_order_item_cancel"> <observer name="inventory" instance="Magento\CatalogInventory\Observer\CancelOrderItemObserver"/> </event> @@ -41,5 +38,9 @@ </event> <event name="admin_system_config_changed_section_cataloginventory"> <observer name="inventory" instance="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"/> + <observer name="invalidatePriceIndex" instance="Magento\CatalogInventory\Observer\InvalidatePriceIndexUponConfigChangeObserver"/> + </event> + <event name="sales_quote_item_collection_products_after_load"> + <observer name="add_stock_items" instance="Magento\CatalogInventory\Observer\AddStockItemsObserver"/> </event> </config> diff --git a/app/code/Magento/CatalogInventory/etc/mview.xml b/app/code/Magento/CatalogInventory/etc/mview.xml index c3d73ff43e8eb..72dda16e8b5bb 100644 --- a/app/code/Magento/CatalogInventory/etc/mview.xml +++ b/app/code/Magento/CatalogInventory/etc/mview.xml @@ -11,4 +11,9 @@ <table name="cataloginventory_stock_item" entity_column="product_id" /> </subscriptions> </view> + <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> + <subscriptions> + <table name="cataloginventory_stock_item" entity_column="product_id" /> + </subscriptions> + </view> </config> diff --git a/app/code/Magento/CatalogInventory/i18n/en_US.csv b/app/code/Magento/CatalogInventory/i18n/en_US.csv index 93406163cbe1b..19b73f847b46d 100644 --- a/app/code/Magento/CatalogInventory/i18n/en_US.csv +++ b/app/code/Magento/CatalogInventory/i18n/en_US.csv @@ -55,11 +55,7 @@ Inventory,Inventory "Only X left Threshold","Only X left Threshold" "Display Products Availability in Stock on Storefront","Display Products Availability in Stock on Storefront" "Product Stock Options","Product Stock Options" -" - Please note that these settings apply to individual items in the cart, not to the entire cart. - "," - Please note that these settings apply to individual items in the cart, not to the entire cart. - " +"Please note that these settings apply to individual items in the cart, not to the entire cart.","Please note that these settings apply to individual items in the cart, not to the entire cart." "Manage Stock","Manage Stock" Backorders,Backorders "Maximum Qty Allowed in Shopping Cart","Maximum Qty Allowed in Shopping Cart" diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index 3472f4368d617..feaaee864ec9b 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -286,6 +286,7 @@ <scopeLabel>[GLOBAL]</scopeLabel> <validation> <rule name="validate-number" xsi:type="boolean">true</rule> + <rule name="validate-greater-than-zero" xsi:type="boolean">true</rule> </validation> <label translate="true">Maximum Qty Allowed in Shopping Cart</label> <dataScope>max_sale_qty</dataScope> @@ -568,7 +569,7 @@ <settings> <scopeLabel>[GLOBAL]</scopeLabel> <validation> - <rule name="validate-digits" xsi:type="boolean">true</rule> + <rule name="validate-integer" xsi:type="boolean">true</rule> <rule name="validate-number" xsi:type="boolean">true</rule> </validation> <label translate="true">Qty Increments</label> diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml index 8e0dbf1278ed2..8b63d29be8154 100644 --- a/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml +++ b/app/code/Magento/CatalogInventory/view/frontend/templates/qtyincrements.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\CatalogInventory\Block\Qtyincrements */ ?> <?php if ($block->getProductQtyIncrements()) : ?> <div class="product pricing"> - <?= /* @escapeNotVerified */ __('%1 is available to buy in increments of %2', $block->getProductName(), $block->getProductQtyIncrements()) ?> + <?= $block->escapeHtml(__('%1 is available to buy in increments of %2', $block->getProductName(), $block->getProductQtyIncrements())) ?> </div> <?php endif ?> diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml index 481ed1297a801..de667d19fadb0 100644 --- a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml +++ b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/composite.phtml @@ -4,30 +4,28 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\CatalogInventory\Block\Stockqty\Composite */ ?> -<?php if ($block->isMsgVisible()): ?> +<?php if ($block->isMsgVisible()) : ?> <div class="availability only"> <a href="#" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass": "active", "baseToggleClass": "expanded", "toggleContainers": "#<?= /* @escapeNotVerified */ $block->getDetailsPlaceholderId() ?>"}}' - id="<?= /* @escapeNotVerified */ $block->getPlaceholderId() ?>" - title="<?= /* @escapeNotVerified */ __('Only %1 left', ($block->getStockQtyLeft())) ?>" + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass": "active", "baseToggleClass": "expanded", "toggleContainers": "#<?= $block->escapeHtmlAttr($block->getDetailsPlaceholderId()) ?>"}}' + id="<?= $block->escapeHtmlAttr($block->getPlaceholderId()) ?>" + title="<?= $block->escapeHtmlAttr(__('Only %1 left', ($block->getStockQtyLeft()))) ?>" class="action show"> - <?= /* @escapeNotVerified */ __('Only %1 left', "<strong>{$block->getStockQtyLeft()}</strong>") ?> + <?= /* @noEscape */ __('Only %1 left', "<strong>{$block->escapeHtml($block->getStockQtyLeft())}</strong>") ?> </a> </div> - <div class="availability only detailed" id="<?= /* @escapeNotVerified */ $block->getDetailsPlaceholderId() ?>"> + <div class="availability only detailed" id="<?= $block->escapeHtmlAttr($block->getDetailsPlaceholderId()) ?>"> <div class="table-wrapper"> <table class="data table"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Product availability') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('Product availability')) ?></caption> <thead> <tr> - <th class="col item" scope="col"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col qty" scope="col"><?= /* @escapeNotVerified */ __('Qty') ?></th> + <th class="col item" scope="col"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col qty" scope="col"><?= $block->escapeHtml(__('Qty')) ?></th> </tr> </thead> <tbody> @@ -35,8 +33,8 @@ <?php $childProductStockQty = $block->getProductStockQty($childProduct); ?> <?php if ($childProductStockQty > 0) : ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"><?= /* @escapeNotVerified */ $childProduct->getName() ?></td> - <td data-th="<?= $block->escapeHtml(__('Qty')) ?>" class="col qty"><?= /* @escapeNotVerified */ $childProductStockQty ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>" class="col item"><?= $block->escapeHtml($childProduct->getName()) ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Qty')) ?>" class="col qty"><?= $block->escapeHtml($childProductStockQty) ?></td> </tr> <?php endif ?> <?php endforeach ?> diff --git a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml index 43fb697de2621..c32cb9dd6ecda 100644 --- a/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml +++ b/app/code/Magento/CatalogInventory/view/frontend/templates/stockqty/default.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\CatalogInventory\Block\Stockqty\DefaultStockqty */ ?> -<?php if ($block->isMsgVisible()): ?> - <div class="availability only" title="<?= /* @escapeNotVerified */ __('Only %1 left', ($block->getStockQtyLeft())) ?>"> - <?= /* @escapeNotVerified */ __('Only %1 left', "<strong>{$block->getStockQtyLeft()}</strong>") ?> +<?php if ($block->isMsgVisible()) : ?> + <div class="availability only" title="<?= $block->escapeHtmlAttr(__('Only %1 left', ($block->getStockQtyLeft()))) ?>"> + <?= /* @noEscape */ __('Only %1 left', "<strong>{$block->escapeHtml($block->getStockQtyLeft())}</strong>") ?> </div> <?php endif ?> diff --git a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php index 6390822b58f4a..184cb6419294f 100644 --- a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php +++ b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php @@ -25,7 +25,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/CatalogRule/Block/Adminhtml/Promo/Widget/Chooser/Sku.php b/app/code/Magento/CatalogRule/Block/Adminhtml/Promo/Widget/Chooser/Sku.php index 333ee845798ec..306d3b9a347b4 100644 --- a/app/code/Magento/CatalogRule/Block/Adminhtml/Promo/Widget/Chooser/Sku.php +++ b/app/code/Magento/CatalogRule/Block/Adminhtml/Promo/Widget/Chooser/Sku.php @@ -207,7 +207,7 @@ protected function _prepareColumns() public function getGridUrl() { return $this->getUrl( - 'catalog_rule/*/chooser', + '*/*/chooser', ['_current' => true, 'current_grid_id' => $this->getId(), 'collapse' => null] ); } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/ApplyRules.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/ApplyRules.php index 85ad74f7bbfe2..4badfa1219e10 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/ApplyRules.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/ApplyRules.php @@ -25,14 +25,14 @@ public function execute() $ruleJob->applyAll(); if ($ruleJob->hasSuccess()) { - $this->messageManager->addSuccess($ruleJob->getSuccess()); + $this->messageManager->addSuccessMessage($ruleJob->getSuccess()); $this->_objectManager->create(\Magento\CatalogRule\Model\Flag::class)->loadSelf()->setState(0)->save(); } elseif ($ruleJob->hasError()) { - $this->messageManager->addError($errorMessage . ' ' . $ruleJob->getError()); + $this->messageManager->addErrorMessage($errorMessage . ' ' . $ruleJob->getError()); } } catch (\Exception $e) { $this->_objectManager->create(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError($errorMessage); + $this->messageManager->addErrorMessage($errorMessage); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php index 8b007031f3305..be8a5a1556193 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php @@ -12,9 +12,14 @@ class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog { /** * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $id = $this->getRequest()->getParam('id'); if ($id) { try { @@ -25,13 +30,13 @@ public function execute() $ruleRepository->deleteById($id); $this->_objectManager->create(\Magento\CatalogRule\Model\Flag::class)->loadSelf()->setState(1)->save(); - $this->messageManager->addSuccess(__('You deleted the rule.')); + $this->messageManager->addSuccessMessage(__('You deleted the rule.')); $this->_redirect('catalog_rule/*/'); return; } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('We can\'t delete this rule right now. Please review the log and try again.') ); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -39,7 +44,7 @@ public function execute() return; } } - $this->messageManager->addError(__('We can\'t find a rule to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a rule to delete.')); $this->_redirect('catalog_rule/*/'); } } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php index 97a5693b18117..945c28b2088f2 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php @@ -24,7 +24,7 @@ public function execute() try { $model = $ruleRepository->get($id); } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { - $this->messageManager->addError(__('This rule no longer exists.')); + $this->messageManager->addErrorMessage(__('This rule no longer exists.')); $this->_redirect('catalog_rule/*'); return; } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php index 0170fc76b6aab..a0a636fd654ad 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php @@ -7,10 +7,14 @@ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filter\LocalizedToNormalizedFactory; +use Magento\Framework\Filter\NormalizedToLocalizedFactory; use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\Filter\Date; -use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -22,20 +26,44 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog */ protected $dataPersistor; + /** + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @var LocalizedToNormalizedFactory + */ + private $localizedToNormalizedFactory; + + /** + * @var NormalizedToLocalizedFactory + */ + private $normalizedToLocalizedFactory; + /** * @param Context $context * @param Registry $coreRegistry * @param Date $dateFilter * @param DataPersistorInterface $dataPersistor + * @param TimezoneInterface $localeDate + * @param LocalizedToNormalizedFactory $localizedToNormalizedFactory + * @param NormalizedToLocalizedFactory $normalizedToLocalizedFactory */ public function __construct( Context $context, Registry $coreRegistry, Date $dateFilter, - DataPersistorInterface $dataPersistor + DataPersistorInterface $dataPersistor, + TimezoneInterface $localeDate, + LocalizedToNormalizedFactory $localizedToNormalizedFactory, + NormalizedToLocalizedFactory $normalizedToLocalizedFactory ) { - $this->dataPersistor = $dataPersistor; parent::__construct($context, $coreRegistry, $dateFilter); + $this->dataPersistor = $dataPersistor; + $this->localeDate = $localeDate; + $this->localizedToNormalizedFactory = $localizedToNormalizedFactory; + $this->normalizedToLocalizedFactory = $normalizedToLocalizedFactory; } /** @@ -60,6 +88,8 @@ public function execute() ['request' => $this->getRequest()] ); $data = $this->getRequest()->getPostValue(); + + $data = $this->formatDateFields($data); $id = $this->getRequest()->getParam('rule_id'); if ($id) { $model = $ruleRepository->get($id); @@ -68,7 +98,7 @@ public function execute() $validateResult = $model->validateData(new \Magento\Framework\DataObject($data)); if ($validateResult !== true) { foreach ($validateResult as $errorMessage) { - $this->messageManager->addError($errorMessage); + $this->messageManager->addErrorMessage($errorMessage); } $this->_getSession()->setPageData($data); $this->dataPersistor->set('catalog_rule', $data); @@ -81,6 +111,9 @@ public function execute() unset($data['rule']); } + unset($data['conditions_serialized']); + unset($data['actions_serialized']); + $model->loadPost($data); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setPageData($data); @@ -88,7 +121,7 @@ public function execute() $ruleRepository->save($model); - $this->messageManager->addSuccess(__('You saved the rule.')); + $this->messageManager->addSuccessMessage(__('You saved the rule.')); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setPageData(false); $this->dataPersistor->clear('catalog_rule'); @@ -111,9 +144,9 @@ public function execute() } return; } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('Something went wrong while saving the rule data. Please review the error log.') ); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -125,4 +158,39 @@ public function execute() } $this->_redirect('catalog_rule/*/'); } + + /** + * Format date fields from localized to internal format. + * + * @param array $data + * @return array + */ + private function formatDateFields(array $data): array + { + $filterInput = $this->localizedToNormalizedFactory->create( + [ + 'options' => [ + 'locale' => $this->_localeResolver->getLocale(), + 'date_format' => $this->localeDate->getDateFormat(\IntlDateFormatter::SHORT), + ], + ] + ); + $filterInternal = $this->normalizedToLocalizedFactory->create( + [ + 'options' => [ + 'date_format' => DateTime::DATE_INTERNAL_FORMAT, + ], + ] + ); + + foreach ($data as $fieldName => $fieldValue) { + if (in_array($fieldName, ['from_date', 'to_date']) && !empty($fieldValue)) { + $fieldValue = $filterInput->filter($fieldValue); + $fieldValue = $filterInternal->filter($fieldValue); + $data[$fieldName] = $fieldValue; + } + } + + return $data; + } } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php index 3d9dcd05f8fac..3d1ac9744ef8b 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php @@ -32,7 +32,7 @@ public function __construct(Context $context, Registry $coreRegistry) /** * Initialize category object in registry * - * @return Category + * @return Category|bool */ protected function _initCategory() { @@ -77,10 +77,11 @@ public function execute() if (!($category = $this->_initCategory())) { return; } + $selected = $this->getRequest()->getPost('selected', ''); $block = $this->_view->getLayout()->createBlock( \Magento\Catalog\Block\Adminhtml\Category\Checkboxes\Tree::class )->setCategoryIds( - [$categoryId] + explode(',', $selected) ); $this->getResponse()->representJson( $block->getTreeJson($category) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php index 5d93e6f216866..6b7c12dfdf463 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php @@ -10,6 +10,9 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Indexer\CacheContext; +/** + * Abstract class for CatalogRule indexers. + */ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInterface, IdentityInterface { /** @@ -66,7 +69,6 @@ public function executeFull() { $this->indexBuilder->reindexFull(); $this->_eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); - //TODO: remove after fix fpc. MAGETWO-50668 $this->getCacheManager()->clean($this->getIdentities()); } @@ -137,8 +139,9 @@ public function executeRow($id) abstract protected function doExecuteRow($id); /** - * @return \Magento\Framework\App\CacheInterface|mixed + * Get cache manager * + * @return \Magento\Framework\App\CacheInterface|mixed * @deprecated 100.0.7 */ private function getCacheManager() diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index f2dd8968a903d..d9e6e338e6d9d 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -7,11 +7,13 @@ namespace Magento\CatalogRule\Model\Indexer; use Magento\Catalog\Model\Product; +use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule; use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; /** * @api @@ -132,9 +134,9 @@ class IndexBuilder private $pricesPersistor; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var TableSwapper */ - private $activeTableSwitcher; + private $tableSwapper; /** * @var ProductLoader @@ -160,7 +162,9 @@ class IndexBuilder * @param RuleProductPricesPersistor|null $pricesPersistor * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher * @param ProductLoader|null $productLoader + * @param TableSwapper|null $tableSwapper * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( RuleCollectionFactory $ruleCollectionFactory, @@ -180,7 +184,8 @@ public function __construct( ReindexRuleProductPrice $reindexRuleProductPrice = null, RuleProductPricesPersistor $pricesPersistor = null, \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, - ProductLoader $productLoader = null + ProductLoader $productLoader = null, + TableSwapper $tableSwapper = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -212,12 +217,11 @@ public function __construct( $this->pricesPersistor = $pricesPersistor ?? ObjectManager::getInstance()->get( RuleProductPricesPersistor::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?? ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class - ); $this->productLoader = $productLoader ?? ObjectManager::getInstance()->get( ProductLoader::class ); + $this->tableSwapper = $tableSwapper ?? + ObjectManager::getInstance()->get(TableSwapper::class); } /** @@ -260,14 +264,14 @@ public function reindexByIds(array $ids) */ protected function doReindexByIds($ids) { - $this->cleanByIds($ids); + $this->cleanProductIndex($ids); $products = $this->productLoader->getProducts($ids); - foreach ($this->getActiveRules() as $rule) { - foreach ($products as $product) { - $this->applyRule($rule, $product); - } + $activeRules = $this->getActiveRules(); + foreach ($products as $product) { + $this->applyRules($activeRules, $product); } + $this->reindexRuleGroupWebsite->execute(); } /** @@ -296,13 +300,6 @@ public function reindexFull() */ protected function doReindexFull() { - $this->connection->truncateTable( - $this->getTable($this->activeTableSwitcher->getAdditionalTableName('catalogrule_product')) - ); - $this->connection->truncateTable( - $this->getTable($this->activeTableSwitcher->getAdditionalTableName('catalogrule_product_price')) - ); - foreach ($this->getAllRules() as $rule) { $this->reindexRuleProduct->execute($rule, $this->batchCount, true); } @@ -310,8 +307,7 @@ protected function doReindexFull() $this->reindexRuleProductPrice->execute($this->batchCount, null, true); $this->reindexRuleGroupWebsite->execute(true); - $this->activeTableSwitcher->switchTable( - $this->connection, + $this->tableSwapper->swapIndexTables( [ $this->getTable('catalogrule_product'), $this->getTable('catalogrule_product_price'), @@ -320,6 +316,28 @@ protected function doReindexFull() ); } + /** + * Clean product index + * + * @param array $productIds + */ + private function cleanProductIndex(array $productIds) + { + $where = ['product_id IN (?)' => $productIds]; + $this->connection->delete($this->getTable('catalogrule_product'), $where); + } + + /** + * Clean product price index + * + * @param array $productIds + */ + private function cleanProductPriceIndex(array $productIds) + { + $where = ['product_id IN (?)' => $productIds]; + $this->connection->delete($this->getTable('catalogrule_product_price'), $where); + } + /** * Clean by product ids * @@ -328,51 +346,35 @@ protected function doReindexFull() */ protected function cleanByIds($productIds) { - $query = $this->connection->deleteFromSelect( - $this->connection - ->select() - ->from($this->resource->getTableName('catalogrule_product'), 'product_id') - ->distinct() - ->where('product_id IN (?)', $productIds), - $this->resource->getTableName('catalogrule_product') - ); - $this->connection->query($query); - - $query = $this->connection->deleteFromSelect( - $this->connection->select() - ->from($this->resource->getTableName('catalogrule_product_price'), 'product_id') - ->distinct() - ->where('product_id IN (?)', $productIds), - $this->resource->getTableName('catalogrule_product_price') - ); - $this->connection->query($query); + $this->cleanProductIndex($productIds); + $this->cleanProductPriceIndex($productIds); } /** + * Assign product to rule + * * @param Rule $rule * @param Product $product - * @return $this - * @throws \Exception - * @SuppressWarnings(PHPMD.NPathComplexity) + * @return void */ - protected function applyRule(Rule $rule, $product) + private function assignProductToRule(Rule $rule, Product $product) { - $ruleId = $rule->getId(); - $productEntityId = $product->getId(); - $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); - if (!$rule->validate($product)) { - return $this; + return; } + $ruleId = (int) $rule->getId(); + $productEntityId = (int) $product->getId(); + $ruleProductTable = $this->getTable('catalogrule_product'); $this->connection->delete( - $this->resource->getTableName('catalogrule_product'), + $ruleProductTable, [ - $this->connection->quoteInto('rule_id = ?', $ruleId), - $this->connection->quoteInto('product_id = ?', $productEntityId) + 'rule_id = ?' => $ruleId, + 'product_id = ?' => $productEntityId, ] ); + $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); $customerGroupIds = $rule->getCustomerGroupIds(); $fromTime = strtotime($rule->getFromDate()); $toTime = strtotime($rule->getToDate()); @@ -383,43 +385,62 @@ protected function applyRule(Rule $rule, $product) $actionStop = $rule->getStopRulesProcessing(); $rows = []; - try { - foreach ($websiteIds as $websiteId) { - foreach ($customerGroupIds as $customerGroupId) { - $rows[] = [ - 'rule_id' => $ruleId, - 'from_time' => $fromTime, - 'to_time' => $toTime, - 'website_id' => $websiteId, - 'customer_group_id' => $customerGroupId, - 'product_id' => $productEntityId, - 'action_operator' => $actionOperator, - 'action_amount' => $actionAmount, - 'action_stop' => $actionStop, - 'sort_order' => $sortOrder, - ]; - - if (count($rows) == $this->batchCount) { - $this->connection->insertMultiple($this->getTable('catalogrule_product'), $rows); - $rows = []; - } + foreach ($websiteIds as $websiteId) { + foreach ($customerGroupIds as $customerGroupId) { + $rows[] = [ + 'rule_id' => $ruleId, + 'from_time' => $fromTime, + 'to_time' => $toTime, + 'website_id' => $websiteId, + 'customer_group_id' => $customerGroupId, + 'product_id' => $productEntityId, + 'action_operator' => $actionOperator, + 'action_amount' => $actionAmount, + 'action_stop' => $actionStop, + 'sort_order' => $sortOrder, + ]; + + if (count($rows) == $this->batchCount) { + $this->connection->insertMultiple($ruleProductTable, $rows); + $rows = []; } } - - if (!empty($rows)) { - $this->connection->insertMultiple($this->resource->getTableName('catalogrule_product'), $rows); - } - } catch (\Exception $e) { - throw $e; } + if ($rows) { + $this->connection->insertMultiple($ruleProductTable, $rows); + } + } + /** + * Apply rule + * + * @param Rule $rule + * @param Product $product + * @return $this + * @throws \Exception + */ + protected function applyRule(Rule $rule, $product) + { + $this->assignProductToRule($rule, $product); $this->reindexRuleProductPrice->execute($this->batchCount, $product); $this->reindexRuleGroupWebsite->execute(); return $this; } + private function applyRules(RuleCollection $ruleCollection, Product $product) + { + foreach ($ruleCollection as $rule) { + $this->assignProductToRule($rule, $product); + } + + $this->cleanProductPriceIndex([$product->getId()]); + $this->reindexRuleProductPrice->execute($this->batchCount, $product); + } + /** + * Retrieve table name + * * @param string $tableName * @return string */ @@ -429,6 +450,8 @@ protected function getTable($tableName) } /** + * Update rule product data + * * @param Rule $rule * @return $this * @deprecated 100.2.0 @@ -454,6 +477,8 @@ protected function updateRuleProductData(Rule $rule) } /** + * Apply all rules + * * @param Product|null $product * @throws \Exception * @return $this @@ -493,6 +518,8 @@ protected function deleteOldData() } /** + * Calculate rule product price + * * @param array $ruleData * @param null $productData * @return float @@ -505,6 +532,8 @@ protected function calcRuleProductPrice($ruleData, $productData = null) } /** + * Get rule products statement + * * @param int $websiteId * @param Product|null $product * @return \Zend_Db_Statement_Interface @@ -518,6 +547,8 @@ protected function getRuleProductsStmt($websiteId, Product $product = null) } /** + * Save rule product prices + * * @param array $arrData * @return $this * @throws \Exception @@ -533,7 +564,7 @@ protected function saveRuleProductPrices($arrData) /** * Get active rules * - * @return array + * @return RuleCollection */ protected function getActiveRules() { @@ -543,7 +574,7 @@ protected function getActiveRules() /** * Get active rules * - * @return array + * @return RuleCollection */ protected function getAllRules() { @@ -551,6 +582,8 @@ protected function getAllRules() } /** + * Get product + * * @param int $productId * @return Product */ @@ -563,6 +596,8 @@ protected function getProduct($productId) } /** + * Log critical exception + * * @param \Exception $e * @return void */ diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php new file mode 100644 index 0000000000000..514c737598793 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogRule\Model\Indexer; + +use Magento\Framework\App\ResourceConnection; + +/** + * @inheritDoc + */ +class IndexerTableSwapper implements IndexerTableSwapperInterface +{ + /** + * Keys are original tables' names, values - created temporary tables. + * + * @var string[] + */ + private $temporaryTables = []; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->resourceConnection = $resource; + } + + /** + * Create temporary table based on given table to use instead of original. + * + * @param string $originalTableName + * + * @return string Created table name. + * @throws \Throwable + */ + private function createTemporaryTable(string $originalTableName): string + { + $temporaryTableName = $this->resourceConnection->getTableName( + $originalTableName . '__temp' . $this->generateRandomSuffix() + ); + + $this->resourceConnection->getConnection()->query( + sprintf( + 'create table %s like %s', + $temporaryTableName, + $this->resourceConnection->getTableName($originalTableName) + ) + ); + + return $temporaryTableName; + } + + /** + * Random suffix for temporary tables not to conflict with each other. + * + * @return string + */ + private function generateRandomSuffix(): string + { + return bin2hex(random_bytes(4)); + } + + /** + * @inheritDoc + */ + public function getWorkingTableName(string $originalTable): string + { + $originalTable = $this->resourceConnection->getTableName($originalTable); + if (!array_key_exists($originalTable, $this->temporaryTables)) { + $this->temporaryTables[$originalTable] + = $this->createTemporaryTable($originalTable); + } + + return $this->temporaryTables[$originalTable]; + } + + /** + * @inheritDoc + */ + public function swapIndexTables(array $originalTablesNames) + { + $toRename = []; + /** @var string[] $toDrop */ + $toDrop = []; + /** @var string[] $temporaryTablesRenamed */ + $temporaryTablesRenamed = []; + //Renaming temporary tables to original tables' names, dropping old + //tables. + foreach ($originalTablesNames as $tableName) { + $tableName = $this->resourceConnection->getTableName($tableName); + $temporaryOriginalName = $this->resourceConnection->getTableName( + $tableName . $this->generateRandomSuffix() + ); + $temporaryTableName = $this->getWorkingTableName($tableName); + $toRename[] = [ + 'oldName' => $tableName, + 'newName' => $temporaryOriginalName + ]; + $toRename[] = [ + 'oldName' => $temporaryTableName, + 'newName' => $tableName + ]; + $toDrop[] = $temporaryOriginalName; + $temporaryTablesRenamed[] = $tableName; + } + + //Swapping tables. + $this->resourceConnection->getConnection()->renameTablesBatch($toRename); + //Cleaning up. + foreach ($temporaryTablesRenamed as $tableName) { + unset($this->temporaryTables[$tableName]); + } + //Removing old ones. + foreach ($toDrop as $tableName) { + $this->resourceConnection->getConnection()->dropTable($tableName); + } + } +} diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapperInterface.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapperInterface.php new file mode 100644 index 0000000000000..dcb2bf4f96659 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapperInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogRule\Model\Indexer; + +/** + * Manage additional tables used while building new index to preserve + * index tables until the process finishes. + */ +interface IndexerTableSwapperInterface +{ + /** + * Get working table name used to build index. + * + * @param string $originalTable + * + * @return string + */ + public function getWorkingTableName(string $originalTable): string; + + /** + * Swap working tables with actual tables to save new indexes. + * + * @param string[] $originalTablesNames + * + * @return void + */ + public function swapIndexTables(array $originalTablesNames); +} diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php new file mode 100644 index 0000000000000..343d71748c875 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Indexer; + +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\CatalogRule\Model\ResourceModel\Rule\Product\Price; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; + +/** + * Class for adding catalog rule prices to price index table. + */ +class ProductPriceIndexModifier implements PriceModifierInterface +{ + /** + * @var Price + */ + private $priceResourceModel; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var string + */ + private $connectionName; + + /** + * @param Price $priceResourceModel + * @param ResourceConnection $resourceConnection + * @param string $connectionName + */ + public function __construct( + Price $priceResourceModel, + ResourceConnection $resourceConnection, + $connectionName = 'indexer' + ) { + $this->priceResourceModel = $priceResourceModel; + $this->resourceConnection = $resourceConnection ?: ObjectManager::getInstance()->get(ResourceConnection::class); + $this->connectionName = $connectionName; + } + + /** + * @inheritdoc + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) + { + $connection = $this->resourceConnection->getConnection($this->connectionName); + + $select = $connection->select(); + + $select->join( + ['cpiw' => $this->priceResourceModel->getTable('catalog_product_index_website')], + 'cpiw.website_id = i.' . $priceTable->getWebsiteField(), + [] + ); + $select->join( + ['cpp' => $this->priceResourceModel->getMainTable()], + 'cpp.product_id = i.' . $priceTable->getEntityField() + . ' AND cpp.customer_group_id = i.' . $priceTable->getCustomerGroupField() + . ' AND cpp.website_id = i.' . $priceTable->getWebsiteField() + . ' AND cpp.rule_date = cpiw.website_date', + [] + ); + if ($entityIds) { + $select->where('i.entity_id IN (?)', $entityIds); + } + + $finalPrice = $priceTable->getFinalPriceField(); + $finalPriceExpr = $select->getConnection()->getLeastSql([ + $priceTable->getFinalPriceField(), + $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $finalPrice), + ]); + $minPrice = $priceTable->getMinPriceField(); + $minPriceExpr = $select->getConnection()->getLeastSql([ + $priceTable->getMinPriceField(), + $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $minPrice), + ]); + $select->columns([ + $finalPrice => $finalPriceExpr, + $minPrice => $minPriceExpr, + ]); + + $query = $connection->updateFromSelect($select, ['i' => $priceTable->getTableName()]); + $connection->query($query); + } +} diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleGroupWebsite.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleGroupWebsite.php index cc5d07b18e0fa..249ed67ef2349 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleGroupWebsite.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleGroupWebsite.php @@ -6,6 +6,10 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; + /** * Reindex information about rule relations with customer groups and websites. */ @@ -27,23 +31,28 @@ class ReindexRuleGroupWebsite private $catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id']; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var TableSwapper */ - private $activeTableSwitcher; + private $tableSwapper; /** * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + * @param ActiveTableSwitcher $activeTableSwitcher + * @param TableSwapper|null $tableSwapper + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, \Magento\Framework\App\ResourceConnection $resource, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + ActiveTableSwitcher $activeTableSwitcher, + TableSwapper $tableSwapper = null ) { $this->dateTime = $dateTime; $this->resource = $resource; - $this->activeTableSwitcher = $activeTableSwitcher; + $this->tableSwapper = $tableSwapper ?? + ObjectManager::getInstance()->get(TableSwapper::class); } /** @@ -61,10 +70,10 @@ public function execute($useAdditionalTable = false) $ruleProductTable = $this->resource->getTableName('catalogrule_product'); if ($useAdditionalTable) { $indexTable = $this->resource->getTableName( - $this->activeTableSwitcher->getAdditionalTableName('catalogrule_group_website') + $this->tableSwapper->getWorkingTableName('catalogrule_group_website') ); $ruleProductTable = $this->resource->getTableName( - $this->activeTableSwitcher->getAdditionalTableName('catalogrule_product') + $this->tableSwapper->getWorkingTableName('catalogrule_product') ); } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php index 534061d593123..eaeb0ced19c88 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php @@ -6,48 +6,64 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Rule; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; + /** * Reindex rule relations with products. */ class ReindexRuleProduct { /** - * @var \Magento\Framework\App\ResourceConnection + * @var ResourceConnection */ private $resource; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var TableSwapper */ - private $activeTableSwitcher; + private $tableSwapper; /** - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @param ResourceConnection $resource + * @param ActiveTableSwitcher $activeTableSwitcher + * @param TableSwapper $tableSwapper + * @param TimezoneInterface $localeDate + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + ResourceConnection $resource, + ActiveTableSwitcher $activeTableSwitcher, + TableSwapper $tableSwapper, + TimezoneInterface $localeDate ) { $this->resource = $resource; - $this->activeTableSwitcher = $activeTableSwitcher; + $this->tableSwapper = $tableSwapper; + $this->localeDate = $localeDate; } /** * Reindex information about rule relations with products. * - * @param \Magento\CatalogRule\Model\Rule $rule + * @param Rule $rule * @param int $batchCount * @param bool $useAdditionalTable * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function execute( - \Magento\CatalogRule\Model\Rule $rule, - $batchCount, - $useAdditionalTable = false - ) { + public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) + { if (!$rule->getIsActive() || empty($rule->getWebsiteIds())) { return false; } @@ -65,27 +81,32 @@ public function execute( $indexTable = $this->resource->getTableName('catalogrule_product'); if ($useAdditionalTable) { $indexTable = $this->resource->getTableName( - $this->activeTableSwitcher->getAdditionalTableName('catalogrule_product') + $this->tableSwapper->getWorkingTableName('catalogrule_product') ); } $ruleId = $rule->getId(); $customerGroupIds = $rule->getCustomerGroupIds(); - $fromTime = strtotime($rule->getFromDate()); - $toTime = strtotime($rule->getToDate()); - $toTime = $toTime ? $toTime + \Magento\CatalogRule\Model\Indexer\IndexBuilder::SECONDS_IN_DAY - 1 : 0; $sortOrder = (int)$rule->getSortOrder(); $actionOperator = $rule->getSimpleAction(); $actionAmount = $rule->getDiscountAmount(); $actionStop = $rule->getStopRulesProcessing(); $rows = []; + foreach ($websiteIds as $websiteId) { + $scopeTz = new \DateTimeZone( + $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ); + $fromTime = (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp(); + $toTime = $rule->getToDate() + ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 + : 0; - foreach ($productIds as $productId => $validationByWebsite) { - foreach ($websiteIds as $websiteId) { + foreach ($productIds as $productId => $validationByWebsite) { if (empty($validationByWebsite[$websiteId])) { continue; } + foreach ($customerGroupIds as $customerGroupId) { $rows[] = [ 'rule_id' => $ruleId, @@ -110,6 +131,7 @@ public function execute( if (!empty($rows)) { $connection->insertMultiple($indexTable, $rows); } + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php index 6a87be3c50a64..11ba87730bec1 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php @@ -6,54 +6,58 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\StoreManagerInterface; + /** * Reindex product prices according rule settings. */ class ReindexRuleProductPrice { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder + * @var RuleProductsSelectBuilder */ private $ruleProductsSelectBuilder; /** - * @var \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator + * @var ProductPriceCalculator */ private $productPriceCalculator; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var TimezoneInterface */ - private $dateTime; + private $localeDate; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor + * @var RuleProductPricesPersistor */ private $pricesPersistor; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param StoreManagerInterface $storeManager * @param RuleProductsSelectBuilder $ruleProductsSelectBuilder * @param ProductPriceCalculator $productPriceCalculator - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor $pricesPersistor + * @param TimezoneInterface $localeDate + * @param RuleProductPricesPersistor $pricesPersistor */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder $ruleProductsSelectBuilder, - \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator $productPriceCalculator, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor $pricesPersistor + StoreManagerInterface $storeManager, + RuleProductsSelectBuilder $ruleProductsSelectBuilder, + ProductPriceCalculator $productPriceCalculator, + TimezoneInterface $localeDate, + RuleProductPricesPersistor $pricesPersistor ) { $this->storeManager = $storeManager; $this->ruleProductsSelectBuilder = $ruleProductsSelectBuilder; $this->productPriceCalculator = $productPriceCalculator; - $this->dateTime = $dateTime; + $this->localeDate = $localeDate; $this->pricesPersistor = $pricesPersistor; } @@ -61,22 +65,16 @@ public function __construct( * Reindex product prices. * * @param int $batchCount - * @param \Magento\Catalog\Model\Product|null $product + * @param Product|null $product * @param bool $useAdditionalTable * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function execute( - $batchCount, - \Magento\Catalog\Model\Product $product = null, - $useAdditionalTable = false - ) { - $fromDate = mktime(0, 0, 0, date('m'), date('d') - 1); - $toDate = mktime(0, 0, 0, date('m'), date('d') + 1); - + public function execute($batchCount, Product $product = null, $useAdditionalTable = false) + { /** * Update products rules prices per each website separately - * because of max join limit in mysql + * because for each website date in website's timezone should be used */ foreach ($this->storeManager->getWebsites() as $website) { $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $product, $useAdditionalTable); @@ -84,6 +82,13 @@ public function execute( $stopFlags = []; $prevKey = null; + $storeGroup = $this->storeManager->getGroup($website->getDefaultGroupId()); + $currentDate = $this->localeDate->scopeDate($storeGroup->getDefaultStoreId(), null, true); + $previousDate = (clone $currentDate)->modify('-1 day'); + $previousDate->setTime(23, 59, 59); + $nextDate = (clone $currentDate)->modify('+1 day'); + $nextDate->setTime(0, 0, 0); + while ($ruleData = $productsStmt->fetch()) { $ruleProductId = $ruleData['product_id']; $productKey = $ruleProductId . @@ -100,12 +105,11 @@ public function execute( } } - $ruleData['from_time'] = $this->roundTime($ruleData['from_time']); - $ruleData['to_time'] = $this->roundTime($ruleData['to_time']); /** * Build prices for each day */ - for ($time = $fromDate; $time <= $toDate; $time += IndexBuilder::SECONDS_IN_DAY) { + foreach ([$previousDate, $currentDate, $nextDate] as $date) { + $time = $date->getTimestamp(); if (($ruleData['from_time'] == 0 || $time >= $ruleData['from_time']) && ($ruleData['to_time'] == 0 || $time <= $ruleData['to_time']) @@ -118,7 +122,7 @@ public function execute( if (!isset($dayPrices[$priceKey])) { $dayPrices[$priceKey] = [ - 'rule_date' => $time, + 'rule_date' => $date, 'website_id' => $ruleData['website_id'], 'customer_group_id' => $ruleData['customer_group_id'], 'product_id' => $ruleProductId, @@ -151,18 +155,7 @@ public function execute( } $this->pricesPersistor->execute($dayPrices, $useAdditionalTable); } - return true; - } - /** - * @param int $timeStamp - * @return int - */ - private function roundTime($timeStamp) - { - if (is_numeric($timeStamp) && $timeStamp != 0) { - $timeStamp = $this->dateTime->timestamp($this->dateTime->date('Y-m-d 00:00:00', $timeStamp)); - } - return $timeStamp; + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php index 853be1888b5b9..9ac23e0b9158c 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php @@ -6,6 +6,10 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; + /** * Persist product prices to index table. */ @@ -22,23 +26,28 @@ class RuleProductPricesPersistor private $dateFormat; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var TableSwapper */ - private $activeTableSwitcher; + private $tableSwapper; /** * @param \Magento\Framework\Stdlib\DateTime $dateFormat * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + * @param ActiveTableSwitcher $activeTableSwitcher + * @param TableSwapper|null $tableSwapper + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\Stdlib\DateTime $dateFormat, \Magento\Framework\App\ResourceConnection $resource, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + ActiveTableSwitcher $activeTableSwitcher, + TableSwapper $tableSwapper = null ) { $this->dateFormat = $dateFormat; $this->resource = $resource; - $this->activeTableSwitcher = $activeTableSwitcher; + $this->tableSwapper = $tableSwapper ?? + ObjectManager::getInstance()->get(TableSwapper::class); } /** @@ -59,29 +68,23 @@ public function execute(array $priceData, $useAdditionalTable = false) $indexTable = $this->resource->getTableName('catalogrule_product_price'); if ($useAdditionalTable) { $indexTable = $this->resource->getTableName( - $this->activeTableSwitcher->getAdditionalTableName('catalogrule_product_price') + $this->tableSwapper->getWorkingTableName('catalogrule_product_price') ); } - $productIds = []; - - try { - foreach ($priceData as $key => $data) { - $productIds['product_id'] = $data['product_id']; - $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); - $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( - $data['latest_start_date'], - false - ); - $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( - $data['earliest_end_date'], - false - ); - } - $connection->insertOnDuplicate($indexTable, $priceData); - } catch (\Exception $e) { - throw $e; + foreach ($priceData as $key => $data) { + $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); + $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( + $data['latest_start_date'], + false + ); + $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( + $data['earliest_end_date'], + false + ); } + $connection->insertOnDuplicate($indexTable, $priceData); + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php index 25d164aeee5c3..f7e3f8c3654e6 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php @@ -6,6 +6,10 @@ namespace Magento\CatalogRule\Model\Indexer; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; + /** * Build select for rule relation with product. */ @@ -32,29 +36,34 @@ class RuleProductsSelectBuilder private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var TableSwapper */ - private $activeTableSwitcher; + private $tableSwapper; /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + * @param ActiveTableSwitcher $activeTableSwitcher + * @param TableSwapper|null $tableSwapper + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Eav\Model\Config $eavConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\EntityManager\MetadataPool $metadataPool, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher + ActiveTableSwitcher $activeTableSwitcher, + TableSwapper $tableSwapper = null ) { $this->eavConfig = $eavConfig; $this->storeManager = $storeManager; $this->metadataPool = $metadataPool; $this->resource = $resource; - $this->activeTableSwitcher = $activeTableSwitcher; + $this->tableSwapper = $tableSwapper ?? + ObjectManager::getInstance()->get(TableSwapper::class); } /** @@ -74,7 +83,7 @@ public function build( $indexTable = $this->resource->getTableName('catalogrule_product'); if ($useAdditionalTable) { $indexTable = $this->resource->getTableName( - $this->activeTableSwitcher->getAdditionalTableName('catalogrule_product') + $this->tableSwapper->getWorkingTableName('catalogrule_product') ); } diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php new file mode 100644 index 0000000000000..13809a381ecb9 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplier.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\CatalogRule\Model\Rule\Condition\Combine; +use Magento\Framework\Exception\InputException; +use Magento\CatalogRule\Model\Rule\Condition\ConditionsToSearchCriteriaMapper; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\AdvancedFilterProcessor; +use Magento\CatalogRule\Model\Rule\Condition\MappableConditionsProcessor; + +/** + * Applies catalog price rule conditions to product collection as filters + */ +class ConditionsToCollectionApplier +{ + /** + * @var ConditionsToSearchCriteriaMapper + */ + private $conditionsToSearchCriteriaMapper; + + /** + * @var AdvancedFilterProcessor + */ + private $searchCriteriaProcessor; + + /** + * @var MappableConditionsProcessor + */ + private $mappableConditionsProcessor; + + /** + * @param ConditionsToSearchCriteriaMapper $conditionsToSearchCriteriaMapper + * @param AdvancedFilterProcessor $searchCriteriaProcessor + * @param MappableConditionsProcessor $mappableConditionsProcessor + */ + public function __construct( + ConditionsToSearchCriteriaMapper $conditionsToSearchCriteriaMapper, + AdvancedFilterProcessor $searchCriteriaProcessor, + MappableConditionsProcessor $mappableConditionsProcessor + ) { + $this->conditionsToSearchCriteriaMapper = $conditionsToSearchCriteriaMapper; + $this->searchCriteriaProcessor = $searchCriteriaProcessor; + $this->mappableConditionsProcessor = $mappableConditionsProcessor; + } + + /** + * Transforms catalog rule conditions to search criteria + * and applies them on product collection + * + * @param Combine $conditions + * @param ProductCollection $productCollection + * @return ProductCollection + * @throws InputException + */ + public function applyConditionsToCollection( + Combine $conditions, + ProductCollection $productCollection + ): ProductCollection { + // rebuild conditions to have only those that we know how to map them to product collection + $mappableConditions = $this->mappableConditionsProcessor->rebuildConditionsTree($conditions); + + // transform conditions to search criteria + $searchCriteria = $this->conditionsToSearchCriteriaMapper->mapConditionsToSearchCriteria($mappableConditions); + + $mappedProductCollection = clone $productCollection; + + // apply search criteria to new version of product collection + $this->searchCriteriaProcessor->process($searchCriteria, $mappedProductCollection); + + return $mappedProductCollection; + } +} diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php index 00808f38c9132..3f396cacd37da 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php @@ -105,6 +105,7 @@ public function build($productId) ->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId()) ->where('t.rule_date = ?', $currentDate) ->order('t.rule_price ' . Select::SQL_ASC) + ->order(BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . '.' . $linkField . ' ' . Select::SQL_ASC) ->limit(1); $priceSelect = $this->baseSelectProcessor->process($priceSelect); diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 715b7a2f3903b..ebfe91504417c 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -6,10 +6,34 @@ namespace Magento\CatalogRule\Model; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogRule\Api\Data\RuleExtensionInterface; use Magento\CatalogRule\Api\Data\RuleInterface; +use Magento\CatalogRule\Helper\Data; +use Magento\CatalogRule\Model\Data\Condition\Converter; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; +use Magento\CatalogRule\Model\ResourceModel\Rule as RuleResourceModel; +use Magento\CatalogRule\Model\Rule\Action\CollectionFactory as RuleCollectionFactory; +use Magento\CatalogRule\Model\Rule\Condition\CombineFactory; +use Magento\Customer\Model\Session; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; +use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Data\FormFactory; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Model\ResourceModel\Iterator; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Catalog Rule data model @@ -17,7 +41,6 @@ * @method \Magento\CatalogRule\Model\Rule setFromDate(string $value) * @method \Magento\CatalogRule\Model\Rule setToDate(string $value) * @method \Magento\CatalogRule\Model\Rule setCustomerGroupIds(string $value) - * @method string getWebsiteIds() * @method \Magento\CatalogRule\Model\Rule setWebsiteIds(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -136,6 +159,21 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I */ protected $ruleConditionConverter; + /** + * @var ConditionsToCollectionApplier + */ + protected $conditionsToCollectionApplier; + + /** + * @var array + */ + private $websitesMap; + + /** + * @var RuleResourceModel + */ + private $ruleResourceModel; + /** * Rule constructor * @@ -161,32 +199,35 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I * @param ExtensionAttributesFactory|null $extensionFactory * @param AttributeValueFactory|null $customAttributeFactory * @param \Magento\Framework\Serialize\Serializer\Json $serializer - * + * @param ConditionsToCollectionApplier $conditionsToCollectionApplier + * @param RuleResourceModel|null $ruleResourceModel * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, - \Magento\Framework\Data\FormFactory $formFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\CatalogRule\Model\Rule\Condition\CombineFactory $combineFactory, - \Magento\CatalogRule\Model\Rule\Action\CollectionFactory $actionCollectionFactory, - \Magento\Catalog\Model\ProductFactory $productFactory, - \Magento\Framework\Model\ResourceModel\Iterator $resourceIterator, - \Magento\Customer\Model\Session $customerSession, - \Magento\CatalogRule\Helper\Data $catalogRuleData, - \Magento\Framework\App\Cache\TypeListInterface $cacheTypesList, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor $ruleProductProcessor, - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + Context $context, + Registry $registry, + FormFactory $formFactory, + TimezoneInterface $localeDate, + CollectionFactory $productCollectionFactory, + StoreManagerInterface $storeManager, + CombineFactory $combineFactory, + RuleCollectionFactory $actionCollectionFactory, + ProductFactory $productFactory, + Iterator $resourceIterator, + Session $customerSession, + Data $catalogRuleData, + TypeListInterface $cacheTypesList, + DateTime $dateTime, + RuleProductProcessor $ruleProductProcessor, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, array $relatedCacheTypes = [], array $data = [], ExtensionAttributesFactory $extensionFactory = null, AttributeValueFactory $customAttributeFactory = null, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + Json $serializer = null, + ConditionsToCollectionApplier $conditionsToCollectionApplier = null, + RuleResourceModel $ruleResourceModel = null ) { $this->_productCollectionFactory = $productCollectionFactory; $this->_storeManager = $storeManager; @@ -200,6 +241,9 @@ public function __construct( $this->_relatedCacheTypes = $relatedCacheTypes; $this->dateTime = $dateTime; $this->_ruleProductProcessor = $ruleProductProcessor; + $this->ruleResourceModel = $ruleResourceModel ?: ObjectManager::getInstance()->get(RuleResourceModel::class); + $this->conditionsToCollectionApplier = $conditionsToCollectionApplier + ?? ObjectManager::getInstance()->get(ConditionsToCollectionApplier::class); parent::__construct( $context, @@ -223,7 +267,7 @@ public function __construct( protected function _construct() { parent::_construct(); - $this->_init(\Magento\CatalogRule\Model\ResourceModel\Rule::class); + $this->_init(RuleResourceModel::class); $this->setIdFieldName('rule_id'); } @@ -255,7 +299,7 @@ public function getActionsInstance() public function getCustomerGroupIds() { if (!$this->hasCustomerGroupIds()) { - $customerGroupIds = $this->_getResource()->getCustomerGroupIds($this->getId()); + $customerGroupIds = $this->ruleResourceModel->getCustomerGroupIds($this->getId()); $this->setData('customer_group_ids', (array)$customerGroupIds); } return $this->_getData('customer_group_ids'); @@ -269,7 +313,7 @@ public function getCustomerGroupIds() public function getNow() { if (!$this->_now) { - return (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); + return (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT); } return $this->_now; } @@ -306,6 +350,11 @@ public function getMatchingProductIds() } $this->getConditions()->collectValidatedAttributes($productCollection); + if ($this->canPreMapProducts()) { + $productCollection = $this->conditionsToCollectionApplier + ->applyConditionsToCollection($this->getConditions(), $productCollection); + } + $this->_resourceIterator->walk( $productCollection->getSelect(), [[$this, 'callbackValidateProduct']], @@ -320,6 +369,21 @@ public function getMatchingProductIds() return $this->_productIds; } + /** + * @return bool + */ + private function canPreMapProducts() + { + $conditions = $this->getConditions(); + + // No need to map products if there is no conditions in rule + if (!$conditions || !$conditions->getConditions()) { + return false; + } + + return true; + } + /** * Callback function for product matching * @@ -348,22 +412,25 @@ public function callbackValidateProduct($args) */ protected function _getWebsitesMap() { - $map = []; - $websites = $this->_storeManager->getWebsites(); - foreach ($websites as $website) { - // Continue if website has no store to be able to create catalog rule for website without store - if ($website->getDefaultStore() === null) { - continue; + if ($this->websitesMap === null) { + $this->websitesMap = []; + $websites = $this->_storeManager->getWebsites(); + foreach ($websites as $website) { + // Continue if website has no store to be able to create catalog rule for website without store + if ($website->getDefaultStore() === null) { + continue; + } + $this->websitesMap[$website->getId()] = $website->getDefaultStore()->getId(); } - $map[$website->getId()] = $website->getDefaultStore()->getId(); } - return $map; + + return $this->websitesMap; } /** * {@inheritdoc} */ - public function validateData(\Magento\Framework\DataObject $dataObject) + public function validateData(DataObject $dataObject) { $result = parent::validateData($dataObject); if ($result === true) { @@ -470,7 +537,7 @@ public function calcProductPriceRule(Product $product, $price) */ protected function _getRulesFromProduct($dateTs, $websiteId, $customerGroupId, $productId) { - return $this->_getResource()->getRulesFromProduct($dateTs, $websiteId, $customerGroupId, $productId); + return $this->ruleResourceModel->getRulesFromProduct($dateTs, $websiteId, $customerGroupId, $productId); } /** @@ -516,10 +583,10 @@ protected function _invalidateCache() */ public function afterSave() { - if ($this->isObjectNew()) { - $this->getMatchingProductIds(); - if (!empty($this->_productIds) && is_array($this->_productIds)) { - $this->_getResource()->addCommitCallback([$this, 'reindex']); + if ($this->isObjectNew() && !$this->_ruleProductProcessor->isIndexerScheduled()) { + $productIds = $this->getMatchingProductIds(); + if (!empty($productIds) && is_array($productIds)) { + $this->ruleResourceModel->addCommitCallback([$this, 'reindex']); } } else { $this->_ruleProductProcessor->getIndexer()->invalidate(); @@ -534,7 +601,13 @@ public function afterSave() */ public function reindex() { - $this->_ruleProductProcessor->reindexList($this->_productIds); + $productIds = $this->_productIds ? array_keys(array_filter($this->_productIds, function (array $data) { + return array_filter($data); + })) : []; + + if (!empty($productIds)) { + $this->_ruleProductProcessor->reindexList($productIds); + } } /** @@ -765,7 +838,7 @@ public function getToDate() /** * {@inheritdoc} * - * @return \Magento\CatalogRule\Api\Data\RuleExtensionInterface|null + * @return RuleExtensionInterface|null */ public function getExtensionAttributes() { @@ -775,10 +848,10 @@ public function getExtensionAttributes() /** * {@inheritdoc} * - * @param \Magento\CatalogRule\Api\Data\RuleExtensionInterface $extensionAttributes + * @param RuleExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes(\Magento\CatalogRule\Api\Data\RuleExtensionInterface $extensionAttributes) + public function setExtensionAttributes(RuleExtensionInterface $extensionAttributes) { return $this->_setExtensionAttributes($extensionAttributes); } @@ -790,8 +863,8 @@ public function setExtensionAttributes(\Magento\CatalogRule\Api\Data\RuleExtensi private function getRuleConditionConverter() { if (null === $this->ruleConditionConverter) { - $this->ruleConditionConverter = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\CatalogRule\Model\Data\Condition\Converter::class); + $this->ruleConditionConverter = ObjectManager::getInstance() + ->get(Converter::class); } return $this->ruleConditionConverter; } diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php new file mode 100644 index 0000000000000..7dc832a28ac0e --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -0,0 +1,308 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Rule\Condition; + +use Magento\Framework\Exception\InputException; +use Magento\Rule\Model\Condition\ConditionInterface; +use Magento\CatalogRule\Model\Rule\Condition\Combine as CombinedCondition; +use Magento\CatalogRule\Model\Rule\Condition\Product as SimpleCondition; +use Magento\Framework\Api\CombinedFilterGroup as FilterGroup; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria; + +/** + * Maps catalog price rule conditions to search criteria + */ +class ConditionsToSearchCriteriaMapper +{ + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilderFactory + */ + private $searchCriteriaBuilderFactory; + + /** + * @var \Magento\Framework\Api\CombinedFilterGroupFactory + */ + private $combinedFilterGroupFactory; + + /** + * @var \Magento\Framework\Api\FilterFactory + */ + private $filterFactory; + + /** + * @param \Magento\Framework\Api\SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory + * @param \Magento\Framework\Api\CombinedFilterGroupFactory $combinedFilterGroupFactory + * @param \Magento\Framework\Api\FilterFactory $filterFactory + */ + public function __construct( + \Magento\Framework\Api\SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, + \Magento\Framework\Api\CombinedFilterGroupFactory $combinedFilterGroupFactory, + \Magento\Framework\Api\FilterFactory $filterFactory + ) { + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->combinedFilterGroupFactory = $combinedFilterGroupFactory; + $this->filterFactory = $filterFactory; + } + + /** + * Maps catalog price rule conditions to search criteria + * + * @param CombinedCondition $conditions + * @return SearchCriteria + * @throws InputException + */ + public function mapConditionsToSearchCriteria(CombinedCondition $conditions): SearchCriteria + { + $filterGroup = $this->mapCombinedConditionToFilterGroup($conditions); + + $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); + + if ($filterGroup !== null) { + $searchCriteriaBuilder->setFilterGroups([$filterGroup]); + } + + return $searchCriteriaBuilder->create(); + } + + /** + * @param ConditionInterface $condition + * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter + * @throws InputException + */ + private function mapConditionToFilterGroup(ConditionInterface $condition) + { + if ($condition->getType() === CombinedCondition::class) { + return $this->mapCombinedConditionToFilterGroup($condition); + } elseif ($condition->getType() === SimpleCondition::class) { + return $this->mapSimpleConditionToFilterGroup($condition); + } + + throw new InputException( + __('Undefined condition type "%1" passed in.', $condition->getType()) + ); + } + + /** + * @param Combine $combinedCondition + * @return null|\Magento\Framework\Api\CombinedFilterGroup + * @throws InputException + */ + private function mapCombinedConditionToFilterGroup(CombinedCondition $combinedCondition) + { + $filters = []; + + foreach ($combinedCondition->getConditions() as $condition) { + $filter = $this->mapConditionToFilterGroup($condition); + + if ($filter === null) { + continue; + } + + // This required to solve cases when condition is configured like: + // "If ALL/ANY of these conditions are FALSE" - we need to reverse SQL operator for this "FALSE" + if ((bool)$combinedCondition->getValue() === false) { + $this->reverseSqlOperatorInFilter($filter); + } + + $filters[] = $filter; + } + + if (count($filters) === 0) { + return null; + } + + return $this->createCombinedFilterGroup($filters, $combinedCondition->getAggregator()); + } + + /** + * @param ConditionInterface $productCondition + * @return FilterGroup|Filter + * @throws InputException + */ + private function mapSimpleConditionToFilterGroup(ConditionInterface $productCondition) + { + if (is_array($productCondition->getValue())) { + return $this->processSimpleConditionWithArrayValue($productCondition); + } + + return $this->createFilter( + $productCondition->getAttribute(), + (string) $productCondition->getValue(), + $productCondition->getOperator() + ); + } + + /** + * @param ConditionInterface $productCondition + * @return FilterGroup + * @throws InputException + */ + private function processSimpleConditionWithArrayValue(ConditionInterface $productCondition): FilterGroup + { + $filters = []; + + foreach ($productCondition->getValue() as $subValue) { + $filters[] = $this->createFilter( + $productCondition->getAttribute(), + (string) $subValue, + $productCondition->getOperator() + ); + } + + $combinationMode = $this->getGlueForArrayValues($productCondition->getOperator()); + + return $this->createCombinedFilterGroup($filters, $combinationMode); + } + + /** + * @param string $operator + * @return string + */ + private function getGlueForArrayValues(string $operator): string + { + if (in_array($operator, ['!=', '!{}', '!()'], true)) { + return 'all'; + } + + return 'any'; + } + + /** + * Reverse sql conditions to their corresponding negative analog + * + * @param Filter $filter + * @return void + * @throws InputException + */ + private function reverseSqlOperatorInFilter(Filter $filter) + { + $operatorsMap = [ + 'eq' => 'neq', + 'neq' => 'eq', + 'gteq' => 'lt', + 'lteq' => 'gt', + 'gt' => 'lteq', + 'lt' => 'gteq', + 'like' => 'nlike', + 'nlike' => 'like', + 'in' => 'nin', + 'nin' => 'in', + ]; + + if (!array_key_exists($filter->getConditionType(), $operatorsMap)) { + throw new InputException( + __( + 'Undefined SQL operator "%1" passed in. Valid operators are: %2', + $filter->getConditionType(), + implode(',', array_keys($operatorsMap)) + ) + ); + } + + $filter->setConditionType( + $operatorsMap[$filter->getConditionType()] + ); + } + + /** + * @param array $filters + * @param string $combinationMode + * @return FilterGroup + * @throws InputException + */ + private function createCombinedFilterGroup(array $filters, string $combinationMode): FilterGroup + { + return $this->combinedFilterGroupFactory->create([ + 'data' => [ + FilterGroup::FILTERS => $filters, + FilterGroup::COMBINATION_MODE => $this->mapRuleAggregatorToSQLAggregator($combinationMode) + ] + ]); + } + + /** + * @param string $field + * @param string $value + * @param string $conditionType + * @return Filter + * @throws InputException + */ + private function createFilter(string $field, string $value, string $conditionType): Filter + { + return $this->filterFactory->create([ + 'data' => [ + Filter::KEY_FIELD => $field, + Filter::KEY_VALUE => $value, + Filter::KEY_CONDITION_TYPE => $this->mapRuleOperatorToSQLCondition($conditionType) + ] + ]); + } + + /** + * Maps catalog price rule operators to their corresponding operators in SQL + * + * @param string $ruleOperator + * @return string + * @throws InputException + */ + private function mapRuleOperatorToSQLCondition(string $ruleOperator): string + { + $operatorsMap = [ + '==' => 'eq', // is + '!=' => 'neq', // is not + '>=' => 'gteq', // equals or greater than + '<=' => 'lteq', // equals or less than + '>' => 'gt', // greater than + '<' => 'lt', // less than + '{}' => 'like', // contains + '!{}' => 'nlike', // does not contains + '()' => 'in', // is one of + '!()' => 'nin', // is not one of + ]; + + if (!array_key_exists($ruleOperator, $operatorsMap)) { + throw new InputException( + __( + 'Undefined rule operator "%1" passed in. Valid operators are: %2', + $ruleOperator, + implode(',', array_keys($operatorsMap)) + ) + ); + } + + return $operatorsMap[$ruleOperator]; + } + + /** + * Map rule combine aggregations to corresponding SQL operator + * + * @param string $ruleAggregator + * @return string + * @throws InputException + */ + private function mapRuleAggregatorToSQLAggregator(string $ruleAggregator): string + { + $operatorsMap = [ + 'all' => 'AND', + 'any' => 'OR', + ]; + + if (!array_key_exists(strtolower($ruleAggregator), $operatorsMap)) { + throw new InputException( + __( + 'Undefined rule aggregator "%1" passed in. Valid operators are: %2', + $ruleAggregator, + implode(',', array_keys($operatorsMap)) + ) + ); + } + + return $operatorsMap[$ruleAggregator]; + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php new file mode 100644 index 0000000000000..63c3f62ad0590 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/MappableConditionsProcessor.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Rule\Condition; + +use Magento\Framework\Exception\InputException; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionProviderInterface; +use Magento\CatalogRule\Model\Rule\Condition\Combine as CombinedCondition; +use Magento\CatalogRule\Model\Rule\Condition\Product as SimpleCondition; + +/** + * Rebuilds catalog price rule conditions tree + * so only those conditions that can be mapped to search criteria are left + * + * Those conditions that can't be mapped are deleted from tree + * If deleted condition is part of combined condition with OR aggregation all this group will be removed + */ +class MappableConditionsProcessor +{ + /** + * @var CustomConditionProviderInterface + */ + private $customConditionProvider; + + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + + /** + * @param CustomConditionProviderInterface $customConditionProvider + * @param \Magento\Eav\Model\Config $eavConfig + */ + public function __construct( + CustomConditionProviderInterface $customConditionProvider, + \Magento\Eav\Model\Config $eavConfig + ) { + $this->customConditionProvider = $customConditionProvider; + $this->eavConfig = $eavConfig; + } + + /** + * @param Combine $conditions + * @return Combine + */ + public function rebuildConditionsTree(CombinedCondition $conditions): CombinedCondition + { + return $this->rebuildCombinedCondition($conditions); + } + + /** + * @param Combine $originalConditions + * @return Combine + * @throws InputException + */ + private function rebuildCombinedCondition(CombinedCondition $originalConditions): CombinedCondition + { + $validConditions = []; + $invalidConditions = []; + + foreach ($originalConditions->getConditions() as $condition) { + if ($condition->getType() === CombinedCondition::class) { + $rebuildSubCondition = $this->rebuildCombinedCondition($condition); + + if (count($rebuildSubCondition->getConditions()) > 0) { + $validConditions[] = $rebuildSubCondition; + } else { + $invalidConditions[] = $rebuildSubCondition; + } + + continue; + } + + if ($condition->getType() === SimpleCondition::class) { + if ($this->validateSimpleCondition($condition)) { + $validConditions[] = $condition; + } else { + $invalidConditions[] = $condition; + } + + continue; + } + + throw new InputException( + __('Undefined condition type "%1" passed in.', $condition->getType()) + ); + } + + // if resulted condition group has left no mappable conditions - we can remove it at all + if (count($invalidConditions) > 0 && strtolower($originalConditions->getAggregator()) === 'any') { + $validConditions = []; + } + + $rebuildCondition = clone $originalConditions; + $rebuildCondition->setConditions($validConditions); + + return $rebuildCondition; + } + + /** + * @param Product $originalConditions + * @return bool + */ + private function validateSimpleCondition(SimpleCondition $originalConditions): bool + { + return $this->canUseFieldForMapping($originalConditions->getAttribute()); + } + + /** + * Checks if condition field is mappable + * + * @param string $fieldName + * @return bool + */ + private function canUseFieldForMapping(string $fieldName): bool + { + // We can map field to search criteria if we have custom processor for it + if ($this->customConditionProvider->hasProcessorForField($fieldName)) { + return true; + } + + // Also we can map field to search criteria if it is an EAV attribute + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $fieldName); + + // We have this weird check for getBackendType() to verify that attribute really exists + // because due to eavConfig behavior even if pass non existing attribute code we still receive AbstractAttribute + // getAttributeId() is not sufficient too because some attributes don't have it - e.g. attribute_set_id + if ($attribute && $attribute->getBackendType() !== null) { + return true; + } + + // In any other cases we can't map field to search criteria + return false; + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php index 51cb6638af48b..ab650c94a0f08 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php @@ -99,6 +99,10 @@ protected function _prepareDatetimeValue($value, \Magento\Framework\Model\Abstra { $attribute = $model->getResource()->getAttribute($this->getAttribute()); if ($attribute && $attribute->getBackendType() == 'datetime') { + if (!$value) { + return null; + } + $this->setValue(strtotime($this->getValue())); $value = strtotime($value); } diff --git a/app/code/Magento/CatalogRule/Model/Rule/Job.php b/app/code/Magento/CatalogRule/Model/Rule/Job.php index 63ff98d4ca5b7..71734eb3c5d46 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Job.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Job.php @@ -8,6 +8,10 @@ * See COPYING.txt for license details. */ +namespace Magento\CatalogRule\Model\Rule; + +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; + /** * Catalog Rule job model * @@ -18,13 +22,8 @@ * @method bool hasSuccess() * @method bool hasError() * - * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\CatalogRule\Model\Rule; - -use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; - -/** + * @author Magento Core Team <core@magentocommerce.com> + * * @api * @since 100.0.2 */ @@ -39,10 +38,14 @@ class Job extends \Magento\Framework\DataObject * Basic object initialization * * @param RuleProductProcessor $ruleProcessor + * @param array $data */ - public function __construct(RuleProductProcessor $ruleProcessor) - { + public function __construct( + RuleProductProcessor $ruleProcessor, + array $data = [] + ) { $this->ruleProcessor = $ruleProcessor; + parent::__construct($data); } /** diff --git a/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php b/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php index 08c3d97b216ed..749ac3cf51249 100644 --- a/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php +++ b/app/code/Magento/CatalogRule/Observer/AddDirtyRulesNotice.php @@ -37,7 +37,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $dirtyRules = $observer->getData('dirty_rules'); if (!empty($dirtyRules)) { if ($dirtyRules->getState()) { - $this->messageManager->addNotice($observer->getData('message')); + $this->messageManager->addNoticeMessage($observer->getData('message')); } } } diff --git a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php index 8a3a821707624..321780eca5632 100644 --- a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php +++ b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php @@ -22,7 +22,7 @@ class RulePricesStorage */ public function getRulePrice($id) { - return isset($this->rulePrices[$id]) ? $this->rulePrices[$id] : false; + return $this->rulePrices[$id] ?? false; } /** diff --git a/app/code/Magento/CatalogRule/Plugin/Indexer/Category.php b/app/code/Magento/CatalogRule/Plugin/Indexer/Category.php index 0ea0fdda31958..50e3703087680 100644 --- a/app/code/Magento/CatalogRule/Plugin/Indexer/Category.php +++ b/app/code/Magento/CatalogRule/Plugin/Indexer/Category.php @@ -35,8 +35,8 @@ public function afterSave( \Magento\Catalog\Model\Category $result ) { /** @var \Magento\Catalog\Model\Category $result */ - $productIds = $result->getAffectedProductIds(); - if ($productIds && !$this->productRuleProcessor->isIndexerScheduled()) { + $productIds = $result->getChangedProductIds(); + if (!empty($productIds) && !$this->productRuleProcessor->isIndexerScheduled()) { $this->productRuleProcessor->reindexList($productIds); } return $result; diff --git a/app/code/Magento/CatalogRule/Plugin/Indexer/Product/Attribute.php b/app/code/Magento/CatalogRule/Plugin/Indexer/Product/Attribute.php index cc808a38db698..7fdffe933db8c 100644 --- a/app/code/Magento/CatalogRule/Plugin/Indexer/Product/Attribute.php +++ b/app/code/Magento/CatalogRule/Plugin/Indexer/Product/Attribute.php @@ -103,7 +103,7 @@ protected function checkCatalogRulesAvailability($attributeCode) if ($disabledRulesCount) { $this->ruleProductProcessor->markIndexerAsInvalid(); - $this->messageManager->addWarning( + $this->messageManager->addWarningMessage( __( 'You disabled %1 Catalog Price Rules based on "%2" attribute.', $disabledRulesCount, diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php index a45a548264a4c..c71b51317fd59 100644 --- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php @@ -89,7 +89,7 @@ public function getValue() { if (null === $this->value) { if ($this->product->hasData(self::PRICE_CODE)) { - $this->value = floatval($this->product->getData(self::PRICE_CODE)) ?: false; + $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; } else { $this->value = $this->getRuleResource() ->getRulePrice( @@ -98,7 +98,7 @@ public function getValue() $this->customerSession->getCustomerGroupId(), $this->product->getId() ); - $this->value = $this->value ? floatval($this->value) : false; + $this->value = $this->value ? (float)$this->value : false; } if ($this->value) { $this->value = $this->priceCurrency->convertAndRound($this->value); diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..d903c5a46c426 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -0,0 +1,193 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- action group to create a new catalog price rule giving a catalogRule entity --> + <actionGroup name="CreateCatalogPriceRule"> + <arguments> + <argument name="catalogRule" defaultValue="CustomCatalogRule"/> + </arguments> + <!-- Go to the admin Catalog rule grid and add a new one --> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <waitForPageLoad time="30" stepKey="waitForPageFullyLoaded"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="addNewRule"/> + + <!-- Fill the form according the attributes of the entity --> + <waitForElementVisible selector="{{AdminCatalogPriceRuleSection.ruleName}}" stepKey="waitRuleNameFieldAppears"/> + <fillField selector="{{AdminCatalogPriceRuleSection.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName"/> + <fillField selector="{{AdminCatalogPriceRuleSection.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription"/> + <selectOption selector="{{AdminCatalogPriceRuleSection.websites}}" parameterArray="{{catalogRule.websites}}" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCatalogPriceRuleSection.customerGroups}}" parameterArray="{{catalogRule.groups}}" stepKey="selectCustomerGroups"/> + + <scrollTo selector="{{AdminCatalogPriceRuleSection.actionsTabTitle}}" stepKey="scrollToActionsSection"/> + <conditionalClick selector="{{AdminCatalogPriceRuleSection.actionsTabTitle}}" dependentSelector="{{AdminCatalogPriceRuleSection.actionsTabBody}}" visible="false" stepKey="openActionsTabIfCollapsed"/> + <selectOption selector="{{AdminCatalogPriceRuleActionsSection.apply}}" userInput="{{catalogRule.simple_action}}" stepKey="discountType"/> + <fillField selector="{{AdminCatalogPriceRuleActionsSection.discountAmount}}" userInput="{{catalogRule.discount_amount}}" stepKey="fillDiscountValue"/> + <selectOption selector="{{AdminCatalogPriceRuleActionsSection.disregardRules}}" userInput="Yes" stepKey="discardSubsequentRules"/> + + <!-- Scroll to top and either save or save and apply after the action group --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="checkSuccessMessage"/> + </actionGroup> + + <actionGroup name="RemoveCatalogPriceRule"> + <arguments> + <argument name="ruleName" defaultValue="CustomCatalogRule.name"/> + </arguments> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminCatalogPriceRuleGridSection.filterByRuleName}}" userInput="{{ruleName}}" stepKey="filterByRuleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <click selector="{{AdminGridTableSection.row('1')}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForEditRuleFormLoad"/> + <waitForElementVisible selector="{{AdminMainActionsSection.delete}}" stepKey="waitDeleteButtonAppears"/> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickToDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForElementVisible"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickToConfirm"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="checkSuccessMessage"/> + </actionGroup> + <!--Add Catalog Rule Condition With product SKU--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsSKU" extends="CreateCatalogPriceRule"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <click selector="{{AdminCatalogPriceRuleSection.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" after="openConditionsTab" stepKey="addNewCondition"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|sku" after="addNewCondition" stepKey="selectTypeCondition"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.targetEllipsis('1')}}" after="selectTypeCondition" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCatalogPriceRuleConditionsSection.targetInput('1', '1')}}" userInput="{{productSku}}" after="clickEllipsis" stepKey="fillProductSku"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.applyButton('1', '1')}}" after="fillProductSku" stepKey="clickApply"/> + </actionGroup> + <!--Add Catalog Rule Condition With Category--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsCategory" extends="CreateCatalogPriceRule"> + <arguments> + <argument name="categoryId" type="string"/> + </arguments> + <click selector="{{AdminCatalogPriceRuleSection.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" after="openConditionsTab" stepKey="addNewCondition"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|category_ids" after="addNewCondition" stepKey="selectTypeCondition"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.targetEllipsis('1')}}" after="selectTypeCondition" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCatalogPriceRuleConditionsSection.targetInput('1', '1')}}" userInput="{{categoryId}}" after="clickEllipsis" stepKey="fillCategoryId"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.applyButton('1', '1')}}" after="fillCategoryId" stepKey="clickApply"/> + </actionGroup> + + <!-- Open rule for Edit --> + <actionGroup name="OpenCatalogPriceRule"> + <arguments> + <argument name="ruleName" type="string" defaultValue="CustomCatalogRule.name"/> + </arguments> + + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminCatalogPriceRuleGridSection.filterByRuleName}}" userInput="{{ruleName}}" stepKey="filterByRuleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <click selector="{{AdminGridTableSection.row('1')}}" stepKey="clickEdit"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad"/> + </actionGroup> + + <actionGroup name="deleteAllCatalogPriceRule"> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <executeInSelenium + function=" + function ($webdriver) use ($I) { + $rows = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)')); + while(!empty($rows)) { + $rows[0]->click(); + $I->waitForPageLoad(30); + $I->click('#delete'); + $I->waitForPageLoad(30); + $I->waitForElementVisible('aside.confirm .modal-footer button.action-accept', 10); + $I->waitForPageLoad(60); + $I->click('aside.confirm .modal-footer button.action-accept'); + $I->waitForPageLoad(60); + $I->waitForLoadingMaskToDisappear(); + $I->waitForElementVisible('#messages div.message-success', 10); + $I->see('You deleted the rule.', '#messages div.message-success'); + $rows = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)')); + } + }" + stepKey="deleteAllCatalogPriceRulesOneByOne"/> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> + + <actionGroup name="AdminStartCreateNewCatalogRuleActionGroup"> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <waitForPageLoad time="30" stepKey="waitForGridPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="addNewRule"/> + <waitForElementVisible selector="{{AdminCatalogPriceRuleSection.ruleName}}" stepKey="waitCreatePageLoaded"/> + </actionGroup> + + <actionGroup name="AdminFillCatalogRuleFormActionGroup"> + <arguments> + <argument name="catalogRule" defaultValue="CustomCatalogRule"/> + </arguments> + + <fillField selector="{{AdminCatalogPriceRuleSection.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName"/> + <fillField selector="{{AdminCatalogPriceRuleSection.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription"/> + <selectOption selector="{{AdminCatalogPriceRuleSection.websites}}" parameterArray="{{catalogRule.websites}}" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCatalogPriceRuleSection.customerGroups}}" parameterArray="{{catalogRule.groups}}" stepKey="selectCustomerGroups"/> + + <click selector="{{AdminCatalogPriceRuleSection.actionsTab}}" stepKey="openActionDropdown"/> + <selectOption selector="{{AdminCatalogPriceRuleActionsSection.apply}}" userInput="{{catalogRule.simple_action}}" stepKey="discountType"/> + <fillField selector="{{AdminCatalogPriceRuleActionsSection.discountAmount}}" userInput="{{catalogRule.discount_amount}}" stepKey="fillDiscountValue"/> + <selectOption selector="{{AdminCatalogPriceRuleActionsSection.disregardRules}}" userInput="Yes" stepKey="discardSubsequentRules"/> + </actionGroup> + + <actionGroup name="AdminFillCatalogRuleConditionActionGroup"> + <arguments> + <argument name="condition" type="string"/> + <argument name="conditionOperator" type="string" defaultValue="is"/> + <argument name="conditionValue" type="string"/> + </arguments> + + <conditionalClick selector="{{AdminCatalogPriceRuleSection.conditionsTab}}" dependentSelector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" visible="false" stepKey="openConditionsTab"/> + <waitForElementVisible selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" stepKey="waitForAddConditionButton"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" stepKey="addNewCondition"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.conditionSelect('1')}}" userInput="{{condition}}" stepKey="selectTypeCondition"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.condition('is')}}" stepKey="clickOnOperator"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.activeOperatorSelect}}" userInput="{{conditionOperator}}" stepKey="selectCondition"/> + <!-- In case we are choosing already selected value - select is not closed automatically --> + <conditionalClick selector="{{AdminCatalogPriceRuleConditionsSection.condition('...')}}" dependentSelector="{{AdminCatalogPriceRuleConditionsSection.activeOperatorSelect}}" visible="true" stepKey="closeSelect"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.condition('...')}}" stepKey="clickToChooseOption3"/> + <waitForElementVisible selector="{{AdminCatalogPriceRuleConditionsSection.activeValueInput}}" stepKey="waitForValueInput"/> + <fillField selector="{{AdminCatalogPriceRuleConditionsSection.activeValueInput}}" userInput="{{conditionValue}}" stepKey="fillConditionValue"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.activeConditionApplyButton}}" stepKey="clickApply"/> + <waitForElementNotVisible selector="{{AdminCatalogPriceRuleConditionsSection.activeConditionApplyButton}}" stepKey="waitForApplyButtonInvisibility"/> + </actionGroup> + + <actionGroup name="AdminSaveAndApplyCatalogPriceRuleActionGroup"> + <scrollToTopOfPage stepKey="scrollToPageTop"/> + <click selector="{{AdminCatalogPriceRuleSection.saveAndApply}}" stepKey="saveAndApply"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="assertSaveSuccess"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Updated rules applied." stepKey="assertApplySuccess"/> + </actionGroup> + + <actionGroup name="AdminSaveCatalogPriceRuleActionGroup" extends="AdminSaveAndApplyCatalogPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminSaveAndApplyCatalogPriceRuleActionGroup. Clicks on the Save button.</description> + </annotations> + + <remove keyForRemoval="saveAndApply"/> + <remove keyForRemoval="assertApplySuccess"/> + <click selector="{{AdminMainActionsSection.save}}" after="scrollToPageTop" stepKey="clickSaveButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml new file mode 100644 index 0000000000000..17ed961c58310 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomCatalogRule" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="websites"> + <item>Main Website</item> + </array> + <array key="groups"> + <item>General</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + </entity> + <entity name="CatalogRule96PercentDiscount" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="groups"> + <item>NOT LOGGED IN</item> + </array> + <array key="websites"> + <item>Main Website</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">96</data> + </entity> + <entity name="CatalogRuleToPercent" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">to_percent</data> + <data key="discount_amount">90</data> + </entity> + <entity name="CatalogRuleToPercent90" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <array key="groups"> + <item>NOT LOGGED IN</item> + </array> + <array key="websites"> + <item>Main Website</item> + </array> + <data key="simple_action">Adjust final price to this percentage</data> + <data key="discount_amount">90</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml new file mode 100644 index 0000000000000..f635f3bb05cfb --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogRuleProductConditions"> + <data key="productSku">Magento\CatalogRule\Model\Rule\Condition\Product|sku</data> + <data key="categoryIds">Magento\CatalogRule\Model\Rule\Condition\Product|category_ids</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogRule/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogRule/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 <insert your license name here>" 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/app/code/Magento/CatalogRule/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogRule/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogRule/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 <insert your license name here>" 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/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml new file mode 100644 index 0000000000000..1f801b9039e4a --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCatalogRule" dataType="catalogRule" type="create" auth="adminFormKey" + url="/catalog_rule/promo_catalog/save/back/edit" method="POST" + returnRegex="~\/id\/(?'id'\d+)\/~" returnIndex="id" successRegex="/messages-message-success/"> + <contentType>application/x-www-form-urlencoded</contentType> + <field key="name">string</field> + <field key="description">string</field> + <field key="is_active">string</field> + <array key="customer_group_ids"> + <value>integer</value> + </array> + <array key="website_ids"> + <value>integer</value> + </array> + <field key="simple_action">string</field> + <field key="discount_amount">string</field> + </operation> + <operation name="DeleteCatalogRule" dataType="catalogRule" type="delete" auth="adminFormKey" + url="/catalog_rule/promo_catalog/delete/id/{return}" method="POST" successRegex="/messages-message-success/"> + </operation> +</operations> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleEditPage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleEditPage.xml new file mode 100644 index 0000000000000..eda619d8bd898 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleEditPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCatalogPriceRuleEditPage" url="catalog_rule/promo_catalog/edit/id/{{ruleId}}/" module="Magento_CatalogRule" area="admin" parameterized="true"> + <section name="AdminMainActionsSection"/> + <section name="AdminCatalogPriceRuleSection"/> + <section name="AdminCatalogPriceRuleActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.xml new file mode 100644 index 0000000000000..063f68f9b5f86 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminCatalogPriceRuleGridPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCatalogPriceRuleGridPage" url="catalog_rule/promo_catalog/" module="Magento_CatalogRule" area="admin"> + <section name="AdminCatalogPriceRuleGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/README.md b/app/code/Magento/CatalogRule/Test/Mftf/README.md new file mode 100644 index 0000000000000..086f52535e00a --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Rule Functional Tests + +The Functional Test Module for **Magento Catalog Rule** module. diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleActionsSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleActionsSection.xml new file mode 100644 index 0000000000000..38e5b8a0c2206 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleActionsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCatalogPriceRuleActionsSection"> + <element name="apply" type="select" selector="[name='simple_action']"/> + <element name="discountAmount" type="input" selector="[name='discount_amount']"/> + <element name="disregardRules" type="select" selector="[name='stop_rules_processing']"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml new file mode 100644 index 0000000000000..5fceb081dbc62 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogPriceRuleConditionsSection"> + <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child" timeout="30"/> + <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true" timeout="30"/> + <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true" timeout="30"/> + <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> + <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true" timeout="30"/> + <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{condition}}']" parameterized="true"/> + <element name="activeOperatorSelect" type="select" selector=".rule-param-edit select[name*='[operator]']"/> + <element name="activeValueInput" type="input" selector=".rule-param-edit [name*='[value]']"/> + <element name="activeConditionApplyButton" type="button" selector=".rule-param-edit .rule-param-apply" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml new file mode 100644 index 0000000000000..5224147c51804 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogPriceRuleGridSection"> + <element name="filterByRuleName" type="input" selector="#promo_catalog_grid_filter_name"/> + <element name="attribute" type="text" selector="//td[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="applyRulesButton" type="button" selector="#apply_rules" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml new file mode 100644 index 0000000000000..a27dfe4d1f24f --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogPriceRuleSection"> + <element name="saveAndApply" type="button" selector="#save_and_apply" timeout="30"/> + <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> + + <element name="ruleName" type="input" selector="[name='name']"/> + <element name="description" type="textarea" selector="[name='description']"/> + <element name="status" type="select" selector="[name='is_active']"/> + + <element name="websites" type="select" selector="[name='website_ids']"/> + <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> + <element name="customerGroups" type="select" selector="[name='customer_group_ids']"/> + <element name="customerGroupsOptions" type="select" selector="[name='customer_group_ids'] option"/> + + <element name="fromDateButton" type="button" selector="[name='from_date'] + button" timeout="15"/> + <element name="toDateButton" type="button" selector="[name='to_date'] + button" timeout="15"/> + <element name="todayDate" type="button" selector="#ui-datepicker-div [data-handler='today']"/> + <element name="priority" type="input" selector="[name='sort_order']"/> + <element name="conditionsTab" type="block" selector="[data-index='block_promo_catalog_edit_tab_conditions']"/> + <element name="actionsTab" type="block" selector="[data-index='actions']"/> + <element name="actionsTabTitle" type="block" selector="[data-index='actions'] .fieldset-wrapper-title"/> + <element name="actionsTabBody" type="block" selector="[data-index='actions'] .admin__fieldset-wrapper-content"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml new file mode 100644 index 0000000000000..94413788bdc21 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Check the price"/> + <title value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <description value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76169"/> + <group value="persistent"/> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">50</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="group_id">1</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Catalog Rule--> + <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CustomCatalogRule"/> + <argument name="categoryId" value="$$createCategory.id$$"/> + </actionGroup> + <click selector="{{AdminCatalogPriceRuleGridSection.applyRulesButton}}" stepKey="clickApplyRules"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Delete the rule --> + <actionGroup ref="RemoveCatalogPriceRule" stepKey="deletePriceRule"> + <argument name="ruleName" value="CustomCatalogRule.name" /> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!--Go to category and check price--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct"/> + + <!--Login to storfront from customer and check price--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="logInFromCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage2"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="45.00" stepKey="checkPriceSimpleProduct2"/> + + <!--Click *Sign Out* and check the price of the Simple Product--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="storefrontSignOut"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage3"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome2"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYou"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="45.00" stepKey="checkPriceSimpleProduct3"/> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickNext"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage4"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="homeCheckWelcome3"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByName($$createProduct.name$$)}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct4"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php index 6178d51644fde..9b80984628b12 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php @@ -58,7 +58,7 @@ public function testGetButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $deleteUrl . '\')', + ) . '\', \'' . $deleteUrl . '\', {data: {}})', 'sort_order' => 20, ]; diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php index 8252b512e7810..5827bff42b038 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php @@ -144,14 +144,12 @@ protected function setUp() ); $this->ruleCollectionFactory = $this->createPartialMock( \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory::class, - ['create', 'addFieldToFilter'] + ['create'] ); $this->backend = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class); $this->select = $this->createMock(\Magento\Framework\DB\Select::class); $this->metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); - $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) - ->disableOriginalConstructor() - ->getMock(); + $metadata = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class); $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); $this->db = $this->createMock(\Zend_Db_Statement_Interface::class); @@ -181,10 +179,16 @@ protected function setUp() $this->rules->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1])); $this->rules->expects($this->any())->method('getCustomerGroupIds')->will($this->returnValue([1])); - $this->ruleCollectionFactory->expects($this->any())->method('create')->will($this->returnSelf()); - $this->ruleCollectionFactory->expects($this->any())->method('addFieldToFilter')->will( - $this->returnValue([$this->rules]) - ); + $ruleCollection = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule\Collection::class); + $this->ruleCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn($ruleCollection); + $ruleCollection->expects($this->once()) + ->method('addFieldToFilter') + ->willReturnSelf(); + $ruleIterator = new \ArrayIterator([$this->rules]); + $ruleCollection->method('getIterator') + ->willReturn($ruleIterator); $this->product->expects($this->any())->method('load')->will($this->returnSelf()); $this->product->expects($this->any())->method('getId')->will($this->returnValue(1)); @@ -213,14 +217,12 @@ protected function setUp() ] ); - $this->reindexRuleProductPrice = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class) - ->disableOriginalConstructor() - ->getMock(); - $this->reindexRuleGroupWebsite = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class) - ->disableOriginalConstructor() - ->getMock(); + $this->reindexRuleProductPrice = $this->createMock( + \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class + ); + $this->reindexRuleGroupWebsite = $this->createMock( + \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class + ); $this->setProperties($this->indexBuilder, [ 'metadataPool' => $this->metadataPool, 'reindexRuleProductPrice' => $this->reindexRuleProductPrice, diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleGroupWebsiteTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleGroupWebsiteTest.php index d60a662193e54..f4a78c16a2792 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleGroupWebsiteTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleGroupWebsiteTest.php @@ -6,6 +6,9 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; + class ReindexRuleGroupWebsiteTest extends \PHPUnit\Framework\TestCase { /** @@ -24,9 +27,9 @@ class ReindexRuleGroupWebsiteTest extends \PHPUnit\Framework\TestCase private $resourceMock; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $activeTableSwitcherMock; + private $tableSwapperMock; protected function setUp() { @@ -36,14 +39,19 @@ protected function setUp() $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->activeTableSwitcherMock = - $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class) + /** @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject $activeTableSwitcherMock */ + $activeTableSwitcherMock = + $this->getMockBuilder(ActiveTableSwitcher::class) ->disableOriginalConstructor() ->getMock(); + $this->tableSwapperMock = $this->getMockForAbstractClass( + IndexerTableSwapperInterface::class + ); $this->model = new \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite( $this->dateTimeMock, $this->resourceMock, - $this->activeTableSwitcherMock + $activeTableSwitcherMock, + $this->tableSwapperMock ); } @@ -55,12 +63,12 @@ public function testExecute() $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock); $this->dateTimeMock->expects($this->once())->method('gmtTimestamp')->willReturn($timeStamp); - $this->activeTableSwitcherMock->expects($this->at(0)) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->at(0)) + ->method('getWorkingTableName') ->with('catalogrule_group_website') ->willReturn('catalogrule_group_website_replica'); - $this->activeTableSwitcherMock->expects($this->at(1)) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->at(1)) + ->method('getWorkingTableName') ->with('catalogrule_product') ->willReturn('catalogrule_product_replica'); diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php index 6d7f0673ed281..c44859a88c202 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php @@ -6,65 +6,61 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; -use Magento\CatalogRule\Model\Indexer\IndexBuilder; +use Magento\Catalog\Model\Product; +use Magento\CatalogRule\Model\Indexer\ProductPriceCalculator; +use Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice; +use Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor; +use Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\GroupInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; class ReindexRuleProductPriceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice + * @var ReindexRuleProductPrice */ private $model; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var RuleProductsSelectBuilder|\PHPUnit_Framework_MockObject_MockObject */ private $ruleProductsSelectBuilderMock; /** - * @var \Magento\CatalogRule\Model\Indexer\ProductPriceCalculator|\PHPUnit_Framework_MockObject_MockObject + * @var ProductPriceCalculator|\PHPUnit_Framework_MockObject_MockObject */ private $productPriceCalculatorMock; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $dateTimeMock; + private $localeDate; /** - * @var \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor|\PHPUnit_Framework_MockObject_MockObject + * @var RuleProductPricesPersistor|\PHPUnit_Framework_MockObject_MockObject */ private $pricesPersistorMock; protected function setUp() { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->ruleProductsSelectBuilderMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productPriceCalculatorMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ProductPriceCalculator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->dateTimeMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) - ->disableOriginalConstructor() - ->getMock(); - $this->pricesPersistorMock = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->model = new \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice( + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->ruleProductsSelectBuilderMock = $this->createMock(RuleProductsSelectBuilder::class); + $this->productPriceCalculatorMock = $this->createMock(ProductPriceCalculator::class); + $this->localeDate = $this->createMock(TimezoneInterface::class); + $this->pricesPersistorMock = $this->createMock(RuleProductPricesPersistor::class); + + $this->model = new ReindexRuleProductPrice( $this->storeManagerMock, $this->ruleProductsSelectBuilderMock, $this->productPriceCalculatorMock, - $this->dateTimeMock, + $this->localeDate, $this->pricesPersistorMock ); } @@ -72,19 +68,32 @@ protected function setUp() public function testExecute() { $websiteId = 234; - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $websiteMock = $this->getMockBuilder(\Magento\Store\Api\Data\WebsiteInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->once())->method('getId')->willReturn($websiteId); - $this->storeManagerMock->expects($this->once())->method('getWebsites')->willReturn([$websiteMock]); - - $statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class) - ->disableOriginalConstructor() - ->getMock(); + $defaultGroupId = 11; + $defaultStoreId = 22; + + $websiteMock = $this->createMock(WebsiteInterface::class); + $websiteMock->expects($this->once()) + ->method('getId') + ->willReturn($websiteId); + $websiteMock->expects($this->once()) + ->method('getDefaultGroupId') + ->willReturn($defaultGroupId); + $this->storeManagerMock->expects($this->once()) + ->method('getWebsites') + ->willReturn([$websiteMock]); + $groupMock = $this->createMock(GroupInterface::class); + $groupMock->method('getId') + ->willReturn($defaultStoreId); + $groupMock->expects($this->once()) + ->method('getDefaultStoreId') + ->willReturn($defaultStoreId); + $this->storeManagerMock->expects($this->once()) + ->method('getGroup') + ->with($defaultGroupId) + ->willReturn($groupMock); + + $productMock = $this->createMock(Product::class); + $statementMock = $this->createMock(\Zend_Db_Statement_Interface::class); $this->ruleProductsSelectBuilderMock->expects($this->once()) ->method('build') ->with($websiteId, $productMock, true) @@ -99,29 +108,22 @@ public function testExecute() 'action_stop' => true ]; - $this->dateTimeMock->expects($this->at(0)) - ->method('date') - ->with('Y-m-d 00:00:00', $ruleData['from_time']) - ->willReturn($ruleData['from_time']); - $this->dateTimeMock->expects($this->at(1)) - ->method('timestamp') - ->with($ruleData['from_time']) - ->willReturn($ruleData['from_time']); - - $this->dateTimeMock->expects($this->at(2)) - ->method('date') - ->with('Y-m-d 00:00:00', $ruleData['to_time']) - ->willReturn($ruleData['to_time']); - $this->dateTimeMock->expects($this->at(3)) - ->method('timestamp') - ->with($ruleData['to_time']) - ->willReturn($ruleData['to_time']); - - $statementMock->expects($this->at(0))->method('fetch')->willReturn($ruleData); - $statementMock->expects($this->at(1))->method('fetch')->willReturn(false); - - $this->productPriceCalculatorMock->expects($this->atLeastOnce())->method('calculate'); - $this->pricesPersistorMock->expects($this->once())->method('execute'); + $this->localeDate->expects($this->once()) + ->method('scopeDate') + ->with($defaultStoreId, null, true) + ->willReturn(new \DateTime()); + + $statementMock->expects($this->at(0)) + ->method('fetch') + ->willReturn($ruleData); + $statementMock->expects($this->at(1)) + ->method('fetch') + ->willReturn(false); + + $this->productPriceCalculatorMock->expects($this->atLeastOnce()) + ->method('calculate'); + $this->pricesPersistorMock->expects($this->once()) + ->method('execute'); $this->assertTrue($this->model->execute(1, $productMock, true)); } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php index b829468396bf0..8a241971ca079 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php @@ -6,80 +6,93 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; +use Magento\CatalogRule\Model\Indexer\ReindexRuleProduct; +use Magento\CatalogRule\Model\Rule; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\ScopeInterface; + class ReindexRuleProductTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\CatalogRule\Model\Indexer\ReindexRuleProduct + * @var ReindexRuleProduct */ private $model; /** - * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ private $resourceMock; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $tableSwapperMock; + + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $activeTableSwitcherMock; + private $localeDateMock; protected function setUp() { - $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->activeTableSwitcherMock = - $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class) - ->disableOriginalConstructor() - ->getMock(); - $this->model = new \Magento\CatalogRule\Model\Indexer\ReindexRuleProduct( + $this->resourceMock = $this->createMock(ResourceConnection::class); + /** @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject $activeTableSwitcherMock */ + $activeTableSwitcherMock = $this->createMock(ActiveTableSwitcher::class); + $this->tableSwapperMock = $this->createMock(IndexerTableSwapperInterface::class); + $this->localeDateMock = $this->createMock(TimezoneInterface::class); + + $this->model = new ReindexRuleProduct( $this->resourceMock, - $this->activeTableSwitcherMock + $activeTableSwitcherMock, + $this->tableSwapperMock, + $this->localeDateMock ); } public function testExecuteIfRuleInactive() { - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(false); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once()) + ->method('getIsActive') + ->willReturn(false); $this->assertFalse($this->model->execute($ruleMock, 100, true)); } public function testExecuteIfRuleWithoutWebsiteIds() { - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); - $ruleMock->expects($this->once())->method('getWebsiteIds')->willReturn(null); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once()) + ->method('getIsActive') + ->willReturn(true); + $ruleMock->expects($this->once()) + ->method('getWebsiteIds') + ->willReturn(null); $this->assertFalse($this->model->execute($ruleMock, 100, true)); } public function testExecute() { + $websiteId = 3; + $websiteTz = 'America/Los_Angeles'; $productIds = [ - 4 => [1 => 1], - 5 => [1 => 1], - 6 => [1 => 1], + 4 => [$websiteId => 1], + 5 => [$websiteId => 1], + 6 => [$websiteId => 1], ]; - $ruleMock = $this->getMockBuilder(\Magento\CatalogRule\Model\Rule::class) - ->disableOriginalConstructor() - ->getMock(); - $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); - $ruleMock->expects($this->exactly(2))->method('getWebsiteIds')->willReturn(1); - $ruleMock->expects($this->once())->method('getMatchingProductIds')->willReturn($productIds); - $this->activeTableSwitcherMock->expects($this->once()) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->once()) + ->method('getWorkingTableName') ->with('catalogrule_product') ->willReturn('catalogrule_product_replica'); - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock); + $connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceMock->expects($this->at(0)) + ->method('getConnection') + ->willReturn($connectionMock); $this->resourceMock->expects($this->at(1)) ->method('getTableName') ->with('catalogrule_product') @@ -89,21 +102,30 @@ public function testExecute() ->with('catalogrule_product_replica') ->willReturn('catalogrule_product_replica'); + $ruleMock = $this->createMock(Rule::class); + $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true); + $ruleMock->expects($this->exactly(2))->method('getWebsiteIds')->willReturn([$websiteId]); + $ruleMock->expects($this->once())->method('getMatchingProductIds')->willReturn($productIds); $ruleMock->expects($this->once())->method('getId')->willReturn(100); $ruleMock->expects($this->once())->method('getCustomerGroupIds')->willReturn([10]); - $ruleMock->expects($this->once())->method('getFromDate')->willReturn('2017-06-21'); - $ruleMock->expects($this->once())->method('getToDate')->willReturn('2017-06-30'); + $ruleMock->expects($this->atLeastOnce())->method('getFromDate')->willReturn('2017-06-21'); + $ruleMock->expects($this->atLeastOnce())->method('getToDate')->willReturn('2017-06-30'); $ruleMock->expects($this->once())->method('getSortOrder')->willReturn(1); $ruleMock->expects($this->once())->method('getSimpleAction')->willReturn('simple_action'); $ruleMock->expects($this->once())->method('getDiscountAmount')->willReturn(43); $ruleMock->expects($this->once())->method('getStopRulesProcessing')->willReturn(true); + $this->localeDateMock->expects($this->once()) + ->method('getConfigTimezone') + ->with(ScopeInterface::SCOPE_WEBSITE, $websiteId) + ->willReturn($websiteTz); + $batchRows = [ [ 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 4, 'action_operator' => 'simple_action', @@ -115,7 +137,7 @@ public function testExecute() 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 5, 'action_operator' => 'simple_action', @@ -130,7 +152,7 @@ public function testExecute() 'rule_id' => 100, 'from_time' => 1498028400, 'to_time' => 1498892399, - 'website_id' => 1, + 'website_id' => $websiteId, 'customer_group_id' => 10, 'product_id' => 6, 'action_operator' => 'simple_action', diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductPricesPersistorTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductPricesPersistorTest.php index 3efe26971627e..0b15352159e74 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductPricesPersistorTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductPricesPersistorTest.php @@ -6,6 +6,9 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; + class RuleProductPricesPersistorTest extends \PHPUnit\Framework\TestCase { /** @@ -24,9 +27,9 @@ class RuleProductPricesPersistorTest extends \PHPUnit\Framework\TestCase private $resourceMock; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $activeTableSwitcherMock; + private $tableSwapperMock; protected function setUp() { @@ -36,14 +39,19 @@ protected function setUp() $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->activeTableSwitcherMock = - $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class) - ->disableOriginalConstructor() - ->getMock(); + /** @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject $activeTableSwitcherMock */ + $activeTableSwitcherMock = + $this->getMockBuilder(ActiveTableSwitcher::class) + ->disableOriginalConstructor() + ->getMock(); + $this->tableSwapperMock = $this->getMockForAbstractClass( + IndexerTableSwapperInterface::class + ); $this->model = new \Magento\CatalogRule\Model\Indexer\RuleProductPricesPersistor( $this->dateTimeMock, $this->resourceMock, - $this->activeTableSwitcherMock + $activeTableSwitcherMock, + $this->tableSwapperMock ); } @@ -64,8 +72,8 @@ public function testExecute() ]; $tableName = 'catalogrule_product_price_replica'; - $this->activeTableSwitcherMock->expects($this->once()) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->once()) + ->method('getWorkingTableName') ->with('catalogrule_product_price') ->willReturn($tableName); @@ -120,8 +128,8 @@ public function testExecuteWithException() ]; $tableName = 'catalogrule_product_price_replica'; - $this->activeTableSwitcherMock->expects($this->once()) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->once()) + ->method('getWorkingTableName') ->with('catalogrule_product_price') ->willReturn($tableName); diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php index 92b4bb353f046..3584ae466354d 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php @@ -7,6 +7,8 @@ namespace Magento\CatalogRule\Test\Unit\Model\Indexer; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; @@ -34,11 +36,6 @@ class RuleProductsSelectBuilderTest extends \PHPUnit\Framework\TestCase */ private $resourceMock; - /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject - */ - private $activeTableSwitcherMock; - /** * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ @@ -49,6 +46,11 @@ class RuleProductsSelectBuilderTest extends \PHPUnit\Framework\TestCase */ private $metadataPoolMock; + /** + * @var IndexerTableSwapperInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $tableSwapperMock; + protected function setUp() { $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) @@ -56,8 +58,9 @@ protected function setUp() $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) ->disableOriginalConstructor() ->getMock(); - $this->activeTableSwitcherMock = - $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class) + /** @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject $activeTableSwitcherMock */ + $activeTableSwitcherMock = + $this->getMockBuilder(ActiveTableSwitcher::class) ->disableOriginalConstructor() ->getMock(); $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -66,13 +69,17 @@ protected function setUp() $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() ->getMock(); + $this->tableSwapperMock = $this->getMockForAbstractClass( + IndexerTableSwapperInterface::class + ); $this->model = new \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder( $this->resourceMock, $this->eavConfigMock, $this->storeManagerMock, $this->metadataPoolMock, - $this->activeTableSwitcherMock + $activeTableSwitcherMock, + $this->tableSwapperMock ); } @@ -92,8 +99,8 @@ public function testBuild() $connectionMock = $this->getMockBuilder(AdapterInterface::class)->disableOriginalConstructor()->getMock(); $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock); - $this->activeTableSwitcherMock->expects($this->once()) - ->method('getAdditionalTableName') + $this->tableSwapperMock->expects($this->once()) + ->method('getWorkingTableName') ->with($ruleTable) ->willReturn($rplTable); diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Product/PriceModifierTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Product/PriceModifierTest.php index b1e27bf973404..ccc86920a7e74 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Product/PriceModifierTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Product/PriceModifierTest.php @@ -56,6 +56,9 @@ public function testModifyPriceIfPriceExists($resultPrice, $expectedPrice) $this->assertEquals($expectedPrice, $this->priceModifier->modifyPrice(100, $this->productMock)); } + /** + * @return array + */ public function modifyPriceDataProvider() { return ['resulted_price_exists' => [150, 150], 'resulted_price_not_exists' => [null, 100]]; diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php new file mode 100644 index 0000000000000..59c0322678759 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Rule/Condition/MappableConditionProcessorTest.php @@ -0,0 +1,1046 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogRule\Test\Unit\Model\Rule\Condition; + +use Magento\Eav\Model\Config as EavConfig; +use Magento\CatalogRule\Model\Rule\Condition\MappableConditionsProcessor; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionProviderInterface; +use Magento\CatalogRule\Model\Rule\Condition\Combine as CombinedCondition; +use Magento\CatalogRule\Model\Rule\Condition\Product as SimpleCondition; + +class MappableConditionProcessorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var MappableConditionsProcessor + */ + private $mappableConditionProcessor; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfigMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $customConditionProcessorBuilderMock; + + protected function setUp() + { + $this->eavConfigMock = $this->getMockBuilder(EavConfig::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMock(); + + $this->customConditionProcessorBuilderMock = $this->getMockBuilder( + CustomConditionProviderInterface::class + )->disableOriginalConstructor() + ->setMethods(['hasProcessorForField']) + ->getMockForAbstractClass(); + + $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->mappableConditionProcessor = $this->objectManagerHelper->getObject( + MappableConditionsProcessor::class, + [ + 'customConditionProvider' => $this->customConditionProcessorBuilderMock, + 'eavConfig' => $this->eavConfigMock, + ] + ); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-2 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + * ] + */ + public function testConditionV1() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-2 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * ] + * ] + * ] + */ + public function testConditionV2() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition( + [ + $simpleCondition1 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-2 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => [] + * ] + * ] + */ + public function testConditionV3() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition([], 'all'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + */ + public function testConditionV4() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'all' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'any' + ); + + $validSubCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition2 + ], + 'all' + ); + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition1, + $validSubCondition2 + ], + 'any' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 is not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + */ + public function testConditionV5() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'all' + ); + + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition2 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when all condition are mappable there must not be any changes to input + */ + public function testConditionV6() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2, + $subCondition1 + ], + 'all' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, true], + [$field3, true], + [$field4, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($inputCondition, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-2 => [ attribute => field-2 ] + * condition-3 => [ attribute => field-3 ] + * ] + * ] + * ] + * ] + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-4 => [ attribute => field-4 ] + * condition-5 => [ attribute => field-5 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-6 => [ attribute => field-6 ] + * condition-7 => [ attribute => field-7 ] + * ] + * ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-3 and condition-5 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => all + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-6 => [ attribute => field-6 ] + * condition-7 => [ attribute => field-7 ] + * ] + * ] + * ] + * ] + * ] + * ] + * ] + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConditionV7() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + $field3 = 'field-3'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition2, + $simpleCondition3 + ], + 'any' + ); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $subCondition1 + ], + 'all' + ); + + $field4 = 'field-4'; + $field5 = 'field-5'; + $field6 = 'field-6'; + $field7 = 'field-7'; + + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $simpleCondition5 = $this->getMockForSimpleCondition($field5); + $simpleCondition6 = $this->getMockForSimpleCondition($field6); + $simpleCondition7 = $this->getMockForSimpleCondition($field7); + + $subCondition3 = $this->getMockForCombinedCondition( + [ + $simpleCondition4, + $simpleCondition5 + ], + 'any' + ); + $subCondition4 = $this->getMockForCombinedCondition( + [ + $simpleCondition6, + $simpleCondition7 + ], + 'any' + ); + $subCondition5 = $this->getMockForCombinedCondition( + [ + $subCondition3, + $subCondition4 + ], + 'all' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition2, + $subCondition5 + ], + 'any' + ); + + $validSubCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition1 + ], + 'all' + ); + $validSubCondition4 = $this->getMockForCombinedCondition( + [ + $subCondition4 + ], + 'all' + ); + + $validResult = $this->getMockForCombinedCondition( + [ + $validSubCondition2, + $validSubCondition4 + ], + 'any' + ); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, true], + [$field2, true], + [$field3, false], + [$field4, true], + [$field5, false], + [$field6, true], + [$field7, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-4 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + */ + public function testConditionV8() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, false], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * input condition tree: + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-1 => [ attribute => field-1 ] + * condition-2 => [ attribute => field-2 ] + * ] + * ] + * combined-condition => + * [ + * aggregation => any + * conditions => + * [ + * condition-3 => [ attribute => field-3 ] + * condition-4 => [ attribute => field-4 ] + * ] + * ] + * condition-5 => [ attribute => field-5 ] + * ] + * ] + * ] + * + * in case when condition-1 and condition-4 are not mappable the result must be next: + * + * [ + * combined-condition => + * [ + * aggregation => any + * conditions => [] + * ] + */ + public function testConditionV9() + { + $field1 = 'field-1'; + $field2 = 'field-2'; + + $simpleCondition1 = $this->getMockForSimpleCondition($field1); + $simpleCondition2 = $this->getMockForSimpleCondition($field2); + $subCondition1 = $this->getMockForCombinedCondition( + [ + $simpleCondition1, + $simpleCondition2 + ], + 'any' + ); + + $field3 = 'field-3'; + $field4 = 'field-4'; + + $simpleCondition3 = $this->getMockForSimpleCondition($field3); + $simpleCondition4 = $this->getMockForSimpleCondition($field4); + $subCondition2 = $this->getMockForCombinedCondition( + [ + $simpleCondition3, + $simpleCondition4 + ], + 'any' + ); + + $field5 = 'field-5'; + $simpleCondition5 = $this->getMockForSimpleCondition($field5); + + $inputCondition = $this->getMockForCombinedCondition( + [ + $subCondition1, + $subCondition2, + $simpleCondition5 + ], + 'any' + ); + + $validResult = $this->getMockForCombinedCondition([], 'any'); + + $this->customConditionProcessorBuilderMock + ->method('hasProcessorForField') + ->will( + $this->returnValueMap( + [ + [$field1, false], + [$field2, true], + [$field3, true], + [$field4, false], + [$field5, true], + ] + ) + ); + + $this->eavConfigMock + ->method('getAttribute') + ->willReturn(null); + + $result = $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + + $this->assertEquals($validResult, $result); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Undefined condition type "olo-lo" passed in. + */ + public function testException() + { + $simpleCondition = $this->getMockForSimpleCondition('field'); + $simpleCondition->setType('olo-lo'); + $inputCondition = $this->getMockForCombinedCondition([$simpleCondition], 'any'); + + $this->mappableConditionProcessor->rebuildConditionsTree($inputCondition); + } + + /** + * @param $subConditions + * @param $aggregator + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockForCombinedCondition($subConditions, $aggregator) + { + $mock = $this->getMockBuilder(CombinedCondition::class) + ->disableOriginalConstructor() + ->setMethods() + ->getMock(); + + $mock->setConditions($subConditions); + $mock->setAggregator($aggregator); + $mock->setType(CombinedCondition::class); + + return $mock; + } + + /** + * @param $attribute + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockForSimpleCondition($attribute) + { + $mock = $this->getMockBuilder(SimpleCondition::class) + ->disableOriginalConstructor() + ->setMethods() + ->getMock(); + + $mock->setAttribute($attribute); + $mock->setType(SimpleCondition::class); + + return $mock; + } +} diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php index d03517ba04137..3fcec04d40999 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php @@ -380,9 +380,38 @@ public function testGetConditionsFieldSetId() $this->assertEquals($expectedResult, $this->rule->getConditionsFieldSetId($formName)); } - public function testReindex() - { - $this->_ruleProductProcessor->expects($this->once())->method('reindexList'); + /** + * @dataProvider reindexDataProvider + * @param array $productIds + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $callCount + * @return void + */ + public function testReindex( + array $productIds, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $callCount + ) { + $this->objectManager->setBackwardCompatibleProperty($this->rule, '_productIds', $productIds); + $this->_ruleProductProcessor->expects($callCount)->method('reindexList'); $this->rule->reindex(); } + + /** + * @return array + */ + public function reindexDataProvider():array + { + return [ + [ + 'productIds' => [ + 1 => [1 =>true], + 2 => [1 =>true], + ], + 'call' => $this->once(), + ], + [ + 'productIds' => [], + 'call' => $this->never(), + ], + ]; + } } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php b/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php index b052ccddbf6b4..25bae43a930bb 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Observer/AddDirtyRulesNoticeTest.php @@ -49,7 +49,7 @@ public function testExecute() $eventObserverMock->expects($this->at(0))->method('getData')->with('dirty_rules')->willReturn($flagMock); $flagMock->expects($this->once())->method('getState')->willReturn(1); $eventObserverMock->expects($this->at(1))->method('getData')->with('message')->willReturn($message); - $this->messageManagerMock->expects($this->once())->method('addNotice')->with($message); + $this->messageManagerMock->expects($this->once())->method('addNoticeMessage')->with($message); $this->observer->execute($eventObserverMock); } } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Plugin/Indexer/CategoryTest.php b/app/code/Magento/CatalogRule/Test/Unit/Plugin/Indexer/CategoryTest.php index 5822e01853deb..71e2093b0e325 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Plugin/Indexer/CategoryTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Plugin/Indexer/CategoryTest.php @@ -32,7 +32,7 @@ protected function setUp() ); $this->subject = $this->createPartialMock( \Magento\Catalog\Model\Category::class, - ['getAffectedProductIds', '__wakeUp'] + ['getChangedProductIds', '__wakeUp'] ); $this->plugin = (new ObjectManager($this))->getObject( @@ -46,7 +46,7 @@ protected function setUp() public function testAfterSaveWithoutAffectedProductIds() { $this->subject->expects($this->any()) - ->method('getAffectedProductIds') + ->method('getChangedProductIds') ->will($this->returnValue([])); $this->productRuleProcessor->expects($this->never()) @@ -60,7 +60,7 @@ public function testAfterSave() $productIds = [1, 2, 3]; $this->subject->expects($this->any()) - ->method('getAffectedProductIds') + ->method('getChangedProductIds') ->will($this->returnValue($productIds)); $this->productRuleProcessor->expects($this->once()) diff --git a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php index 797097f8a5346..3c0cb0f0e05f8 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php @@ -155,7 +155,7 @@ protected function setUp() */ public function testGetValue() { - $coreStoreId = 1; + $storeId = 5; $coreWebsiteId = 1; $productId = 1; $customerGroupId = 1; @@ -166,13 +166,13 @@ public function testGetValue() $this->coreStoreMock->expects($this->once()) ->method('getId') - ->will($this->returnValue($coreStoreId)); + ->willReturn($storeId); $this->coreStoreMock->expects($this->once()) ->method('getWebsiteId') ->will($this->returnValue($coreWebsiteId)); $this->dataTimeMock->expects($this->once()) ->method('scopeDate') - ->with($this->equalTo($coreStoreId)) + ->with($storeId) ->will($this->returnValue($dateTime)); $this->customerSessionMock->expects($this->once()) ->method('getCustomerGroupId') diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json index b2b3a80183ae6..768b6e3897cbf 100644 --- a/app/code/Magento/CatalogRule/composer.json +++ b/app/code/Magento/CatalogRule/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-rule", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-rule": "100.2.*", "magento/module-catalog": "102.0.*", @@ -17,7 +17,7 @@ "magento/module-catalog-rule-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.2", + "version": "101.0.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogRule/etc/di.xml b/app/code/Magento/CatalogRule/etc/di.xml index 4b368b1cef89a..e0d91db542390 100644 --- a/app/code/Magento/CatalogRule/etc/di.xml +++ b/app/code/Magento/CatalogRule/etc/di.xml @@ -126,4 +126,42 @@ </argument> </arguments> </type> + <preference for="Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface" type="Magento\CatalogRule\Model\Indexer\IndexerTableSwapper" /> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface"> + <arguments> + <argument name="priceModifiers" xsi:type="array"> + <item name="catalogRulePriceModifier" xsi:type="object">Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier"> + <arguments> + <argument name="priceModifiers" xsi:type="array"> + <item name="catalogRulePriceModifier" xsi:type="object">Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier</item> + </argument> + </arguments> + </type> + <virtualType name="CatalogRuleCustomConditionProvider" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\CustomConditionProvider"> + <arguments> + <argument name="customConditionProcessors" xsi:type="array"> + <item name="category_ids" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\ProductCategoryCondition</item> + </argument> + </arguments> + </virtualType> + <virtualType name="CatalogRuleAdvancedFilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\AdvancedFilterProcessor"> + <arguments> + <argument name="defaultConditionProcessor" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\ConditionProcessor\DefaultCondition</argument> + <argument name="customConditionProvider" xsi:type="object">CatalogRuleCustomConditionProvider</argument> + </arguments> + </virtualType> + <type name="Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier"> + <arguments> + <argument name="searchCriteriaProcessor" xsi:type="object">CatalogRuleAdvancedFilterProcessor</argument> + </arguments> + </type> + <type name="Magento\CatalogRule\Model\Rule\Condition\MappableConditionsProcessor"> + <arguments> + <argument name="customConditionProvider" xsi:type="object">CatalogRuleCustomConditionProvider</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogRule/etc/indexer.xml b/app/code/Magento/CatalogRule/etc/indexer.xml index 08ed456457bfe..e648ea567631c 100644 --- a/app/code/Magento/CatalogRule/etc/indexer.xml +++ b/app/code/Magento/CatalogRule/etc/indexer.xml @@ -14,4 +14,9 @@ <title translate="true">Catalog Product Rule Indexed product/rule association + + + + + diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 2990c03ae0159..35efe33461afc 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -18,11 +18,15 @@
    -
    + + +
    + + diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml index 1798b1112426c..db5492d3c47c5 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml +++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml @@ -4,17 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -/**@var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset $block */ +/** @var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset $block */ ?> getElement() ?> getFieldSetId() != null ? $block->getFieldSetId() : $_element->getHtmlId() ?>
    -
    serialize(['class']) ?> class="fieldset"> - getLegend() ?> +
    serialize(['class']) ?> class="fieldset"> + escapeHtml($_element->getLegend()) ?>
    - getComment()): ?> + getComment()) : ?>
    escapeHtml($_element->getComment()) ?>
    @@ -30,9 +29,10 @@ require([ "prototype" ], function(VarienRulesForm){ -window. = new VarienRulesForm('', 'getNewChildUrl() ?>'); -getReadonly()): ?> - getHtmlId() ?>.setReadonly(true); +window. = new VarienRulesForm('', + 'getNewChildUrl() ?>'); +getReadonly()) : ?> + getHtmlId() ?>.setReadonly(true); }); diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml index 1572c8f0d4e48..4ed782edd4eec 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml +++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/form.phtml @@ -3,11 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?>
    getFormHtml() ?>
    - diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogRuleConfigurable/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/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogRuleConfigurable/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/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/README.md b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/README.md new file mode 100644 index 0000000000000..3d271b8325e60 --- /dev/null +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Rule Configurable Functional Tests + +The Functional Test Module for **Magento Catalog Rule Configurable** module. diff --git a/app/code/Magento/CatalogRuleConfigurable/composer.json b/app/code/Magento/CatalogRuleConfigurable/composer.json index 71d03c535ae5d..1ab3ff11b52a7 100644 --- a/app/code/Magento/CatalogRuleConfigurable/composer.json +++ b/app/code/Magento/CatalogRuleConfigurable/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-rule-configurable", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-configurable-product": "100.2.*", "magento/framework": "101.0.*", "magento/module-catalog": "102.0.*", @@ -13,7 +13,7 @@ "magento/module-catalog-rule": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php index 59d33ddbfd60b..68e4233e8deaf 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php @@ -201,16 +201,16 @@ public function getCurrency($attribute) public function getAttributeInputType($attribute) { $dataType = $attribute->getBackend()->getType(); - $imputType = $attribute->getFrontend()->getInputType(); - if ($imputType == 'select' || $imputType == 'multiselect') { + $inputType = $attribute->getFrontend()->getInputType(); + if ($inputType == 'select' || $inputType == 'multiselect') { return 'select'; } - if ($imputType == 'boolean') { + if ($inputType == 'boolean') { return 'yesno'; } - if ($imputType == 'price') { + if ($inputType == 'price') { return 'price'; } diff --git a/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php new file mode 100644 index 0000000000000..0be43ce6ff1fb --- /dev/null +++ b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php @@ -0,0 +1,40 @@ +response = $response; + } + + /** + * Check is current page cacheable + * + * @return bool + */ + public function isPageCacheable() + { + $pragma = $this->response->getHeader('pragma')->getFieldValue(); + return ($pragma == 'cache'); + } +} diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php index 86195aa31f0e4..d6a30f4f3141d 100644 --- a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php @@ -6,7 +6,6 @@ */ namespace Magento\CatalogSearch\Controller\Advanced; -use Magento\Catalog\Model\Layer\Resolver; use Magento\CatalogSearch\Model\Advanced as ModelAdvanced; use Magento\Framework\App\Action\Context; use Magento\Framework\UrlFactory; @@ -45,7 +44,7 @@ public function __construct( } /** - * @return void + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -58,7 +57,9 @@ public function execute() $defaultUrl = $this->_urlFactory->create() ->addQueryParams($this->getRequest()->getQueryValue()) ->getUrl('*/*/'); - $this->getResponse()->setRedirect($this->_redirect->error($defaultUrl)); + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setUrl($this->_redirect->error($defaultUrl)); + return $resultRedirect; } } } diff --git a/app/code/Magento/CatalogSearch/Controller/Result/Index.php b/app/code/Magento/CatalogSearch/Controller/Result/Index.php index f3990da3a325e..22958b64d444d 100644 --- a/app/code/Magento/CatalogSearch/Controller/Result/Index.php +++ b/app/code/Magento/CatalogSearch/Controller/Result/Index.php @@ -9,9 +9,9 @@ use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Session; use Magento\Framework\App\Action\Context; -use Magento\Framework\App\ResourceConnection; use Magento\Store\Model\StoreManagerInterface; use Magento\Search\Model\QueryFactory; +use Magento\Search\Model\PopularSearchTerms; class Index extends \Magento\Framework\App\Action\Action { @@ -64,34 +64,88 @@ public function __construct( * Display search result * * @return void + * + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { $this->layerResolver->create(Resolver::CATALOG_LAYER_SEARCH); + /* @var $query \Magento\Search\Model\Query */ $query = $this->_queryFactory->get(); - $query->setStoreId($this->_storeManager->getStore()->getId()); + $storeId = $this->_storeManager->getStore()->getId(); + $query->setStoreId($storeId); + + $queryText = $query->getQueryText(); + + if ($queryText != '') { + $catalogSearchHelper = $this->_objectManager->get(\Magento\CatalogSearch\Helper\Data::class); - if ($query->getQueryText() != '') { - if ($this->_objectManager->get(\Magento\CatalogSearch\Helper\Data::class)->isMinQueryLength()) { - $query->setId(0)->setIsActive(1)->setIsProcessed(1); + $getAdditionalRequestParameters = $this->getRequest()->getParams(); + unset($getAdditionalRequestParameters[QueryFactory::QUERY_VAR_NAME]); + + if (empty($getAdditionalRequestParameters) && + $this->_objectManager->get(PopularSearchTerms::class)->isCacheable($queryText, $storeId) + ) { + $this->getCacheableResult($catalogSearchHelper, $query); } else { - $query->saveIncrementalPopularity(); + $this->getNotCacheableResult($catalogSearchHelper, $query); + } + } else { + $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + } + } - $redirect = $query->getRedirect(); - if ($redirect && $this->_url->getCurrentUrl() !== $redirect) { - $this->getResponse()->setRedirect($redirect); - return; - } + /** + * Return cacheable result + * + * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper + * @param \Magento\Search\Model\Query $query + * @return void + */ + private function getCacheableResult($catalogSearchHelper, $query) + { + if (!$catalogSearchHelper->isMinQueryLength()) { + $redirect = $query->getRedirect(); + if ($redirect && $this->_url->getCurrentUrl() !== $redirect) { + $this->getResponse()->setRedirect($redirect); + return; } + } - $this->_objectManager->get(\Magento\CatalogSearch\Helper\Data::class)->checkNotes(); + $catalogSearchHelper->checkNotes(); + + $this->_view->loadLayout(); + $this->_view->renderLayout(); + } - $this->_view->loadLayout(); - $this->_view->renderLayout(); + /** + * Return not cacheable result + * + * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper + * @param \Magento\Search\Model\Query $query + * @return void + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getNotCacheableResult($catalogSearchHelper, $query) + { + if ($catalogSearchHelper->isMinQueryLength()) { + $query->setId(0)->setIsActive(1)->setIsProcessed(1); } else { - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + $query->saveIncrementalPopularity(); + $redirect = $query->getRedirect(); + if ($redirect && $this->_url->getCurrentUrl() !== $redirect) { + $this->getResponse()->setRedirect($redirect); + return; + } } + + $catalogSearchHelper->checkNotes(); + + $this->_view->loadLayout(); + $this->getResponse()->setNoCacheHeaders(); + $this->_view->renderLayout(); } } diff --git a/app/code/Magento/CatalogSearch/Controller/SearchTermsLog/Save.php b/app/code/Magento/CatalogSearch/Controller/SearchTermsLog/Save.php new file mode 100644 index 0000000000000..a4a843c636cd0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Controller/SearchTermsLog/Save.php @@ -0,0 +1,95 @@ +storeManager = $storeManager; + $this->catalogSearchHelper = $catalogSearchHelper; + $this->queryFactory = $queryFactory; + $this->resultJsonFactory = $resultJsonFactory; + } + + /** + * Save search term + * + * @return Json + */ + public function execute() + { + /* @var $query \Magento\Search\Model\Query */ + $query = $this->queryFactory->get(); + + $query->setStoreId($this->storeManager->getStore()->getId()); + + if ($query->getQueryText() != '') { + try { + if ($this->catalogSearchHelper->isMinQueryLength()) { + $query->setId(0)->setIsActive(1)->setIsProcessed(1); + } else { + $query->saveIncrementalPopularity(); + } + $responseContent = ['success' => true, 'error_message' => '']; + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $responseContent = ['success' => false, 'error_message' => $e]; + } + } else { + $responseContent = ['success' => false, 'error_message' => __('Search term is empty')]; + } + + /** @var Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($responseContent); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php index a2242ff0f355b..5887c76e8ddc2 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation; use Magento\Catalog\Model\Product; -use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder; use Magento\Customer\Model\Session; use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; @@ -19,7 +20,7 @@ use Magento\Framework\App\ObjectManager; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * DataProvider for Catalog search Mysql. */ class DataProvider implements DataProviderInterface { @@ -48,23 +49,31 @@ class DataProvider implements DataProviderInterface */ private $connection; + /** + * @var QueryBuilder; + */ + private $queryBuilder; + /** * @param Config $eavConfig * @param ResourceConnection $resource * @param ScopeResolverInterface $scopeResolver * @param Session $customerSession + * @param QueryBuilder|null $queryBuilder */ public function __construct( Config $eavConfig, ResourceConnection $resource, ScopeResolverInterface $scopeResolver, - Session $customerSession + Session $customerSession, + QueryBuilder $queryBuilder = null ) { $this->eavConfig = $eavConfig; $this->resource = $resource; $this->connection = $resource->getConnection(); $this->scopeResolver = $scopeResolver; $this->customerSession = $customerSession; + $this->queryBuilder = $queryBuilder ?: ObjectManager::getInstance()->get(QueryBuilder::class); } /** @@ -79,47 +88,13 @@ public function getDataSet( $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $bucket->getField()); - $select = $this->getSelect(); - - $select->joinInner( - ['entities' => $entityIdsTable->getName()], - 'main_table.entity_id = entities.entity_id', - [] + $select = $this->queryBuilder->build( + $attribute, + $entityIdsTable->getName(), + $currentScope, + $this->customerSession->getCustomerGroupId() ); - if ($attribute->getAttributeCode() === 'price') { - /** @var \Magento\Store\Model\Store $store */ - $store = $this->scopeResolver->getScope($currentScope); - if (!$store instanceof \Magento\Store\Model\Store) { - throw new \RuntimeException('Illegal scope resolved'); - } - $table = $this->resource->getTableName('catalog_product_index_price'); - $select->from(['main_table' => $table], null) - ->columns([BucketInterface::FIELD_VALUE => 'main_table.min_price']) - ->where('main_table.customer_group_id = ?', $this->customerSession->getCustomerGroupId()) - ->where('main_table.website_id = ?', $store->getWebsiteId()); - } else { - $currentScopeId = $this->scopeResolver->getScope($currentScope) - ->getId(); - $table = $this->resource->getTableName( - 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '') - ); - $subSelect = $select; - $subSelect->from(['main_table' => $table], ['main_table.entity_id', 'main_table.value']) - ->distinct() - ->joinLeft( - ['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')], - 'main_table.source_id = stock_index.product_id', - [] - ) - ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) - ->where('main_table.store_id = ? ', $currentScopeId) - ->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK); - $parentSelect = $this->getSelect(); - $parentSelect->from(['main_table' => $subSelect], ['main_table.value']); - $select = $parentSelect; - } - return $select; } @@ -130,12 +105,4 @@ public function execute(Select $select) { return $this->connection->fetchAssoc($select); } - - /** - * @return Select - */ - private function getSelect() - { - return $this->connection->select(); - } } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php new file mode 100644 index 0000000000000..7ebf71f424439 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php @@ -0,0 +1,184 @@ +resource = $resource; + $this->scopeResolver = $scopeResolver; + $this->inventoryConfig = $inventoryConfig; + $this->priceTableResolver = $priceTableResolver + ?: ObjectManager::getInstance()->get(IndexScopeResolverInterface::class); + $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); + } + + /** + * Build select. + * + * @param AbstractAttribute $attribute + * @param string $tableName + * @param int $currentScope + * @param int $customerGroupId + * + * @return Select + */ + public function build( + AbstractAttribute $attribute, + string $tableName, + int $currentScope, + int $customerGroupId + ) : Select { + $select = $this->resource->getConnection()->select(); + $select->joinInner( + ['entities' => $tableName], + 'main_table.entity_id = entities.entity_id', + [] + ); + + if ($attribute->getAttributeCode() === 'price') { + return $this->buildQueryForPriceAttribute($currentScope, $customerGroupId, $select); + } + + return $this->buildQueryForAttribute($currentScope, $attribute, $select); + } + + /** + * Build select for price attribute. + * + * @param int $currentScope + * @param int $customerGroupId + * @param Select $select + * + * @return Select + */ + private function buildQueryForPriceAttribute( + int $currentScope, + int $customerGroupId, + Select $select + ) : Select { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->scopeResolver->getScope($currentScope); + if (!$store instanceof \Magento\Store\Model\Store) { + throw new \RuntimeException('Illegal scope resolved'); + } + $websiteId = $store->getWebsiteId(); + + $tableName = $this->priceTableResolver->resolve( + 'catalog_product_index_price', + [ + $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + (string)$websiteId + ), + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$customerGroupId + ), + ] + ); + $select->from(['main_table' => $tableName], null) + ->columns([BucketInterface::FIELD_VALUE => 'main_table.min_price']) + ->where('main_table.customer_group_id = ?', $customerGroupId) + ->where('main_table.website_id = ?', $websiteId); + + return $select; + } + + /** + * Build select for attribute. + * + * @param int $currentScope + * @param AbstractAttribute $attribute + * @param Select $select + * + * @return Select + */ + private function buildQueryForAttribute( + int $currentScope, + AbstractAttribute $attribute, + Select $select + ) : Select { + $currentScopeId = $this->scopeResolver->getScope($currentScope)->getId(); + $table = $this->resource->getTableName( + 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '') + ); + $select->from(['main_table' => $table], ['main_table.entity_id', 'main_table.value']) + ->distinct() + ->joinLeft( + ['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')], + 'main_table.source_id = stock_index.product_id', + [] + ) + ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) + ->where('main_table.store_id = ? ', $currentScopeId); + + if (!$this->inventoryConfig->isShowOutOfStock($currentScopeId)) { + $select->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK); + } + + $parentSelect = $this->resource->getConnection()->select(); + $parentSelect->from(['main_table' => $select], ['main_table.value']); + return $parentSelect; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php index 8b7a7ed214e36..d2e653ac9b9ae 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php @@ -6,17 +6,21 @@ namespace Magento\CatalogSearch\Model\Adapter\Mysql\Dynamic; use Magento\Catalog\Model\Layer\Filter\Price\Range; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Customer\Model\Session; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\DB\Select; +use Magento\Framework\Indexer\DimensionFactory; use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface as MysqlDataProviderInterface; use Magento\Framework\Search\Dynamic\DataProviderInterface; use Magento\Framework\Search\Dynamic\IntervalFactory; use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\StoreManager; +use \Magento\Framework\Search\Request\IndexScopeResolverInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -58,6 +62,16 @@ class DataProvider implements DataProviderInterface */ private $storeManager; + /** + * @var IndexScopeResolverInterface + */ + private $priceTableResolver; + + /** + * @var DimensionFactory|null + */ + private $dimensionFactory; + /** * @param ResourceConnection $resource * @param Range $range @@ -65,6 +79,8 @@ class DataProvider implements DataProviderInterface * @param MysqlDataProviderInterface $dataProvider * @param IntervalFactory $intervalFactory * @param StoreManager $storeManager + * @param IndexScopeResolverInterface|null $priceTableResolver + * @param DimensionFactory|null $dimensionFactory */ public function __construct( ResourceConnection $resource, @@ -72,7 +88,9 @@ public function __construct( Session $customerSession, MysqlDataProviderInterface $dataProvider, IntervalFactory $intervalFactory, - StoreManager $storeManager = null + StoreManager $storeManager = null, + IndexScopeResolverInterface $priceTableResolver = null, + DimensionFactory $dimensionFactory = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -81,6 +99,10 @@ public function __construct( $this->dataProvider = $dataProvider; $this->intervalFactory = $intervalFactory; $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManager::class); + $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get( + IndexScopeResolverInterface::class + ); + $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); } /** @@ -104,16 +126,30 @@ public function getAggregations(\Magento\Framework\Search\Dynamic\EntityStorage ]; $select = $this->getSelect(); - - $tableName = $this->resource->getTableName('catalog_product_index_price'); + $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $customerGroupId = $this->customerSession->getCustomerGroupId(); + + $tableName = $this->priceTableResolver->resolve( + 'catalog_product_index_price', + [ + $this->dimensionFactory->create( + WebsiteDimensionProvider::DIMENSION_NAME, + (string)$websiteId + ), + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$customerGroupId + ), + ] + ); /** @var Table $table */ $table = $entityStorage->getSource(); $select->from(['main_table' => $tableName], []) ->where('main_table.entity_id in (select entity_id from ' . $table->getName() . ')') ->columns($aggregation); - $select = $this->setCustomerGroupId($select); - $select->where('main_table.website_id = ?', $this->storeManager->getStore()->getWebsiteId()); + $select->where('customer_group_id = ?', $customerGroupId); + $select->where('main_table.website_id = ?', $websiteId); return $this->connection->fetchRow($select); } @@ -192,13 +228,4 @@ private function getSelect() { return $this->connection->select(); } - - /** - * @param Select $select - * @return Select - */ - private function setCustomerGroupId($select) - { - return $select->where('customer_group_id = ?', $this->customerSession->getCustomerGroupId()); - } } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php index 6bf735e2141cc..94432bbfe4a71 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php @@ -12,7 +12,13 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\Dimension; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DataProvider { /** @@ -32,20 +38,28 @@ class DataProvider */ protected $categoryFactory; + /** + * @var TableResolver + */ + private $tableResolver; + /** * DataProvider constructor. * @param ResourceConnection $resource * @param ScopeResolverInterface $scopeResolver * @param Resolver $layerResolver + * @param TableResolver|null $tableResolver */ public function __construct( ResourceConnection $resource, ScopeResolverInterface $scopeResolver, - Resolver $layerResolver + Resolver $layerResolver, + TableResolver $tableResolver = null ) { $this->resource = $resource; $this->scopeResolver = $scopeResolver; $this->layer = $layerResolver->get(); + $this->tableResolver = $tableResolver ?: ObjectManager::getInstance()->get(TableResolver::class); } /** @@ -69,9 +83,18 @@ public function aroundGetDataSet( $currentScopeId = $this->scopeResolver->getScope($dimensions['scope']->getValue())->getId(); $currentCategory = $this->layer->getCurrentCategory(); + $catalogCategoryProductDimension = new Dimension(\Magento\Store\Model\Store::ENTITY, $currentScopeId); + + $catalogCategoryProductTableName = $this->tableResolver->resolve( + AbstractAction::MAIN_INDEX_TABLE, + [ + $catalogCategoryProductDimension + ] + ); + $derivedTable = $this->resource->getConnection()->select(); $derivedTable->from( - ['main_table' => $this->resource->getTableName('catalog_category_product_index')], + ['main_table' => $catalogCategoryProductTableName], [ 'value' => 'category_id' ] diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php index c4413d002e19c..0c2e8a4ef7aa2 100644 --- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php @@ -10,9 +10,16 @@ use Magento\Search\Model\QueryFactory; use Magento\Search\Model\Autocomplete\DataProviderInterface; use Magento\Search\Model\Autocomplete\ItemFactory; +use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; +use Magento\Store\Model\ScopeInterface; class DataProvider implements DataProviderInterface { + /** + * Autocomplete limit + */ + private static $CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit'; + /** * Query factory * @@ -27,16 +34,25 @@ class DataProvider implements DataProviderInterface */ protected $itemFactory; + /** + * Scope Config Object + * + * @var ScopeConfig + */ + private $scopeConfig; + /** * @param QueryFactory $queryFactory * @param ItemFactory $itemFactory */ public function __construct( QueryFactory $queryFactory, - ItemFactory $itemFactory + ItemFactory $itemFactory, + ScopeConfig $scopeConfig ) { $this->queryFactory = $queryFactory; $this->itemFactory = $itemFactory; + $this->scopeConfig = $scopeConfig; } /** @@ -46,6 +62,10 @@ public function getItems() { $collection = $this->getSuggestCollection(); $query = $this->queryFactory->get()->getQueryText(); + $limit = (int) $this->scopeConfig->getValue( + static::$CONFIG_AUTOCOMPLETE_LIMIT, + ScopeInterface::SCOPE_STORE + ); $result = []; foreach ($collection as $item) { $resultItem = $this->itemFactory->create([ @@ -58,7 +78,7 @@ public function getItems() $result[] = $resultItem; } } - return $result; + return ($limit) ? array_splice($result, 0, $limit) : $result; } /** diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 9009a40c19dff..cba34cd40132f 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -12,6 +12,7 @@ use Magento\Framework\Search\Request\Config as SearchRequestConfig; use Magento\Framework\Search\Request\DimensionFactory; use Magento\Store\Model\StoreManagerInterface; +use Magento\Indexer\Model\ProcessManager; /** * Provide functionality for Fulltext Search indexing. @@ -71,6 +72,11 @@ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\F */ private $indexScopeState; + /** + * @var ProcessManager + */ + private $processManager; + /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -81,6 +87,8 @@ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\F * @param array $data * @param IndexSwitcherInterface $indexSwitcher * @param Scope\State $indexScopeState + * @param ProcessManager $processManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( FullFactory $fullActionFactory, @@ -91,7 +99,8 @@ public function __construct( SearchRequestConfig $searchRequestConfig, array $data, IndexSwitcherInterface $indexSwitcher = null, - State $indexScopeState = null + State $indexScopeState = null, + ProcessManager $processManager = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -106,8 +115,12 @@ public function __construct( if (null === $indexScopeState) { $indexScopeState = ObjectManager::getInstance()->get(State::class); } + if (null === $processManager) { + $processManager = ObjectManager::getInstance()->get(ProcessManager::class); + } $this->indexSwitcher = $indexSwitcher; $this->indexScopeState = $indexScopeState; + $this->processManager = $processManager; } /** @@ -123,11 +136,12 @@ public function execute($ids) $saveHandler = $this->indexerHandlerFactory->create([ 'data' => $this->data ]); + foreach ($storeIds as $storeId) { $dimension = $this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId]); $productIds = array_unique(array_merge($ids, $this->fulltextResource->getRelationsByChild($ids))); $saveHandler->deleteIndex([$dimension], new \ArrayObject($productIds)); - $saveHandler->saveIndex([$dimension], $this->fullAction->rebuildStoreIndex($storeId, $ids)); + $saveHandler->saveIndex([$dimension], $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } } @@ -139,20 +153,16 @@ public function execute($ids) public function executeFull() { $storeIds = array_keys($this->storeManager->getStores()); - /** @var IndexerHandler $saveHandler */ - $saveHandler = $this->indexerHandlerFactory->create([ - 'data' => $this->data - ]); + + $userFunctions = []; foreach ($storeIds as $storeId) { - $dimensions = [$this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId])]; - $this->indexScopeState->useTemporaryIndex(); + $userFunctions[$storeId] = function () use ($storeId) { + return $this->executeFullByStore($storeId); + }; + } - $saveHandler->cleanIndex($dimensions); - $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId)); + $this->processManager->execute($userFunctions); - $this->indexSwitcher->switchIndex($dimensions); - $this->indexScopeState->useRegularIndex(); - } $this->fulltextResource->resetSearchResults(); $this->searchRequestConfig->reset(); } @@ -178,4 +188,26 @@ public function executeRow($id) { $this->execute([$id]); } + + /** + * Execute full indexation by storeID + * + * @param int $storeId + */ + private function executeFullByStore($storeId) + { + /** @var IndexerHandler $saveHandler */ + $saveHandler = $this->indexerHandlerFactory->create([ + 'data' => $this->data + ]); + + $dimensions = [$this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId])]; + $this->indexScopeState->useTemporaryIndex(); + + $saveHandler->cleanIndex($dimensions); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId)); + + $this->indexSwitcher->switchIndex($dimensions); + $this->indexScopeState->useRegularIndex(); + } } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 07ff47610f330..7a94dcb78cfd5 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -6,10 +6,16 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Store\Model\Store; /** + * Catalog search full text search data provider. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * @api * @since 100.0.3 */ @@ -101,6 +107,23 @@ class DataProvider */ private $attributeOptions = []; + /** + * Cache searchable attributes by backend type + * + * @var array + */ + private $searchableAttributesByBackendType = []; + + /** + * Adjusts a size of filtered rows for searchable products. Filtered rows counts by the following condition: + * entity_id > X AND entity_id < X + BatchSize * antiGapMultiplier + * It will help in case a lot of gaps between entity_id in product table, when selected amount of products will be + * less than batch size + * + * @var int + */ + private $antiGapMultiplier; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -110,6 +133,7 @@ class DataProvider * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param int $antiGapMultiplier */ public function __construct( ResourceConnection $resource, @@ -119,7 +143,8 @@ public function __construct( \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + int $antiGapMultiplier = 5 ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -130,6 +155,7 @@ public function __construct( $this->storeManager = $storeManager; $this->engine = $engineProvider->get(); $this->metadata = $metadataPool->getMetadata(ProductInterface::class); + $this->antiGapMultiplier = $antiGapMultiplier; } /** @@ -150,7 +176,7 @@ private function getTable($table) * @param array $staticFields * @param array|int $productIds * @param int $lastProductId - * @param int $limit + * @param int $batch * @return array * @since 100.0.3 */ @@ -159,9 +185,47 @@ public function getSearchableProducts( array $staticFields, $productIds = null, $lastProductId = 0, - $limit = 100 + $batch = 100 + ) { + + $select = $this->getSelectForSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $batch); + if ($productIds === null) { + $select->where( + 'e.entity_id < ?', + $lastProductId ? $this->antiGapMultiplier * $batch + $lastProductId + 1 : $batch + 1 + ); + } + $products = $this->connection->fetchAll($select); + if ($productIds === null && !$products) { + // try to search without limit entity_id by batch size for cover case with a big gap between entity ids + $products = $this->connection->fetchAll( + $this->getSelectForSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $batch) + ); + } + + return $products; + } + + /** + * Get Select object for searchable products + * + * @param int $storeId + * @param array $staticFields + * @param array|int $productIds + * @param int $lastProductId + * @param int $batch + * @return Select + */ + private function getSelectForSearchableProducts( + $storeId, + array $staticFields, + $productIds, + $lastProductId, + $batch ) { - $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); + $lastProductId = (int)$lastProductId; + $select = $this->connection->select() ->useStraightJoin(true) ->from( @@ -174,15 +238,65 @@ public function getSearchableProducts( [] ); + $this->joinAttribute($select, 'visibility', $storeId, $this->engine->getAllowedVisibility()); + $this->joinAttribute($select, 'status', $storeId, [Status::STATUS_ENABLED]); + if ($productIds !== null) { $select->where('e.entity_id IN (?)', $productIds); } + $select->where('e.entity_id > ?', $lastProductId); + $select->order('e.entity_id'); + $select->limit($batch); - $select->where('e.entity_id > ?', $lastProductId)->limit($limit)->order('e.entity_id'); + return $select; + } - $result = $this->connection->fetchAll($select); + /** + * Join attribute to searchable product for filtration + * + * @param Select $select + * @param string $attributeCode + * @param int $storeId + * @param array $whereValue + */ + private function joinAttribute(Select $select, $attributeCode, $storeId, array $whereValue) + { + $linkField = $this->metadata->getLinkField(); + $attribute = $this->getSearchableAttribute($attributeCode); + $attributeTable = $this->getTable('catalog_product_entity_' . $attribute->getBackendType()); + $defaultAlias = $attributeCode . '_default'; + $storeAlias = $attributeCode . '_store'; + + $whereCondition = $this->connection->getCheckSql( + $storeAlias . '.value_id > 0', + $storeAlias . '.value', + $defaultAlias . '.value' + ); - return $result; + $select->join( + [$defaultAlias => $attributeTable], + $this->connection->quoteInto( + $defaultAlias . '.' . $linkField . '= e.' . $linkField . ' AND ' . $defaultAlias . '.attribute_id = ?', + $attribute->getAttributeId() + ) . $this->connection->quoteInto( + ' AND ' . $defaultAlias . '.store_id = ?', + Store::DEFAULT_STORE_ID + ), + [] + )->joinLeft( + [$storeAlias => $attributeTable], + $this->connection->quoteInto( + $storeAlias . '.' . $linkField . '= e.' . $linkField . ' AND ' . $storeAlias . '.attribute_id = ?', + $attribute->getAttributeId() + ) . $this->connection->quoteInto( + ' AND ' . $storeAlias . '.store_id = ?', + $storeId + ), + [] + )->where( + $whereCondition . ' IN (?)', + $whereValue + ); } /** @@ -204,29 +318,41 @@ public function getSearchableAttributes($backendType = null) /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ $attributes = $productAttributes->getItems(); + /** + * @deprecated Event argument catelogsearch_searchable_attributes_load_after. + * @see catalogsearch_searchable_attributes_load_after instead. + */ $this->eventManager->dispatch( 'catelogsearch_searchable_attributes_load_after', ['engine' => $this->engine, 'attributes' => $attributes] ); + $this->eventManager->dispatch( + 'catalogsearch_searchable_attributes_load_after', + ['engine' => $this->engine, 'attributes' => $attributes] + ); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); + $this->searchableAttributes[$attribute->getAttributeId()] = $attribute; + $this->searchableAttributes[$attribute->getAttributeCode()] = $attribute; } - - $this->searchableAttributes = $attributes; } if ($backendType !== null) { - $attributes = []; - foreach ($this->searchableAttributes as $attributeId => $attribute) { + if (isset($this->searchableAttributesByBackendType[$backendType])) { + return $this->searchableAttributesByBackendType[$backendType]; + } + $this->searchableAttributesByBackendType[$backendType] = []; + foreach ($this->searchableAttributes as $attribute) { if ($attribute->getBackendType() == $backendType) { - $attributes[$attributeId] = $attribute; + $this->searchableAttributesByBackendType[$backendType][$attribute->getAttributeId()] = $attribute; } } - return $attributes; + return $this->searchableAttributesByBackendType[$backendType]; } return $this->searchableAttributes; @@ -242,16 +368,8 @@ public function getSearchableAttributes($backendType = null) public function getSearchableAttribute($attribute) { $attributes = $this->getSearchableAttributes(); - if (is_numeric($attribute)) { - if (isset($attributes[$attribute])) { - return $attributes[$attribute]; - } - } elseif (is_string($attribute)) { - foreach ($attributes as $attributeModel) { - if ($attributeModel->getAttributeCode() == $attribute) { - return $attributeModel; - } - } + if (isset($attributes[$attribute])) { + return $attributes[$attribute]; } return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); @@ -339,7 +457,7 @@ public function getProductAttributes($storeId, array $productIds, array $attribu } if ($selects) { - $select = $this->connection->select()->union($selects, \Magento\Framework\DB\Select::SQL_UNION_ALL); + $select = $this->connection->select()->union($selects, Select::SQL_UNION_ALL); $query = $this->connection->query($select); while ($row = $query->fetch()) { $entityId = $productLinkFieldsToEntityIdMap[$row[$linkField]]; @@ -377,9 +495,9 @@ private function getProductTypeInstance($typeId) public function getProductChildIds($productId, $typeId) { $typeInstance = $this->getProductTypeInstance($typeId); - $relation = $typeInstance->isComposite( - $this->getProductEmulator($typeId) - ) ? $typeInstance->getRelationInfo() : false; + $relation = $typeInstance->isComposite($this->getProductEmulator($typeId)) + ? $typeInstance->getRelationInfo() + : false; if ($relation && $relation->getTable() && $relation->getParentFieldName() && $relation->getChildFieldName()) { $select = $this->connection->select()->from( @@ -425,7 +543,7 @@ private function getProductEmulator($typeId) * @param array $indexData * @param array $productData * @param int $storeId - * @return string + * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @since 100.0.3 */ @@ -454,10 +572,9 @@ public function prepareProductIndex($indexData, $productData, $storeId) } } } - foreach ($indexData as $entityId => $attributeData) { - foreach ($attributeData as $attributeId => $attributeValue) { - $value = $this->getAttributeValue($attributeId, $attributeValue, $storeId); + foreach ($attributeData as $attributeId => $attributeValues) { + $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); if (!empty($value)) { if (isset($index[$attributeId])) { $index[$attributeId][$entityId] = $value; @@ -488,41 +605,60 @@ public function prepareProductIndex($indexData, $productData, $storeId) * Retrieve attribute source value for search * * @param int $attributeId - * @param mixed $valueId + * @param mixed $valueIds * @param int $storeId * @return string */ - private function getAttributeValue($attributeId, $valueId, $storeId) + private function getAttributeValue($attributeId, $valueIds, $storeId) { $attribute = $this->getSearchableAttribute($attributeId); - $value = $this->engine->processAttributeValue($attribute, $valueId); + $value = $this->engine->processAttributeValue($attribute, $valueIds); + if (false !== $value) { + $optionValue = $this->getAttributeOptionValue($attributeId, $valueIds, $storeId); + if (null === $optionValue) { + $value = preg_replace('/\s+/iu', ' ', trim(strip_tags($value))); + } else { + $value = implode($this->separator, array_filter([$value, $optionValue])); + } + } + + return $value; + } - if (false !== $value - && $attribute->getIsSearchable() - && $attribute->usesSource() - && $this->engine->allowAdvancedIndex() + /** + * Get attribute option value + * + * @param int $attributeId + * @param int|string $valueIds + * @param int $storeId + * @return null|string + */ + private function getAttributeOptionValue($attributeId, $valueIds, $storeId) + { + $optionKey = $attributeId . '-' . $storeId; + $attributeValueIds = explode(',', $valueIds); + $attributeOptionValue = ''; + if (!array_key_exists($optionKey, $this->attributeOptions) ) { - if (!isset($this->attributeOptions[$attributeId][$storeId])) { + $attribute = $this->getSearchableAttribute($attributeId); + if ($this->engine->allowAdvancedIndex() + && $attribute->getIsSearchable() + && $attribute->usesSource() + ) { $attribute->setStoreId($storeId); $options = $attribute->getSource()->toOptionArray(); - $this->attributeOptions[$attributeId][$storeId] = array_combine( - array_column($options, 'value'), - array_column($options, 'label') - ); + $this->attributeOptions[$optionKey] = array_column($options, 'label', 'value'); + } else { + $this->attributeOptions[$optionKey] = null; } + } - $valueText = ''; - if (isset($this->attributeOptions[$attributeId][$storeId][$valueId])) { - $valueText = $this->attributeOptions[$attributeId][$storeId][$valueId]; + foreach ($attributeValueIds as $attributeValueId) { + if (isset($this->attributeOptions[$optionKey][$attributeValueId])) { + $attributeOptionValue .= $this->attributeOptions[$optionKey][$attributeValueId] . ' '; } - - $pieces = array_filter(array_merge([$value], [$valueText])); - - $value = implode($this->separator, $pieces); } - $value = preg_replace('/\\s+/siu', ' ', trim(strip_tags($value))); - - return $value; + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 639c0e8ca66f0..8eccc93ef303e 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; @@ -197,6 +198,13 @@ class Full */ private $dataProvider; + /** + * Batch size for searchable product ids + * + * @var int + */ + private $batchSize; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -218,6 +226,7 @@ class Full * @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param DataProvider $dataProvider + * @param int $batchSize * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -240,7 +249,8 @@ public function __construct( \Magento\Framework\Indexer\ConfigInterface $indexerConfig, \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory, \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - DataProvider $dataProvider = null + DataProvider $dataProvider = null, + $batchSize = 500 ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -264,6 +274,7 @@ public function __construct( $this->metadataPool = $metadataPool ?: ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); $this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class); + $this->batchSize = $batchSize; } /** @@ -297,27 +308,32 @@ protected function getTable($table) /** * Get parents IDs of product IDs to be re-indexed * + * @deprecated as it not used in the class anymore and duplicates another API method + * @see \Magento\CatalogSearch\Model\ResourceModel\Fulltext::getRelationsByChild() + * * @param int[] $entityIds * @return int[] + * @throws \Exception */ protected function getProductIdsFromParents(array $entityIds) { - /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ - $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $fieldForParent = $metadata->getLinkField(); + $connection = $this->connection; + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $select = $this->connection + $select = $connection ->select() - ->from(['relation' => $this->getTable('catalog_product_relation')], []) + ->from( + ['relation' => $this->getTable('catalog_product_relation')], + [] + ) ->distinct(true) ->where('child_id IN (?)', $entityIds) - ->where('parent_id NOT IN (?)', $entityIds) ->join( ['cpe' => $this->getTable('catalog_product_entity')], - 'relation.parent_id = cpe.' . $fieldForParent, + 'relation.parent_id = cpe.' . $linkField, ['cpe.entity_id'] ); - return $this->connection->fetchCol($select); + return $connection->fetchCol($select); } /** @@ -335,7 +351,7 @@ protected function getProductIdsFromParents(array $entityIds) public function rebuildStoreIndex($storeId, $productIds = null) { if ($productIds !== null) { - $productIds = array_unique(array_merge($productIds, $this->getProductIdsFromParents($productIds))); + $productIds = array_unique($productIds); } // prepare searchable attributes @@ -354,7 +370,7 @@ public function rebuildStoreIndex($storeId, $productIds = null) $lastProductId = 0; $products = $this->dataProvider - ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize); while (count($products) > 0) { $productsIds = array_column($products, 'entity_id'); $relatedProducts = $this->getRelatedProducts($products); @@ -365,12 +381,6 @@ public function rebuildStoreIndex($storeId, $productIds = null) foreach ($products as $productData) { $lastProductId = $productData['entity_id']; - if (!$this->isProductVisible($productData['entity_id'], $productsAttributes) || - !$this->isProductEnabled($productData['entity_id'], $productsAttributes) - ) { - continue; - } - $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]]; if (isset($relatedProducts[$productData['entity_id']])) { $childProductsIndex = $this->getChildProductsIndex( @@ -388,7 +398,7 @@ public function rebuildStoreIndex($storeId, $productIds = null) yield $productData['entity_id'] => $index; } $products = $this->dataProvider - ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize); }; } @@ -412,25 +422,6 @@ private function getRelatedProducts($products) return array_filter($relatedProducts); } - /** - * Performs check that product is visible on Store Front - * - * Check that product is visible on Store Front using visibility attribute - * and allowed visibility values. - * - * @param int $productId - * @param array $productsAttributes - * @return bool - */ - private function isProductVisible($productId, array $productsAttributes) - { - $visibility = $this->dataProvider->getSearchableAttribute('visibility'); - $allowedVisibility = $this->engine->getAllowedVisibility(); - return isset($productsAttributes[$productId]) && - isset($productsAttributes[$productId][$visibility->getId()]) && - in_array($productsAttributes[$productId][$visibility->getId()], $allowedVisibility); - } - /** * Performs check that product is enabled on Store Front * @@ -445,8 +436,7 @@ private function isProductEnabled($productId, array $productsAttributes) { $status = $this->dataProvider->getSearchableAttribute('status'); $allowedStatuses = $this->catalogProductStatus->getVisibleStatusIds(); - return isset($productsAttributes[$productId]) && - isset($productsAttributes[$productId][$status->getId()]) && + return isset($productsAttributes[$productId][$status->getId()]) && in_array($productsAttributes[$productId][$status->getId()], $allowedStatuses); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php new file mode 100644 index 0000000000000..fe68c6321e472 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php @@ -0,0 +1,45 @@ +fulltextIndexerProcessor = $fulltextIndexerProcessor; + } + + /** + * Mark fulltext indexer as invalid post-deletion of category. + * + * @param Resource $subjectCategory + * @param Resource $resultCategory + * @return Resource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(Resource $subjectCategory, Resource $resultCategory): Resource + { + $this->fulltextIndexerProcessor->markIndexerAsInvalid(); + + return $resultCategory; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php index ea5bb8be17c74..931d7571a9014 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php @@ -75,7 +75,7 @@ public function __construct( Batch $batch, IndexScopeResolverInterface $indexScopeResolver, array $data, - $batchSize = 100 + $batchSize = 500 ) { $this->indexScopeResolver = $indexScopeResolver; $this->indexStructure = $indexStructure; diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index 7aac6e98fc044..7b239d84bf962 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -91,12 +91,10 @@ protected function _getItemsData() return $this->itemDataBuilder->build(); } - $productSize = $productCollection->getSize(); - $options = $attribute->getFrontend() ->getSelectOptions(); foreach ($options as $option) { - $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize); + $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData); } return $this->itemDataBuilder->build(); @@ -108,17 +106,16 @@ protected function _getItemsData() * @param array $option * @param boolean $isAttributeFilterable * @param array $optionsFacetedData - * @param int $productSize * @return void */ - private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize) + private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData) { $value = $this->getOptionValue($option); if ($value === false) { return; } $count = $this->getOptionCount($value, $optionsFacetedData); - if ($isAttributeFilterable && (!$this->isOptionReducesResults($count, $productSize) || $count === 0)) { + if ($isAttributeFilterable && $count === 0) { return; } diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e61a886a41d6f..e9fb1070fedd5 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -111,12 +111,9 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = ''; + $to = null; } - $label = $this->renderRangeLabel( - empty($from) ? 0 : $from, - empty($to) ? 0 : $to - ); + $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; $data[] = [ @@ -141,7 +138,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($toPrice === null) { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php index 4212912af9930..831780631c124 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php @@ -70,6 +70,13 @@ public function allowAdvancedIndex() return true; } + /** + * Is attribute filterable as term cache + * + * @var array + */ + private $termFilterableAttributeAttributeCache = []; + /** * Is Attribute Filterable as Term * @@ -78,10 +85,16 @@ public function allowAdvancedIndex() */ private function isTermFilterableAttribute($attribute) { - return ($attribute->getIsVisibleInAdvancedSearch() - || $attribute->getIsFilterable() - || $attribute->getIsFilterableInSearch()) - && in_array($attribute->getFrontendInput(), ['select', 'multiselect']); + $attributeId = $attribute->getAttributeId(); + if (!isset($this->termFilterableAttributeAttributeCache[$attributeId])) { + $this->termFilterableAttributeAttributeCache[$attributeId] = + in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) + && ($attribute->getIsVisibleInAdvancedSearch() + || $attribute->getIsFilterable() + || $attribute->getIsFilterableInSearch()); + } + + return $this->termFilterableAttributeAttributeCache[$attributeId]; } /** @@ -94,7 +107,10 @@ public function processAttributeValue($attribute, $value) && in_array($attribute->getFrontendInput(), ['text', 'textarea']) ) { $result = $value; - } elseif ($this->isTermFilterableAttribute($attribute)) { + } elseif ($this->isTermFilterableAttribute($attribute) + || ($attribute->getIsSearchable() + && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) + ) { $result = ''; } @@ -102,12 +118,13 @@ public function processAttributeValue($attribute, $value) } /** - * Prepare index array as a string glued by separator + * Prepare index array as a string glued by separator. + * * Support 2 level array gluing * * @param array $index * @param string $separator - * @return string + * @return array */ public function prepareEntityIndex($index, $separator = ' ') { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php index df1f9c89791f6..fd00664a225e9 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php @@ -55,7 +55,7 @@ public function processAttributeValue($attribute, $value); * * @param array $index * @param string $separator - * @return string + * @return array */ public function prepareEntityIndex($index, $separator = ' '); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php index 15349e91c3fe9..e9737d0aa0599 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php @@ -82,17 +82,20 @@ public function getRelationsByChild($childIds) { $connection = $this->getConnection(); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $select = $connection->select()->from( - ['relation' => $this->getTable('catalog_product_relation')], - [] - )->join( - ['cpe' => $this->getTable('catalog_product_entity')], - 'cpe.' . $linkField . ' = relation.parent_id', - ['cpe.entity_id'] - )->where( - 'relation.child_id IN (?)', - $childIds - )->distinct(true); + $select = $connection + ->select() + ->from( + ['relation' => $this->getTable('catalog_product_relation')], + [] + )->distinct(true) + ->join( + ['cpe' => $this->getTable('catalog_product_entity')], + 'cpe.' . $linkField . ' = relation.parent_id', + ['cpe.entity_id'] + )->where( + 'relation.child_id IN (?)', + $childIds + ); return $connection->fetchCol($select); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 379b21813860a..68274ee5043f5 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -366,15 +366,21 @@ protected function _renderFiltersBefore() 'search_result.'. TemporaryStorage::FIELD_SCORE . ' ' . $this->relevanceOrderDirection ); } + return parent::_renderFiltersBefore(); + } + /** + * @inheritdoc + */ + protected function _beforeLoad() + { /* * This order is required to force search results be the same * for the same requests and products with the same relevance * NOTE: this does not replace existing orders but ADDs one more */ $this->setOrder('entity_id'); - - return parent::_renderFiltersBefore(); + return parent::_beforeLoad(); } /** diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index ae9311b452627..b1fc35e0ed323 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogSearch\Model\ResourceModel\Search; +use Magento\Search\Model\SearchCollectionInterface; + /** * Search collection * @@ -15,7 +15,7 @@ * @api * @since 100.0.2 */ -class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection implements \Magento\Search\Model\SearchCollectionInterface +class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection implements SearchCollectionInterface { /** * Attribute collection @@ -123,7 +123,8 @@ public function addSearchFilter($query) $this->_searchQuery = $query; $this->addFieldToFilter( $this->getEntity()->getLinkField(), - ['in' => new \Zend_Db_Expr($this->_getSearchEntityIdsSql($query))]); + ['in' => new \Zend_Db_Expr($this->_getSearchEntityIdsSql($query))] + ); return $this; } diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php index fc93b86f5da5e..98dff9e6fbd56 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php @@ -16,7 +16,6 @@ use Magento\Catalog\Model\Product; /** - * Class CustomAttributeFilter * Applies filters by custom attributes to base select */ class CustomAttributeFilter @@ -71,13 +70,13 @@ public function __construct( * Applies filters by custom attributes to base select * * @param Select $select - * @param FilterInterface[] ...$filters + * @param FilterInterface[] $filters * @return Select * @throws \Magento\Framework\Exception\LocalizedException * @throws \InvalidArgumentException * @throws \DomainException */ - public function apply(Select $select, FilterInterface ... $filters) + public function apply(Select $select, FilterInterface ...$filters) { $select = clone $select; $mainTableAlias = $this->extractTableAliasFromSelect($select); @@ -141,7 +140,6 @@ private function getJoinConditions($attrId, $mainTable, $joinTable) { return [ sprintf('`%s`.`entity_id` = `%s`.`entity_id`', $mainTable, $joinTable), - sprintf('`%s`.`source_id` = `%s`.`source_id`', $mainTable, $joinTable), $this->conditionManager->generateCondition( sprintf('%s.attribute_id', $joinTable), '=', diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php index dadce2ed0240c..512dd69aad952 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -7,9 +7,21 @@ namespace Magento\CatalogSearch\Model\Search\FilterMapper; use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver; +use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; +use Magento\Framework\Search\Request\Dimension; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +use Magento\Customer\Model\Context as CustomerContext; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; +use Magento\Store\Model\Indexer\WebsiteDimensionProvider; /** * Strategy which processes exclusions from general rules + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ExclusionStrategy implements FilterStrategyInterface { @@ -34,19 +46,53 @@ class ExclusionStrategy implements FilterStrategyInterface */ private $validFields = ['price', 'category_ids']; + /** + * @var TableResolver + */ + private $tableResolver; + + /** + * @var IndexScopeResolverInterface + */ + private $priceTableResolver; + + /** + * @var DimensionFactory + */ + private $dimensionFactory; + + /** + * @var Context + */ + private $httpContext; + /** * @param \Magento\Framework\App\ResourceConnection $resourceConnection * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param AliasResolver $aliasResolver + * @param TableResolver|null $tableResolver + * @param DimensionFactory $dimensionFactory + * @param IndexScopeResolverInterface $priceTableResolver + * @param Context $httpContext */ public function __construct( \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Store\Model\StoreManagerInterface $storeManager, - AliasResolver $aliasResolver + AliasResolver $aliasResolver, + TableResolver $tableResolver = null, + DimensionFactory $dimensionFactory = null, + IndexScopeResolverInterface $priceTableResolver = null, + Context $httpContext = null ) { $this->resourceConnection = $resourceConnection; $this->storeManager = $storeManager; $this->aliasResolver = $aliasResolver; + $this->tableResolver = $tableResolver ?: ObjectManager::getInstance()->get(TableResolver::class); + $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); + $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get( + IndexScopeResolverInterface::class + ); + $this->httpContext = $httpContext ?: ObjectManager::getInstance()->get(Context::class); } /** @@ -81,7 +127,17 @@ private function applyPriceFilter( \Magento\Framework\DB\Select $select ) { $alias = $this->aliasResolver->getAlias($filter); - $tableName = $this->resourceConnection->getTableName('catalog_product_index_price'); + $websiteId = $this->storeManager->getWebsite()->getId(); + $tableName = $this->priceTableResolver->resolve( + 'catalog_product_index_price', + [ + $this->dimensionFactory->create(WebsiteDimensionProvider::DIMENSION_NAME, (string)$websiteId), + $this->dimensionFactory->create( + CustomerGroupDimensionProvider::DIMENSION_NAME, + (string)$this->httpContext->getValue(CustomerContext::CONTEXT_GROUP) + ) + ] + ); $mainTableAlias = $this->extractTableAliasFromSelect($select); $select->joinInner( @@ -90,7 +146,7 @@ private function applyPriceFilter( ], $this->resourceConnection->getConnection()->quoteInto( sprintf('%s.entity_id = price_index.entity_id AND price_index.website_id = ?', $mainTableAlias), - $this->storeManager->getWebsite()->getId() + $websiteId ), [] ); @@ -112,7 +168,18 @@ private function applyCategoryFilter( \Magento\Framework\DB\Select $select ) { $alias = $this->aliasResolver->getAlias($filter); - $tableName = $this->resourceConnection->getTableName('catalog_category_product_index'); + + $catalogCategoryProductDimension = new Dimension( + \Magento\Store\Model\Store::ENTITY, + $this->storeManager->getStore()->getId() + ); + + $tableName = $this->tableResolver->resolve( + AbstractAction::MAIN_INDEX_TABLE, + [ + $catalogCategoryProductDimension + ] + ); $mainTableAlias = $this->extractTableAliasFromSelect($select); $select->joinInner( diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 6f6a5eed642e7..a5b9a2b235c6c 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -101,7 +101,7 @@ private function generateRequest($attributeType, $container, $useFulltext) } } /** @var $attribute Attribute */ - if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price', 'sku'], true)) { + if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price'], true)) { // Some fields have their own specific handlers continue; } diff --git a/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php new file mode 100644 index 0000000000000..a2aa327ca5d41 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php @@ -0,0 +1,31 @@ +getData(self::SEARCH_ENGINE_VALUE_PATH); + if ($searchEngine === 'mysql') { + $data = $subject->getData(); + $data['groups']['search']['fields']['enable_eav_indexer']['value'] = 1; + + $subject->setData($data); + } + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminAssertQueryLengthHintsActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminAssertQueryLengthHintsActionGroup.xml new file mode 100644 index 0000000000000..61861f322b941 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminAssertQueryLengthHintsActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontSearchProductActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontSearchProductActionGroup.xml new file mode 100644 index 0000000000000..faa6e9bcee18c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontSearchProductActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchConstData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchConstData.xml new file mode 100644 index 0000000000000..b09428bfcd34a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchConstData.xml @@ -0,0 +1,14 @@ + + + + + + This value must be compatible with the corresponding setting in the configured search engine + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml new file mode 100644 index 0000000000000..09ecc41cfaffa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml @@ -0,0 +1,36 @@ + + + + + + CheckUseSystemValueCheckbox + + + MinQueryLengthValue1 + + + MinQueryLengthValue2 + + + + 1 + UncheckUseSystemValueCheckbox + + + 2 + UncheckUseSystemValueCheckbox + + + + false + + + true + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogSearch/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogSearch/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/app/code/Magento/CatalogSearch/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogSearch/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogSearch/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/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml new file mode 100644 index 0000000000000..65c4ba2787ffd --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml @@ -0,0 +1,24 @@ + + + + + + + + + + boolean + + integer + + + + + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml new file mode 100644 index 0000000000000..1914ede1e6b6f --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml @@ -0,0 +1,12 @@ + + + + +
    + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/README.md b/app/code/Magento/CatalogSearch/Test/Mftf/README.md new file mode 100644 index 0000000000000..5ee0e968a4d3a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Search Functional Tests + +The Functional Test Module for **Magento Catalog Search** module. diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.xml new file mode 100644 index 0000000000000..eb7601304dacb --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchConfigurationSection.xml @@ -0,0 +1,17 @@ + + + + +
    + + + + +
    +
    diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontMinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontMinimalQueryLengthForCatalogSearchTest.xml new file mode 100644 index 0000000000000..6162f43b311d5 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontMinimalQueryLengthForCatalogSearchTest.xml @@ -0,0 +1,47 @@ + + + + + + + + + <description value="Minimal query length for catalog search"/> + <stories value="Catalog search"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15905"/> + <useCaseId value="MAGETWO-73989"/> + <group value="catalogSearch"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SetMinQueryLengthToOne" stepKey="setMinQueryLengthToOne"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="SetMinQueryLengthToDefault" stepKey="setMinimumQueryLengthToDefault"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminAssertQueryLengthHintsActionGroup" stepKey="assertQueryLengthHints"/> + <actionGroup ref="StorefrontSearchProductActionGroup" stepKey="searchForProducts"> + <argument name="product" value="$$createProduct$$"/> + <argument name="searchPhrase" value="s"/> + </actionGroup> + <createData entity="SetMinQueryLengthToTwo" stepKey="setMinQueryLengthToTwo"/> + <actionGroup ref="StorefrontSearchProductActionGroup" stepKey="searchForProductsAgain"> + <argument name="product" value="$$createProduct$$"/> + <argument name="searchPhrase" value="si"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php index 9aa97f8e6b52a..891f008979e17 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php @@ -5,6 +5,9 @@ */ namespace Magento\CatalogSearch\Test\Unit\Controller\Advanced; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ResultTest extends \PHPUnit\Framework\TestCase { public function testResultActionFiltersSetBeforeLoadLayout() @@ -49,4 +52,90 @@ function ($added) use (&$filters) { ); $instance->execute(); } + + public function testUrlSetOnException() + { + $redirectResultMock = $this->createMock(\Magento\Framework\Controller\Result\Redirect::class); + $redirectResultMock->expects($this->once()) + ->method('setUrl'); + + $redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $redirectFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($redirectResultMock); + + $catalogSearchAdvanced = $this->createPartialMock( + \Magento\CatalogSearch\Model\Advanced::class, + ['addFilters'] + ); + + $catalogSearchAdvanced->expects($this->once())->method('addFilters')->will( + $this->throwException(new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase("Test Exception") + )) + ); + + $responseMock = $this->createMock(\Magento\Framework\Webapi\Response::class); + $requestMock = $this->createPartialMock( + \Magento\Framework\App\Request\Http::class, + ['getQueryValue'] + ); + $requestMock->expects($this->any())->method('getQueryValue')->willReturn(['key' => 'value']); + + $redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); + $redirectMock->expects($this->any())->method('error')->with('urlstring'); + + $messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); + + $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + + $contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); + $contextMock->expects($this->any()) + ->method('getRequest') + ->willReturn($requestMock); + $contextMock->expects($this->any()) + ->method('getResponse') + ->willReturn($responseMock); + $contextMock->expects($this->any()) + ->method('getRedirect') + ->willReturn($redirectMock); + $contextMock->expects($this->any()) + ->method('getMessageManager') + ->willReturn($messageManagerMock); + $contextMock->expects($this->any()) + ->method('getEventManager') + ->willReturn($eventManagerMock); + $contextMock->expects($this->any()) + ->method('getResultRedirectFactory') + ->willReturn($redirectFactoryMock); + + $urlMock = $this->createMock(\Magento\Framework\Url::class); + $urlMock->expects($this->once()) + ->method('addQueryParams') + ->willReturnSelf(); + $urlMock->expects($this->once()) + ->method('getUrl') + ->willReturn("urlstring"); + + $urlFactoryMock = $this->createMock(\Magento\Framework\UrlFactory::class); + $urlFactoryMock->expects($this->once()) + ->method('create') + ->will($this->returnValue($urlMock)); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + /** @var \Magento\CatalogSearch\Controller\Advanced\Result $instance */ + $instance = $objectManager->getObject( + \Magento\CatalogSearch\Controller\Advanced\Result::class, + ['context' => $contextMock, + 'catalogSearchAdvanced' => $catalogSearchAdvanced, + 'urlFactory' => $urlFactoryMock + ] + ); + $this->assertEquals($redirectResultMock, $instance->execute()); + } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php new file mode 100644 index 0000000000000..72379c3819dea --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Test\Unit\Model\Adapter\Mysql\Aggregation\DataProvider; + +use Magento\CatalogInventory\Model\Configuration as CatalogInventoryConfiguration; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Store\Model\Store; + +/** + * Test for Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class QueryBuilderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var QueryBuilder + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $scopeResolverMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $adapterMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $inventoryConfigMock; + + protected function setUp() + { + $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->scopeResolverMock = $this->createMock(ScopeResolverInterface::class); + $this->adapterMock = $this->createMock(AdapterInterface::class); + $this->inventoryConfigMock = $this->createMock(CatalogInventoryConfiguration::class); + + $this->resourceConnectionMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->adapterMock); + + $this->indexScopeResolverMock = $this->createMock( + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class + ); + $this->dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + $this->dimensionFactoryMock = $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class); + $this->dimensionFactoryMock->method('create')->willReturn($this->dimensionMock); + $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $storeMock->method('getId')->willReturn(1); + $storeMock->method('getWebsiteId')->willReturn(1); + $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($storeMock); + $this->indexScopeResolverMock->method('resolve')->willReturn('catalog_product_index_price'); + + $this->model = new QueryBuilder( + $this->resourceConnectionMock, + $this->scopeResolverMock, + $this->inventoryConfigMock, + $this->indexScopeResolverMock, + $this->dimensionFactoryMock + ); + } + + public function testBuildWithPriceAttributeCode() + { + $tableName = 'test_table'; + $scope = 1; + $selectMock = $this->createMock(Select::class); + $attributeMock = $this->createMock(AbstractAttribute::class); + $storeMock = $this->createMock(Store::class); + + $this->adapterMock->expects($this->atLeastOnce())->method('select') + ->willReturn($selectMock); + $selectMock->expects($this->once())->method('joinInner') + ->with(['entities' => $tableName], 'main_table.entity_id = entities.entity_id', []); + $attributeMock->expects($this->once())->method('getAttributeCode') + ->willReturn('price'); + $this->scopeResolverMock->expects($this->once())->method('getScope') + ->with($scope)->willReturn($storeMock); + $storeMock->expects($this->once())->method('getWebsiteId')->willReturn(1); + $selectMock->expects($this->once())->method('from') + ->with(['main_table' => 'catalog_product_index_price'], null) + ->willReturn($selectMock); + $selectMock->expects($this->once())->method('columns') + ->with(['value' => 'main_table.min_price']) + ->willReturn($selectMock); + $selectMock->expects($this->exactly(2))->method('where') + ->withConsecutive( + ['main_table.customer_group_id = ?', 1], + ['main_table.website_id = ?', 1] + )->willReturn($selectMock); + + $this->model->build($attributeMock, $tableName, $scope, 1); + } + + public function testBuildWithNotPriceAttributeCode() + { + $tableName = 'test_table'; + $scope = 1; + $selectMock = $this->createMock(Select::class); + $attributeMock = $this->createMock(AbstractAttribute::class); + $storeMock = $this->createMock(Store::class); + + $this->adapterMock->expects($this->atLeastOnce())->method('select') + ->willReturn($selectMock); + $selectMock->expects($this->once())->method('joinInner') + ->with(['entities' => $tableName], 'main_table.entity_id = entities.entity_id', []); + $attributeMock->expects($this->once())->method('getBackendType') + ->willReturn('decimal'); + $this->scopeResolverMock->expects($this->once())->method('getScope') + ->with($scope)->willReturn($storeMock); + $storeMock->expects($this->once())->method('getId')->willReturn(1); + $this->resourceConnectionMock->expects($this->exactly(2))->method('getTableName') + ->withConsecutive( + ['catalog_product_index_eav_decimal'], + ['cataloginventory_stock_status'] + )->willReturnOnConsecutiveCalls( + 'catalog_product_index_eav_decimal', + 'cataloginventory_stock_status' + ); + + $selectMock->expects($this->exactly(2))->method('from') + ->withConsecutive( + [ + ['main_table' => 'catalog_product_index_eav_decimal'], + ['main_table.entity_id', 'main_table.value'] + ], + [['main_table' => $selectMock], ['main_table.value']] + ) + ->willReturn($selectMock); + $selectMock->expects($this->once())->method('distinct')->willReturn($selectMock); + $selectMock->expects($this->once())->method('joinLeft') + ->with( + ['stock_index' => 'cataloginventory_stock_status'], + 'main_table.source_id = stock_index.product_id', + [] + )->willReturn($selectMock); + $attributeMock->expects($this->once())->method('getAttributeId')->willReturn(3); + $selectMock->expects($this->exactly(3))->method('where') + ->withConsecutive( + ['main_table.attribute_id = ?', 3], + ['main_table.store_id = ? ', 1], + ['stock_index.stock_status = ?', Stock::STOCK_IN_STOCK] + )->willReturn($selectMock); + $this->inventoryConfigMock->expects($this->once())->method('isShowOutOfStock')->with(1)->willReturn(false); + + $this->model->build($attributeMock, $tableName, $scope, 1); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php index 4305bc5cb0706..7c558f60b7433 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php @@ -7,6 +7,7 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Adapter\Mysql\Aggregation; use Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider; +use Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder; use Magento\Eav\Model\Config; use Magento\Customer\Model\Session; use Magento\Framework\App\ResourceConnection; @@ -21,6 +22,8 @@ use Magento\Framework\DB\Ddl\Table; /** + * Test for Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProviderTest extends \PHPUnit\Framework\TestCase @@ -55,6 +58,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $adapterMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $queryBuilderMock; + protected function setUp() { $this->eavConfigMock = $this->createMock(Config::class); @@ -63,72 +71,53 @@ protected function setUp() $this->sessionMock = $this->createMock(Session::class); $this->adapterMock = $this->createMock(AdapterInterface::class); $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->adapterMock); + $this->queryBuilderMock = $this->createMock(QueryBuilder::class); $this->model = new DataProvider( $this->eavConfigMock, $this->resourceConnectionMock, $this->scopeResolverMock, - $this->sessionMock + $this->sessionMock, + $this->queryBuilderMock ); } - public function testGetDataSetUsesFrontendPriceIndexerTableIfAttributeIsPrice() + public function testGetDataSet() { $storeId = 1; - $attributeCode = 'price'; + $attributeCode = 'my_decimal'; $scopeMock = $this->createMock(Store::class); $scopeMock->expects($this->any())->method('getId')->willReturn($storeId); + $dimensionMock = $this->createMock(Dimension::class); $dimensionMock->expects($this->any())->method('getValue')->willReturn($storeId); + $this->scopeResolverMock->expects($this->any())->method('getScope')->with($storeId)->willReturn($scopeMock); $bucketMock = $this->createMock(BucketInterface::class); $bucketMock->expects($this->once())->method('getField')->willReturn($attributeCode); + $attributeMock = $this->createMock(Attribute::class); - $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); - $this->eavConfigMock->expects($this->once()) - ->method('getAttribute')->with(Product::ENTITY, $attributeCode) - ->willReturn($attributeMock); + $this->eavConfigMock->expects($this->once())->method('getAttribute') + ->with(Product::ENTITY, $attributeCode)->willReturn($attributeMock); - $selectMock = $this->createMock(Select::class); - $selectMock->expects($this->any())->method('from')->willReturnSelf(); - $selectMock->expects($this->any())->method('where')->willReturnSelf(); - $selectMock->expects($this->any())->method('columns')->willReturnSelf(); - $this->adapterMock->expects($this->once())->method('select')->willReturn($selectMock); $tableMock = $this->createMock(Table::class); + $tableMock->expects($this->once())->method('getName')->willReturn('test'); + + $this->sessionMock->expects($this->once())->method('getCustomerGroupId')->willReturn(1); + + $this->queryBuilderMock->expects($this->once())->method('build') + ->with($attributeMock, 'test', $storeId, 1); $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); } - public function testGetDataSetUsesFrontendPriceIndexerTableForDecimalAttributes() + public function testExecute() { - $storeId = 1; - $attributeCode = 'my_decimal'; - - $scopeMock = $this->createMock(Store::class); - $scopeMock->expects($this->any())->method('getId')->willReturn($storeId); - $dimensionMock = $this->createMock(Dimension::class); - $dimensionMock->expects($this->any())->method('getValue')->willReturn($storeId); - $this->scopeResolverMock->expects($this->any())->method('getScope')->with($storeId)->willReturn($scopeMock); - - $bucketMock = $this->createMock(BucketInterface::class); - $bucketMock->expects($this->once())->method('getField')->willReturn($attributeCode); - $attributeMock = $this->createMock(Attribute::class); - $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); - $this->eavConfigMock->expects($this->once()) - ->method('getAttribute')->with(Product::ENTITY, $attributeCode) - ->willReturn($attributeMock); - $selectMock = $this->createMock(Select::class); - $selectMock->expects($this->any())->method('from')->willReturnSelf(); - $selectMock->expects($this->any())->method('distinct')->willReturnSelf(); - $selectMock->expects($this->any())->method('where')->willReturnSelf(); - $selectMock->expects($this->any())->method('columns')->willReturnSelf(); - $selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); - $selectMock->expects($this->any())->method('group')->willReturnSelf(); - $this->adapterMock->expects($this->any())->method('select')->willReturn($selectMock); - $tableMock = $this->createMock(Table::class); - $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); + $this->adapterMock->expects($this->once())->method('fetchAssoc')->with($selectMock); + + $this->model->execute($selectMock); } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php index 1aeeb0d9bd731..1186dd6936cc6 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php @@ -74,6 +74,18 @@ protected function setUp() $this->mysqlDataProviderMock = $this->createMock(DataProviderInterface::class); $this->intervalFactoryMock = $this->createMock(IntervalFactory::class); $this->storeManagerMock = $this->createMock(StoreManager::class); + $this->indexScopeResolverMock = $this->createMock( + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class + ); + $this->dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + $this->dimensionFactoryMock = $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class); + $this->dimensionFactoryMock->method('create')->willReturn($this->dimensionMock); + $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $storeMock->method('getId')->willReturn(1); + $storeMock->method('getWebsiteId')->willReturn(1); + $this->storeManagerMock->method('getStore')->willReturn($storeMock); + $this->indexScopeResolverMock->method('resolve')->willReturn('catalog_product_index_price'); + $this->sessionMock->method('getCustomerGroupId')->willReturn(1); $this->model = new DataProvider( $this->resourceConnectionMock, @@ -81,7 +93,9 @@ protected function setUp() $this->sessionMock, $this->mysqlDataProviderMock, $this->intervalFactoryMock, - $this->storeManagerMock + $this->storeManagerMock, + $this->indexScopeResolverMock, + $this->dimensionFactoryMock ); } @@ -97,10 +111,6 @@ public function testGetAggregationsUsesFrontendPriceIndexerTable() $entityStorageMock = $this->createMock(EntityStorage::class); $entityStorageMock->expects($this->any())->method('getSource')->willReturn($tableMock); - $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn(42); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $this->model->getAggregations($entityStorageMock); } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index f4c512916465f..01108358da2e0 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -314,6 +314,9 @@ public function testProcessTermFilter($frontendInput, $fieldValue, $isNegation, $this->assertSame($expected, $this->removeWhitespaces($actualResult)); } + /** + * @return array + */ public function testTermFilterDataProvider() { return [ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php index ac3e84a5c8fef..a04affcf810c1 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php @@ -99,6 +99,9 @@ protected function setUp() ->willReturn($this->store); } + /** + * @return array + */ public function addFiltersDataProvider() { return array_merge( @@ -269,6 +272,11 @@ private function createBackend($table) return $backend; } + /** + * @param string $optionText + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function createSource($optionText = 'optionText') { $source = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class) @@ -281,6 +289,9 @@ private function createSource($optionText = 'optionText') return $source; } + /** + * @return array + */ private function addFiltersPriceDataProvider() { return [ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php index 75daf438f7bf2..618942cced1d0 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php @@ -25,6 +25,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $itemFactory; + /** + * @var Magento\Framework\App\Config\ScopeConfigInterface |\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + /** * @var \Magento\Search\Model\ResourceModel\Query\Collection |\PHPUnit_Framework_MockObject_MockObject */ @@ -60,11 +65,17 @@ protected function setUp() ->setMethods(['create']) ->getMock(); + $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->setMethods(['getValue']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->model = $helper->getObject( \Magento\CatalogSearch\Model\Autocomplete\DataProvider::class, [ 'queryFactory' => $queryFactory, - 'itemFactory' => $this->itemFactory + 'itemFactory' => $this->itemFactory, + 'scopeConfig' => $this->scopeConfig ] ); } @@ -72,6 +83,7 @@ protected function setUp() public function testGetItems() { $queryString = 'string'; + $limit = 3; $expected = ['title' => $queryString, 'num_results' => 100500]; $collection = [ ['query_text' => 'string1', 'num_results' => 1], @@ -80,6 +92,8 @@ public function testGetItems() ['query_text' => 'string100', 'num_results' => 100], ['query_text' => $queryString, 'num_results' => 100500] ]; + $this->scopeConfig->method('getValue') + ->willReturn($limit); $this->buildCollection($collection); $this->query->expects($this->once()) ->method('getQueryText') @@ -105,8 +119,12 @@ public function testGetItems() $this->itemFactory->expects($this->any())->method('create')->willReturn($itemMock); $result = $this->model->getItems(); $this->assertEquals($expected, $result[0]->toArray()); + $this->assertEquals($limit, count($result)); } + /** + * @param array $data + */ private function buildCollection(array $data) { $collectionData = []; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php index 60d4ef5f55f02..fb95ffbf88779 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php @@ -54,6 +54,11 @@ class FulltextTest extends \PHPUnit\Framework\TestCase */ private $indexSwitcher; + /** + * @var \Magento\Indexer\Model\ProcessManager + */ + private $processManager; + protected function setUp() { $this->fullAction = $this->getClassMock(\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class); @@ -89,6 +94,10 @@ protected function setUp() ->setMethods(['switchIndex']) ->getMock(); + $this->processManager = new \Magento\Indexer\Model\ProcessManager( + $this->getClassMock(\Magento\Framework\App\ResourceConnection::class) + ); + $objectManagerHelper = new ObjectManagerHelper($this); $this->model = $objectManagerHelper->getObject( \Magento\CatalogSearch\Model\Indexer\Fulltext::class, @@ -101,6 +110,7 @@ protected function setUp() 'searchRequestConfig' => $this->searchRequestConfig, 'data' => [], 'indexSwitcher' => $this->indexSwitcher, + 'processManager' => $this->processManager, ] ); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php index abc0fdd1069fe..69e2c33d02d1a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php @@ -321,10 +321,6 @@ public function testGetItemsWithoutApply() ->method('build') ->will($this->returnValue($builtData)); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); - $expectedFilterItems = [ $this->createFilterItem(0, $builtData[0]['label'], $builtData[0]['value'], $builtData[0]['count']), $this->createFilterItem(1, $builtData[1]['label'], $builtData[1]['value'], $builtData[1]['count']), @@ -383,9 +379,6 @@ public function testGetItemsOnlyWithResults() $this->fulltextCollection->expects($this->once()) ->method('getFacetedData') ->willReturn($facetedData); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); $this->itemDataBuilder->expects($this->once()) ->method('addItemData') diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index c0f1f3fcaa5e6..ffdb849c81d65 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -206,6 +206,12 @@ protected function getFilterBuilder() return $filterBuilder; } + /** + * @param MockObject $filterBuilder + * @param array $filters + * + * @return MockObject + */ protected function addFiltersToFilterBuilder(MockObject $filterBuilder, array $filters) { $i = 1; @@ -222,6 +228,9 @@ protected function addFiltersToFilterBuilder(MockObject $filterBuilder, array $f return $filterBuilder; } + /** + * @return MockObject + */ protected function createFilter() { $filter = $this->getMockBuilder(\Magento\Framework\Api\Filter::class) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php index 1ff1131e5f002..5fa5b0333c6ba 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php @@ -91,6 +91,9 @@ public function testBaseSelectFullTextSearchStrategy( ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/CustomAttributeFilterCheckTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/CustomAttributeFilterCheckTest.php index 2022492ed1c86..175407bda677f 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/CustomAttributeFilterCheckTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/CustomAttributeFilterCheckTest.php @@ -78,6 +78,9 @@ public function testIsCustomPositive($attributeFrontEndType) ); } + /** + * @return array + */ public function dataProviderForIsCustomPositive() { return [ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php index 7c6cafd7e9924..09591532f9f06 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php @@ -15,6 +15,9 @@ use Magento\Framework\Search\Request\Filter\Term; use Magento\Store\Api\Data\WebsiteInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ExclusionStrategyTest extends \PHPUnit\Framework\TestCase { /** @@ -50,10 +53,31 @@ protected function setUp() $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->aliasResolverMock = $this->createMock(AliasResolver::class); + $this->indexScopeResolverMock = $this->createMock( + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class + ); + $this->tableResolverMock = $this->createMock( + \Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver::class + ); + $this->dimensionMock = $this->createMock(\Magento\Framework\Indexer\Dimension::class); + $this->dimensionFactoryMock = $this->createMock(\Magento\Framework\Indexer\DimensionFactory::class); + $this->dimensionFactoryMock->method('create')->willReturn($this->dimensionMock); + $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $storeMock->method('getId')->willReturn(1); + $storeMock->method('getWebsiteId')->willReturn(1); + $this->storeManagerMock->method('getStore')->willReturn($storeMock); + $this->indexScopeResolverMock->method('resolve')->willReturn('catalog_product_index_price'); + $this->httpContextMock = $this->createMock(\Magento\Framework\App\Http\Context::class); + $this->httpContextMock->method('getValue')->willReturn(1); + $this->model = new ExclusionStrategy( $this->resourceConnectionMock, $this->storeManagerMock, - $this->aliasResolverMock + $this->aliasResolverMock, + $this->tableResolverMock, + $this->dimensionFactoryMock, + $this->indexScopeResolverMock, + $this->httpContextMock ); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php index c393f91f21fe1..02d6bec162c56 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php @@ -120,6 +120,13 @@ private function createDimensionMock($name, $value) return $dimension; } + /** + * @param $callNumber + * @param $tableName + * @param $isTableExist + * + * @return mixed + */ private function mockDropTable($callNumber, $tableName, $isTableExist) { $this->connection->expects($this->at($callNumber++)) @@ -135,6 +142,12 @@ private function mockDropTable($callNumber, $tableName, $isTableExist) return $callNumber; } + /** + * @param $callNumber + * @param $tableName + * + * @return mixed + */ private function mockFulltextTable($callNumber, $tableName) { $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/QueryChecker/FullTextSearchCheckTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/QueryChecker/FullTextSearchCheckTest.php index bb6e4ab8b4281..d13dcc11628f2 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/QueryChecker/FullTextSearchCheckTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/QueryChecker/FullTextSearchCheckTest.php @@ -78,6 +78,9 @@ public function testInvalidArgumentException2() $this->fullTextSearchCheck->isRequiredForQuery($filterMock); } + /** + * @return array + */ public function positiveDataProvider() { $boolQueryMock = $this->getBoolQueryMock(); @@ -114,6 +117,9 @@ public function positiveDataProvider() ]; } + /** + * @return array + */ public function negativeDataProvider() { $emptyBoolQueryMock = $this->getBoolQueryMock(); @@ -147,6 +153,9 @@ public function negativeDataProvider() ]; } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getMatchQueryMock() { $matchQueryMock = $this->getMockBuilder(\Magento\Framework\Search\Request\QueryInterface::class) @@ -161,6 +170,9 @@ private function getMatchQueryMock() return $matchQueryMock; } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getBoolQueryMock() { $boolQueryMock = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\BoolExpression::class) @@ -175,6 +187,9 @@ private function getBoolQueryMock() return $boolQueryMock; } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getFilterQueryMock() { $filterQueryMock = $this->getMockBuilder(\Magento\Framework\Search\Request\Query\Filter::class) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php index 259c8e5d7f897..189f1e2cf0cf3 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGeneratorTest.php @@ -9,6 +9,9 @@ use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorResolver; use Magento\CatalogSearch\Model\Search\RequestGenerator\GeneratorInterface; +/** + * Test for \Magento\CatalogSearch\Model\Search\RequestGenerator + */ class RequestGeneratorTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -61,7 +64,7 @@ public function attributesProvider() return [ [ [ - 'quick_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], + 'quick_search_container' => ['queries' => 1, 'filters' => 0, 'aggregations' => 0], 'advanced_search_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0], 'catalog_view_container' => ['queries' => 0, 'filters' => 0, 'aggregations' => 0] ], @@ -239,6 +242,11 @@ private function createAttributeMock($attributeOptions) return $attribute; } + /** + * @param $value + * + * @return int|void + */ private function countVal(&$value) { return !empty($value) ? count($value) : 0; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/SelectContainer/SelectContainerBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/SelectContainer/SelectContainerBuilderTest.php index ef4d8d314825b..374d0390f937c 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/SelectContainer/SelectContainerBuilderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/SelectContainer/SelectContainerBuilderTest.php @@ -190,6 +190,9 @@ public function testBuildByRequest() ); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function mockQuery() { return $this->getMockBuilder(QueryInterface::class) @@ -197,6 +200,9 @@ private function mockQuery() ->getMockForAbstractClass(); } + /** + * @return array + */ private function mockFilters() { $visibilityFilter = $this->getMockBuilder(Term::class) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php index 1521b38d8c298..cfddc07bceecc 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php @@ -275,6 +275,9 @@ function (FilterInterface $filter) { $this->tableMapper->addTables($select, $request); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getSelectMock() { return $this->getMockBuilder(\Magento\Framework\DB\Select::class) @@ -282,6 +285,9 @@ private function getSelectMock() ->getMock(); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getRequestMock() { return $this->getMockBuilder(\Magento\Framework\Search\RequestInterface::class) @@ -289,6 +295,9 @@ private function getRequestMock() ->getMock(); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getQueryMock() { return $this->getMockBuilder(QueryInterface::class) @@ -296,6 +305,9 @@ private function getQueryMock() ->getMockForAbstractClass(); } + /** + * @return array + */ private function getDifferentFiltersMock() { $visibilityFilter = $this->getMockBuilder(Term::class) @@ -316,6 +328,9 @@ private function getDifferentFiltersMock() return [$visibilityFilter, $customFilter, $nonCustomFilter]; } + /** + * @return array + */ private function getSameFiltersMock() { $visibilityFilter1 = $this->getMockBuilder(Term::class) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php new file mode 100644 index 0000000000000..0b07b7af875ce --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Test\Unit\Plugin; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class EnableEavIndexerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\CatalogSearch\Plugin\EnableEavIndexer + */ + private $model; + + /** + * @var \Magento\Config\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $config; + + protected function setUp() + { + $this->config = $this->getMockBuilder(\Magento\Config\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['getData', 'setData']) + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + \Magento\CatalogSearch\Plugin\EnableEavIndexer::class + ); + } + + public function testBeforeSave() + { + $this->config->expects($this->once())->method('getData')->willReturn('elasticsearch'); + $this->config->expects($this->never())->method('setData')->willReturnSelf(); + + $this->model->beforeSave($this->config); + } + + public function testBeforeSaveMysqlSearchEngine() + { + $this->config->expects($this->at(0))->method('getData')->willReturn('mysql'); + $this->config->expects($this->at(1))->method('getData')->willReturn([]); + $this->config->expects($this->once())->method('setData')->willReturnSelf(); + + $this->model->beforeSave($this->config); + } +} diff --git a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php new file mode 100644 index 0000000000000..eb05b343f3c7a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Ui\DataProvider\Product; + +use Magento\Framework\Data\Collection; +use Magento\CatalogSearch\Model\ResourceModel\Search\Collection as SearchCollection; +use Magento\Ui\DataProvider\AddFilterToCollectionInterface; + +/** + * Adds FullText search to Product Data Provider + */ +class AddFulltextFilterToCollection implements AddFilterToCollectionInterface +{ + /** + * Search Collection + * + * @var SearchCollection + */ + private $searchCollection; + + /** + * @param SearchCollection $searchCollection + */ + public function __construct(SearchCollection $searchCollection) + { + $this->searchCollection = $searchCollection; + } + + /** + * {@inheritdoc} + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function addFilter(Collection $collection, $field, $condition = null) + { + /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ + if (isset($condition['fulltext']) && !empty($condition['fulltext'])) { + $this->searchCollection->addBackendSearchFilter($condition['fulltext']); + $productIds = $this->searchCollection->load()->getAllIds(); + $collection->addIdFilter($productIds); + } + } +} diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 85c9073160330..b7c5c23a8a2c4 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -2,20 +2,25 @@ "name": "magento/module-catalog-search", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", + "magento/module-indexer": "100.2.*", "magento/module-search": "100.2.*", "magento/module-customer": "101.0.*", "magento/module-directory": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-backend": "100.2.*", "magento/module-theme": "100.2.*", + "magento/module-ui": "101.0.*", "magento/module-catalog-inventory": "100.2.*", "magento/framework": "101.0.*" }, + "suggest": { + "magento/module-config": "101.0.*" + }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml index d323f0e95d9de..2d41d17889e49 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml @@ -19,4 +19,19 @@ <type name="Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab\Front"> <plugin name="search_weigh" type="Magento\CatalogSearch\Block\Plugin\FrontTabPlugin" /> </type> + <type name="Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider"> + <arguments> + <argument name="addFilterStrategies" xsi:type="array"> + <item name="fulltext" xsi:type="object">Magento\CatalogSearch\Ui\DataProvider\Product\AddFulltextFilterToCollection</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ProductLink\Search"> + <arguments> + <argument name="filter" xsi:type="object">Magento\CatalogSearch\Ui\DataProvider\Product\AddFulltextFilterToCollection</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Category"> + <plugin name="fulltext_search_indexer" type="Magento\CatalogSearch\Model\Indexer\Fulltext\Model\Plugin\Category"/> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml index 0eeb6ab33871e..c358062b88a41 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml @@ -19,13 +19,32 @@ <field id="engine" canRestore="1"> <backend_model>Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend\Engine</backend_model> </field> - <field id="min_query_length" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="min_query_length" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minimal Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.</comment> </field> - <field id="max_query_length" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="max_query_length" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Maximum Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine.</comment> + </field> + <field id="max_count_cacheable_search_terms" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Number of top search results to cache</label> + <comment>Number of popular search terms to be cached for faster response. Use “0” to cache all results after a term is searched for the second time.</comment> + <validate>validate-digits</validate> + </field> + <field id="autocomplete_limit" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Autocomplete Limit</label> + <validate>validate-digits</validate> + </field> + <field id="enable_eav_indexer" translate="label" type="select" sortOrder="18" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable EAV Indexer</label> + <comment>Enable/Disable Product EAV indexer to improve indexation speed. Make sure that indexer is not used by 3rd party extensions.</comment> + <depends> + <field id="engine" separator="," negative="1">mysql</field> + </depends> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> </section> diff --git a/app/code/Magento/CatalogSearch/etc/config.xml b/app/code/Magento/CatalogSearch/etc/config.xml index d5ff194813b9c..7ea15c6caa590 100644 --- a/app/code/Magento/CatalogSearch/etc/config.xml +++ b/app/code/Magento/CatalogSearch/etc/config.xml @@ -13,8 +13,11 @@ </seo> <search> <engine>mysql</engine> - <min_query_length>1</min_query_length> + <min_query_length>3</min_query_length> <max_query_length>128</max_query_length> + <max_count_cacheable_search_terms>100</max_count_cacheable_search_terms> + <autocomplete_limit>8</autocomplete_limit> + <enable_eav_indexer>1</enable_eav_indexer> </search> </catalog> </default> diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 546f4a80e53a4..98e23078cd69f 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -310,4 +310,28 @@ <argument name="name" xsi:type="string">catalog_view_container</argument> </arguments> </type> + <type name="Magento\CatalogSearch\Model\Search\FilterMapper\ExclusionStrategy"> + <arguments> + <argument name="priceTableResolver" xsi:type="object"> + Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Adapter\Mysql\Dynamic\DataProvider"> + <arguments> + <argument name="priceTableResolver" xsi:type="object"> + Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder"> + <arguments> + <argument name="priceTableResolver" xsi:type="object"> + Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver + </argument> + </arguments> + </type> + <type name="Magento\Config\Model\Config"> + <plugin name="config_enable_eav_indexer" type="Magento\CatalogSearch\Plugin\EnableEavIndexer" /> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/etc/indexer.xml b/app/code/Magento/CatalogSearch/etc/indexer.xml index 9726f5372311d..a0b9ca10afccb 100644 --- a/app/code/Magento/CatalogSearch/etc/indexer.xml +++ b/app/code/Magento/CatalogSearch/etc/indexer.xml @@ -9,8 +9,6 @@ <indexer id="catalogsearch_fulltext" view_id="catalogsearch_fulltext" class="Magento\CatalogSearch\Model\Indexer\Fulltext"> <title translate="true">Catalog Search Rebuild Catalog product fulltext search index - - diff --git a/app/code/Magento/CatalogSearch/etc/module.xml b/app/code/Magento/CatalogSearch/etc/module.xml index fd31faa083926..55d1cacf1a9f3 100644 --- a/app/code/Magento/CatalogSearch/etc/module.xml +++ b/app/code/Magento/CatalogSearch/etc/module.xml @@ -10,6 +10,7 @@ + diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml index d7bfb2e6b4a5c..6f9eb6e20666e 100644 --- a/app/code/Magento/CatalogSearch/etc/search_request.xml +++ b/app/code/Magento/CatalogSearch/etc/search_request.xml @@ -19,7 +19,6 @@ - diff --git a/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..c7293783dc609 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..c7293783dc609 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogSearch/i18n/en_US.csv b/app/code/Magento/CatalogSearch/i18n/en_US.csv index 9121520774ccc..b5b11348bc7f1 100644 --- a/app/code/Magento/CatalogSearch/i18n/en_US.csv +++ b/app/code/Magento/CatalogSearch/i18n/en_US.csv @@ -37,3 +37,6 @@ name,name "Minimal Query Length","Minimal Query Length" "Maximum Query Length","Maximum Query Length" "Rebuild Catalog product fulltext search index","Rebuild Catalog product fulltext search index" +"Please enter a valid price range.","Please enter a valid price range." +"This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.","This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact." +"This value must be compatible with the corresponding setting in the configured search engine.","This value must be compatible with the corresponding setting in the configured search engine." diff --git a/app/code/Magento/CatalogSearch/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/CatalogSearch/view/adminhtml/ui_component/product_listing.xml new file mode 100644 index 0000000000000..24aa4a8919db8 --- /dev/null +++ b/app/code/Magento/CatalogSearch/view/adminhtml/ui_component/product_listing.xml @@ -0,0 +1,12 @@ + + ++ + + + diff --git a/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_result_index.xml index e54b45093589a..043e48c085e59 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_result_index.xml @@ -9,16 +9,16 @@ - - + + positions:list-secondary - - + + product_list_toolbar @@ -36,6 +36,11 @@ + + + Magento\CatalogSearch\Block\SearchTermsLog + + diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml index 53a301022873b..779a0a2875d1d 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> -helper('Magento\CatalogSearch\Helper\Data')->getMaxQueryLength();?> - +helper(\Magento\CatalogSearch\Helper\Data::class)->getMaxQueryLength();?> +
    -
    - getSearchableAttributes() as $_attribute): ?> + escapeHtml(__('Search Settings')) ?>
    + getSearchableAttributes() as $_attribute) : ?> getAttributeCode() ?> -
    -
    @@ -147,8 +157,8 @@ require([ } }, messages: { - 'price[to]': {'greater-than-equals-to': 'Please enter a valid price range.'}, - 'price[from]': {'less-than-equals-to': 'Please enter a valid price range.'} + 'price[to]': {'greater-than-equals-to': 'escapeJs(__('Please enter a valid price range.')) ?>'}, + 'price[from]': {'less-than-equals-to': 'escapeJs(__('Please enter a valid price range.')) ?>'} } }); }); diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml index 09098b1ccd003..cdbc831c8e73e 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/link.phtml @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var \Magento\CatalogSearch\Helper\Data $helper */ -$helper = $this->helper('Magento\CatalogSearch\Helper\Data'); +$helper = $this->helper(\Magento\CatalogSearch\Helper\Data::class); ?> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml index 83808df5b95e4..325a76d04c835 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml @@ -3,52 +3,49 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -getResultCount()): ?> +getResultCount()) : ?> - +getSearchCriterias(); ?> -getResultCount()): ?> +getResultCount()) : ?>
    - - + escapeHtml(__("Don't see what you're looking for?")) ?> + escapeHtml(__('Modify your search.')) ?>
    -getResultCount()): ?> - +getResultCount()) : ?> + getSearchCriterias(); ?> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml index 2757ae3b5f7ed..273d6da4fc3ca 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml @@ -3,33 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -getResultCount()): ?> +getResultCount()) : ?> getChildHtml('tagged_product_list_rss_link') ?> - - +
    - getNoResultText()) ? $block->getNoResultText() : __('Your search returned no results.') ?> - getAdditionalHtml() ?> - getNoteMessages()):?> - -
    + escapeHtml($block->getNoResultText() ? $block->getNoResultText() : __('Your search returned no results.')) ?> + getAdditionalHtml() ?> + getNoteMessages()) : ?> + +
    diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml new file mode 100644 index 0000000000000..38ef11933a46f --- /dev/null +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/search_terms_log.phtml @@ -0,0 +1,17 @@ + +getSearchTermsLog()->isPageCacheable()) : ?> + + diff --git a/app/code/Magento/CatalogSearch/view/frontend/web/js/search-terms-log.js b/app/code/Magento/CatalogSearch/view/frontend/web/js/search-terms-log.js new file mode 100644 index 0000000000000..8638a837f56b9 --- /dev/null +++ b/app/code/Magento/CatalogSearch/view/frontend/web/js/search-terms-log.js @@ -0,0 +1,21 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mageUtils' +], function ($, utils) { + 'use strict'; + + return function (data) { + $.ajax({ + method: 'GET', + url: data.url, + data: { + 'q': utils.getUrlParameters(window.location.href).q + } + }); + }; +}); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php index 6aa33f37cd31f..beed1e18582c1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php @@ -5,12 +5,16 @@ */ namespace Magento\CatalogUrlRewrite\Model\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory; use Magento\Framework\App\ObjectManager; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +/** + * Model for generate url rewrites for children categories. + */ class ChildrenUrlRewriteGenerator { /** @@ -28,15 +32,22 @@ class ChildrenUrlRewriteGenerator */ private $mergeDataProviderPrototype; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory + * @param CategoryRepositoryInterface|null $categoryRepository */ public function __construct( ChildrenCategoriesProvider $childrenCategoriesProvider, CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, - MergeDataProviderFactory $mergeDataProviderFactory = null + MergeDataProviderFactory $mergeDataProviderFactory = null, + CategoryRepositoryInterface $categoryRepository = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; @@ -44,6 +55,8 @@ public function __construct( $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); } $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + $this->categoryRepository = $categoryRepository + ?: ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); } /** @@ -57,14 +70,18 @@ public function __construct( public function generate($storeId, Category $category, $rootCategoryId = null) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $childCategory->setStoreId($storeId); - $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); - /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ - $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); - $mergeDataProvider->merge( - $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) - ); + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + if ($childrenIds) { + foreach ($childrenIds as $childId) { + /** @var Category $childCategory */ + $childCategory = $this->categoryRepository->get($childId, $storeId); + $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); + /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ + $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); + $mergeDataProvider->merge( + $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) + ); + } } return $mergeDataProvider->getData(); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php index 17d12ba563ebd..f3984bf7d62ab 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php @@ -6,9 +6,14 @@ namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Category; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\CategoryFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; +use Magento\Store\Model\Store; +/** + * Perform url updating for children categories. + */ class Move { /** @@ -21,16 +26,24 @@ class Move */ private $childrenCategoriesProvider; + /** + * @var CategoryFactory + */ + private $categoryFactory; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider + * @param CategoryFactory $categoryFactory */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, - ChildrenCategoriesProvider $childrenCategoriesProvider + ChildrenCategoriesProvider $childrenCategoriesProvider, + CategoryFactory $categoryFactory ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; + $this->categoryFactory = $categoryFactory; } /** @@ -51,20 +64,57 @@ public function afterChangeParent( Category $newParent, $afterCategoryId ) { - $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); - $category->getResource()->saveAttribute($category, 'url_path'); - $this->updateUrlPathForChildren($category); + $categoryStoreId = $category->getStoreId(); + foreach ($category->getStoreIds() as $storeId) { + $category->setStoreId($storeId); + if (!$this->isGlobalScope($storeId)) { + $this->updateCategoryUrlKeyForStore($category); + $category->unsUrlPath(); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->getResource()->saveAttribute($category, 'url_path'); + $this->updateUrlPathForChildren($category); + } + } + $category->setStoreId($categoryStoreId); return $result; } /** + * Set category url_key according to current category store id. + * + * @param Category $category + * @return void + */ + private function updateCategoryUrlKeyForStore(Category $category) + { + $item = $this->categoryFactory->create(); + $item->setStoreId($category->getStoreId()); + $item->load($category->getId()); + $category->setUrlKey($item->getUrlKey()); + } + + /** + * Check is global scope. + * + * @param int|null $storeId + * @return bool + */ + private function isGlobalScope($storeId) + { + return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + } + + /** + * Updates url_path for child categories. + * * @param Category $category * @return void */ - protected function updateUrlPathForChildren($category) + private function updateUrlPathForChildren($category) { foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { + $childCategory->setStoreId($category->getStoreId()); $childCategory->unsUrlPath(); $childCategory->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($childCategory)); $childCategory->getResource()->saveAttribute($childCategory, 'url_path'); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/UpdateUrlPath.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/UpdateUrlPath.php new file mode 100644 index 0000000000000..75959c3872fe0 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/UpdateUrlPath.php @@ -0,0 +1,116 @@ +categoryUrlPathGenerator = $categoryUrlPathGenerator; + $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; + $this->urlPersist = $urlPersist; + $this->storeViewService = $storeViewService; + } + + /** + * Perform url updating for different stores + * + * @param CategoryResource $subject + * @param CategoryResource $result + * @param AbstractModel $category + * @return CategoryResource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + CategoryResource $subject, + CategoryResource $result, + AbstractModel $category + ) { + $parentCategoryId = $category->getParentId(); + if ($category->isObjectNew() + && !$category->isInRootCategoryList() + && !empty($parentCategoryId)) { + foreach ($category->getStoreIds() as $storeId) { + if (!$this->isGlobalScope($storeId) + && $this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( + $storeId, + $parentCategoryId, + Category::ENTITY + ) + ) { + $category->setStoreId($storeId); + $this->updateUrlPathForCategory($category, $subject); + $this->urlPersist->replace($this->categoryUrlRewriteGenerator->generate($category)); + } + } + } + return $result; + } + + /** + * Check that store id is in global scope + * + * @param int|null $storeId + * @return bool + */ + private function isGlobalScope(int $storeId): bool + { + return null === $storeId || $storeId === Store::DEFAULT_STORE_ID; + } + + /** + * @param Category $category + * @param \Magento\Catalog\Model\ResourceModel\Category $categoryResource + */ + private function updateUrlPathForCategory(Category $category, CategoryResource $categoryResource) + { + $category->unsUrlPath(); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $categoryResource->saveAttribute($category, 'url_path'); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index ee20b0e934b5d..2961dc4358970 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -8,6 +8,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; +/** + * Class for generation category url_path. + */ class CategoryUrlPathGenerator { /** @@ -58,12 +61,13 @@ public function __construct( } /** - * Build category URL path + * Build category URL path. * * @param \Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $category + * @param null|\Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $parentCategory * @return string */ - public function getUrlPath($category) + public function getUrlPath($category, $parentCategory = null) { if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { return ''; @@ -77,15 +81,18 @@ public function getUrlPath($category) return $category->getUrlPath(); } if ($this->isNeedToGenerateUrlPathForParent($category)) { - $parentPath = $this->getUrlPath( - $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) - ); + $parentCategory = $parentCategory ?? + $this->categoryRepository->get($category->getParentId(), $category->getStoreId()); + $parentPath = $this->getUrlPath($parentCategory); $path = $parentPath === '' ? $path : $parentPath . '/' . $path; } + return $path; } /** + * Define whether we should generate URL path for parent. + * * @param \Magento\Catalog\Model\Category $category * @return bool */ diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index b2da0ab39f31f..a86604672e2b4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -15,6 +15,9 @@ use Magento\Framework\App\ObjectManager; use Magento\UrlRewrite\Model\MergeDataProviderFactory; +/** + * Generate list of urls. + */ class CategoryUrlRewriteGenerator { /** Entity type code */ @@ -84,6 +87,8 @@ public function __construct( } /** + * Generate list of urls. + * * @param \Magento\Catalog\Model\Category $category * @param bool $overrideStoreUrls * @param int|null $rootCategoryId @@ -119,6 +124,7 @@ protected function generateForGlobalScope( $mergeDataProvider = clone $this->mergeDataProviderPrototype; $categoryId = $category->getId(); foreach ($category->getStoreIds() as $storeId) { + $category->setStoreId($storeId); if (!$this->isGlobalScope($storeId) && $this->isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls) ) { @@ -131,6 +137,8 @@ protected function generateForGlobalScope( } /** + * Checks if urls should be overridden for store. + * * @param int $storeId * @param int $categoryId * @param bool $overrideStoreUrls diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php index a7cc894c9a022..c382d5f826386 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php @@ -61,7 +61,7 @@ public function generate($storeId, Product $product, ObjectRegistry $productCate $anchorCategoryIds = $category->getAnchorsAbove(); if ($anchorCategoryIds) { foreach ($anchorCategoryIds as $anchorCategoryId) { - $anchorCategory = $this->categoryRepository->get($anchorCategoryId); + $anchorCategory = $this->categoryRepository->get($anchorCategoryId, $storeId); $urls[] = $this->urlRewriteFactory->create() ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) ->setEntityId($product->getId()) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 9c5c37b51f0b2..6b838f83d31e4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -208,7 +208,7 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ public function isCategoryProperForGenerating(Category $category, $storeId) { $parentIds = $category->getParentIds(); - if (count($parentIds) >= 2) { + if (is_array($parentIds) && count($parentIds) >= 2) { $rootCategoryId = $parentIds[1]; return $rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId(); } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php index 2e192d895c6d5..3e858e96500c5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php @@ -3,9 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Model; -use Magento\Store\Model\Store; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; class ProductUrlPathGenerator { @@ -19,36 +26,36 @@ class ProductUrlPathGenerator protected $productUrlSuffix = []; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $scopeConfig; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator + * @var CategoryUrlPathGenerator */ protected $categoryUrlPathGenerator; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param ScopeConfigInterface $scopeConfig * @param CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param ProductRepositoryInterface $productRepository */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + StoreManagerInterface $storeManager, + ScopeConfigInterface $scopeConfig, + CategoryUrlPathGenerator $categoryUrlPathGenerator, + ProductRepositoryInterface $productRepository ) { $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; @@ -59,8 +66,8 @@ public function __construct( /** * Retrieve Product Url path (with category if exists) * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Catalog\Model\Category $category + * @param Product $product + * @param Category $category * * @return string */ @@ -80,10 +87,10 @@ public function getUrlPath($product, $category = null) /** * Prepare URL Key with stored product data (fallback for "Use Default Value" logic) * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string */ - protected function prepareProductDefaultUrlKey(\Magento\Catalog\Model\Product $product) + protected function prepareProductDefaultUrlKey(Product $product) { $storedProduct = $this->productRepository->getById($product->getId()); $storedUrlKey = $storedProduct->getUrlKey(); @@ -93,9 +100,9 @@ protected function prepareProductDefaultUrlKey(\Magento\Catalog\Model\Product $p /** * Retrieve Product Url path with suffix * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param int $storeId - * @param \Magento\Catalog\Model\Category $category + * @param Category $category * @return string */ public function getUrlPathWithSuffix($product, $storeId, $category = null) @@ -106,8 +113,8 @@ public function getUrlPathWithSuffix($product, $storeId, $category = null) /** * Get canonical product url path * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Catalog\Model\Category|null $category + * @param Product $product + * @param Category|null $category * @return string */ public function getCanonicalUrlPath($product, $category = null) @@ -119,24 +126,27 @@ public function getCanonicalUrlPath($product, $category = null) /** * Generate product url key based on url_key entered by merchant or product name * - * @param \Magento\Catalog\Model\Product $product - * @return string + * @param Product $product + * @return string|null */ public function getUrlKey($product) { - return $product->getUrlKey() === false ? false : $this->prepareProductUrlKey($product); + $generatedProductUrlKey = $this->prepareProductUrlKey($product); + return ($product->getUrlKey() === false || empty($generatedProductUrlKey)) ? null : $generatedProductUrlKey; } /** * Prepare url key for product * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string */ - protected function prepareProductUrlKey(\Magento\Catalog\Model\Product $product) + protected function prepareProductUrlKey(Product $product) { - $urlKey = $product->getUrlKey(); - return $product->formatUrlKey($urlKey === '' || $urlKey === null ? $product->getName() : $urlKey); + $urlKey = (string)$product->getUrlKey(); + $urlKey = trim(strtolower($urlKey)); + + return $urlKey ?: $product->formatUrlKey($product->getName()); } /** @@ -154,7 +164,7 @@ protected function getProductUrlSuffix($storeId = null) if (!isset($this->productUrlSuffix[$storeId])) { $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( self::XML_PATH_PRODUCT_URL_SUFFIX, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ); } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/WebapiProductUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/WebapiProductUrlPathGenerator.php new file mode 100644 index 0000000000000..487943c2305b4 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/WebapiProductUrlPathGenerator.php @@ -0,0 +1,73 @@ +collectionFactory = $collectionFactory; + } + + /** + * @inheritdoc + */ + protected function prepareProductUrlKey(\Magento\Catalog\Model\Product $product) + { + $urlKey = $product->getUrlKey(); + if ($urlKey === '' || $urlKey === null) { + $urlKey = $this->prepareUrlKey($product->formatUrlKey($product->getName())); + } + + return $product->formatUrlKey($urlKey); + } + + /** + * Crete url key if it does not exist yet. + * + * @param string $urlKey + * @return string + */ + private function prepareUrlKey(string $urlKey) : string + { + /** @var ProductCollection $collection */ + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter('url_key', ['like' => $urlKey]); + if ($collection->getSize() !== 0) { + $urlKey = $urlKey . '-1'; + $urlKey = $this->prepareUrlKey($urlKey); + } + + return $urlKey; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 022a78be00197..41d9f5b51a165 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -135,6 +135,7 @@ class AfterImportDataObserver implements ObserverInterface 'url_path', 'name', 'visibility', + 'save_rewrites_history' ]; /** @@ -199,6 +200,7 @@ public function __construct( /** * Action after data import. + * * Save new url rewrites and remove old if exist. * * @param Observer $observer @@ -230,11 +232,8 @@ public function execute(Observer $observer) protected function _populateForUrlGeneration($rowData) { $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]); - if (empty($newSku) || !isset($newSku['entity_id'])) { - return null; - } - if ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE - && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) { + $oldSku = $this->import->getOldSku(); + if (!$this->isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku)) { return null; } $rowData['entity_id'] = $newSku['entity_id']; @@ -267,6 +266,33 @@ protected function _populateForUrlGeneration($rowData) } /** + * Check is need to populate data for url generation + * + * @param array $rowData + * @param array $newSku + * @param array $oldSku + * @return bool + */ + private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): bool + { + if (( + (empty($newSku) || !isset($newSku['entity_id'])) + || ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE + && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) + || (array_key_exists(strtolower($rowData[ImportProduct::COL_SKU]), $oldSku) + && !isset($rowData[self::URL_KEY_ATTRIBUTE_CODE]) + && $this->import->getBehavior() === ImportExport::BEHAVIOR_APPEND) + ) + && !isset($rowData["categories"]) + ) { + return false; + } + return true; + } + + /** + * Add store id to product data. + * * @param \Magento\Catalog\Model\Product $product * @param array $rowData * @return void @@ -436,6 +462,8 @@ protected function currentUrlRewritesRegenerate() } /** + * Generate url-rewrite for outogenerated url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -470,6 +498,8 @@ protected function generateForAutogenerated($url, $category) } /** + * Generate url-rewrite for custom url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -503,6 +533,8 @@ protected function generateForCustom($url, $category) } /** + * Retrieve category from url metadata. + * * @param UrlRewrite $url * @return Category|null|bool */ @@ -517,6 +549,8 @@ protected function retrieveCategoryFromMetadata($url) } /** + * Check, category suited for url-rewrite generation. + * * @param \Magento\Catalog\Model\Category $category * @param int $storeId * @return bool diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index 539e5c3f42f15..745c302d619a1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; @@ -12,6 +13,9 @@ use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\ResourceModel\Group\CollectionFactory; +use Magento\Store\Model\ResourceModel\Group\Collection as StoreGroupCollection; +use Magento\Framework\App\ObjectManager; /** * Generates Category Url Rewrites after save and Products Url Rewrites assigned to the category that's being saved @@ -43,12 +47,18 @@ class CategoryProcessUrlRewriteSavingObserver implements ObserverInterface */ private $dataUrlRewriteClassNames; + /** + * @var CollectionFactory + */ + private $storeGroupFactory; + /** * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator * @param UrlRewriteHandler $urlRewriteHandler * @param UrlRewriteBunchReplacer $urlRewriteBunchReplacer * @param DatabaseMapPool $databaseMapPool * @param string[] $dataUrlRewriteClassNames + * @param CollectionFactory|null $storeGroupFactory */ public function __construct( CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, @@ -56,15 +66,18 @@ public function __construct( UrlRewriteBunchReplacer $urlRewriteBunchReplacer, DatabaseMapPool $databaseMapPool, $dataUrlRewriteClassNames = [ - DataCategoryUrlRewriteDatabaseMap::class, - DataProductUrlRewriteDatabaseMap::class - ] + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ], + CollectionFactory $storeGroupFactory = null ) { $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; $this->urlRewriteHandler = $urlRewriteHandler; $this->urlRewriteBunchReplacer = $urlRewriteBunchReplacer; $this->databaseMapPool = $databaseMapPool; $this->dataUrlRewriteClassNames = $dataUrlRewriteClassNames; + $this->storeGroupFactory = $storeGroupFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); } /** @@ -82,17 +95,24 @@ public function execute(\Magento\Framework\Event\Observer $observer) return; } + if (!$category->hasData('store_id')) { + $this->setCategoryStoreId($category); + } + $mapsGenerated = false; - if ($category->dataHasChangedFor('url_key') - || $category->dataHasChangedFor('is_anchor') - || $category->getChangedProductIds() - ) { + if ($this->isCategoryHasChanged($category)) { if ($category->dataHasChangedFor('url_key')) { $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); } - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); - $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + if ($this->isChangedOnlyProduct($category)) { + $productUrlRewriteResult = + $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } else { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } $mapsGenerated = true; } @@ -102,6 +122,63 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } + /** + * Check is category changed changed. + * + * @param Category $category + * @return bool + */ + private function isCategoryHasChanged(Category $category): bool + { + if ($category->dataHasChangedFor('url_key') + || $category->dataHasChangedFor('is_anchor') + || !empty($category->getChangedProductIds())) { + return true; + } + + return false; + } + + /** + * Check is only product changed. + * + * @param Category $category + * @return bool + */ + private function isChangedOnlyProduct(Category $category): bool + { + if (!empty($category->getChangedProductIds()) + && !$category->dataHasChangedFor('is_anchor') + && !$category->dataHasChangedFor('url_key')) { + return true; + } + + return false; + } + + /** + * In case store_id is not set for category then we can assume that it was passed through product import. + * Store group must have only one root category, so receiving category's path and checking if one of it parts + * is the root category for store group, we can set default_store_id value from it to category. + * it prevents urls duplication for different stores + * ("Default Category/category/sub" and "Default Category2/category/sub") + * + * @param Category $category + * @return void + */ + private function setCategoryStoreId($category) + { + /** @var StoreGroupCollection $storeGroupCollection */ + $storeGroupCollection = $this->storeGroupFactory->create(); + + foreach ($storeGroupCollection as $storeGroup) { + /** @var \Magento\Store\Model\Group $storeGroup */ + if (in_array($storeGroup->getRootCategoryId(), explode('/', $category->getPath()))) { + $category->setStoreId($storeGroup->getDefaultStoreId()); + } + } + } + /** * Resets used data maps to free up memory and temporary tables * diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index eb54f0427c11a..865931ef5b388 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -8,11 +8,14 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; -use Magento\Framework\Event\Observer; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\Framework\Event\ObserverInterface; use Magento\Store\Model\Store; +/** + * Class observer to initiate generation category url_path. + */ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { /** @@ -30,22 +33,32 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface */ protected $storeViewService; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService + * @param CategoryRepositoryInterface $categoryRepository */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, ChildrenCategoriesProvider $childrenCategoriesProvider, - StoreViewService $storeViewService + StoreViewService $storeViewService, + CategoryRepositoryInterface $categoryRepository ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->storeViewService = $storeViewService; + $this->categoryRepository = $categoryRepository; } /** + * Generate Category Url Path. + * * @param \Magento\Framework\Event\Observer $observer * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -54,48 +67,73 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Category $category */ $category = $observer->getEvent()->getCategory(); - $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); + $useDefaultAttribute = !empty($category->getData('use_default')['url_key']); if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); - if (empty($resultUrlKey)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); - } - $category->setUrlKey($resultUrlKey) - ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $this->updateUrlKey($category, $resultUrlKey); + } elseif ($useDefaultAttribute) { if (!$category->isObjectNew()) { - $category->getResource()->saveAttribute($category, 'url_path'); - if ($category->dataHasChangedFor('url_path')) { - $this->updateUrlPathForChildren($category); - } + $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); + $this->updateUrlKey($category, $resultUrlKey); + } + $category->setUrlKey(null)->setUrlPath(null); + } + } + + /** + * Update Url Key. + * + * @param Category $category + * @param string $urlKey + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function updateUrlKey(Category $category, string $urlKey) + { + if (empty($urlKey)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); + } + $category->setUrlKey($urlKey) + ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + if (!$category->isObjectNew()) { + $category->getResource()->saveAttribute($category, 'url_path'); + if ($category->dataHasChangedFor('url_path')) { + $this->updateUrlPathForChildren($category); } } } /** + * Update URL path for children. + * * @param Category $category * @return void */ protected function updateUrlPathForChildren(Category $category) { - $children = $this->childrenCategoriesProvider->getChildren($category, true); - if ($this->isGlobalScope($category->getStoreId())) { - foreach ($children as $child) { + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + foreach ($childrenIds as $childId) { foreach ($category->getStoreIds() as $storeId) { if ($this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( $storeId, - $child->getId(), + $childId, Category::ENTITY )) { - $child->setStoreId($storeId); + $child = $this->categoryRepository->get($childId, $storeId); $this->updateUrlPathForCategory($child); } } } } else { + $children = $this->childrenCategoriesProvider->getChildren($category, true); foreach ($children as $child) { + /** @var Category $child */ $child->setStoreId($category->getStoreId()); - $this->updateUrlPathForCategory($child); + if ($child->getParentId() === $category->getId()) { + $this->updateUrlPathForCategory($child, $category); + } else { + $this->updateUrlPathForCategory($child); + } } } } @@ -112,13 +150,16 @@ protected function isGlobalScope($storeId) } /** + * Update URL path for category. + * * @param Category $category + * @param Category|null $parentCategory * @return void */ - protected function updateUrlPathForCategory(Category $category) + protected function updateUrlPathForCategory(Category $category, Category $parentCategory = null) { $category->unsUrlPath(); - $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category, $parentCategory)); $category->getResource()->saveAttribute($category, 'url_path'); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index e4ccd0b869db7..6eda8dd0b61ee 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -6,11 +6,15 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\UrlRewrite\Model\UrlPersistInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\Framework\Event\ObserverInterface; +/** + * Class ProductProcessUrlRewriteSavingObserver + */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** @@ -23,22 +27,33 @@ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface */ private $urlPersist; + /** + * @var ProductUrlPathGenerator + */ + private $productUrlPathGenerator; + /** * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $productUrlPathGenerator */ public function __construct( ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $productUrlPathGenerator = null ) { $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; + $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() + ->get(ProductUrlPathGenerator::class); } /** * Generate urls for UrlRewrite and save it in storage + * * @param \Magento\Framework\Event\Observer $observer * @return void + * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -50,14 +65,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) || $product->getIsChangedWebsites() || $product->dataHasChangedFor('visibility') ) { - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::REDIRECT_TYPE => 0, - UrlRewrite::STORE_ID => $product->getStoreId() - ]); - if ($product->isVisibleInSiteVisibility()) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php index fc2056e83ec70..12334a2a773cb 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php @@ -13,7 +13,12 @@ use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +/** + * Observer to assign the products to website. + */ class ProductToWebsiteChangeObserver implements ObserverInterface { /** @@ -58,23 +63,30 @@ public function __construct( * Generate urls for UrlRewrite and save it in storage * * @param \Magento\Framework\Event\Observer $observer + * @throws NoSuchEntityException + * @throws UrlAlreadyExistsException * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { foreach ($observer->getEvent()->getProducts() as $productId) { + $storeId = $this->request->getParam('store_id', Store::DEFAULT_STORE_ID); + $product = $this->productRepository->getById( $productId, false, - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) + $storeId ); - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ]); - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + if (!empty($this->productUrlRewriteGenerator->generate($product))) { + $this->urlPersist->deleteByData([ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeId, + ]); + if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { + $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + } } } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php index b201ae31b680a..28afff56c019f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php @@ -33,6 +33,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); - $product->setUrlKey($this->productUrlPathGenerator->getUrlKey($product)); + $urlKey = $this->productUrlPathGenerator->getUrlKey($product); + if (null !== $urlKey) { + $product->setUrlKey($urlKey); + } } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index 9892465d1538a..3bbbcd4052c4a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -3,27 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Observer; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; +use Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\UrlRewrite\Model\MergeDataProvider; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Class for management url rewrites. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class UrlRewriteHandler { /** - * @var \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider + * @var ChildrenCategoriesProvider */ protected $childrenCategoriesProvider; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator + * @var CategoryUrlRewriteGenerator */ protected $categoryUrlRewriteGenerator; /** - * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator + * @var ProductUrlRewriteGenerator */ protected $productUrlRewriteGenerator; /** - * @var \Magento\UrlRewrite\Model\UrlPersistInterface + * @var UrlPersistInterface */ protected $urlPersist; @@ -33,44 +56,51 @@ class UrlRewriteHandler protected $isSkippedProduct; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory + * @var CollectionFactory */ protected $productCollectionFactory; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator + * @var CategoryProductUrlPathGenerator */ private $categoryBasedProductRewriteGenerator; /** - * @var \Magento\UrlRewrite\Model\MergeDataProvider + * @var MergeDataProvider */ private $mergeDataProviderPrototype; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var Json */ private $serializer; /** - * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider - * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator - * @param \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator $productUrlRewriteGenerator - * @param \Magento\UrlRewrite\Model\UrlPersistInterface $urlPersist - * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory - * @param \Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator - * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @var ProductScopeRewriteGenerator + */ + private $productScopeRewriteGenerator; + + /** + * @param ChildrenCategoriesProvider $childrenCategoriesProvider + * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator + * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator + * @param UrlPersistInterface $urlPersist + * @param CollectionFactory $productCollectionFactory + * @param CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator + * @param MergeDataProviderFactory|null $mergeDataProviderFactory + * @param Json|null $serializer + * @param ProductScopeRewriteGenerator|null $productScopeRewriteGenerator */ public function __construct( - \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider, - \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, - \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator $productUrlRewriteGenerator, - \Magento\UrlRewrite\Model\UrlPersistInterface $urlPersist, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, - \Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator, - \Magento\UrlRewrite\Model\MergeDataProviderFactory $mergeDataProviderFactory = null, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + ChildrenCategoriesProvider $childrenCategoriesProvider, + CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator, + ProductUrlRewriteGenerator $productUrlRewriteGenerator, + UrlPersistInterface $urlPersist, + CollectionFactory $productCollectionFactory, + CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator, + MergeDataProviderFactory $mergeDataProviderFactory = null, + Json $serializer = null, + ProductScopeRewriteGenerator $productScopeRewriteGenerator = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator; @@ -79,58 +109,29 @@ public function __construct( $this->productCollectionFactory = $productCollectionFactory; $this->categoryBasedProductRewriteGenerator = $categoryBasedProductRewriteGenerator; - if (!isset($mergeDataProviderFactory)) { - $mergeDataProviderFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\UrlRewrite\Model\MergeDataProviderFactory::class - ); - } - + $objectManager = ObjectManager::getInstance(); + $mergeDataProviderFactory = $mergeDataProviderFactory ?: $objectManager->get(MergeDataProviderFactory::class); $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); - - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Serialize\Serializer\Json::class - ); + $this->serializer = $serializer ?: $objectManager->get(Json::class); + $this->productScopeRewriteGenerator = $productScopeRewriteGenerator + ?: $objectManager->get(ProductScopeRewriteGenerator::class); } /** - * Generate url rewrites for products assigned to category + * Generates URL rewrites for products assigned to category. * - * @param \Magento\Catalog\Model\Category $category + * @param Category $category * @return array */ - public function generateProductUrlRewrites(\Magento\Catalog\Model\Category $category) + public function generateProductUrlRewrites(Category $category): array { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $this->isSkippedProduct[$category->getEntityId()] = []; - $saveRewriteHistory = $category->getData('save_rewrites_history'); - $storeId = $category->getStoreId(); + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeId = (int)$category->getStoreId(); if ($category->getChangedProductIds()) { - $this->isSkippedProduct[$category->getEntityId()] = $category->getAffectedProductIds(); - /* @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ - $collection = $this->productCollectionFactory->create() - ->setStoreId($storeId) - ->addIdFilter($category->getAffectedProductIds()) - ->addAttributeToSelect('visibility') - ->addAttributeToSelect('name') - ->addAttributeToSelect('url_key') - ->addAttributeToSelect('url_path'); - - $collection->setPageSize(1000); - $pageCount = $collection->getLastPageNumber(); - $currentPage = 1; - while ($currentPage <= $pageCount) { - $collection->setCurPage($currentPage); - foreach ($collection as $product) { - $product->setStoreId($storeId); - $product->setData('save_rewrites_history', $saveRewriteHistory); - $mergeDataProvider->merge( - $this->productUrlRewriteGenerator->generate($product, $category->getEntityId()) - ); - } - $collection->clear(); - $currentPage++; - } + $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory); } else { $mergeDataProvider->merge( $this->getCategoryProductsUrlRewrites( @@ -157,21 +158,75 @@ public function generateProductUrlRewrites(\Magento\Catalog\Model\Category $cate } /** - * @param \Magento\Catalog\Model\Category $category + * Update product url rewrites for changed product. + * + * @param Category $category + * @return array + */ + public function updateProductUrlRewritesForChangedProduct(Category $category): array + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $this->isSkippedProduct[$category->getEntityId()] = []; + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeIds = $this->getCategoryStoreIds($category); + + if ($category->getChangedProductIds()) { + foreach ($storeIds as $storeId) { + $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Delete category rewrites for children. + * + * @param Category $category + * @return void + */ + public function deleteCategoryRewritesForChildren(Category $category) + { + $categoryIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + $categoryIds[] = $category->getId(); + foreach ($categoryIds as $categoryId) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => + $categoryId, + UrlRewrite::ENTITY_TYPE => + CategoryUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + $this->urlPersist->deleteByData( + [ + UrlRewrite::METADATA => + $this->serializer->serialize(['category_id' => $categoryId]), + UrlRewrite::ENTITY_TYPE => + ProductUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + } + } + + /** + * Get category products url rewrites. + * + * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory * @param int|null $rootCategoryId * @return array */ private function getCategoryProductsUrlRewrites( - \Magento\Catalog\Model\Category $category, + Category $category, $storeId, $saveRewriteHistory, $rootCategoryId = null ) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ + /** @var Collection $productCollection */ $productCollection = $this->productCollectionFactory->create(); $productCollection->addCategoriesFilter(['eq' => [$category->getEntityId()]]) @@ -199,30 +254,65 @@ private function getCategoryProductsUrlRewrites( } /** - * @param \Magento\Catalog\Model\Category $category + * Generates product URL rewrites. + * + * @param MergeDataProvider $mergeDataProvider + * @param Category $category + * @param int $storeId + * @param bool $saveRewriteHistory * @return void */ - public function deleteCategoryRewritesForChildren(\Magento\Catalog\Model\Category $category) - { - $categoryIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); - $categoryIds[] = $category->getId(); - foreach ($categoryIds as $categoryId) { - $this->urlPersist->deleteByData( - [ - \Magento\UrlRewrite\Service\V1\Data\UrlRewrite::ENTITY_ID => - $categoryId, - \Magento\UrlRewrite\Service\V1\Data\UrlRewrite::ENTITY_TYPE => - \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::ENTITY_TYPE, - ] - ); - $this->urlPersist->deleteByData( - [ - \Magento\UrlRewrite\Service\V1\Data\UrlRewrite::METADATA => - $this->serializer->serialize(['category_id' => $categoryId]), - \Magento\UrlRewrite\Service\V1\Data\UrlRewrite::ENTITY_TYPE => - \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE, - ] - ); + private function generateChangedProductUrls( + MergeDataProvider $mergeDataProvider, + Category $category, + int $storeId, + bool $saveRewriteHistory + ) { + $this->isSkippedProduct[$category->getEntityId()] = $category->getAffectedProductIds(); + + $categoryStoreIds = [$storeId]; + // If category is changed in the Global scope when need to regenerate product URL rewrites for all other scopes. + if ($this->productScopeRewriteGenerator->isGlobalScope($storeId)) { + $categoryStoreIds = $this->getCategoryStoreIds($category); } + + foreach ($categoryStoreIds as $categoryStoreId) { + /* @var Collection $collection */ + $collection = $this->productCollectionFactory->create() + ->setStoreId($categoryStoreId) + ->addIdFilter($category->getChangedProductIds()) + ->addAttributeToSelect('visibility') + ->addAttributeToSelect('name') + ->addAttributeToSelect('url_key') + ->addAttributeToSelect('url_path'); + + $collection->setPageSize(1000); + $pageCount = $collection->getLastPageNumber(); + $currentPage = 1; + while ($currentPage <= $pageCount) { + $collection->setCurPage($currentPage); + foreach ($collection as $product) { + $product->setData('save_rewrites_history', $saveRewriteHistory); + $product->setStoreId($categoryStoreId); + $mergeDataProvider->merge( + $this->productUrlRewriteGenerator->generate($product, $category->getEntityId()) + ); + } + $collection->clear(); + $currentPage++; + } + } + } + + /** + * Gets category store IDs without Global Store. + * + * @param Category $category + * @return array + */ + private function getCategoryStoreIds(Category $category): array + { + $ids = $category->getStoreIds(); + return array_filter($ids); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php new file mode 100644 index 0000000000000..5908bde7c5a5f --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -0,0 +1,90 @@ +request = $request; + } + + /** + * Add 'save_rewrites_history' param to the product data + * + * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper + * @param InputParamsResolverController $subject + * @param array $result + * @return array + */ + public function afterResolve(InputParamsResolverController $subject, array $result): array + { + $route = $subject->getRoute(); + $serviceMethodName = $route->getServiceMethod(); + $serviceClassName = $route->getServiceClass(); + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) + && $this->isCustomAttributesExists($requestBodyParams)) { + foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === 'save_rewrites_history') { + foreach ($result as $resultItem) { + if ($resultItem instanceof Product) { + $resultItem->setData('save_rewrites_history', (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + return $result; + } + + /** + * Check that product save method called + * + * @param string $serviceClassName + * @param string $serviceMethodName + * @return bool + */ + private function isProductSaveCalled(string $serviceClassName, string $serviceMethodName): bool + { + return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; + } + + /** + * Check is any custom options exists in product data + * + * @param array $requestBodyParams + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams): bool + { + return !empty($requestBodyParams['product']['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/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/app/code/Magento/CatalogUrlRewrite/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/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/app/code/Magento/CatalogUrlRewrite/Test/Mftf/README.md b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/README.md new file mode 100644 index 0000000000000..785d0cce48c3e --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Url Rewrite Functional Tests + +The Functional Test Module for **Magento Catalog Url Rewrite** module. diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml new file mode 100644 index 0000000000000..d52395342c092 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -0,0 +1,71 @@ + + + + + + + + + <description value="Check that URL for product rewritten correctly"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13913"/> + <useCaseId value="MAGETWO-73534"/> + <group value="catalog"/> + <group value="catalogUrlRewrite"/> + </annotations> + <before> + <!--Create product--> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openProductEditPage"/> + <!--Switch to Default Store view--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectDefaultStoreView"> + <argument name="storeViewName" value="_defaultStore"/> + </actionGroup> + + <!--Set use default url--> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSearchEngineOptimizationTab"/> + <waitForElementVisible selector="{{AdminProductSEOSection.useDefaultUrl}}" time="30" stepKey="waitForUseDefaultUrlCheckbox"/> + <uncheckOption selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="uncheckUseDefaultUrlCheckbox"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$createProduct.custom_attributes[url_key]$$-updated" stepKey="changeUrlKey"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Select product and go toUpdate Attribute page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGrid"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridBySku"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="selectFilteredProduct"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickBulkUpdateAttributes"/> + <waitForPageLoad stepKey="waitForUpdateAttributesPageLoad"/> + <seeInCurrentUrl url="{{AdminProductUpdateAttributesPage.url}}" stepKey="seeInUrlAttributeUpdatePage"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.website}}" stepKey="openWebsitesTab"/> + <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> + <click selector="{{AdminUpdateAttributesHeaderSection.saveButton}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) were updated." stepKey="seeSaveSuccessMessage"/> + <!--Got to Store front product page and check url--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$-updated)}}" stepKey="navigateToSimpleProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$-updated)}}" stepKey="seeProductNewUrl"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="seeCorrectSku"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml new file mode 100644 index 0000000000000..ce3f98ee0058a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="RewriteStoreLevelUrlKeyOfChildCategoryTest"> + <annotations> + <title value="Rewriting Store-level URL key of child category"/> + <stories value="MAGETWO-89619: #13513: Magento ignore store-level url_key of child category in URL rewrite process for global scope"/> + <description value="Rewriting Store-level URL key of child category"/> + <features value="CatalogUrlRewrite"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96558"/> + <group value="catalog_url_rewrite"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> + + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SubCategoryWithParent" stepKey="subCategory"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="navigateToCreatedSubCategory"> + <argument name="category" value="$$subCategory$$"/> + </actionGroup> + + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"/> + + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyForSubCategory"> + <argument name="value" value="{{BagsCategory.url_key}}"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="navigateToCreatedDefaultCategory"> + <argument name="category" value="$$defaultCategory$$"/> + </actionGroup> + + <actionGroup ref="ChangeSeoUrlKey" stepKey="changeSeoUrlKeyForDefaultCategory"> + <argument name="value" value="{{GearCategory.url_key}}"/> + </actionGroup> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"/> + + <actionGroup ref="StorefrontGoToSubCategoryPage" stepKey="goToSubCategoryPage"> + <argument name="parentCategory" value="$$defaultCategory$$"/> + <argument name="subCategory" value="$$subCategory$$"/> + <argument name="urlPath" value="{{GearCategory.url_key}}/{{BagsCategory.url_key}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CanonicalUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CanonicalUrlRewriteGeneratorTest.php index eb18e26510389..f3b2bc674b898 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CanonicalUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CanonicalUrlRewriteGeneratorTest.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile + namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php index 3f641256b1259..f832293706567 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php @@ -31,6 +31,12 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $categoryRepository; + + /** + * @inheritdoc + */ protected function setUp() { $this->serializerMock = $this->getMockBuilder(Json::class) @@ -47,6 +53,9 @@ protected function setUp() $this->categoryUrlRewriteGenerator = $this->getMockBuilder( \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); + $this->categoryRepository = $this->getMockBuilder( + \Magento\Catalog\Model\CategoryRepository::class + )->disableOriginalConstructor()->getMock(); $mergeDataProviderFactory = $this->createPartialMock( \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create'] @@ -59,14 +68,15 @@ protected function setUp() [ 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, - 'mergeDataProviderFactory' => $mergeDataProviderFactory + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'categoryRepository' => $this->categoryRepository, ] ); } public function testNoChildrenCategories() { - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) ->will($this->returnValue([])); $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); @@ -76,14 +86,16 @@ public function testGenerate() { $storeId = 'store_id'; $saveRewritesHistory = 'flag'; + $childId = 2; $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); - $childCategory->expects($this->once())->method('setStoreId')->with($storeId); $childCategory->expects($this->once())->method('setData') ->with('save_rewrites_history', $saveRewritesHistory); - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) - ->will($this->returnValue([$childCategory])); + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) + ->willReturn([$childId]); + $this->categoryRepository->expects($this->once())->method('get') + ->with($childId, $storeId)->willReturn($childCategory); $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') ->will($this->returnValue($saveRewritesHistory)); $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php index 2ee2473290306..fbc620a6d741a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile + namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; @@ -50,11 +50,14 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $this->urlRewriteFactory->expects($this->once())->method('create') ->willReturn($this->urlRewrite); - $mergeDataProviderFactory = $this->createPartialMock(\Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create']); + $mergeDataProviderFactory = $this->createPartialMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'] + ); $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); - $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( + $this->currentUrlRewritesRegenerator = (new ObjectManager($this))->getObject( \Magento\CatalogUrlRewrite\Model\Category\CurrentUrlRewritesRegenerator::class, [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php index f91a55c11b974..85e8837027151 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Category; +use Magento\Catalog\Model\CategoryFactory; use Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\Move as CategoryMovePlugin; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; @@ -39,6 +40,11 @@ class MoveTest extends \PHPUnit\Framework\TestCase */ private $categoryMock; + /** + * @var CategoryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryFactory; + /** * @var CategoryMovePlugin */ @@ -55,28 +61,44 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getChildren']) ->getMock(); + $this->categoryFactory = $this->getMockBuilder(CategoryFactory::class) + ->disableOriginalConstructor() + ->getMock(); $this->subjectMock = $this->getMockBuilder(CategoryResourceModel::class) ->disableOriginalConstructor() ->getMock(); $this->categoryMock = $this->getMockBuilder(Category::class) ->disableOriginalConstructor() - ->setMethods(['getResource', 'setUrlPath']) + ->setMethods(['getResource', 'setUrlPath', 'getStoreIds', 'getStoreId', 'setStoreId']) ->getMock(); $this->plugin = $this->objectManager->getObject( CategoryMovePlugin::class, [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGeneratorMock, - 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock + 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock, + 'categoryFactory' => $this->categoryFactory ] ); } + /** + * Tests url updating for children categories. + */ public function testAfterChangeParent() { $urlPath = 'test/path'; - $this->categoryMock->expects($this->once()) - ->method('getResource') + $storeIds = [1]; + $originalCategory = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $this->categoryFactory->method('create') + ->willReturn($originalCategory); + + $this->categoryMock->method('getResource') ->willReturn($this->subjectMock); + $this->categoryMock->expects($this->once()) + ->method('getStoreIds') + ->willReturn($storeIds); $this->childrenCategoriesProviderMock->expects($this->once()) ->method('getChildren') ->with($this->categoryMock, true) @@ -85,9 +107,6 @@ public function testAfterChangeParent() ->method('getUrlPath') ->with($this->categoryMock) ->willReturn($urlPath); - $this->categoryMock->expects($this->once()) - ->method('getResource') - ->willReturn($this->subjectMock); $this->categoryMock->expects($this->once()) ->method('setUrlPath') ->with($urlPath); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/UpdateUrlPathTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/UpdateUrlPathTest.php new file mode 100644 index 0000000000000..a09620f0797ab --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/UpdateUrlPathTest.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Category; + +/** + * Unit test for Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\UpdateUrlPath class + */ +class UpdateUrlPathTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; + + /** + * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlRewriteGenerator; + + /** + * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeViewService; + + /** + * @var \Magento\UrlRewrite\Model\UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlPersist; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Category|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryResource; + + /** + * @var \Magento\Catalog\Model\Category|\PHPUnit_Framework_MockObject_MockObject + */ + private $category; + + /** + * @var \Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\UpdateUrlPath + */ + private $updateUrlPathPlugin; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->categoryUrlPathGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::class + ) + ->disableOriginalConstructor() + ->setMethods(['getUrlPath']) + ->getMock(); + $this->categoryUrlRewriteGenerator = $this->getMockBuilder( + \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class + ) + ->disableOriginalConstructor() + ->setMethods(['generate']) + ->getMock(); + $this->categoryResource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category::class) + ->disableOriginalConstructor() + ->setMethods(['saveAttribute']) + ->getMock(); + $this->category = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getStoreId', + 'getParentId', + 'isObjectNew', + 'isInRootCategoryList', + 'getStoreIds', + 'setStoreId', + 'unsUrlPath', + 'setUrlPath' + ] + ) + ->getMock(); + $this->storeViewService = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Service\V1\StoreViewService::class) + ->disableOriginalConstructor() + ->setMethods(['doesEntityHaveOverriddenUrlPathForStore']) + ->getMock(); + $this->urlPersist = $this->getMockBuilder(\Magento\UrlRewrite\Model\UrlPersistInterface::class) + ->disableOriginalConstructor() + ->setMethods(['replace']) + ->getMockForAbstractClass(); + + $this->updateUrlPathPlugin = $this->objectManager->getObject( + \Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\UpdateUrlPath::class, + [ + 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGenerator, + 'urlPersist' => $this->urlPersist, + 'storeViewService' => $this->storeViewService + ] + ); + } + + public function testAroundSaveWithoutRootCategory() + { + $this->category->expects($this->atLeastOnce())->method('getParentId')->willReturn(0); + $this->category->expects($this->atLeastOnce())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->atLeastOnce())->method('isInRootCategoryList')->willReturn(false); + $this->category->expects($this->never())->method('getStoreIds'); + + $this->assertEquals( + $this->categoryResource, + $this->updateUrlPathPlugin->afterSave($this->categoryResource, $this->categoryResource, $this->category) + ); + } + + public function testAroundSaveWithRootCategory() + { + $parentId = 1; + $categoryStoreIds = [0,1,2]; + $generatedUrlPath = 'parent_category/child_category'; + + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->with($this->category) + ->willReturn($generatedUrlPath); + $this->category->expects($this->atLeastOnce())->method('getParentId')->willReturn($parentId); + $this->category->expects($this->atLeastOnce())->method('isObjectNew')->willReturn(true); + $this->category->expects($this->atLeastOnce())->method('isInRootCategoryList')->willReturn(false); + $this->category->expects($this->atLeastOnce())->method('getStoreIds')->willReturn($categoryStoreIds); + $this->category->expects($this->once())->method('setStoreId')->with($categoryStoreIds[2])->willReturnSelf(); + $this->category->expects($this->once())->method('unsUrlPath')->willReturnSelf(); + $this->category->expects($this->once())->method('setUrlPath')->with($generatedUrlPath)->willReturnSelf(); + $this->storeViewService->expects($this->exactly(2))->method('doesEntityHaveOverriddenUrlPathForStore') + ->willReturnMap( + [ + [ + $categoryStoreIds[1], $parentId, 'catalog_category', false + ], + [ + $categoryStoreIds[2], $parentId, 'catalog_category', true + ] + ] + ); + $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path') + ->willReturnSelf(); + $generatedUrlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class) + ->disableOriginalConstructor() + ->getMock(); + $this->categoryUrlRewriteGenerator->expects($this->once())->method('generate')->with($this->category) + ->willReturn([$generatedUrlRewrite]); + $this->urlPersist->expects($this->once())->method('replace')->with([$generatedUrlRewrite])->willReturnSelf(); + + $this->assertEquals( + $this->categoryResource, + $this->updateUrlPathPlugin->afterSave($this->categoryResource, $this->categoryResource, $this->category) + ); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php index 7297d150a8e6f..e804fb9d08e54 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php @@ -157,6 +157,61 @@ public function testGetUrlPathWithParent( $this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category)); } + /** + * @return array + */ + public function getUrlPathWithParentCategoryDataProvider(): array + { + $requireGenerationLevel = CategoryUrlPathGenerator::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING; + $noGenerationLevel = CategoryUrlPathGenerator::MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING - 1; + return [ + [13, 'url-key', false, $requireGenerationLevel, 10, 'parent-path', 'parent-path/url-key'], + [13, 'url-key', false, $requireGenerationLevel, Category::TREE_ROOT_ID, null, 'url-key'], + [13, 'url-key', true, $noGenerationLevel, Category::TREE_ROOT_ID, null, 'url-key'], + ]; + } + + /** + * Test receiving Url Path when parent category is presented. + * + * @param int $parentId + * @param string $urlKey + * @param bool $isCategoryNew + * @param bool $level + * @param int $parentCategoryParentId + * @param null|string $parentUrlPath + * @param string $result + * @dataProvider getUrlPathWithParentCategoryDataProvider + */ + public function testGetUrlPathWithParentCategory( + int $parentId, + string $urlKey, + bool $isCategoryNew, + bool $level, + int $parentCategoryParentId, + $parentUrlPath, + string $result + ) { + $urlPath = null; + $this->category->expects($this->any())->method('getParentId')->willReturn($parentId); + $this->category->expects($this->any())->method('getLevel')->willReturn($level); + $this->category->expects($this->any())->method('getUrlPath')->willReturn($urlPath); + $this->category->expects($this->any())->method('getUrlKey')->willReturn($urlKey); + $this->category->expects($this->any())->method('isObjectNew')->willReturn($isCategoryNew); + + $methods = ['getUrlPath', 'getParentId']; + $parentCategoryMock = $this->createPartialMock(\Magento\Catalog\Model\Category::class, $methods); + $parentCategoryMock->expects($this->any())->method('getParentId')->willReturn($parentCategoryParentId); + $parentCategoryMock->expects($this->any())->method('getUrlPath')->willReturn($parentUrlPath); + + $this->categoryRepository->expects($this->any()) + ->method('get') + ->with($parentCategoryParentId) + ->willReturn($parentCategoryMock); + + $this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category, $parentCategoryMock)); + } + /** * @return array */ diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php index 2a6c81f26b1b8..1353dbff17b0b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlRewriteGeneratorTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogUrlRewrite\Test\Unit\Model; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -78,7 +76,10 @@ function ($value) { ->disableOriginalConstructor()->getMock(); $this->category = $this->createMock(\Magento\Catalog\Model\Category::class); $this->categoryRepository = $this->createMock(\Magento\Catalog\Api\CategoryRepositoryInterface::class); - $mergeDataProviderFactory = $this->createPartialMock(\Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create']); + $mergeDataProviderFactory = $this->createPartialMock( + \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, + ['create'] + ); $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); @@ -125,7 +126,10 @@ public function testGenerationForGlobalScope() $this->currentUrlRewritesRegenerator->expects($this->any())->method('generate') ->with(1, $this->category, $categoryId) ->will($this->returnValue(['category-3' => $current])); - $categoryForSpecificStore = $this->createPartialMock(\Magento\Catalog\Model\Category::class, ['getUrlKey', 'getUrlPath']); + $categoryForSpecificStore = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + ['getUrlKey', 'getUrlPath'] + ); $this->categoryRepository->expects($this->once())->method('get')->willReturn($categoryForSpecificStore); $this->assertEquals( diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php index b32b0216b9bdf..14f30eb7607d3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Test\Unit\Model; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; @@ -72,10 +73,11 @@ protected function setUp() public function getUrlPathDataProvider() { return [ - 'path based on url key' => ['url-key', null, 'url-key'], - 'path based on product name 1' => ['', 'product-name', 'product-name'], - 'path based on product name 2' => [null, 'product-name', 'product-name'], - 'path based on product name 3' => [false, 'product-name', 'product-name'] + 'path based on url key uppercase' => ['Url Key', null, 0, 'url key'], + 'path based on url key' => ['url-key', null, 0, 'url-key'], + 'path based on product name 1' => ['', 'product-name', 1, 'product-name'], + 'path based on product name 2' => [null, 'product-name', 1, 'product-name'], + 'path based on product name 3' => [false, 'product-name', 1, 'product-name'] ]; } @@ -83,15 +85,17 @@ public function getUrlPathDataProvider() * @dataProvider getUrlPathDataProvider * @param string|null|bool $urlKey * @param string|null|bool $productName + * @param int $formatterCalled * @param string $result */ - public function testGetUrlPath($urlKey, $productName, $result) + public function testGetUrlPath($urlKey, $productName, $formatterCalled, $result) { $this->product->expects($this->once())->method('getData')->with('url_path') ->will($this->returnValue(null)); $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($urlKey)); $this->product->expects($this->any())->method('getName')->will($this->returnValue($productName)); - $this->product->expects($this->once())->method('formatUrlKey')->will($this->returnArgument(0)); + $this->product->expects($this->exactly($formatterCalled)) + ->method('formatUrlKey')->will($this->returnArgument(0)); $this->assertEquals($result, $this->productUrlPathGenerator->getUrlPath($this->product, null)); } @@ -105,7 +109,7 @@ public function testGetUrlKey($productUrlKey, $expectedUrlKey) { $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($productUrlKey)); $this->product->expects($this->any())->method('formatUrlKey')->will($this->returnValue($productUrlKey)); - $this->assertEquals($expectedUrlKey, $this->productUrlPathGenerator->getUrlKey($this->product)); + $this->assertSame($expectedUrlKey, $this->productUrlPathGenerator->getUrlKey($this->product)); } /** @@ -114,7 +118,7 @@ public function testGetUrlKey($productUrlKey, $expectedUrlKey) public function getUrlKeyDataProvider() { return [ - 'URL Key use default' => [false, false], + 'URL Key use default' => [false, null], 'URL Key empty' => ['product-url', 'product-url'], ]; } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php index 83957214f69fc..2580322ceddab 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlRewriteGeneratorTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\CatalogUrlRewrite\Test\Unit\Model; use Magento\Catalog\Model\Category; @@ -58,7 +56,8 @@ protected function setUp() { $this->product = $this->createMock(\Magento\Catalog\Model\Product::class); $this->categoriesCollection = $this->getMockBuilder( - \Magento\Catalog\Model\ResourceModel\Category\Collection::class) + \Magento\Catalog\Model\ResourceModel\Category\Collection::class + ) ->disableOriginalConstructor()->getMock(); $this->product->expects($this->any())->method('getCategoryCollection') ->will($this->returnValue($this->categoriesCollection)); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php new file mode 100644 index 0000000000000..b12da6243a903 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; +use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteMovingObserver; +use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\UrlRewrite\Model\UrlPersistInterface; + +/** + * Class CategoryProcessUrlRewriteMovingObserverTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CategoryProcessUrlRewriteMovingObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryProcessUrlRewriteMovingObserver + */ + private $observer; + + /** + * @var CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlRewriteGeneratorMock; + + /** + * @var UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlPersistMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var UrlRewriteHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlRewriteHandlerMock; + + /** + * @var DatabaseMapPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $databaseMapPoolMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->categoryUrlRewriteGeneratorMock = $this->createMock(CategoryUrlRewriteGenerator::class); + $this->urlPersistMock = $this->createMock(UrlPersistInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->urlRewriteHandlerMock = $this->createMock(UrlRewriteHandler::class); + /** @var UrlRewriteBunchReplacer|\PHPUnit_Framework_MockObject_MockObject $urlRewriteMock */ + $urlRewriteMock = $this->createMock(UrlRewriteBunchReplacer::class); + $this->databaseMapPoolMock = $this->createMock(DatabaseMapPool::class); + + $this->observer = new CategoryProcessUrlRewriteMovingObserver( + $this->categoryUrlRewriteGeneratorMock, + $this->urlPersistMock, + $this->scopeConfigMock, + $this->urlRewriteHandlerMock, + $urlRewriteMock, + $this->databaseMapPoolMock, + [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ] + ); + } + + /** + * Test category process rewrite url by changing the parent + * + * @return void + */ + public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() + { + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCategory']) + ->getMock(); + $categoryMock = $this->createPartialMock(Category::class, [ + 'dataHasChangedFor', + 'getEntityId', + 'getStoreId', + 'setData' + ]); + + $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') + ->willReturn(true); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $this->scopeConfigMock->expects($this->once())->method('isSetFlag') + ->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true); + $this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate') + ->with($categoryMock, true)->willReturn(['category-url-rewrite']); + $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites') + ->with($categoryMock)->willReturn(['product-url-rewrite']); + $this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf(); + + $this->observer->execute($observerMock); + } + + /** + * Test category process rewrite url without changing the parent + * + * @return void + */ + public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() + { + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCategory']) + ->getMock(); + $categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); + $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') + ->willReturn(false); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php new file mode 100644 index 0000000000000..634dae5643c02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteSavingObserverTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; +use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver; +use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Group\CollectionFactory; + +/** + * Tests Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver. + */ +class CategoryProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryProcessUrlRewriteSavingObserver + */ + private $observer; + + /** + * @var CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlRewriteGeneratorMock; + + /** + * @var UrlRewriteHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlRewriteHandlerMock; + + /** + * @var UrlRewriteBunchReplacer|\PHPUnit_Framework_MockObject_MockObject $urlRewriteMock + */ + private $urlRewriteBunchReplacerMock; + + /** + * @var DatabaseMapPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $databaseMapPoolMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->categoryUrlRewriteGeneratorMock = $this->createMock(CategoryUrlRewriteGenerator::class); + $this->urlRewriteHandlerMock = $this->createMock(UrlRewriteHandler::class); + $this->urlRewriteBunchReplacerMock = $this->createMock(UrlRewriteBunchReplacer::class); + $this->databaseMapPoolMock = $this->createMock(DatabaseMapPool::class); + /** @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject $storeGroupFactoryMock */ + $storeGroupCollectionFactoryMock = $this->createMock(CollectionFactory::class); + + $this->observer = $objectManager->getObject( + CategoryProcessUrlRewriteSavingObserver::class, + [ + 'categoryUrlRewriteGenerator' => $this->categoryUrlRewriteGeneratorMock, + 'urlRewriteHandler' => $this->urlRewriteHandlerMock, + 'urlRewriteBunchReplacer' => $this->urlRewriteBunchReplacerMock, + 'databaseMapPool' => $this->databaseMapPoolMock, + 'dataUrlRewriteClassNames' => [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ], + 'storeGroupFactory' => $storeGroupCollectionFactoryMock, + ] + ); + } + + /** + * Covers case when only associated products are changed for category. + * + * @return void + */ + public function testExecuteCategoryOnlyProductHasChanged() + { + $productId = 120; + $productRewrites = ['product-url-rewrite']; + + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + /** @var Event|\PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->createMock(Event::class); + /** @var Category|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ + $categoryMock = $this->createPartialMock( + Category::class, + [ + 'hasData', + 'dataHasChangedFor', + 'getChangedProductIds', + ] + ); + + $categoryMock->expects($this->once())->method('hasData')->with('store_id')->willReturn(true); + $categoryMock->expects($this->exactly(2))->method('getChangedProductIds')->willReturn([$productId]); + $categoryMock->expects($this->any())->method('dataHasChangedFor') + ->willReturnMap( + [ + ['url_key', false], + ['is_anchor', false], + ] + ); + $eventMock->expects($this->once())->method('getData')->with('category')->willReturn($categoryMock); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('updateProductUrlRewritesForChangedProduct') + ->with($categoryMock) + ->willReturn($productRewrites); + + $this->urlRewriteBunchReplacerMock->expects($this->once()) + ->method('doBunchReplace') + ->with($productRewrites, 10000); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index 1b4d1e08aa208..b6208456d5e12 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -3,37 +3,51 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; class CategoryUrlPathAutogeneratorObserverTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver */ - protected $categoryUrlPathAutogeneratorObserver; + /** + * @var \Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver + */ + private $categoryUrlPathAutogeneratorObserver; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $categoryUrlPathGenerator; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlPathGenerator; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $childrenCategoriesProvider; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $childrenCategoriesProvider; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $observer; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $observer; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $category; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $category; /** * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeViewService; + private $storeViewService; /** * @var \Magento\Catalog\Model\ResourceModel\Category|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryResource; + private $categoryResource; + /** + * @inheritDoc + */ protected function setUp() { $this->observer = $this->createPartialMock( @@ -41,16 +55,15 @@ protected function setUp() ['getEvent', 'getCategory'] ); $this->categoryResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Category::class); - $this->category = $this->createPartialMock(\Magento\Catalog\Model\Category::class, [ - 'setUrlKey', - 'setUrlPath', + $this->category = $this->createPartialMock( + \Magento\Catalog\Model\Category::class, + [ 'dataHasChangedFor', - 'isObjectNew', 'getResource', - 'getUrlKey', 'getStoreId', - 'getData' - ]); + 'formatUrlKey' + ] + ); $this->category->expects($this->any())->method('getResource')->willReturn($this->categoryResource); $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); $this->observer->expects($this->any())->method('getCategory')->willReturn($this->category); @@ -73,106 +86,127 @@ protected function setUp() ); } - public function testSetCategoryUrlAndCategoryPath() + /** + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValueDataProvider + */ + public function testShouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValue($isObjectNew) { - $this->category->expects($this->once())->method('getUrlKey')->willReturn('category'); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->willReturn('urk_key'); - $this->category->expects($this->once())->method('setUrlKey')->with('urk_key')->willReturnSelf(); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn('url_path'); - $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); - + $expectedUrlKey = 'formatted_url_key'; + $expectedUrlPath = 'generated_url_path'; + $categoryData = ['use_default' => ['url_key' => 0], 'url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->willReturn($expectedUrlKey); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn($expectedUrlPath); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertEquals($expectedUrlKey, $this->category->getUrlKey()); + $this->assertEquals($expectedUrlPath, $this->category->getUrlPath()); + $this->categoryResource->expects($this->never())->method('saveAttribute'); } - public function testExecuteWithoutUrlKeyAndUrlPathUpdating() + /** + * @return array + */ + public function shouldFormatUrlKeyAndGenerateUrlPathIfUrlKeyIsNotUsingDefaultValueDataProvider() { - $this->category->expects($this->once())->method('getUrlKey')->willReturn(false); - $this->category->expects($this->never())->method('setUrlKey'); - $this->category->expects($this->never())->method('setUrlPath'); - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + return [ + [true], + [false], + ]; } /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Invalid URL key + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValueDataProvider */ - public function testExecuteWithException() + public function testShouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValue($isObjectNew) { - $categoryName = 'test'; - $categoryData = ['url_key' => 0]; - $this->category->expects($this->once())->method('getUrlKey')->willReturn($categoryName); - $this->category->expects($this->once()) - ->method('getData') - ->with('use_default') - ->willReturn($categoryData); - $this->categoryUrlPathGenerator->expects($this->once()) - ->method('getUrlKey') - ->with($this->category) - ->willReturn(null); + $categoryData = ['use_default' => ['url_key' => 1], 'url_key' => 'some_key', 'url_path' => 'some_path']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->category->expects($this->any())->method('formatUrlKey')->willReturn('formatted_key'); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertNull($this->category->getUrlKey()); + $this->assertNull($this->category->getUrlPath()); } - public function testUrlKeyAndUrlPathUpdating() + /** + * @return array + */ + public function shouldResetUrlPathAndUrlKeyIfUrlKeyIsUsingDefaultValueDataProvider() { - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->with($this->category) - ->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->with($this->category) - ->willReturn('url_path'); - - $this->category->expects($this->once())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->once())->method('setUrlKey')->with('url_key')->willReturnSelf(); - $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); - // break code execution - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); + return [ + [true], + [false], + ]; + } + /** + * @param $useDefaultUrlKey + * @param $isObjectNew + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider shouldThrowExceptionIfUrlKeyIsEmptyDataProvider + */ + public function testShouldThrowExceptionIfUrlKeyIsEmpty($useDefaultUrlKey, $isObjectNew) + { + $this->expectExceptionMessage('Invalid URL key'); + $categoryData = ['use_default' => ['url_key' => $useDefaultUrlKey], 'url_key' => '', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew($isObjectNew); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn(''); + $this->category->expects($this->any())->method('formatUrlKey')->willReturn(''); + $this->assertEquals($isObjectNew, $this->category->isObjectNew()); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + $this->assertEquals($categoryData['url_key'], $this->category->getUrlKey()); + $this->assertEquals($categoryData['url_path'], $this->category->getUrlPath()); } - public function testUrlPathAttributeNoUpdatingIfCategoryIsNew() + /** + * @return array + */ + public function shouldThrowExceptionIfUrlKeyIsEmptyDataProvider() { - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(true); - $this->categoryResource->expects($this->never())->method('saveAttribute'); - - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + return [ + [0, false], + [0, true], + [1, false], + ]; } public function testUrlPathAttributeUpdating() { - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); - + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $expectedUrlKey = 'formatted_url_key'; + $expectedUrlPath = 'generated_url_path'; + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn($expectedUrlKey); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn($expectedUrlPath); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); - - // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); - $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChanged() { + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); // break code execution $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); @@ -181,13 +215,12 @@ public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChange public function testChildrenUrlPathAttributeUpdatingForSpecificStore() { + $categoryData = ['url_key' => 'some_key', 'url_path' => '']; + $this->category->setData($categoryData); + $this->category->isObjectNew(false); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('generated_url_key'); $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('generated_url_path'); - - $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); - $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); - $this->category->expects($this->exactly(2))->method('isObjectNew')->willReturn(false); $this->category->expects($this->any())->method('dataHasChangedFor')->willReturn(true); // only for specific store $this->category->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); @@ -195,15 +228,18 @@ public function testChildrenUrlPathAttributeUpdatingForSpecificStore() $childCategoryResource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Category::class) ->disableOriginalConstructor()->getMock(); $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) - ->setMethods([ - 'getUrlPath', - 'setUrlPath', - 'getResource', - 'getStore', - 'getStoreId', - 'setStoreId' - ]) - ->disableOriginalConstructor()->getMock(); + ->setMethods( + [ + 'getUrlPath', + 'setUrlPath', + 'getResource', + 'getStore', + 'getStoreId', + 'setStoreId' + ] + ) + ->disableOriginalConstructor() + ->getMock(); $childCategory->expects($this->any())->method('getResource')->willReturn($childCategoryResource); $childCategory->expects($this->once())->method('setStoreId')->with(1); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php index d294f6d022ef3..39317b42af989 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -103,7 +103,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => false, 'isChangedCategories' => false, 'visibilityResult' => true, - 'expectedDeleteCount' => 1, 'expectedReplaceCount' => 1, ], @@ -113,7 +112,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => false, 'isChangedCategories' => false, 'visibilityResult' => true, - 'expectedDeleteCount' => 0, 'expectedReplaceCount' => 0 ], 'visibility changed' => [ @@ -122,7 +120,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => false, 'isChangedCategories' => false, 'visibilityResult' => true, - 'expectedDeleteCount' => 1, 'expectedReplaceCount' => 1 ], 'websites changed' => [ @@ -131,7 +128,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => true, 'isChangedCategories' => false, 'visibilityResult' => true, - 'expectedDeleteCount' => 1, 'expectedReplaceCount' => 1 ], 'categories changed' => [ @@ -140,7 +136,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => false, 'isChangedCategories' => true, 'visibilityResult' => true, - 'expectedDeleteCount' => 1, 'expectedReplaceCount' => 1 ], 'url changed invisible' => [ @@ -149,7 +144,6 @@ public function urlKeyDataProvider() 'isChangedWebsites' => false, 'isChangedCategories' => false, 'visibilityResult' => false, - 'expectedDeleteCount' => 1, 'expectedReplaceCount' => 0 ], ]; @@ -161,7 +155,6 @@ public function urlKeyDataProvider() * @param bool $isChangedWebsites * @param bool $isChangedCategories * @param bool $visibilityResult - * @param int $expectedDeleteCount * @param int $expectedReplaceCount * * @dataProvider urlKeyDataProvider @@ -172,7 +165,6 @@ public function testExecuteUrlKey( $isChangedWebsites, $isChangedCategories, $visibilityResult, - $expectedDeleteCount, $expectedReplaceCount ) { $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue(12)); @@ -194,13 +186,6 @@ public function testExecuteUrlKey( ->method('getIsChangedCategories') ->will($this->returnValue($isChangedCategories)); - $this->urlPersist->expects($this->exactly($expectedDeleteCount))->method('deleteByData')->with([ - UrlRewrite::ENTITY_ID => $this->product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::REDIRECT_TYPE => 0, - UrlRewrite::STORE_ID => $this->product->getStoreId() - ]); - $this->product->expects($this->any()) ->method('isVisibleInSiteVisibility') ->will($this->returnValue($visibilityResult)); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php new file mode 100644 index 0000000000000..f383c949b4295 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogUrlRewrite\Observer\ProductToWebsiteChangeObserver; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Store\Model\Store; + +/** + * Test for ProductToWebsiteChangeObserver + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductToWebsiteChangeObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * @var ProductUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $productUrlRewriteGenerator; + + /** + * @var UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlPersist; + + /** + * @var Event|\PHPUnit_Framework_MockObject_MockObject + */ + private $event; + + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observer; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $product; + + /** + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductToWebsiteChangeObserver + */ + private $model; + + /** + * @var int + */ + private $productId; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->productId = 3; + + $this->urlPersist = $this->getMockBuilder(UrlPersistInterface::class) + ->setMethods(['deleteByData', 'replace']) + ->getMockForAbstractClass(); + $this->productRepository = $this->getMockBuilder(ProductRepositoryInterface::class) + ->setMethods(['getById']) + ->getMockForAbstractClass(); + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getVisibility']) + ->getMock(); + $this->product->expects($this->any()) + ->method('getId') + ->willReturn($this->productId); + $this->productRepository->expects($this->any()) + ->method('getById') + ->with($this->productId, false, Store::DEFAULT_STORE_ID) + ->willReturn($this->product); + $this->productUrlRewriteGenerator = $this->getMockBuilder(ProductUrlRewriteGenerator::class) + ->disableOriginalConstructor() + ->setMethods(['generate']) + ->getMock(); + $this->event = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProducts']) + ->getMock(); + $this->event->expects($this->any()) + ->method('getProducts') + ->willReturn([$this->productId]); + $this->observer = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getEvent']) + ->getMock(); + $this->observer->expects($this->any()) + ->method('getEvent') + ->willReturn($this->event); + $this->request = $this->getMockBuilder(RequestInterface::class) + ->setMethods(['getParam']) + ->getMockForAbstractClass(); + $this->request->expects($this->any()) + ->method('getParam') + ->with('store_id', Store::DEFAULT_STORE_ID) + ->willReturn(Store::DEFAULT_STORE_ID); + + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject( + ProductToWebsiteChangeObserver::class, + [ + 'productUrlRewriteGenerator' => $this->productUrlRewriteGenerator, + 'urlPersist' => $this->urlPersist, + 'productRepository' => $this->productRepository, + 'request' => $this->request + ] + ); + } + + /** + * @param array $urlRewriteGeneratorResult + * @param int $numberDeleteByData + * @param int $productVisibility + * @param int $numberReplace + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException + * @dataProvider executeDataProvider + */ + public function testExecute( + array $urlRewriteGeneratorResult, + int $numberDeleteByData, + int $productVisibility, + int $numberReplace + ) { + $this->productUrlRewriteGenerator->expects($this->any()) + ->method('generate') + ->willReturn($urlRewriteGeneratorResult); + $this->urlPersist->expects($this->exactly($numberDeleteByData)) + ->method('deleteByData') + ->with( + [ + UrlRewrite::ENTITY_ID => $this->productId, + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => Store::DEFAULT_STORE_ID + ] + ); + $this->product->expects($this->any()) + ->method('getVisibility') + ->willReturn($productVisibility); + $this->urlPersist->expects($this->exactly($numberReplace)) + ->method('replace') + ->with($urlRewriteGeneratorResult); + + $this->model->execute($this->observer); + } + + /** + * Data provider for testExecute + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [[], 0, Visibility::VISIBILITY_NOT_VISIBLE, 0], + [['someRewrite'], 1, Visibility::VISIBILITY_NOT_VISIBLE, 0], + [['someRewrite'], 1, Visibility::VISIBILITY_BOTH, 1], + ]; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php new file mode 100644 index 0000000000000..001543da8a2d7 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductUrlKeyAutogeneratorObserverTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; + +/** + * Unit tests for \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver class + */ +class ProductUrlKeyAutogeneratorObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $productUrlPathGenerator; + + /** @var \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver */ + private $productUrlKeyAutogeneratorObserver; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->productUrlPathGenerator = $this->getMockBuilder(ProductUrlPathGenerator::class) + ->disableOriginalConstructor() + ->setMethods(['getUrlKey']) + ->getMock(); + + $this->productUrlKeyAutogeneratorObserver = (new ObjectManagerHelper($this))->getObject( + \Magento\CatalogUrlRewrite\Observer\ProductUrlKeyAutogeneratorObserver::class, + [ + 'productUrlPathGenerator' => $this->productUrlPathGenerator + ] + ); + } + + public function testExecuteWithUrlKey() + { + $urlKey = 'product_url_key'; + + $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods(['setUrlKey']) + ->getMock(); + $product->expects($this->atLeastOnce())->method('setUrlKey')->with($urlKey); + $event = $this->getMockBuilder(\Magento\Framework\Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + $event->expects($this->atLeastOnce())->method('getProduct')->willReturn($product); + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $observer */ + $observer = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getEvent']) + ->getMock(); + $observer->expects($this->atLeastOnce())->method('getEvent')->willReturn($event); + $this->productUrlPathGenerator->expects($this->atLeastOnce())->method('getUrlKey')->with($product) + ->willReturn($urlKey); + + $this->productUrlKeyAutogeneratorObserver->execute($observer); + } + + public function testExecuteWithEmptyUrlKey() + { + $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods(['setUrlKey']) + ->getMock(); + $product->expects($this->never())->method('setUrlKey'); + $event = $this->getMockBuilder(\Magento\Framework\Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + $event->expects($this->atLeastOnce())->method('getProduct')->willReturn($product); + /** @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject $observer */ + $observer = $this->getMockBuilder(\Magento\Framework\Event\Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getEvent']) + ->getMock(); + $observer->expects($this->atLeastOnce())->method('getEvent')->willReturn($event); + $this->productUrlPathGenerator->expects($this->atLeastOnce())->method('getUrlKey')->with($product) + ->willReturn(null); + + $this->productUrlKeyAutogeneratorObserver->execute($observer); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php new file mode 100644 index 0000000000000..9e611039e97cc --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Webapi\Controller\Rest\InputParamsResolver; +use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver as InputParamsResolverPlugin; +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Catalog\Model\Product; +use Magento\Webapi\Controller\Rest\Router\Route; +use Magento\Catalog\Api\ProductRepositoryInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Unit test for InputParamsResolver plugin + */ +class InputParamsResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var string + */ + private $saveRewritesHistory; + + /** + * @var array + */ + private $requestBodyParams; + + /** + * @var array + */ + private $result; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var InputParamsResolver|MockObject + */ + private $subject; + + /** + * @var RestRequest|MockObject + */ + private $request; + + /** + * @var Product|MockObject + */ + private $product; + + /** + * @var Route|MockObject + */ + private $route; + + /** + * @var InputParamsResolverPlugin + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->saveRewritesHistory = 'save_rewrites_history'; + $this->requestBodyParams = [ + 'product' => [ + 'sku' => 'test', + 'custom_attributes' => [ + [ + 'attribute_code' => $this->saveRewritesHistory, + 'value' => 1 + ] + ] + ] + ]; + + $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); + $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); + $this->request->method('getBodyParams') + ->willReturn($this->requestBodyParams); + $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); + $this->subject->method('getRoute') + ->willReturn($this->route); + $this->product = $this->createPartialMock(Product::class, ['setData']); + + $this->result = [false, $this->product, 'test']; + + $this->objectManager = new ObjectManager($this); + $this->plugin = $this->objectManager->getObject( + InputParamsResolverPlugin::class, + [ + 'request' => $this->request + ] + ); + } + + public function testAfterResolve() + { + $this->route->method('getServiceClass') + ->willReturn(ProductRepositoryInterface::class); + $this->route->method('getServiceMethod') + ->willReturn('save'); + $this->product->expects($this->once()) + ->method('setData') + ->with($this->saveRewritesHistory, true); + + $this->plugin->afterResolve($this->subject, $this->result); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewriteTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewriteTest.php index 763f78ac1fea6..40f7642f35383 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewriteTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewriteTest.php @@ -27,6 +27,9 @@ protected function setUp() ->getMockForAbstractClass(); } + /** + * @return \Magento\Ui\DataProvider\Modifier\ModifierInterface|object + */ protected function createModel() { return $this->objectManager->getObject(ProductUrlRewrite::class, [ diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index 55cbab2077cef..37edd1b59f1a7 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-url-rewrite", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-backend": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-catalog-import-export": "100.2.*", @@ -13,8 +13,11 @@ "magento/framework": "101.0.*", "magento/module-ui": "101.0.*" }, + "suggest": { + "magento/module-webapi": "100.2.*" + }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index 2d421417bfdc0..f6426677e8ce8 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -19,6 +19,7 @@ <type name="Magento\Catalog\Model\ResourceModel\Category"> <plugin name="category_move_plugin" type="Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\Move"/> <plugin name="category_delete_plugin" type="Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\Remove"/> + <plugin name="update_url_path_for_different_stores" type="Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\UpdateUrlPath"/> </type> <type name="Magento\UrlRewrite\Model\StorageInterface"> <plugin name="storage_plugin" type="Magento\CatalogUrlRewrite\Model\Category\Plugin\Storage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..34b7487725d76 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator" type="Magento\CatalogUrlRewrite\Model\WebapiProductUrlPathGenerator"/> + <type name="Magento\Webapi\Controller\Rest\InputParamsResolver"> + <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver" sortOrder="1" disabled="false" /> + </type> +</config> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_soap/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..ac8beb362f0fb --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_soap/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator" type="Magento\CatalogUrlRewrite\Model\WebapiProductUrlPathGenerator"/> +</config> diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 373b88049c7b5..41f5123f3b772 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -11,6 +11,10 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Widget\Block\BlockInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\View\LayoutFactory; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\ActionInterface; /** * Catalog Products List widget block @@ -94,6 +98,21 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem */ private $json; + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * @var \Magento\Framework\Url\EncoderInterface + */ + private $urlEncoder; + + /** + * @var \Magento\Framework\View\Element\RendererList + */ + private $rendererListBlock; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory @@ -104,6 +123,10 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem * @param \Magento\Widget\Helper\Conditions $conditionsHelper * @param array $data * @param Json|null $json + * @param LayoutFactory|null $layoutFactory + * @param \Magento\Framework\Url\EncoderInterface|null $urlEncoder + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Catalog\Block\Product\Context $context, @@ -114,7 +137,9 @@ public function __construct( \Magento\CatalogWidget\Model\Rule $rule, \Magento\Widget\Helper\Conditions $conditionsHelper, array $data = [], - Json $json = null + Json $json = null, + LayoutFactory $layoutFactory = null, + EncoderInterface $urlEncoder = null ) { $this->productCollectionFactory = $productCollectionFactory; $this->catalogProductVisibility = $catalogProductVisibility; @@ -123,6 +148,8 @@ public function __construct( $this->rule = $rule; $this->conditionsHelper = $conditionsHelper; $this->json = $json ?: ObjectManager::getInstance()->get(Json::class); + $this->layoutFactory = $layoutFactory ?: ObjectManager::getInstance()->get(LayoutFactory::class); + $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance()->get(EncoderInterface::class); parent::__construct( $context, $data @@ -151,6 +178,7 @@ protected function _construct() * Get key pieces for caching block content * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getCacheKeyInfo() { @@ -160,14 +188,16 @@ public function getCacheKeyInfo() return [ 'CATALOG_PRODUCTS_LIST_WIDGET', - $this->getPriceCurrency()->getCurrencySymbol(), + $this->getPriceCurrency()->getCurrency()->getCode(), $this->_storeManager->getStore()->getId(), $this->_design->getDesignTheme()->getId(), $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP), - intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), + (int)$this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->getProductsPerPage(), $conditions, - $this->json->serialize($this->getRequest()->getParams()) + $this->json->serialize($this->getRequest()->getParams()), + $this->getTemplate(), + $this->getTitle() ]; } @@ -194,7 +224,7 @@ public function getProductPriceHtml( ? $arguments['display_minimal_price'] : true; - /** @var \Magento\Framework\Pricing\Render $priceRender */ + /** @var \Magento\Framework\Pricing\Render $priceRender */ $priceRender = $this->getLayout()->getBlock('product.price.render.default'); $price = ''; @@ -208,6 +238,43 @@ public function getProductPriceHtml( return $price; } + /** + * @inheritdoc + */ + protected function getDetailsRendererList() + { + if (empty($this->rendererListBlock)) { + /** @var $layout \Magento\Framework\View\LayoutInterface */ + $layout = $this->layoutFactory->create(['cacheable' => false]); + $layout->getUpdate()->addHandle('catalog_widget_product_list')->load(); + $layout->generateXml(); + $layout->generateElements(); + + $this->rendererListBlock = $layout->getBlock('category.product.type.widget.details.renderers'); + } + + return $this->rendererListBlock; + } + + /** + * Get post parameters. + * + * @param Product $product + * @return array + */ + public function getAddToCartPostParams(Product $product): array + { + $url = $this->getAddToCartUrl($product); + + return [ + 'action' => $url, + 'data' => [ + 'product' => $product->getEntityId(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($url), + ] + ]; + } + /** * {@inheritdoc} */ @@ -221,6 +288,7 @@ protected function _beforeToHtml() * Prepare and return product collection * * @return \Magento\Catalog\Model\ResourceModel\Product\Collection + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function createCollection() { @@ -336,7 +404,7 @@ public function getPagerHtml() if (!$this->pager) { $this->pager = $this->getLayout()->createBlock( \Magento\Catalog\Block\Product\Widget\Html\Pager::class, - 'widget.products.list.pager' + $this->getWidgetPagerBlockName() ); $this->pager->setUseContainer(true) @@ -396,4 +464,37 @@ private function getPriceCurrency() } return $this->priceCurrency; } + + /** + * @inheritdoc + */ + public function getAddToCartUrl($product, $additional = []) + { + $requestingPageUrl = $this->getRequest()->getParam('requesting_page_url'); + + if (!empty($requestingPageUrl)) { + $additional['useUencPlaceholder'] = true; + $url = parent::getAddToCartUrl($product, $additional); + return str_replace('%25uenc%25', $this->urlEncoder->encode($requestingPageUrl), $url); + } + + return parent::getAddToCartUrl($product, $additional); + } + + /** + * Get widget block name + * + * @return string + */ + private function getWidgetPagerBlockName() + { + $pageName = $this->getData('page_var_name'); + $pagerBlockName = 'widget.products.list.pager'; + + if (!$pageName) { + return $pagerBlockName; + } + + return $pagerBlockName . '.' . $pageName; + } } diff --git a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php index 04ae7c6a2d750..9f7962301cab4 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php +++ b/app/code/Magento/CatalogWidget/Block/Product/Widget/Conditions.php @@ -51,7 +51,7 @@ class Conditions extends Template implements RendererInterface /** * @var string */ - protected $_template = 'product/widget/conditions.phtml'; + protected $_template = 'Magento_CatalogWidget::product/widget/conditions.phtml'; /** * @param \Magento\Framework\Data\Form\Element\Factory $elementFactory diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index 9c679b8bfe9b0..f1c91d31231cf 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -9,6 +9,7 @@ */ namespace Magento\CatalogWidget\Model\Rule\Condition; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ProductCategoryList; /** @@ -82,12 +83,17 @@ public function __construct( public function loadAttributeOptions() { $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode(); + $productAttributes = array_filter( + $productAttributes, + function ($attribute) { + return $attribute->getFrontendLabel() && + $attribute->getFrontendInput() !== 'text' && + $attribute->getAttributeCode() !== ProductInterface::STATUS; + } + ); $attributes = []; foreach ($productAttributes as $attribute) { - if (!$attribute->getFrontendLabel() || $attribute->getFrontendInput() == 'text') { - continue; - } $attributes[$attribute->getAttributeCode()] = $attribute->getFrontendLabel(); } @@ -119,8 +125,21 @@ public function addToCollection($collection) $attribute = $this->getAttributeObject(); if ($collection->isEnabledFlat()) { - $alias = array_keys($collection->getSelect()->getPart('from'))[0]; - $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.' . $attribute->getAttributeCode(); + if ($this->isEnabledInFlat($attribute)) { + $alias = array_keys($collection->getSelect()->getPart('from'))[0]; + $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.' . $attribute->getAttributeCode(); + } else { + $alias = 'at_' . $attribute->getAttributeCode(); + if (!in_array($alias, array_keys($collection->getSelect()->getPart('from')))) { + $collection->joinAttribute( + $attribute->getAttributeCode(), + 'catalog_product/'.$attribute->getAttributeCode(), + 'entity_id' + ); + } + + $this->joinedAttributes[$attribute->getAttributeCode()] = $alias . '.value'; + } return $this; } @@ -150,8 +169,6 @@ protected function addGlobalAttribute( \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute, \Magento\Catalog\Model\ResourceModel\Product\Collection $collection ) { - $storeId = $this->storeManager->getStore()->getId(); - switch ($attribute->getBackendType()) { case 'decimal': case 'datetime': @@ -160,10 +177,15 @@ protected function addGlobalAttribute( $collection->addAttributeToSelect($attribute->getAttributeCode(), 'inner'); break; default: - $alias = 'at_' . md5($this->getId()) . $attribute->getAttributeCode(); + $alias = 'at_' . sha1($this->getId()) . $attribute->getAttributeCode(); + + $connection = $this->_productResource->getConnection(); + $storeId = $connection->getIfNullSql($alias . '.store_id', $this->storeManager->getStore()->getId()); + $linkField = $attribute->getEntity()->getLinkField(); + $collection->getSelect()->join( - [$alias => $collection->getTable('catalog_product_index_eav')], - "($alias.entity_id = e.entity_id) AND ($alias.store_id = $storeId)" . + [$alias => $collection->getTable('catalog_product_entity_varchar')], + "($alias.$linkField = e.$linkField) AND ($alias.store_id = $storeId)" . " AND ($alias.attribute_id = {$attribute->getId()})", [] ); @@ -236,4 +258,15 @@ public function collectValidatedAttributes($productCollection) { return $this->addToCollection($productCollection); } + + /** + * @param \Magento\Framework\DataObject $attribute + * @return bool + */ + private function isEnabledInFlat(\Magento\Framework\DataObject $attribute): bool + { + return $attribute->getData('backend_type') === 'static' + || (int) $attribute->getData('used_in_product_listing') === 1 + || (int) $attribute->getData('used_for_sort_by') === 1; + } } diff --git a/app/code/Magento/CatalogWidget/Setup/UpgradeData.php b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php new file mode 100644 index 0000000000000..5ebdbe2390d51 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogWidget\Setup; + +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\CatalogWidget\Model\Rule\Condition\Product as ConditionProduct; +use Magento\Framework\Serialize\Serializer\Json as Serializer; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\UpgradeDataInterface; + +/** + * Upgrade data for CatalogWidget module. + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * @var Serializer + */ + private $serializer; + + /** + * @param Serializer $serializer + */ + public function __construct( + Serializer $serializer + ) { + $this->serializer = $serializer; + } + + /** + * @inheritdoc + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + if (version_compare($context->getVersion(), '2.0.1', '<')) { + $this->replaceIsWithIsOneOf($setup); + } + } + + /** + * Replace 'is' condition with 'is one of' in database. + * + * If 'is' product list condition is used with multiple skus it should be replaced by 'is one of' condition. + * + * @param ModuleDataSetupInterface $setup + */ + private function replaceIsWithIsOneOf(ModuleDataSetupInterface $setup) + { + $tableName = $setup->getTable('widget_instance'); + $connection = $setup->getConnection(); + $select = $connection->select() + ->from( + $tableName, + [ + 'instance_id', + 'widget_parameters', + ] + )->where('instance_type = ? ', ProductsList::class); + + $result = $setup->getConnection()->fetchAll($select); + + if ($result) { + $updatedData = $this->updateWidgetData($result); + + $connection->insertOnDuplicate( + $tableName, + $updatedData + ); + } + } + + /** + * Replace 'is' condition with 'is one of' in widget parameters. + * + * @param array $result + * @return array + */ + private function updateWidgetData(array $result): array + { + return array_map( + function ($widgetData) { + $widgetParameters = $this->serializer->unserialize($widgetData['widget_parameters']); + foreach ($widgetParameters['conditions'] as &$condition) { + if (ConditionProduct::class === $condition['type'] && + 'sku' === $condition['attribute'] && + '==' === $condition['operator']) { + $condition['operator'] = '()'; + } + } + $widgetData['widget_parameters'] = $this->serializer->serialize($widgetParameters); + + return $widgetData; + }, + $result + ); + } +} diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml new file mode 100644 index 0000000000000..3631e56fcdcf0 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCmsBlockWithCatalogProductListWidget"> + <arguments> + <argument name="conditionAttribute" type="string"/> + <argument name="conditionOperator" type="string"/> + <argument name="conditionValue" type="string"/> + </arguments> + <fillField selector="{{AdminCmsBlockContentSection.content}}" userInput="" stepKey="makeContentFieldEmpty"/> + + <click selector="{{AdminCmsBlockContentSection.insertWidgetButton}}" stepKey="clickInsertWidgetButton"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" time="10" stepKey="waitForInsertWidgetFrame"/> + + <selectOption selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" userInput="Catalog Products List" stepKey="selectCatalogProductListOption"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="waitForConditionsElementBecomeAvailable"/> + + <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickToAddCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCondition}}" stepKey="waitForSelectBoxOpened"/> + + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{conditionAttribute}}" stepKey="selectConditionsSelectBox"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="seeConditionsAdded"/> + + <click selector="{{AdminNewWidgetSection.conditionOperator}}" stepKey="clickToConditionIs"/> + <selectOption selector="{{AdminNewWidgetSection.conditionOperatorSelect('1')}}" userInput="{{conditionOperator}}" stepKey="selectOperator"/> + + <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickAddConditionItem"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.setRuleParameter}}" stepKey="waitForConditionFieldOpened"/> + + <fillField selector="{{AdminNewWidgetSection.setRuleParameter}}" userInput="{{conditionValue}}" stepKey="setConditionValue"/> + <click selector="{{AdminNewWidgetSection.insertWidget}}" stepKey="clickInsertWidget"/> + + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForInsertWidgetSaved"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see userInput="You saved the block." stepKey="seeSavedBlockMsgOnForm"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml new file mode 100644 index 0000000000000..3f29ed5eb021b --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCatalogProductWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateWidgetActionGroup. Adds Product Attributes/Buttons to a Widget. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCategory}}" after="clickWidgetOptions" stepKey="waitForSelectCategoryButtonVisible"/> + <click selector="{{AdminNewWidgetSection.selectCategory}}" stepKey="clickOnSelectCategory"/> + <waitForPageLoad stepKey="waitForCategoryTreeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory('Default Category')}}" stepKey="clickToExpandDefaultCategory"/> + <waitForElementVisible selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="waitForCategoryVisible"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForWidgetPageLoaded"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/LICENSE.txt b/app/code/Magento/CatalogWidget/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CatalogWidget/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 <insert your license name here>" 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/app/code/Magento/CatalogWidget/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CatalogWidget/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CatalogWidget/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 <insert your license name here>" 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/app/code/Magento/CatalogWidget/Test/Mftf/README.md b/app/code/Magento/CatalogWidget/Test/Mftf/README.md new file mode 100644 index 0000000000000..2ba00559524cb --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Catalog Widget Functional Tests + +The Functional Test Module for **Magento Catalog Widget** module. diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml new file mode 100644 index 0000000000000..95cc73ae92d4e --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/AdminCatalogProductListWidgetOperatorsTest.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCatalogProductListWidgetOperatorsTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="MAGETWO-90930: Problems with operator more/less in the 'catalog Products List' widget"/> + <title value="Checking operator more/less in the 'catalog Products List' widget"/> + <description value="Check 'less than', 'equals or greater than', 'equals or less than' operators"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96479"/> + <group value="catalogWidget"/> + <group value="WYSIWYGDisabled"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">50</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + <field key="price">100</field> + </createData> + <createData entity="DefaultCmsBlock" stepKey="createPreReqBlock"/> + <!--User log in on back-end as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidget"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="greater than"/> + <argument name="conditionValue" value="20"/> + </actionGroup> + + <!--Go to Catalog > Categories (choose category where created products)--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandAll"/> + <waitForElementVisible selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="waitForCategoryVisible"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickCategoryLink"/> + + <!--Categories > Content > Add CMS Block: name saved block--> + <waitForElementVisible selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="waitForContentSection"/> + <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.addCMSBlock}}" visible="false" stepKey="openContentSection"/> + + <selectOption selector="{{AdminCategoryContentSection.addCMSBlock}}" userInput="{{DefaultCmsBlock.title}}" stepKey="selectSavedBlock"/> + + <!--Display Settings > Display Mode: Static block only--> + <waitForElementVisible selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" stepKey="waitForDisplaySettingsSection"/> + <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> + <selectOption userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}" stepKey="selectStaticBlockOnlyOption"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCategoryWithProducts"/> + <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage1"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice10"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100"/> + + <!--Open block with widget.--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage2"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetLessThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="less than"/> + <argument name="conditionValue" value="20"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage2"/> + + <!--Check operators Greater than--> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice10"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="dontSeeElementByPrice50"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100"/> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage3"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetEqualsOrGreaterThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="equals or greater than"/> + <argument name="conditionValue" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage3"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice10a"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50a"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100a"/> + + <!--Open block with widget--> + <actionGroup ref="NavigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage4"> + <argument name="cmsBlock" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateCmsBlockWithCatalogProductListWidget" stepKey="adminCreateBlockWithWidgetEqualsOrLessThan"> + <argument name="conditionAttribute" value="Price"/> + <argument name="conditionOperator" value="equals or less than"/> + <argument name="conditionValue" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleCategory.name$$)}}" stepKey="goToStorefrontCategoryPage4"/> + + <!--Check operators Greater than--> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice10b"/> + <seeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50b"/> + <dontSeeElement selector="{{StorefrontWidgetsSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100b"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index e871ed4359d5c..5de8b9d9632fc 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -87,8 +87,8 @@ protected function setUp() { $this->collectionFactory = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor()->getMock(); + ->setMethods(['create']) + ->disableOriginalConstructor()->getMock(); $this->visibility = $this->getMockBuilder(\Magento\Catalog\Model\Product\Visibility::class) ->setMethods(['getVisibleInCatalogIds']) ->disableOriginalConstructor() @@ -144,10 +144,14 @@ public function testGetCacheKeyInfo() $this->productsList->setData('conditions', 'some_serialized_conditions'); $this->productsList->setData('page_var_name', 'page_number'); + $this->productsList->setTemplate('test_template'); + $this->productsList->setData('title', 'test_title'); $this->request->expects($this->once())->method('getParam')->with('page_number')->willReturn(1); $this->request->expects($this->once())->method('getParams')->willReturn('request_params'); - $this->priceCurrency->expects($this->once())->method('getCurrencySymbol')->willReturn('$'); + $currency = $this->createMock(\Magento\Directory\Model\Currency::class); + $currency->expects($this->once())->method('getCode')->willReturn('USD'); + $this->priceCurrency->expects($this->once())->method('getCurrency')->willReturn($currency); $this->serializer->expects($this->any()) ->method('serialize') @@ -157,14 +161,16 @@ public function testGetCacheKeyInfo() $cacheKey = [ 'CATALOG_PRODUCTS_LIST_WIDGET', - '$', + 'USD', 1, 'blank', 'context_group', 1, 5, 'some_serialized_conditions', - json_encode('request_params') + json_encode('request_params'), + 'test_template', + 'test_title' ]; $this->assertEquals($cacheKey, $this->productsList->getCacheKeyInfo()); } @@ -249,9 +255,10 @@ public function testGetPagerHtml() * Test public `createCollection` method and protected `getPageSize` method via `createCollection` * * @param bool $pagerEnable - * @param int $productsCount - * @param int $productsPerPage - * @param int $expectedPageSize + * @param int $productsCount + * @param int $productsPerPage + * @param int $expectedPageSize + * * @dataProvider createCollectionDataProvider */ public function testCreateCollection($pagerEnable, $productsCount, $productsPerPage, $expectedPageSize) @@ -309,6 +316,9 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP $this->assertSame($collection, $this->productsList->createCollection()); } + /** + * @return array + */ public function createCollectionDataProvider() { return [ @@ -380,6 +390,7 @@ public function testGetIdentities() /** * @param $collection + * * @return \PHPUnit_Framework_MockObject_MockObject */ private function getConditionsForCollection($collection) diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php index 09270b6b41fc7..d255a9940ad9f 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -17,6 +17,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $model; + /** + * @var \Magento\Catalog\Model\ResourceModel\Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $productResource; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -33,9 +38,9 @@ protected function setUp() $storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); $storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); - $productResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - $productResource->expects($this->once())->method('loadAllAttributes')->willReturnSelf(); - $productResource->expects($this->once())->method('getAttributesByCode')->willReturn([]); + $this->productResource = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); + $this->productResource->expects($this->once())->method('loadAllAttributes')->willReturnSelf(); + $this->productResource->expects($this->once())->method('getAttributesByCode')->willReturn([]); $productCategoryList = $this->getMockBuilder(\Magento\Catalog\Model\ProductCategoryList::class) ->disableOriginalConstructor() ->getMock(); @@ -45,7 +50,7 @@ protected function setUp() [ 'config' => $eavConfig, 'storeManager' => $storeManager, - 'productResource' => $productResource, + 'productResource' => $this->productResource, 'productCategoryList' => $productCategoryList, 'data' => [ 'rule' => $ruleMock, @@ -67,6 +72,14 @@ public function testAddToCollection() $this->attributeMock->expects($this->once())->method('isScopeGlobal')->willReturn(true); $this->attributeMock->expects($this->once())->method('isScopeGlobal')->willReturn(true); $this->attributeMock->expects($this->once())->method('getBackendType')->willReturn('multiselect'); + + $entityMock = $this->createMock(\Magento\Eav\Model\Entity\AbstractEntity::class); + $entityMock->expects($this->once())->method('getLinkField')->willReturn('entitiy_id'); + $this->attributeMock->expects($this->once())->method('getEntity')->willReturn($entityMock); + $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + + $this->productResource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connection); + $this->model->addToCollection($collectionMock); } diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 44d1d6296eadd..ea21a1b9a6315 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-catalog-widget", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-widget": "101.0.*", "magento/module-backend": "100.2.*", @@ -14,7 +14,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogWidget/etc/module.xml b/app/code/Magento/CatalogWidget/etc/module.xml index 8954f11f954f7..1f2d84bef2d6b 100644 --- a/app/code/Magento/CatalogWidget/etc/module.xml +++ b/app/code/Magento/CatalogWidget/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CatalogWidget" setup_version="2.0.0"> + <module name="Magento_CatalogWidget" setup_version="2.0.1"> <sequence> <module name="Magento_Catalog"/> <module name="Magento_Widget"/> diff --git a/app/code/Magento/CatalogWidget/etc/widget.xml b/app/code/Magento/CatalogWidget/etc/widget.xml index 3d54c314c6622..67589dbe2e151 100644 --- a/app/code/Magento/CatalogWidget/etc/widget.xml +++ b/app/code/Magento/CatalogWidget/etc/widget.xml @@ -33,7 +33,7 @@ <parameter name="template" xsi:type="select" required="true" visible="true"> <label translate="true">Template</label> <options> - <option name="default" value="product/widget/content/grid.phtml" selected="true"> + <option name="default" value="Magento_CatalogWidget::product/widget/content/grid.phtml" selected="true"> <label translate="true">Products Grid Template</label> </option> </options> diff --git a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml index d40cc3dc6d9ba..0e21f9e42c995 100644 --- a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml +++ b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\CatalogWidget\Block\Product\Widget\Conditions $block */ $element = $block->getElement(); diff --git a/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 0000000000000..4fe7af7f34683 --- /dev/null +++ b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.widget.details.renderers"> + <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> + </block> + </body> +</page> diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 201d6ffe4c683..6e9a84543193e 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -3,28 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile +use Magento\Framework\App\Action\Action; /** @var \Magento\CatalogWidget\Block\Product\ProductsList $block */ ?> -<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())): ?> -<?php - $type = 'widget-product-grid'; +<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())) : ?> + <?php + $type = 'widget-product-grid'; - $mode = 'grid'; + $mode = 'grid'; - $image = 'new_products_content_widget_grid'; - $items = $block->getProductCollection()->getItems(); + $image = 'new_products_content_widget_grid'; + $items = $block->getProductCollection()->getItems(); - $showWishlist = true; - $showCompare = true; - $showCart = true; - $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW; - $description = false; -?> + $showWishlist = true; + $showCompare = true; + $showCart = true; + $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW; + $description = false; + ?> <div class="block widget block-products-list <?= /* @noEscape */ $mode ?>"> - <?php if ($block->getTitle()): ?> + <?php if ($block->getTitle()) : ?> <div class="block-title"> <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> </div> @@ -34,9 +33,8 @@ <div class="products-<?= /* @noEscape */ $mode ?> <?= /* @noEscape */ $mode ?>"> <ol class="product-items <?= /* @noEscape */ $type ?>"> <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?php if ($iterator++ != 1): ?></li><?php endif ?> - <li class="product-item"> + <?php foreach ($items as $_item) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> <div class="product-item-info"> <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> <?= $block->getImage($_item, $image)->toHtml() ?> @@ -49,57 +47,63 @@ <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> - <?php - echo $block->getProductPriceHtml($_item, $type); - ?> - - <?php if ($templateType): ?> + <?php if ($templateType) : ?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <?= $block->getProductPriceHtml($_item, $type) ?> + + <?= $block->getProductDetailsHtml($_item) ?> + + <?php if ($showWishlist || $showCompare || $showCart) : ?> + <div class="product-item-inner"> + <div class="product-item-actions"> + <?php if ($showCart) : ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) : ?> + <?php $postParams = $block->getAddToCartPostParams($_item); ?> + <form data-role="tocart-form" + data-product-sku="<?= $block->escapeHtml($_item->getSku()) ?>" + action="<?= $block->escapeUrl($postParams['action']) ?>" + method="post"> + <input type="hidden" name="product" + value="<?= $block->escapeHtmlAttr($postParams['data']['product']) ?>"> + <input type="hidden" + name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" + value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <?= $block->getBlockHtml('formkey') ?> + <button type="submit" + title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + </form> + <?php else : ?> + <?php if ($_item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> + <?php endif; ?> + </div> + <?php endif; ?> + <?php if ($showWishlist || $showCompare) : ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> + <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class);?> + <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> - <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> - <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php index 45e885d0dbd46..589a94243efa5 100644 --- a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php +++ b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php @@ -5,11 +5,13 @@ */ namespace Magento\Checkout\Api\Data; +use Magento\Framework\Api\ExtensibleDataInterface; + /** * Interface PaymentDetailsInterface * @api */ -interface PaymentDetailsInterface +interface PaymentDetailsInterface extends ExtensibleDataInterface { /**#@+ * Constants defined for keys of array, makes typos less likely diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php index 57ca4b7b2e606..95d63bb0eda95 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php @@ -11,6 +11,8 @@ use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\Message\InterpretationStrategyInterface; use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; /** * Shopping cart item render block @@ -91,6 +93,11 @@ class Renderer extends \Magento\Framework\View\Element\Template implements */ private $messageInterpretationStrategy; + /** + * @var ItemResolverInterface + */ + private $itemResolver; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Helper\Product\Configuration $productConfig @@ -102,6 +109,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements * @param \Magento\Framework\Module\Manager $moduleManager * @param InterpretationStrategyInterface $messageInterpretationStrategy * @param array $data + * @param ItemResolverInterface|null $itemResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ @@ -115,7 +123,8 @@ public function __construct( PriceCurrencyInterface $priceCurrency, \Magento\Framework\Module\Manager $moduleManager, InterpretationStrategyInterface $messageInterpretationStrategy, - array $data = [] + array $data = [], + ItemResolverInterface $itemResolver = null ) { $this->priceCurrency = $priceCurrency; $this->imageBuilder = $imageBuilder; @@ -127,6 +136,7 @@ public function __construct( $this->_isScopePrivate = true; $this->moduleManager = $moduleManager; $this->messageInterpretationStrategy = $messageInterpretationStrategy; + $this->itemResolver = $itemResolver ?: ObjectManager::getInstance()->get(ItemResolverInterface::class); } /** @@ -172,7 +182,7 @@ public function getProduct() */ public function getProductForThumbnail() { - return $this->getProduct(); + return $this->itemResolver->getFinalProduct($this->getItem()); } /** diff --git a/app/code/Magento/Checkout/Block/Cart/Shipping.php b/app/code/Magento/Checkout/Block/Cart/Shipping.php index 7b0ab1bc03e5b..c52b7fe18814f 100644 --- a/app/code/Magento/Checkout/Block/Cart/Shipping.php +++ b/app/code/Magento/Checkout/Block/Cart/Shipping.php @@ -74,7 +74,8 @@ public function getJsLayout() foreach ($this->layoutProcessors as $processor) { $this->jsLayout = $processor->process($this->jsLayout); } - return $this->serializer->serialize($this->jsLayout); + + return json_encode($this->jsLayout, JSON_HEX_TAG); } /** @@ -94,6 +95,6 @@ public function getBaseUrl() */ public function getSerializedCheckoutConfig() { - return $this->serializer->serialize($this->getCheckoutConfig()); + return json_encode($this->getCheckoutConfig(), JSON_HEX_TAG); } } diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 5c237eecf0a9f..5e3234e9f4cc8 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -67,7 +67,7 @@ public function __construct( } /** - * Returns minicart config + * Returns minicart config. * * @return array */ @@ -82,7 +82,8 @@ public function getConfig() 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), - 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), + 'storeId' => $this->_storeManager->getStore()->getId(), ]; } @@ -132,6 +133,7 @@ public function getShoppingCartUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getUpdateItemQtyUrl() { @@ -143,6 +145,7 @@ public function getUpdateItemQtyUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getRemoveItemUrl() { diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php index d3d3adbe40f38..375c564f29059 100644 --- a/app/code/Magento/Checkout/Block/Cart/Totals.php +++ b/app/code/Magento/Checkout/Block/Cart/Totals.php @@ -69,7 +69,8 @@ public function getJsLayout() foreach ($this->layoutProcessors as $processor) { $this->jsLayout = $processor->process($this->jsLayout); } - return parent::getJsLayout(); + + return json_encode($this->jsLayout, JSON_HEX_TAG); } /** diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index d93475a4744ca..5a01f524edeb1 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -6,10 +6,14 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Customer\Model\Session; use Magento\Directory\Helper\Data as DirectoryHelper; +/** + * Fields attribute merger. + */ class AttributeMerger { /** @@ -46,6 +50,7 @@ class AttributeMerger 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'email2', 'length' => 'validate-length', @@ -67,7 +72,7 @@ class AttributeMerger private $customerRepository; /** - * @var \Magento\Customer\Api\Data\CustomerInterface + * @var CustomerInterface */ private $customer; @@ -269,6 +274,7 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) { $isFirstLine = $lineIndex === 0; $line = [ + 'label' => __("%1: Line %2", $attributeConfig['label'], $lineIndex + 1), 'component' => 'Magento_Ui/js/form/element/abstract', 'config' => [ // customScope is used to group elements within a single form e.g. they can be validated separately @@ -309,6 +315,8 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi } /** + * Returns default attribute value. + * * @param string $attributeCode * @return null|string */ @@ -346,7 +354,9 @@ protected function getDefaultValue($attributeCode) } /** - * @return \Magento\Customer\Api\Data\CustomerInterface|null + * Returns logged customer. + * + * @return CustomerInterface|null */ protected function getCustomer() { @@ -394,9 +404,9 @@ protected function orderCountryOptions(array $countryOptions) ]]; foreach ($countryOptions as $countryOption) { if (empty($countryOption['value']) || in_array($countryOption['value'], $this->topCountryCodes)) { - array_push($headOptions, $countryOption); + $headOptions[] = $countryOption; } else { - array_push($tailOptions, $countryOption); + $tailOptions[] = $countryOption; } } return array_merge($headOptions, $tailOptions); diff --git a/app/code/Magento/Checkout/Block/Checkout/DirectoryDataProcessor.php b/app/code/Magento/Checkout/Block/Checkout/DirectoryDataProcessor.php index 1d5bb5bb07d81..587dd06d89106 100644 --- a/app/code/Magento/Checkout/Block/Checkout/DirectoryDataProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/DirectoryDataProcessor.php @@ -141,9 +141,9 @@ private function orderCountryOptions(array $countryOptions) ]]; foreach ($countryOptions as $countryOption) { if (empty($countryOption['value']) || in_array($countryOption['value'], $topCountryCodes)) { - array_push($headOptions, $countryOption); + $headOptions[] = $countryOption; } else { - array_push($tailOptions, $countryOption); + $tailOptions[] = $countryOption; } } return array_merge($headOptions, $tailOptions); diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index f47e514948d69..c5d4d68b06225 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -6,8 +6,11 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Checkout\Helper\Data; +use Magento\Customer\Model\AttributeMetadataDataProvider; +use Magento\Customer\Model\Options; use Magento\Framework\App\ObjectManager; use Magento\Store\Api\StoreResolverInterface; +use Magento\Ui\Component\Form\AttributeMapper; /** * Class LayoutProcessor @@ -15,12 +18,12 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcessorInterface { /** - * @var \Magento\Customer\Model\AttributeMetadataDataProvider + * @var AttributeMetadataDataProvider */ private $attributeMetadataDataProvider; /** - * @var \Magento\Ui\Component\Form\AttributeMapper + * @var AttributeMapper */ protected $attributeMapper; @@ -30,7 +33,7 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso protected $merger; /** - * @var \Magento\Customer\Model\Options + * @var Options */ private $options; @@ -50,30 +53,21 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso private $shippingConfig; /** - * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider - * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper + * @param AttributeMetadataDataProvider $attributeMetadataDataProvider + * @param AttributeMapper $attributeMapper * @param AttributeMerger $merger + * @param Options|null $options */ public function __construct( - \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider, - \Magento\Ui\Component\Form\AttributeMapper $attributeMapper, - AttributeMerger $merger + AttributeMetadataDataProvider $attributeMetadataDataProvider, + AttributeMapper $attributeMapper, + AttributeMerger $merger, + Options $options = null ) { $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; $this->attributeMapper = $attributeMapper; $this->merger = $merger; - } - - /** - * @deprecated 100.0.11 - * @return \Magento\Customer\Model\Options - */ - private function getOptions() - { - if (!is_object($this->options)) { - $this->options = ObjectManager::getInstance()->get(\Magento\Customer\Model\Options::class); - } - return $this->options; + $this->options = $options ?? ObjectManager::getInstance()->get(Options::class); } /** @@ -143,8 +137,8 @@ private function convertElementsToSelect($elements, $attributesToConvert) public function process($jsLayout) { $attributesToConvert = [ - 'prefix' => [$this->getOptions(), 'getNamePrefixOptions'], - 'suffix' => [$this->getOptions(), 'getNameSuffixOptions'], + 'prefix' => [$this->options, 'getNamePrefixOptions'], + 'suffix' => [$this->options, 'getNameSuffixOptions'], ]; $elements = $this->getAddressAttributes(); diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index bc3cd43a024a6..e01d5835b4cf0 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -38,7 +38,7 @@ class Onepage extends \Magento\Framework\View\Element\Template protected $layoutProcessors; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var \Magento\Framework\Serialize\SerializerInterface */ private $serializer; @@ -48,8 +48,9 @@ class Onepage extends \Magento\Framework\View\Element\Template * @param \Magento\Checkout\Model\CompositeConfigProvider $configProvider * @param array $layoutProcessors * @param array $data - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @throws \RuntimeException + * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param \Magento\Framework\Serialize\SerializerInterface $serializerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -57,7 +58,8 @@ public function __construct( \Magento\Checkout\Model\CompositeConfigProvider $configProvider, array $layoutProcessors = [], array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + \Magento\Framework\Serialize\SerializerInterface $serializerInterface = null ) { parent::__construct($context, $data); $this->formKey = $formKey; @@ -65,18 +67,19 @@ public function __construct( $this->jsLayout = isset($data['jsLayout']) && is_array($data['jsLayout']) ? $data['jsLayout'] : []; $this->configProvider = $configProvider; $this->layoutProcessors = $layoutProcessors; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializerInterface ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); } /** - * @return string + * @inheritdoc */ public function getJsLayout() { foreach ($this->layoutProcessors as $processor) { $this->jsLayout = $processor->process($this->jsLayout); } + return $this->serializer->serialize($this->jsLayout); } @@ -114,11 +117,13 @@ public function getBaseUrl() } /** + * Retrieve serialized checkout config. + * * @return bool|string * @since 100.2.0 */ public function getSerializedCheckoutConfig() { - return $this->serializer->serialize($this->getCheckoutConfig()); + return $this->serializer->serialize($this->getCheckoutConfig()); } } diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php index 6b3774f7e38f8..b7f6a163845e0 100644 --- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php +++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php @@ -45,7 +45,8 @@ protected function _beforeToHtml() 'container' => $this, 'is_catalog_product' => $this->_isCatalogProduct, 'or_position' => $this->_orPosition, - 'checkout_session' => $this->_checkoutSession + 'checkout_session' => $this->_checkoutSession, + 'is_shopping_cart' => true ] ); return $this; diff --git a/app/code/Magento/Checkout/Block/Registration.php b/app/code/Magento/Checkout/Block/Registration.php index 91ec85c1db0ed..e880230f50a74 100644 --- a/app/code/Magento/Checkout/Block/Registration.php +++ b/app/code/Magento/Checkout/Block/Registration.php @@ -91,7 +91,7 @@ public function getEmailAddress() */ public function getCreateAccountUrl() { - return $this->getUrl('checkout/account/create'); + return $this->getUrl('checkout/account/delegateCreate'); } /** diff --git a/app/code/Magento/Checkout/Controller/Account/Create.php b/app/code/Magento/Checkout/Controller/Account/Create.php index 89fbb1890a821..55b4ae66d01e0 100644 --- a/app/code/Magento/Checkout/Controller/Account/Create.php +++ b/app/code/Magento/Checkout/Controller/Account/Create.php @@ -8,6 +8,10 @@ use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\NoSuchEntityException; +/** + * @deprecated + * @see DelegateCreate + */ class Create extends \Magento\Framework\App\Action\Action { /** diff --git a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php new file mode 100644 index 0000000000000..b532be744edb2 --- /dev/null +++ b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Controller\Account; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Checkout\Model\Session; +use Magento\Sales\Api\OrderCustomerDelegateInterface; + +/** + * Redirect guest customer for registration. + */ +class DelegateCreate extends Action +{ + /** + * @var OrderCustomerDelegateInterface + */ + private $delegateService; + + /** + * @var Session + */ + private $session; + + /** + * @param Context $context + * @param OrderCustomerDelegateInterface $customerDelegation + * @param Session $session + */ + public function __construct( + Context $context, + OrderCustomerDelegateInterface $customerDelegation, + Session $session + ) { + parent::__construct($context); + $this->delegateService = $customerDelegation; + $this->session = $session; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var string|null $orderId */ + $orderId = $this->session->getLastOrderId(); + if (!$orderId) { + return $this->resultRedirectFactory->create()->setPath('/'); + } + + return $this->delegateService->delegateNew((int)$orderId); + } +} diff --git a/app/code/Magento/Checkout/Controller/Cart.php b/app/code/Magento/Checkout/Controller/Cart.php index f6c59562ee942..7258ab9921226 100644 --- a/app/code/Magento/Checkout/Controller/Cart.php +++ b/app/code/Magento/Checkout/Controller/Cart.php @@ -118,12 +118,7 @@ protected function getBackUrl($defaultUrl = null) return $returnUrl; } - $shouldRedirectToCart = $this->_scopeConfig->getValue( - 'checkout/cart/redirect_to_cart', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - - if ($shouldRedirectToCart || $this->getRequest()->getParam('in_cart')) { + if ($this->shouldRedirectToCart() || $this->getRequest()->getParam('in_cart')) { if ($this->getRequest()->getActionName() == 'add' && !$this->getRequest()->getParam('in_cart')) { $this->_checkoutSession->setContinueShoppingUrl($this->_redirect->getRefererUrl()); } @@ -132,4 +127,15 @@ protected function getBackUrl($defaultUrl = null) return $defaultUrl; } + + /** + * @return bool + */ + private function shouldRedirectToCart() + { + return $this->_scopeConfig->isSetFlag( + 'checkout/cart/redirect_to_cart', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php index 8831b92f3ec86..4aebde6b3b0c0 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Add.php +++ b/app/code/Magento/Checkout/Controller/Cart/Add.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -75,11 +74,19 @@ protected function _initProduct() * Add product to shopping cart action * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if (!$this->_formKeyValidator->validate($this->getRequest())) { + $this->messageManager->addErrorMessage( + __('Your session has expired') + ); return $this->resultRedirectFactory->create()->setPath('*/*/'); } @@ -122,11 +129,21 @@ public function execute() if (!$this->_checkoutSession->getNoCartRedirect(true)) { if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $product->getName() - ); - $this->messageManager->addSuccessMessage($message); + if ($this->shouldRedirectToCart()) { + $message = __( + 'You added %1 to your shopping cart.', + $product->getName() + ); + $this->messageManager->addSuccessMessage($message); + } else { + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $product->getName(), + 'cart_url' => $this->getCartUrl(), + ] + ); + } } return $this->goBack(null, $product); } @@ -147,8 +164,7 @@ public function execute() $url = $this->_checkoutSession->getRedirectUrl(true); if (!$url) { - $cartUrl = $this->_objectManager->get(\Magento\Checkout\Helper\Cart::class)->getCartUrl(); - $url = $this->_redirect->getRedirectUrl($cartUrl); + $url = $this->_redirect->getRedirectUrl($this->getCartUrl()); } return $this->goBack($url); @@ -188,4 +204,23 @@ protected function goBack($backUrl = null, $product = null) $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result) ); } + + /** + * @return string + */ + private function getCartUrl() + { + return $this->_url->getUrl('checkout/cart', ['_secure' => true]); + } + + /** + * @return bool + */ + private function shouldRedirectToCart() + { + return $this->_scopeConfig->isSetFlag( + 'checkout/cart/redirect_to_cart', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index 8654bdbde5893..1876d3ca37d94 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php +++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php @@ -41,6 +41,8 @@ public function execute() } } $this->cart->save(); + } else { + $this->messageManager->addErrorMessage(__('Please select at least one product to add to cart')); } return $this->_goBack(); } diff --git a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php index 56215814d2cf6..8be364ee99a85 100644 --- a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php +++ b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php @@ -61,11 +61,19 @@ public function __construct( * Initialize coupon * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if (!$this->_formKeyValidator->validate($this->getRequest())) { + return $this->_goBack(); + } + $couponCode = $this->getRequest()->getParam('remove') == 1 ? '' : trim($this->getRequest()->getParam('coupon_code')); @@ -95,14 +103,14 @@ public function execute() if (!$itemsCount) { if ($isCodeLengthValid && $coupon->getId()) { $this->_checkoutSession->getQuote()->setCouponCode($couponCode)->save(); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __( 'You used coupon code "%1".', $escaper->escapeHtml($couponCode) ) ); } else { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( 'The coupon code "%1" is not valid.', $escaper->escapeHtml($couponCode) @@ -111,14 +119,14 @@ public function execute() } } else { if ($isCodeLengthValid && $coupon->getId() && $couponCode == $cartQuote->getCouponCode()) { - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __( 'You used coupon code "%1".', $escaper->escapeHtml($couponCode) ) ); } else { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( 'The coupon code "%1" is not valid.', $escaper->escapeHtml($couponCode) @@ -127,12 +135,12 @@ public function execute() } } } else { - $this->messageManager->addSuccess(__('You canceled the coupon code.')); + $this->messageManager->addSuccessMessage(__('You canceled the coupon code.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('We cannot apply the coupon code.')); + $this->messageManager->addErrorMessage(__('We cannot apply the coupon code.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } diff --git a/app/code/Magento/Checkout/Controller/Cart/Delete.php b/app/code/Magento/Checkout/Controller/Cart/Delete.php index 5687e0cad0710..98277072fdd99 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Delete.php +++ b/app/code/Magento/Checkout/Controller/Cart/Delete.php @@ -15,14 +15,19 @@ class Delete extends \Magento\Checkout\Controller\Cart */ public function execute() { - if (!$this->_formKeyValidator->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->_formKeyValidator->validate($this->getRequest())) { return $this->resultRedirectFactory->create()->setPath('*/*/'); } $id = (int)$this->getRequest()->getParam('id'); if ($id) { try { - $this->cart->removeItem($id)->save(); + $this->cart->removeItem($id); + // We should set Totals to be recollected once more because of Cart model as usually is loading + // before action executing and in case when triggerRecollect setted as true recollecting will + // executed and the flag will be true already. + $this->cart->getQuote()->setTotalsCollectedFlag(false); + $this->cart->save(); } catch (\Exception $e) { $this->messageManager->addError(__('We can\'t remove the item.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php index 118611263220b..a6eb61169363c 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php @@ -12,11 +12,16 @@ class UpdateItemOptions extends \Magento\Checkout\Controller\Cart * Update product configuration for a cart item * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $id = (int)$this->getRequest()->getParam('id'); $params = $this->getRequest()->getParams(); diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php new file mode 100644 index 0000000000000..ac4a93e6066a4 --- /dev/null +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Controller\Cart; + +use Magento\Checkout\Model\Cart\RequestQuantityProcessor; +use Magento\Framework\App\Action\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; +use Magento\Quote\Model\Quote\Item; +use Psr\Log\LoggerInterface; + +class UpdateItemQty extends \Magento\Framework\App\Action\Action +{ + /** + * @var RequestQuantityProcessor + */ + private $quantityProcessor; + + /** + * @var FormKeyValidator + */ + private $formKeyValidator; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @var Json + */ + private $json; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context, + * @param RequestQuantityProcessor $quantityProcessor + * @param FormKeyValidator $formKeyValidator + * @param CheckoutSession $checkoutSession + * @param Json $json + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + RequestQuantityProcessor $quantityProcessor, + FormKeyValidator $formKeyValidator, + CheckoutSession $checkoutSession, + Json $json, + LoggerInterface $logger + ) { + $this->quantityProcessor = $quantityProcessor; + $this->formKeyValidator = $formKeyValidator; + $this->checkoutSession = $checkoutSession; + $this->json = $json; + $this->logger = $logger; + parent::__construct($context); + } + + /** + * @return void + */ + public function execute() + { + try { + if (!$this->formKeyValidator->validate($this->getRequest())) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + + $cartData = $this->getRequest()->getParam('cart'); + if (!is_array($cartData)) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + + $cartData = $this->quantityProcessor->process($cartData); + $quote = $this->checkoutSession->getQuote(); + + foreach ($cartData as $itemId => $itemInfo) { + $item = $quote->getItemById($itemId); + $qty = isset($itemInfo['qty']) ? (double)$itemInfo['qty'] : 0; + if ($item) { + $this->updateItemQuantity($item, $qty); + } + } + + $this->jsonResponse(); + } catch (LocalizedException $e) { + $this->jsonResponse($e->getMessage()); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $this->jsonResponse('Something went wrong while saving the page. Please refresh the page and try again.'); + } + } + + /** + * Updates quote item quantity. + * + * @param Item $item + * @param float $qty + * @throws LocalizedException + */ + private function updateItemQuantity(Item $item, float $qty) + { + if ($qty > 0) { + $item->setQty($qty); + + if ($item->getHasError()) { + throw new LocalizedException(__($item->getMessage())); + } + } + } + + /** + * JSON response builder. + * + * @param string $error + * @return void + */ + private function jsonResponse(string $error = '') + { + $this->getResponse()->representJson( + $this->json->serialize($this->getResponseData($error)) + ); + } + + /** + * Returns response data. + * + * @param string $error + * @return array + */ + private function getResponseData(string $error = ''): array + { + $response = [ + 'success' => true, + ]; + + if (!empty($error)) { + $response = [ + 'success' => false, + 'error_message' => $error, + ]; + } + + return $response; + } +} diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php index 09fa1bd64f8c6..90335f8fe164f 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php @@ -1,13 +1,49 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Checkout\Controller\Cart; +use Magento\Checkout\Model\Cart\RequestQuantityProcessor; + class UpdatePost extends \Magento\Checkout\Controller\Cart { + /** + * @var RequestQuantityProcessor + */ + private $quantityProcessor; + + /** + * @param \Magento\Framework\App\Action\Context $context + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator + * @param \Magento\Checkout\Model\Cart $cart + * @param RequestQuantityProcessor $quantityProcessor + */ + public function __construct( + \Magento\Framework\App\Action\Context $context, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Checkout\Model\Session $checkoutSession, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator, + \Magento\Checkout\Model\Cart $cart, + RequestQuantityProcessor $quantityProcessor = null + ) { + parent::__construct( + $context, + $scopeConfig, + $checkoutSession, + $storeManager, + $formKeyValidator, + $cart + ); + + $this->quantityProcessor = $quantityProcessor ?: $this->_objectManager->get(RequestQuantityProcessor::class); + } + /** * Empty customer's shopping cart * @@ -34,20 +70,10 @@ protected function _updateShoppingCart() try { $cartData = $this->getRequest()->getParam('cart'); if (is_array($cartData)) { - $filter = new \Zend_Filter_LocalizedToNormalized( - ['locale' => $this->_objectManager->get( - \Magento\Framework\Locale\ResolverInterface::class - )->getLocale()] - ); - foreach ($cartData as $index => $data) { - if (isset($data['qty'])) { - $cartData[$index]['qty'] = $filter->filter(trim($data['qty'])); - } - } if (!$this->cart->getCustomerSession()->getCustomerId() && $this->cart->getQuote()->getCustomerId()) { $this->cart->getQuote()->setCustomerId(null); } - + $cartData = $this->quantityProcessor->process($cartData); $cartData = $this->cart->suggestItemsQty($cartData); $this->cart->updateItems($cartData)->save(); } diff --git a/app/code/Magento/Checkout/Controller/Index/Index.php b/app/code/Magento/Checkout/Controller/Index/Index.php index 0a5b7f190e3d3..0902782b72d83 100644 --- a/app/code/Magento/Checkout/Controller/Index/Index.php +++ b/app/code/Magento/Checkout/Controller/Index/Index.php @@ -32,11 +32,35 @@ public function execute() return $this->resultRedirectFactory->create()->setPath('checkout/cart'); } - $this->_customerSession->regenerateId(); + // generate session ID only if connection is unsecure according to issues in session_regenerate_id function. + // @see http://php.net/manual/en/function.session-regenerate-id.php + if (!$this->isSecureRequest()) { + $this->_customerSession->regenerateId(); + } $this->_objectManager->get(\Magento\Checkout\Model\Session::class)->setCartWasUpdated(false); $this->getOnepage()->initCheckout(); $resultPage = $this->resultPageFactory->create(); $resultPage->getConfig()->getTitle()->set(__('Checkout')); return $resultPage; } + + /** + * Checks if current request uses SSL and referer also is secure. + * + * @return bool + */ + private function isSecureRequest(): bool + { + $request = $this->getRequest(); + + $referrer = $request->getHeader('referer'); + $secure = false; + + if ($referrer) { + $scheme = parse_url($referrer, PHP_URL_SCHEME); + $secure = $scheme === 'https'; + } + + return $secure && $request->isSecure(); + } } diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php index c84aec336a589..a17fa44776555 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php @@ -58,7 +58,7 @@ public function __construct( */ public function execute() { - if (!$this->getFormKeyValidator()->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->getFormKeyValidator()->validate($this->getRequest())) { return $this->resultRedirectFactory->create()->setPath('*/cart/'); } $itemId = (int)$this->getRequest()->getParam('item_id'); diff --git a/app/code/Magento/Checkout/Controller/Sidebar/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Sidebar/UpdateItemQty.php index e9a968681bf3d..3ca511cce6f4e 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/UpdateItemQty.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/UpdateItemQty.php @@ -50,12 +50,12 @@ public function __construct( } /** - * @return $this + * @inheritdoc */ public function execute() { $itemId = (int)$this->getRequest()->getParam('item_id'); - $itemQty = (int)$this->getRequest()->getParam('item_qty'); + $itemQty = $this->getRequest()->getParam('item_qty'); try { $this->sidebar->checkQuoteItem($itemId); diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index ddb077462ef10..9154e9c99478e 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -82,7 +82,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -98,7 +98,8 @@ public function getSectionData() 'items' => $this->getRecentItems(), 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), - 'website_id' => $this->getQuote()->getStore()->getWebsiteId() + 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), + 'storeId' => $this->getQuote()->getStore()->getStoreId(), ]; } @@ -158,11 +159,10 @@ protected function getRecentItems() : $item->getProduct(); $products = $this->catalogUrl->getRewriteByProductStore([$product->getId() => $item->getStoreId()]); - if (!isset($products[$product->getId()])) { - continue; + if (isset($products[$product->getId()])) { + $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); + $item->getProduct()->setUrlDataObject($urlDataObject); } - $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); - $item->getProduct()->setUrlDataObject($urlDataObject); } $items[] = $this->itemPoolInterface->getItemData($item); } diff --git a/app/code/Magento/Checkout/CustomerData/DefaultItem.php b/app/code/Magento/Checkout/CustomerData/DefaultItem.php index 6e917366c9cd2..21580d1275d0c 100644 --- a/app/code/Magento/Checkout/CustomerData/DefaultItem.php +++ b/app/code/Magento/Checkout/CustomerData/DefaultItem.php @@ -6,6 +6,9 @@ namespace Magento\Checkout\CustomerData; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; + /** * Default item */ @@ -36,12 +39,24 @@ class DefaultItem extends AbstractItem */ protected $checkoutHelper; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + + /** + * @var ItemResolverInterface + */ + private $itemResolver; + /** * @param \Magento\Catalog\Helper\Image $imageHelper * @param \Magento\Msrp\Helper\Data $msrpHelper * @param \Magento\Framework\UrlInterface $urlBuilder * @param \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool * @param \Magento\Checkout\Helper\Data $checkoutHelper + * @param \Magento\Framework\Escaper|null $escaper + * @param ItemResolverInterface|null $itemResolver * @codeCoverageIgnore */ public function __construct( @@ -49,13 +64,17 @@ public function __construct( \Magento\Msrp\Helper\Data $msrpHelper, \Magento\Framework\UrlInterface $urlBuilder, \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool, - \Magento\Checkout\Helper\Data $checkoutHelper + \Magento\Checkout\Helper\Data $checkoutHelper, + \Magento\Framework\Escaper $escaper = null, + ItemResolverInterface $itemResolver = null ) { $this->configurationPool = $configurationPool; $this->imageHelper = $imageHelper; $this->msrpHelper = $msrpHelper; $this->urlBuilder = $urlBuilder; $this->checkoutHelper = $checkoutHelper; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); + $this->itemResolver = $itemResolver ?: ObjectManager::getInstance()->get(ItemResolverInterface::class); } /** @@ -64,6 +83,8 @@ public function __construct( protected function doGetItemData() { $imageHelper = $this->imageHelper->init($this->getProductForThumbnail(), 'mini_cart_product_thumbnail'); + $productName = $this->escaper->escapeHtml($this->item->getProduct()->getName()); + return [ 'options' => $this->getOptionList(), 'qty' => $this->item->getQty() * 1, @@ -71,7 +92,7 @@ protected function doGetItemData() 'configure_url' => $this->getConfigureUrl(), 'is_visible_in_site_visibility' => $this->item->getProduct()->isVisibleInSiteVisibility(), 'product_id' => $this->item->getProduct()->getId(), - 'product_name' => $this->item->getProduct()->getName(), + 'product_name' => $productName, 'product_sku' => $this->item->getProduct()->getSku(), 'product_url' => $this->getProductUrl(), 'product_has_url' => $this->hasProductUrl(), @@ -105,7 +126,7 @@ protected function getOptionList() */ protected function getProductForThumbnail() { - return $this->getProduct(); + return $this->itemResolver->getFinalProduct($this->item); } /** diff --git a/app/code/Magento/Checkout/Helper/Data.php b/app/code/Magento/Checkout/Helper/Data.php index b3c2e17e5d678..497137b3f7d0e 100644 --- a/app/code/Magento/Checkout/Helper/Data.php +++ b/app/code/Magento/Checkout/Helper/Data.php @@ -9,6 +9,7 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Store\Model\ScopeInterface; +use Magento\Sales\Api\PaymentFailuresInterface; /** * Checkout default helper @@ -20,6 +21,9 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper { const XML_PATH_GUEST_CHECKOUT = 'checkout/options/guest_checkout'; + /** + * @deprecated + */ const XML_PATH_CUSTOMER_MUST_BE_LOGGED = 'checkout/options/customer_must_be_logged'; /** @@ -52,6 +56,11 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ protected $priceCurrency; + /** + * @var PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -60,6 +69,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param PriceCurrencyInterface $priceCurrency + * @param PaymentFailuresInterface|null $paymentFailures * @codeCoverageIgnore */ public function __construct( @@ -69,7 +79,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, - PriceCurrencyInterface $priceCurrency + PriceCurrencyInterface $priceCurrency, + PaymentFailuresInterface $paymentFailures = null ) { $this->_storeManager = $storeManager; $this->_checkoutSession = $checkoutSession; @@ -77,6 +88,8 @@ public function __construct( $this->_transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->priceCurrency = $priceCurrency; + $this->paymentFailures = $paymentFailures ? : \Magento\Framework\App\ObjectManager::getInstance() + ->get(PaymentFailuresInterface::class); parent::__construct($context); } @@ -154,7 +167,7 @@ public function getPriceInclTax($item) } $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); $taxAmount = $item->getTaxAmount() + $item->getDiscountTaxCompensation(); - $price = floatval($qty) ? ($item->getRowTotal() + $taxAmount) / $qty : 0; + $price = (float)$qty ? ($item->getRowTotal() + $taxAmount) / $qty : 0; return $this->priceCurrency->round($price); } @@ -181,7 +194,7 @@ public function getBasePriceInclTax($item) { $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); $taxAmount = $item->getBaseTaxAmount() + $item->getBaseDiscountTaxCompensation(); - $price = floatval($qty) ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; + $price = (float)$qty ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; return $this->priceCurrency->round($price); } @@ -202,126 +215,13 @@ public function getBaseSubtotalInclTax($item) * @param string $message * @param string $checkoutType * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function sendPaymentFailedEmail($checkout, $message, $checkoutType = 'onepage') - { - $this->inlineTranslation->suspend(); - - $template = $this->scopeConfig->getValue( - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - - $copyTo = $this->_getEmails('checkout/payment_failed/copy_to', $checkout->getStoreId()); - $copyMethod = $this->scopeConfig->getValue( - 'checkout/payment_failed/copy_method', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $bcc = []; - if ($copyTo && $copyMethod == 'bcc') { - $bcc = $copyTo; - } - - $_receiver = $this->scopeConfig->getValue( - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $sendTo = [ - [ - 'email' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - 'name' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - ], - ]; - - if ($copyTo && $copyMethod == 'copy') { - foreach ($copyTo as $email) { - $sendTo[] = ['email' => $email, 'name' => null]; - } - } - $shippingMethod = ''; - if ($shippingInfo = $checkout->getShippingAddress()->getShippingMethod()) { - $data = explode('_', $shippingInfo); - $shippingMethod = $data[0]; - } - - $paymentMethod = ''; - if ($paymentInfo = $checkout->getPayment()) { - $paymentMethod = $paymentInfo->getMethod(); - } - - $items = ''; - foreach ($checkout->getAllVisibleItems() as $_item) { - /* @var $_item \Magento\Quote\Model\Quote\Item */ - $items .= - $_item->getProduct()->getName() . ' x ' . $_item->getQty() . ' ' . $checkout->getStoreCurrencyCode() - . ' ' . $_item->getProduct()->getFinalPrice( - $_item->getQty() - ) . "\n"; - } - $total = $checkout->getStoreCurrencyCode() . ' ' . $checkout->getGrandTotal(); - - foreach ($sendTo as $recipient) { - $transport = $this->_transportBuilder->setTemplateIdentifier( - $template - )->setTemplateOptions( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => Store::DEFAULT_STORE_ID - ] - )->setTemplateVars( - [ - 'reason' => $message, - 'checkoutType' => $checkoutType, - 'dateAndTime' => $this->_localeDate->formatDateTime( - new \DateTime(), - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::MEDIUM - ), - 'customer' => $checkout->getCustomerFirstname() . ' ' . $checkout->getCustomerLastname(), - 'customerEmail' => $checkout->getCustomerEmail(), - 'billingAddress' => $checkout->getBillingAddress(), - 'shippingAddress' => $checkout->getShippingAddress(), - 'shippingMethod' => $this->scopeConfig->getValue( - 'carriers/' . $shippingMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'paymentMethod' => $this->scopeConfig->getValue( - 'payment/' . $paymentMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'items' => nl2br($items), - 'total' => $total, - ] - )->setFrom( - $this->scopeConfig->getValue( - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ) - )->addTo( - $recipient['email'], - $recipient['name'] - )->addBcc( - $bcc - )->getTransport(); - - $transport->sendMessage(); - } - - $this->inlineTranslation->resume(); + public function sendPaymentFailedEmail( + \Magento\Quote\Model\Quote $checkout, + string $message, + string $checkoutType = 'onepage' + ): \Magento\Checkout\Helper\Data { + $this->paymentFailures->handle((int)$checkout->getId(), $message, $checkoutType); return $this; } @@ -393,6 +293,7 @@ public function isContextCheckout() * * @return boolean * @codeCoverageIgnore + * @deprecated */ public function isCustomerMustBeLogged() { diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index be5692a894865..0eb59fc70d92f 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -13,9 +13,11 @@ /** * Shopping cart model + * * @api + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @deprecated 100.1.0 + * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead */ class Cart extends DataObject implements CartInterface { @@ -353,22 +355,10 @@ protected function _getProductRequest($requestInfo) public function addProduct($productInfo, $requestInfo = null) { $product = $this->_getProduct($productInfo); - $request = $this->_getProductRequest($requestInfo); $productId = $product->getId(); if ($productId) { - $stockItem = $this->stockRegistry->getStockItem($productId, $product->getStore()->getWebsiteId()); - $minimumQty = $stockItem->getMinSaleQty(); - //If product quantity is not specified in request and there is set minimal qty for it - if ($minimumQty - && $minimumQty > 0 - && !$request->getQty() - ) { - $request->setQty($minimumQty); - } - } - - if ($productId) { + $request = $this->getQtyRequest($product, $requestInfo); try { $result = $this->getQuote()->addProduct($product, $request); } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -424,8 +414,9 @@ public function addProductsByIds($productIds) } $product = $this->_getProduct($productId); if ($product->getId() && $product->isVisibleInCatalog()) { + $request = $this->getQtyRequest($product); try { - $this->getQuote()->addProduct($product); + $this->getQuote()->addProduct($product, $request); } catch (\Exception $e) { $allAdded = false; } @@ -746,4 +737,26 @@ private function getRequestInfoFilter() } return $this->requestInfoFilter; } + + /** + * Get request quantity + * + * @param Product $product + * @param \Magento\Framework\DataObject|int|array $request + * @return int|DataObject + */ + private function getQtyRequest($product, $request = 0) + { + $request = $this->_getProductRequest($request); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $minimumQty = $stockItem->getMinSaleQty(); + //If product quantity is not specified in request and there is set minimal qty for it + if ($minimumQty + && $minimumQty > 0 + && !$request->getQty() + ) { + $request->setQty($minimumQty); + } + return $request; + } } diff --git a/app/code/Magento/Checkout/Model/Cart/CartInterface.php b/app/code/Magento/Checkout/Model/Cart/CartInterface.php index 2f4b679381740..890e6a5012ea5 100644 --- a/app/code/Magento/Checkout/Model/Cart/CartInterface.php +++ b/app/code/Magento/Checkout/Model/Cart/CartInterface.php @@ -12,7 +12,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> - * @deprecated 100.1.0 + * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead */ interface CartInterface { diff --git a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php index d8d0003d8ca7e..673e8a3b5937b 100644 --- a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php +++ b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php @@ -5,8 +5,10 @@ */ namespace Magento\Checkout\Model\Cart; +use Magento\Checkout\CustomerData\DefaultItem; +use Magento\Framework\App\ObjectManager; + /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api */ class ImageProvider @@ -18,20 +20,29 @@ class ImageProvider /** * @var \Magento\Checkout\CustomerData\ItemPoolInterface + * @deprecated No need for the pool as images are resolved in the default item implementation + * @see \Magento\Checkout\CustomerData\DefaultItem::getProductForThumbnail */ protected $itemPool; + /** + * @var \Magento\Checkout\CustomerData\DefaultItem + */ + private $customerDataItem; + /** * @param \Magento\Quote\Api\CartItemRepositoryInterface $itemRepository * @param \Magento\Checkout\CustomerData\ItemPoolInterface $itemPool - * @codeCoverageIgnore + * @param DefaultItem|null $customerDataItem */ public function __construct( \Magento\Quote\Api\CartItemRepositoryInterface $itemRepository, - \Magento\Checkout\CustomerData\ItemPoolInterface $itemPool + \Magento\Checkout\CustomerData\ItemPoolInterface $itemPool, + \Magento\Checkout\CustomerData\DefaultItem $customerDataItem = null ) { $this->itemRepository = $itemRepository; $this->itemPool = $itemPool; + $this->customerDataItem = $customerDataItem ?: ObjectManager::getInstance()->get(DefaultItem::class); } /** @@ -45,9 +56,10 @@ public function getImages($cartId) $items = $this->itemRepository->getList($cartId); /** @var \Magento\Quote\Model\Quote\Item $cartItem */ foreach ($items as $cartItem) { - $allData = $this->itemPool->getItemData($cartItem); + $allData = $this->customerDataItem->getItemData($cartItem); $itemData[$cartItem->getItemId()] = $allData['product_image']; } + return $itemData; } } diff --git a/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php b/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php new file mode 100644 index 0000000000000..971b35c8f3e3d --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Model\Cart; + +use Magento\Framework\Locale\ResolverInterface; + +class RequestQuantityProcessor +{ + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * RequestQuantityProcessor constructor. + * @param ResolverInterface $localeResolver + */ + public function __construct( + ResolverInterface $localeResolver + ) { + $this->localeResolver = $localeResolver; + } + + /** + * Process cart request data + * + * @param array $cartData + * @return array + */ + public function process(array $cartData): array + { + $filter = new \Zend\I18n\Filter\NumberParse($this->localeResolver->getLocale()); + + foreach ($cartData as $index => $data) { + if (isset($data['qty'])) { + $data['qty'] = is_string($data['qty']) ? trim($data['qty']) : $data['qty']; + $cartData[$index]['qty'] = $filter->filter($data['qty']); + } + } + + return $cartData; + } +} diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index b5727bf8f365e..3adee10e21e8e 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -8,27 +8,40 @@ use Magento\Catalog\Helper\Product\ConfigurationPool; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Model\Context as CustomerContext; use Magento\Customer\Model\Session as CustomerSession; use Magento\Customer\Model\Url as CustomerUrlManager; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Locale\FormatInterface as LocaleFormat; use Magento\Framework\UrlInterface; use Magento\Quote\Api\CartItemRepositoryInterface as QuoteItemRepository; use Magento\Quote\Api\CartTotalRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\ShippingMethodManagementInterface as ShippingMethodManager; use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Store\Model\ScopeInterface; +use Magento\Ui\Component\Form\Element\Multiline; /** + * Default Config Provider. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ class DefaultConfigProvider implements ConfigProviderInterface { + /** + * @var AttributeOptionManagementInterface + */ + private $attributeOptionManager; + /** * @var CheckoutHelper */ @@ -159,6 +172,11 @@ class DefaultConfigProvider implements ConfigProviderInterface */ protected $urlBuilder; + /** + * @var AddressMetadataInterface + */ + private $addressMetadata; + /** * @param CheckoutHelper $checkoutHelper * @param Session $checkoutSession @@ -186,6 +204,8 @@ class DefaultConfigProvider implements ConfigProviderInterface * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement * @param UrlInterface $urlBuilder + * @param AddressMetadataInterface $addressMetadata + * @param AttributeOptionManagementInterface $attributeOptionManager * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -215,7 +235,9 @@ public function __construct( \Magento\Shipping\Model\Config $shippingMethodConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, - UrlInterface $urlBuilder + UrlInterface $urlBuilder, + AddressMetadataInterface $addressMetadata = null, + AttributeOptionManagementInterface $attributeOptionManager = null ) { $this->checkoutHelper = $checkoutHelper; $this->checkoutSession = $checkoutSession; @@ -243,23 +265,41 @@ public function __construct( $this->storeManager = $storeManager; $this->paymentMethodManagement = $paymentMethodManagement; $this->urlBuilder = $urlBuilder; + $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); + $this->attributeOptionManager = $attributeOptionManager ?? + ObjectManager::getInstance()->get(AttributeOptionManagementInterface::class); } /** - * {@inheritdoc} + * Return configuration array. + * + * @return array|mixed + * @throws \Magento\Framework\Exception\LocalizedException */ public function getConfig() { - $quoteId = $this->checkoutSession->getQuote()->getId(); + $quote = $this->checkoutSession->getQuote(); + $quoteId = $quote->getId(); + $email = $quote->getShippingAddress()->getEmail(); + $quoteItemData = $this->getQuoteItemData(); $output['formKey'] = $this->formKey->getFormKey(); $output['customerData'] = $this->getCustomerData(); $output['quoteData'] = $this->getQuoteData(); - $output['quoteItemData'] = $this->getQuoteItemData(); + $output['quoteItemData'] = $quoteItemData; + $output['quoteMessages'] = $this->getQuoteItemsMessages($quoteItemData); $output['isCustomerLoggedIn'] = $this->isCustomerLoggedIn(); $output['selectedShippingMethod'] = $this->getSelectedShippingMethod(); + if ($email && !$this->isCustomerLoggedIn()) { + $shippingAddressFromData = $this->getAddressFromData($quote->getShippingAddress()); + $billingAddressFromData = $this->getAddressFromData($quote->getBillingAddress()); + $output['shippingAddressFromData'] = $shippingAddressFromData; + if ($shippingAddressFromData != $billingAddressFromData) { + $output['billingAddressFromData'] = $billingAddressFromData; + } + $output['validatedEmailValue'] = $email; + } $output['storeCode'] = $this->getStoreCode(); $output['isGuestCheckoutAllowed'] = $this->isGuestCheckoutAllowed(); - $output['isCustomerLoginRequired'] = $this->isCustomerLoginRequired(); $output['registerUrl'] = $this->getRegisterUrl(); $output['checkoutUrl'] = $this->getCheckoutUrl(); $output['defaultSuccessPageUrl'] = $this->getDefaultSuccessPageUrl(); @@ -268,14 +308,15 @@ public function getConfig() $output['staticBaseUrl'] = $this->getStaticBaseUrl(); $output['priceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getQuoteCurrencyCode() + $quote->getQuoteCurrencyCode() ); $output['basePriceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getBaseCurrencyCode() + $quote->getBaseCurrencyCode() ); $output['postCodes'] = $this->postCodesConfig->getPostCodes(); $output['imageData'] = $this->imageProvider->getImages($quoteId); + $output['totalsData'] = $this->getTotalsData(); $output['shippingPolicy'] = [ 'isEnabled' => $this->scopeConfig->isSetFlag( @@ -289,6 +330,10 @@ public function getConfig() ) ) ]; + $output['useQty'] = $this->scopeConfig->isSetFlag( + 'checkout/cart_link/use_qty', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); $output['activeCarriers'] = $this->getActiveCarriers(); $output['originCountryCode'] = $this->getOriginCountryCode(); $output['paymentMethods'] = $this->getPaymentMethods(); @@ -298,7 +343,7 @@ public function getConfig() } /** - * Is autocomplete enabled for storefront + * Is autocomplete enabled for storefront. * * @return string * @codeCoverageIgnore @@ -312,7 +357,7 @@ private function isAutocompleteEnabled() } /** - * Retrieve customer data + * Retrieve customer data. * * @return array */ @@ -324,13 +369,36 @@ private function getCustomerData() $customerData = $customer->__toArray(); foreach ($customer->getAddresses() as $key => $address) { $customerData['addresses'][$key]['inline'] = $this->getCustomerAddressInline($address); + if ($address->getCustomAttributes()) { + $customerData['addresses'][$key]['custom_attributes'] = $this->filterNotVisibleAttributes( + $customerData['addresses'][$key]['custom_attributes'] + ); + } } } return $customerData; } /** - * Set additional customer address data + * Filter not visible on storefront custom attributes. + * + * @param array $attributes + * @return array + */ + private function filterNotVisibleAttributes(array $attributes) + { + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + unset($attributes[$attributeMetadata->getAttributeCode()]); + } + } + + return $this->setLabelsToAttributes($attributes); + } + + /** + * Set additional customer address data. * * @param \Magento\Customer\Api\Data\AddressInterface $address * @return string @@ -345,7 +413,7 @@ private function getCustomerAddressInline($address) } /** - * Retrieve quote data + * Retrieve quote data. * * @return array */ @@ -370,7 +438,7 @@ private function getQuoteData() } /** - * Retrieve quote item data + * Retrieve quote item data. * * @return array */ @@ -387,13 +455,14 @@ private function getQuoteItemData() $quoteItem->getProduct(), 'product_thumbnail_image' )->getUrl(); + $quoteItemData[$index]['message'] = $quoteItem->getMessage(); } } return $quoteItemData; } /** - * Retrieve formatted item options view + * Retrieve formatted item options view. * * @param \Magento\Quote\Api\Data\CartItemInterface $item * @return array @@ -417,7 +486,7 @@ protected function getFormattedOptionValue($item) } /** - * Retrieve customer registration URL + * Retrieve customer registration URL. * * @return string * @codeCoverageIgnore @@ -428,7 +497,7 @@ public function getRegisterUrl() } /** - * Retrieve checkout URL + * Retrieve checkout URL. * * @return string * @codeCoverageIgnore @@ -439,7 +508,7 @@ public function getCheckoutUrl() } /** - * Retrieve checkout URL + * Retrieve checkout URL. * * @return string * @codeCoverageIgnore @@ -450,7 +519,7 @@ public function pageNotFoundUrl() } /** - * Retrieve default success page URL + * Retrieve default success page URL. * * @return string * @codeCoverageIgnore @@ -461,7 +530,7 @@ public function getDefaultSuccessPageUrl() } /** - * Retrieve selected shipping method + * Retrieve selected shipping method. * * @return array|null */ @@ -480,6 +549,39 @@ private function getSelectedShippingMethod() return $shippingMethodData; } + /** + * Create address data appropriate to fill checkout address form. + * + * @param AddressInterface $address + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAddressFromData(AddressInterface $address): array + { + $addressData = []; + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + continue; + } + $attributeCode = $attributeMetadata->getAttributeCode(); + $attributeData = $address->getData($attributeCode); + if ($attributeData) { + if ($attributeMetadata->getFrontendInput() === Multiline::NAME) { + $attributeData = is_array($attributeData) ? $attributeData : explode("\n", $attributeData); + $attributeData = (object)$attributeData; + } + if ($attributeMetadata->isUserDefined()) { + $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES][$attributeCode] = $attributeData; + continue; + } + $addressData[$attributeCode] = $attributeData; + } + } + + return $addressData; + } + /** * Retrieve store code * @@ -492,7 +594,7 @@ private function getStoreCode() } /** - * Check if guest checkout is allowed + * Check if guest checkout is allowed. * * @return bool * @codeCoverageIgnore @@ -503,7 +605,7 @@ private function isGuestCheckoutAllowed() } /** - * Check if customer is logged in + * Check if customer is logged in. * * @return bool * @codeCoverageIgnore @@ -514,18 +616,7 @@ private function isCustomerLoggedIn() } /** - * Check if customer must be logged in to proceed with checkout - * - * @return bool - * @codeCoverageIgnore - */ - private function isCustomerLoginRequired() - { - return $this->checkoutHelper->isCustomerMustBeLogged(); - } - - /** - * Return forgot password URL + * Return forgot password URL. * * @return string * @codeCoverageIgnore @@ -547,7 +638,8 @@ protected function getStaticBaseUrl() } /** - * Return quote totals data + * Return quote totals data. + * * @return array */ private function getTotalsData() @@ -578,7 +670,8 @@ private function getTotalsData() } /** - * Returns active carriers codes + * Returns active carriers codes. + * * @return array */ private function getActiveCarriers() @@ -591,7 +684,8 @@ private function getActiveCarriers() } /** - * Returns origin country code + * Returns origin country code. + * * @return string */ private function getOriginCountryCode() @@ -604,7 +698,8 @@ private function getOriginCountryCode() } /** - * Returns array of payment methods + * Returns array of payment methods. + * * @return array */ private function getPaymentMethods() @@ -621,4 +716,73 @@ private function getPaymentMethods() } return $paymentMethods; } + + /** + * Set Labels to custom Attributes. + * + * @param array $customAttributes + * @return array + */ + private function setLabelsToAttributes(array $customAttributes) : array + { + if (!empty($customAttributes)) { + foreach ($customAttributes as $customAttributeCode => $customAttribute) { + $attributeOptionLabels = $this->getAttributeLabels($customAttribute, $customAttributeCode); + if (!empty($attributeOptionLabels)) { + $customAttributes[$customAttributeCode]['label'] = implode(', ', $attributeOptionLabels); + } + } + } + + return $customAttributes; + } + + /** + * Get Labels by CustomAttribute and CustomAttributeCode. + * + * @param array $customAttribute + * @param string $customAttributeCode + * @return array + */ + private function getAttributeLabels(array $customAttribute, string $customAttributeCode) : array + { + $attributeOptionLabels = []; + + if (!empty($customAttribute['value'])) { + $customAttributeValues = explode(',', $customAttribute['value']); + $attributeOptions = $this->attributeOptionManager->getItems( + \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, + $customAttributeCode + ); + + if (!empty($attributeOptions)) { + foreach ($attributeOptions as $attributeOption) { + $attributeOptionValue = $attributeOption->getValue(); + if (in_array($attributeOptionValue, $customAttributeValues)) { + $attributeOptionLabels[] = $attributeOption->getLabel() ?? $attributeOptionValue; + } + } + } + } + + return $attributeOptionLabels; + } + + /** + * Get notification messages for the quote items + * + * @param array $quoteItemData + * @return array + */ + private function getQuoteItemsMessages(array $quoteItemData): array + { + $quoteItemsMessages = []; + if ($quoteItemData) { + foreach ($quoteItemData as $item) { + $quoteItemsMessages[$item['item_id']] = $item['message']; + } + } + + return $quoteItemsMessages; + } } diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 6779da354faf8..507411a19f965 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -6,10 +6,15 @@ namespace Magento\Checkout\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Model\Quote; /** + * Guest payment information management model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPaymentInformationManagementInterface @@ -50,6 +55,11 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa */ private $logger; + /** + * @var ResourceConnection + */ + private $connectionPull; + /** * @param \Magento\Quote\Api\GuestBillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\GuestPaymentMethodManagementInterface $paymentMethodManagement @@ -57,6 +67,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository + * @param ResourceConnection $connectionPool * @codeCoverageIgnore */ public function __construct( @@ -65,7 +76,8 @@ public function __construct( \Magento\Quote\Api\GuestCartManagementInterface $cartManagement, \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + ResourceConnection $connectionPull = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; @@ -73,10 +85,11 @@ public function __construct( $this->paymentInformationManagement = $paymentInformationManagement; $this->quoteIdMaskFactory = $quoteIdMaskFactory; $this->cartRepository = $cartRepository; + $this->connectionPull = $connectionPull ?: ObjectManager::getInstance()->get(ResourceConnection::class); } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformationAndPlaceOrder( $cartId, @@ -84,26 +97,40 @@ public function savePaymentInformationAndPlaceOrder( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + $salesConnection = $this->connectionPull->getConnection('sales'); + $checkoutConnection = $this->connectionPull->getConnection('checkout'); + $salesConnection->beginTransaction(); + $checkoutConnection->beginTransaction(); + try { - $orderId = $this->cartManagement->placeOrder($cartId); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new CouldNotSaveException( - __($e->getMessage()), - $e - ); + $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + try { + $orderId = $this->cartManagement->placeOrder($cartId); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new CouldNotSaveException( + __($e->getMessage()), + $e + ); + } catch (\Exception $e) { + $this->getLogger()->critical($e); + throw new CouldNotSaveException( + __('An error occurred on the server. Please try to place the order again.'), + $e + ); + } + $salesConnection->commit(); + $checkoutConnection->commit(); } catch (\Exception $e) { - $this->getLogger()->critical($e); - throw new CouldNotSaveException( - __('An error occurred on the server. Please try to place the order again.'), - $e - ); + $salesConnection->rollBack(); + $checkoutConnection->rollBack(); + throw $e; } + return $orderId; } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformation( $cartId, @@ -111,20 +138,26 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + /** @var Quote $quote */ + $quote = $this->cartRepository->getActive($quoteIdMask->getQuoteId()); + if ($billingAddress) { $billingAddress->setEmail($email); - $this->billingAddressManagement->assign($cartId, $billingAddress); + $quote->removeAddress($quote->getBillingAddress()->getId()); + $quote->setBillingAddress($billingAddress); + $quote->setDataChanges(true); } else { - $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); - $this->cartRepository->getActive($quoteIdMask->getQuoteId())->getBillingAddress()->setEmail($email); + $quote->getBillingAddress()->setEmail($email); } + $this->limitShippingCarrier($quote); $this->paymentMethodManagement->set($cartId, $paymentMethod); return true; } /** - * {@inheritDoc} + * @inheritdoc */ public function getPaymentInformation($cartId) { @@ -145,4 +178,21 @@ private function getLogger() } return $this->logger; } + + /** + * Limits shipping rates request by carrier from shipping address. + * + * @param Quote $quote + * + * @return void + * @see \Magento\Shipping\Model\Shipping::collectRates + */ + private function limitShippingCarrier(Quote $quote) + { + $shippingAddress = $quote->getShippingAddress(); + if ($shippingAddress && $shippingAddress->getShippingMethod()) { + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + } + } } diff --git a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php index 12a8838a7e9ed..4defcb16e7b52 100644 --- a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php +++ b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Checkout\Model\Layout; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; /** * Abstract totals processor. @@ -35,12 +37,14 @@ public function __construct( } /** + * Sort total information based on configuration settings. + * * @param array $totals * @return array */ public function sortTotals($totals) { - $configData = $this->scopeConfig->getValue('sales/totals_sort'); + $configData = $this->scopeConfig->getValue('sales/totals_sort', ScopeInterface::SCOPE_STORES); foreach ($totals as $code => &$total) { //convert JS naming style to config naming style $code = str_replace('-', '_', $code); diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 3d6b0aa0cdc12..c2576e2830522 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -109,14 +109,19 @@ public function savePaymentInformation( $quoteRepository = $this->getCartRepository(); /** @var \Magento\Quote\Model\Quote $quote */ $quote = $quoteRepository->getActive($cartId); + $customerId = $quote->getBillingAddress() + ->getCustomerId(); + if (!$billingAddress->getCustomerId() && $customerId) { + //It's necessary to verify the price rules with the customer data + $billingAddress->setCustomerId($customerId); + } $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($billingAddress); $quote->setDataChanges(true); $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { - $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); - $shippingCarrier = array_shift($shippingDataArray); - $shippingAddress->setLimitCarrier($shippingCarrier); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); } } $this->paymentMethodManagement->set($cartId, $paymentMethod); diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 31513d25a9ce1..8c73dee3588f1 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -10,8 +10,11 @@ use Magento\Quote\Model\QuoteIdMaskFactory; /** + * Represents the session data for the checkout process + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Session extends \Magento\Framework\Session\SessionManager { @@ -219,6 +222,15 @@ public function getQuote() $quote = $this->quoteRepository->getActive($this->getQuoteId()); } + $customerId = $this->_customer + ? $this->_customer->getId() + : $this->_customerSession->getCustomerId(); + + if ($quote->getData('customer_id') && $quote->getData('customer_id') !== $customerId) { + $quote = $this->quoteFactory->create(); + throw new \Magento\Framework\Exception\NoSuchEntityException(); + } + /** * If current currency code of quote is not equal current currency code of store, * need recalculate totals of quote. It is possible if customer use currency switcher or @@ -285,6 +297,8 @@ public function getQuote() } /** + * Return the quote's key + * * @return string * @codeCoverageIgnore */ @@ -294,6 +308,8 @@ protected function _getQuoteIdKey() } /** + * Set the current session's quote id + * * @param int $quoteId * @return void * @codeCoverageIgnore @@ -304,6 +320,8 @@ public function setQuoteId($quoteId) } /** + * Return the current quote's ID + * * @return int * @codeCoverageIgnore */ @@ -337,6 +355,11 @@ public function loadCustomerQuote() $this->quoteRepository->save( $customerQuote->merge($this->getQuote())->collectTotals() ); + $newQuote = $this->quoteRepository->get($customerQuote->getId()); + $this->quoteRepository->save( + $newQuote->collectTotals() + ); + $customerQuote = $newQuote; } $this->setQuoteId($customerQuote->getId()); @@ -357,6 +380,8 @@ public function loadCustomerQuote() } /** + * Associate data to a specified step of the checkout process + * * @param string $step * @param array|string $data * @param bool|string|null $value @@ -383,6 +408,8 @@ public function setStepData($step, $data, $value = null) } /** + * Return the data associated to a specified step + * * @param string|null $step * @param string|null $data * @return array|string|bool @@ -406,8 +433,7 @@ public function getStepData($step = null, $data = null) } /** - * Destroy/end a session - * Unset all data associated with object + * Destroy/end a session and unset all data associated with it * * @return $this */ @@ -443,6 +469,8 @@ public function clearHelperData() } /** + * Revert the state of the checkout to the beginning + * * @return $this * @codeCoverageIgnore */ @@ -453,6 +481,8 @@ public function resetCheckout() } /** + * Replace the quote in the session with a specified object + * * @param Quote $quote * @return $this */ @@ -505,7 +535,9 @@ public function restoreQuote() } /** - * @param $isQuoteMasked bool + * Flag whether or not the quote uses a masked quote id + * + * @param bool $isQuoteMasked * @return void * @codeCoverageIgnore */ @@ -515,6 +547,8 @@ protected function setIsQuoteMasked($isQuoteMasked) } /** + * Return if the quote has a masked quote id + * * @return bool|null * @codeCoverageIgnore */ diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index d8142d033f78c..400bd2f7f85e7 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -18,6 +18,8 @@ use Magento\Framework\App\ObjectManager; /** + * Class ShippingInformationManagement + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInformationManagementInterface @@ -49,7 +51,6 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf /** * @var QuoteAddressValidator - * @deprecated 100.2.0 */ protected $addressValidator; @@ -98,8 +99,8 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector - * @param CartExtensionFactory|null $cartExtensionFactory, - * @param ShippingAssignmentFactory|null $shippingAssignmentFactory, + * @param CartExtensionFactory|null $cartExtensionFactory + * @param ShippingAssignmentFactory|null $shippingAssignmentFactory * @param ShippingFactory|null $shippingFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -141,31 +142,35 @@ public function saveAddressInformation( $cartId, \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation ) { - $address = $addressInformation->getShippingAddress(); - $billingAddress = $addressInformation->getBillingAddress(); - $carrierCode = $addressInformation->getShippingCarrierCode(); - $methodCode = $addressInformation->getShippingMethodCode(); - - if (!$address->getCustomerAddressId()) { - $address->setCustomerAddressId(null); - } - - if (!$address->getCountryId()) { - throw new StateException(__('Shipping address is not set')); - } - /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); - $address->setLimitCarrier($carrierCode); - $quote = $this->prepareShippingAssignment($quote, $address, $carrierCode . '_' . $methodCode); $this->validateQuote($quote); - $quote->setIsMultiShipping(false); - if ($billingAddress) { - $quote->setBillingAddress($billingAddress); + $address = $addressInformation->getShippingAddress(); + if (!$address || !$address->getCountryId()) { + throw new StateException(__('Shipping address is not set')); + } + if (!$address->getCustomerAddressId()) { + $address->setCustomerAddressId(null); } try { + $billingAddress = $addressInformation->getBillingAddress(); + if ($billingAddress) { + if (!$billingAddress->getCustomerAddressId()) { + $billingAddress->setCustomerAddressId(null); + } + $this->addressValidator->validateForCart($quote, $billingAddress); + $quote->setBillingAddress($billingAddress); + } + + $this->addressValidator->validateForCart($quote, $address); + $carrierCode = $addressInformation->getShippingCarrierCode(); + $address->setLimitCarrier($carrierCode); + $methodCode = $addressInformation->getShippingMethodCode(); + $quote = $this->prepareShippingAssignment($quote, $address, $carrierCode . '_' . $methodCode); + $quote->setIsMultiShipping(false); + $this->quoteRepository->save($quote); } catch (\Exception $e) { $this->logger->critical($e); @@ -174,7 +179,9 @@ public function saveAddressInformation( $shippingAddress = $quote->getShippingAddress(); - if (!$shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod())) { + if (!$quote->getIsVirtual() + && !$shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()) + ) { throw new NoSuchEntityException( __('Carrier with such method not found: %1, %2', $carrierCode, $methodCode) ); @@ -203,6 +210,8 @@ protected function validateQuote(\Magento\Quote\Model\Quote $quote) } /** + * Prepare shipping assignment. + * * @param CartInterface $quote * @param AddressInterface $address * @param string $method diff --git a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php index d926e33d54113..6bc7965ff5e34 100644 --- a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php +++ b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class SalesQuoteSaveAfterObserver + */ class SalesQuoteSaveAfterObserver implements ObserverInterface { /** @@ -24,15 +27,18 @@ public function __construct(\Magento\Checkout\Model\Session $checkoutSession) } /** + * Assign quote to session + * * @param \Magento\Framework\Event\Observer $observer * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { + /* @var \Magento\Quote\Model\Quote $quote */ $quote = $observer->getEvent()->getQuote(); - /* @var $quote \Magento\Quote\Model\Quote */ + if ($quote->getIsCheckoutCart()) { - $this->checkoutSession->getQuoteId($quote->getId()); + $this->checkoutSession->setQuoteId($quote->getId()); } } } diff --git a/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php new file mode 100644 index 0000000000000..3791b28917e97 --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Model\Quote; + +use Magento\Quote\Model\Quote; + +/** + * Clear quote addresses after all items were removed. + */ +class ResetQuoteAddresses +{ + /** + * @param Quote $quote + * @param Quote $result + * @param mixed $itemId + * + * @return Quote + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRemoveItem(Quote $quote, Quote $result, $itemId): Quote + { + if (empty($result->getAllVisibleItems())) { + foreach ($result->getAllAddresses() as $address) { + $result->removeAddress($address->getId()); + } + } + + return $result; + } +} diff --git a/app/code/Magento/Checkout/Setup/InstallData.php b/app/code/Magento/Checkout/Setup/InstallData.php index 38879e06d65ac..58b118c482307 100644 --- a/app/code/Magento/Checkout/Setup/InstallData.php +++ b/app/code/Magento/Checkout/Setup/InstallData.php @@ -816,7 +816,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $connection->commit(); } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); throw $e; } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml new file mode 100644 index 0000000000000..b8686f93a4039 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Checkout select Check/Money billing method --> + <actionGroup name="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup"> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" dependentSelector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoBillingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterBillingMethodSelection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..858dbfc5e76f3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertAdminEmailValidationMessageOnCheckoutActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminEmailValidationMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)."/> + </arguments> + <waitForElementVisible selector="{{AdminOrderFormAccountSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{AdminOrderFormAccountSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml new file mode 100644 index 0000000000000..69fe27ff07460 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementInvisibleActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontElementInvisibleActionGroup"> + <arguments> + <argument name="selector" type="string"/> + <argument name="userInput" type="string"/> + </arguments> + <dontSee selector="{{selector}}" userInput="{{userInput}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementVisibleActionGroup.xml new file mode 100644 index 0000000000000..b99c4b7f5bf16 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontElementVisibleActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontElementVisibleActionGroup"> + <arguments> + <argument name="selector" type="string"/> + <argument name="userInput" type="string"/> + </arguments> + <see selector="{{selector}}" userInput="{{userInput}}" stepKey="assertElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..0f7e75f062ab4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)."/> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml new file mode 100644 index 0000000000000..f5832bba30e7d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -0,0 +1,143 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Checkout select Check/Money Order payment --> + <actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup"> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{PaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" dependentSelector="{{PaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + </actionGroup> + + <!-- Checkout select Flat Rate shipping method --> + <actionGroup name="CheckoutSelectFlatRateShippingMethodActionGroup"> + <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" visible="true" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> + </actionGroup> + + <!-- Go to checkout from cart --> + <actionGroup name="GoToCheckoutFromCartActionGroup"> + <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertCheckoutCartUrl"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + </actionGroup> + + <!-- Checkout place order --> + <actionGroup name="CheckoutPlaceOrderActionGroup"> + <arguments> + <argument name="orderNumberMessage"/> + <argument name="emailYouMessage"/> + </arguments> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{orderNumberMessage}}" stepKey="seeOrderNumber"/> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{emailYouMessage}}" stepKey="seeEmailYou"/> + </actionGroup> + + <!-- Logged in user checkout filling shipping section --> + <actionGroup name="LoggedInUserCheckoutFillingShippingSectionActionGroup"> + <arguments> + <argument name="customerVar" defaultValue="CustomerEntityOne"/> + <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> + </arguments> + <waitForElementVisible selector="{{CheckoutShippingSection.firstName}}" stepKey="waitForFirstNameFieldAppears" time="30"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementVisible selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForSelectFirstShippingMethodAppears"/> + <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + </actionGroup> + + <!--Verify country options in checkout top destination section--> + <actionGroup name="VerifyTopDestinationsCountry"> + <arguments> + <argument name="country" type="string"/> + <argument name="placeNumber"/> + </arguments> + <waitForElement selector="{{StorefrontCheckoutCartSummarySection.blockSummary}}" stepKey="waitBlockSummaryLoaded"/> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="openShippingDetails"/> + <see selector="{{StorefrontCheckoutCartSummarySection.countryParameterized('placeNumber')}}" userInput="{{country}}" stepKey="seeCountry"/> + </actionGroup> + + <!--Click Place Order button--> + <actionGroup name="ClickPlaceOrderActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> + + <!-- Place order with logged the user --> + <actionGroup name="PlaceOrderWithLoggedUserActionGroup"> + <arguments> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> + <!--First available payment method will be selected if value is not passed for paymentMethod--> + <argument name="paymentMethod" defaultValue="" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <conditionalClick selector="{{CheckoutPaymentSection.checkPaymentMethodByName('paymentMethod')}}" dependentSelector="{{CheckoutPaymentSection.checkPaymentMethodByName('paymentMethod')}}" + visible="true" stepKey="checkPaymentMethodIfExist"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> + + <actionGroup name="CheckoutSelectShippingMethodActionGroup"> + <arguments> + <!-- First available shipping method will be selected if value is not passed for shippingMethod --> + <argument name="shippingMethod" defaultValue="" type="string"/> + </arguments> + <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethod)}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethod)}}" visible="true" stepKey="selectShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPaymentMethod"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + </actionGroup> + + <actionGroup name="AssertStorefrontErrorMessageOnOrderSubmit"> + <arguments> + <argument name="errorMessage" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrderNoWait}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrderNoWait}}" stepKey="clickPlaceOrder"/> + <waitForText selector="{{StorefrontMessagesSection.errorMessage}}" userInput="{{errorMessage}}" time="30" stepKey="seeShippingMethodError"/> + </actionGroup> + + <!-- Check shipping method in checkout --> + <actionGroup name="CheckShippingMethodInCheckoutActionGroup"> + <arguments> + <argument name="shippingMethod"/> + </arguments> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeElement selector="{{CheckoutPaymentSection.isPaymentSection}}" stepKey="assertPaymentSection"/> + <see userInput="{{shippingMethod}}" selector="{{CheckoutPaymentSection.shippingMethodInformation}}" stepKey="assertshippingMethodInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml new file mode 100644 index 0000000000000..c06ff0cb96b58 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Go to checkout from minicart --> + <actionGroup name="GoToCheckoutFromMinicartActionGroup"> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElement selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitMiniCartSectionShow" /> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.goToCheckout}}" time="30" stepKey="waitForGoToCheckoutButtonVisible"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickGoToCheckoutButton"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..7fe9fcb74719d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Guest checkout filling billing section --> + <actionGroup name="GuestCheckoutFillNewBillingAddressActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading3" /> + <fillField selector="{{CheckoutPaymentSection.guestFirstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutPaymentSection.guestLastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutPaymentSection.guestStreet}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutPaymentSection.guestCity}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutPaymentSection.guestRegion}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutPaymentSection.guestPostcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutPaymentSection.guestTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + </actionGroup> + + <actionGroup name="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <arguments> + <argument name="customerAddress"/> + <!-- the classPrefix argument is to specifically select the inputs of the correct form + this is to prevent having 3 action groups doing essentially the same thing --> + <argument name="classPrefix" type="string" defaultValue=""/> + </arguments> + <fillField stepKey="fillFirstName" selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" userInput="{{customerAddress.firstname}}"/> + <fillField stepKey="fillLastName" selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" userInput="{{customerAddress.lastname}}"/> + <fillField stepKey="fillPhoneNumber" selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" userInput="{{customerAddress.telephone}}"/> + <fillField stepKey="fillStreetAddress1" selector="{{classPrefix}} {{CheckoutShippingSection.street}}" userInput="{{customerAddress.street[0]}}"/> + <fillField stepKey="fillCityName" selector="{{classPrefix}} {{CheckoutShippingSection.city}}" userInput="{{customerAddress.city}}"/> + <selectOption stepKey="selectState" selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="{{customerAddress.state}}"/> + <fillField stepKey="fillZip" selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" userInput="{{customerAddress.postcode}}"/> + <selectOption stepKey="selectCounty" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{customerAddress.country_id}}"/> + <waitForPageLoad stepKey="waitForFormUpdate2"/> + </actionGroup> + + <actionGroup name="clearCheckoutAddressPopupFieldsActionGroup"> + <arguments> + <argument name="classPrefix" type="string" defaultValue=""/> + </arguments> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" stepKey="clearFieldLastName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress1"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> + </actionGroup> + + <actionGroup name="GuestCheckoutFillNewBillingAddressWithoutEmailActionGroup" + extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="enterEmail"/> + <selectOption selector="{{CheckoutPaymentSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="selectCounty" /> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..6ea2ccb08d8d3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="GuestCheckoutFillNewShippingAddressActionGroup"> + <arguments> + <argument name="customer" type="entity"/> + <argument name="address" type="entity"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street}}" stepKey="fillStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{address.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}" stepKey="fillZipCode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{address.telephone}}" stepKey="fillPhone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml new file mode 100644 index 0000000000000..19aeeef8e2bc0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Guest checkout filling shipping section --> + <actionGroup name="GuestCheckoutFillingShippingSectionActionGroup"> + <arguments> + <argument name="customerVar" defaultValue="CustomerEntityOne"/> + <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> + </actionGroup> + + <actionGroup name="GuestCheckoutFillingShippingSectionWithoutPaymentsActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.isPaymentSection}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + </actionGroup> + + <actionGroup name="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country}}" after="enterPostcode" stepKey="selectCountry"/> + <remove keyForRemoval="selectRegion"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..c23d860e4660d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Assert That Shipping And Billing Address are the same--> + <actionGroup name="AssertThatShippingAndBillingAddressTheSame"> + <!--Make sure that shipping and billing addresses are different--> + <see userInput="Shipping Address" stepKey="seeShippingAddress"/> + <see userInput="Billing Address" stepKey="seeBillingAddress"/> + <!--Get shipping and billing addresses--> + <grabTextFrom selector="{{StorefrontIdentityOfDefaultBillingAndShippingAddressSection.shippingAddress}}" stepKey="shippingAddr"/> + <grabTextFrom selector="{{StorefrontIdentityOfDefaultBillingAndShippingAddressSection.billingAddress}}" stepKey="billingAddr"/> + <assertEquals actual="$billingAddr" expected="$shippingAddr" stepKey="assert"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml new file mode 100644 index 0000000000000..9c4d16ff500a0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Logged in user checkout add new adress shipping section --> + <actionGroup name="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.addStreet}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.addCity}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.addState}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.addPostcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.addTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <click selector="{{CheckoutShippingSection.addSaveButton}}" stepKey="clickSaveAdressAdd"/> + <waitForPageLoad stepKey="waitPageLoad"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.state}}" stepKey="seeRegionSelected"/> + </actionGroup> + + <actionGroup name="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup" + extends="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup"> + <remove keyForRemoval="selectRegion"/> + <remove keyForRemoval="seeRegionSelected"/> + <selectOption selector="{{CheckoutShippingSection.addCountry}}" userInput="{{customerAddressVar.country}}" after="enterPostcode" stepKey="enterCountry"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" after="waitPageLoad" stepKey="seeCitySelected"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillNewBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..920b688986830 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillNewBillingAddressActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Logged in user checkout add new address Payment section --> + <actionGroup name="LoggedInUserCheckoutFillNewBillingAddressActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <fillField selector="{{CheckoutPaymentSection.guestFirstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutPaymentSection.guestLastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutPaymentSection.guestStreet}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutPaymentSection.guestCity}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutPaymentSection.guestRegion}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutPaymentSection.guestPostcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutPaymentSection.guestTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml new file mode 100644 index 0000000000000..007fcea0d3c17 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginAsCustomerOnCheckoutPageActionGroup"> + <annotations> + <description>DEPRECATED: Please use StorefrontLoginCustomerOnCheckoutPageActionGroup. Login customer on storefront checkout page during shipping step</description> + </annotations> + <arguments> + <argument name="customer" type="entity"/> + </arguments> + <waitForPageLoad stepKey="waitForCheckoutShippingSectionToLoad"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{CheckoutShippingSection.password}}" stepKey="waitForElementVisible"/> + <fillField selector="{{CheckoutShippingSection.password}}" userInput="{{customer.password}}" stepKey="fillPasswordField"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <waitForElementVisible selector="{{CheckoutShippingSection.loginButton}}" stepKey="waitForLoginButtonVisible"/> + <doubleClick selector="{{CheckoutShippingSection.loginButton}}" stepKey="clickLoginBtn"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear3"/> + <waitForPageLoad stepKey="waitToBeLoggedIn"/> + <waitForElementNotVisible selector="{{CheckoutShippingSection.email}}" stepKey="waitForEmailInvisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/NavigateToCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/NavigateToCheckoutActionGroup.xml new file mode 100644 index 0000000000000..6bc210988e683 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/NavigateToCheckoutActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateToCheckoutActionGroup"> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickCart"/> + <click selector="{{StorefrontMiniCartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml new file mode 100644 index 0000000000000..cea9d968b58e1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/OpenStoreFrontCheckoutShippingPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenStoreFrontCheckoutShippingPageActionGroup"> + <amOnPage url="{{CheckoutShippingPage.url}}" stepKey="amOnCheckoutShippingPage"/> + <waitForPageLoad stepKey="waitForCheckoutShippingPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartActionGroup.xml new file mode 100644 index 0000000000000..d27ffccb725fd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Add Product to Cart from the product page and check message and product count in Minicart --> + <actionGroup name="StorefrontAddProductToCartActionGroup"> + <arguments> + <argument name="product"/> + <argument name="productCount" type="string"/> + </arguments> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> + <!-- @TODO: Use general message selector after MQE-694 is fixed --> + <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> + <waitForText userInput="{{productCount}}" selector="{{StorefrontMiniCartSection.quantity}}" time="30" stepKey="assertProductCount"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..0b37921fba6fd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddProductToCartFromCategoryPageActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddProductToCartFromCategoryPageActionGroup"> + <annotations> + <description>Adds the Product to the shopping cart from the Category page.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + </arguments> + + <scrollTo selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" stepKey="scrollToProduct"/> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" stepKey="moveMouseOverProduct"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.productAddToCartByName(productName)}}" stepKey="waitForAddToCartButtonVisible"/> + <click selector="{{StorefrontCategoryProductSection.productAddToCartByName(productName)}}" stepKey="clickOnAddToCartButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 0000000000000..e1108e53e6edd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Estimate Shipping and Tax Data in Cart --> + <actionGroup name="StorefrontAssertCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{StorefrontCheckoutCartSummarySection.stateProvinceInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml new file mode 100644 index 0000000000000..d874485eb58f1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Shipping Method Is Checked on Cart --> + <actionGroup name="StorefrontAssertCartShippingMethodSelectedActionGroup"> + <arguments> + <argument name="carrierCode" type="string"/> + <argument name="methodCode" type="string"/> + </arguments> + <seeCheckboxIsChecked selector="{{StorefrontCheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" stepKey="assertShippingMethodIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml new file mode 100644 index 0000000000000..82d7e12105b8c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Estimate Shipping and Tax Data on Checkout --> + <actionGroup name="StorefrontAssertCheckoutEstimateShippingInformationActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml new file mode 100644 index 0000000000000..33f2852f1f0ad --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Shipping Method by Name Is Checked on Checkout --> + <actionGroup name="StorefrontAssertCheckoutShippingMethodSelectedActionGroup"> + <arguments> + <argument name="shippingMethod"/> + </arguments> + <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="assertShippingMethodByNameIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml new file mode 100644 index 0000000000000..02c362bf34058 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert guest shipping info on checkout --> + <actionGroup name="StorefrontAssertGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="assertEmailAddress"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="assertFirstName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="assertLastName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="assertCompany"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="assertAddressFirstLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="assertAddressSecondLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="assertCity"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountry"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvince"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCode"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="assertPhoneNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.xml new file mode 100644 index 0000000000000..03caa558e4272 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertMiniCartItemCountActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertMiniCartItemCountActionGroup"> + <arguments> + <argument name="productCount" type="string"/> + <argument name="productCountText" type="string"/> + </arguments> + <see selector="{{StorefrontMinicartSection.productCount}}" userInput="{{productCount}}" stepKey="seeProductCountInCart"/> + <see selector="{{StorefrontMinicartSection.visibleItemsCountText}}" userInput="{{productCountText}}" stepKey="seeNumberOfItemDisplayInMiniCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml new file mode 100644 index 0000000000000..e49eb401b71f1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert shipping method is present in cart --> + <actionGroup name="StorefrontAssertShippingMethodPresentInCartActionGroup"> + <arguments> + <argument name="shippingMethod" type="string"/> + </arguments> + <see selector="{{StorefrontCheckoutCartSummarySection.shippingMethodLabel}}" userInput="{{shippingMethod}}" stepKey="assertShippingMethodIsPresentInCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml new file mode 100644 index 0000000000000..b08aa9cf09397 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAttachOptionFileActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAttachOptionFileActionGroup"> + <annotations> + <description>Attaches the provided File to the provided Product Option on a Storefront Product page.</description> + </annotations> + <arguments> + <argument name="optionTitle" type="string"/> + <argument name="file" defaultValue="MagentoLogo.file" /> + </arguments> + + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(optionTitle)}}" userInput="{{file}}" stepKey="attachFile"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 0000000000000..02fdeb40d8d09 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Fill Estimate Shipping and Tax fields --> + <actionGroup name="StorefrontCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="estimateAddress" defaultValue="EstimateAddressCalifornia"/> + </arguments> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="clickOnEstimateShippingAndTax"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.country}}" stepKey="waitForCountrySelectorIsVisible"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{estimateAddress.country}}" stepKey="selectCountry"/> + <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{estimateAddress.state}}" stepKey="selectStateProvince"/> + <waitForLoadingMaskToDisappear stepKey="waitForStateLoadingMaskDisappear"/> + <fillField selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="{{estimateAddress.zipCode}}" stepKey="fillZipPostalCodeField"/> + <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.xml new file mode 100644 index 0000000000000..0a29cba11d4a9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup"> + <arguments> + <argument name="itemsText" type="string"/> + </arguments> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickOnCheckOutButtonInMiniCart"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" userInput="{{itemsText}}" stepKey="seeOrderSummaryText"/> + <seeElement selector="{{CheckoutOrderSummarySection.miniCartTab}}" stepKey="clickOnOrderSummaryTab"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..e1f532e7d495a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillEmailFieldOnCheckoutActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <fillField selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmailField"/> + <doubleClick selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickToMoveFocusFromEmailInput"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml new file mode 100644 index 0000000000000..e7669d62c79a0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Fill data in checkout shipping section --> + <actionGroup name="StorefrontFillGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="fillEmailAddressField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="fillFirstNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="fillLastNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="fillCompanyField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="fillStreetAddressFirstLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="fillStreetAddressSecondLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="fillCityField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="fillPhoneNumberField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontLoginCustomerOnCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontLoginCustomerOnCheckoutPageActionGroup.xml new file mode 100644 index 0000000000000..cabee02b3afc1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontLoginCustomerOnCheckoutPageActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontLoginCustomerOnCheckoutPageActionGroup"> + <annotations> + <description>Login customer on storefront checkout page during shipping step</description> + </annotations> + <arguments> + <argument name="customerEmail" type="string" defaultValue="{{Simple_US_Customer.email}}"/> + <argument name="customerPassword" type="string" defaultValue="Simple_US_Customer.password"/> + </arguments> + <waitForPageLoad stepKey="waitForCheckoutShippingSectionToLoad"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerEmail}}" stepKey="fillEmailField"/> + <waitForPageLoad stepKey="waitForAllRefreshedAfterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.password}}" stepKey="waitForPasswordFieldVisible"/> + <fillField selector="{{CheckoutShippingSection.password}}" userInput="{{customerPassword}}" stepKey="fillPasswordField"/> + <waitForPageLoad stepKey="waitForAllRefreshedAfterPassword"/> + <waitForElementVisible selector="{{CheckoutShippingSection.loginButton}}" stepKey="waitForLoginButtonVisible"/> + <click selector="{{CheckoutShippingSection.loginButton}}" stepKey="clickLoginBtn"/> + <waitForPageLoad stepKey="waitForAllRefreshedAfterLogin"/> + <waitForElementNotVisible selector="{{CheckoutShippingSection.email}}" stepKey="waitForEmailFieldInvisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml new file mode 100644 index 0000000000000..a1a3860d07b3b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="clickViewAndEditCartFromMiniCart"> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="viewAndEditCart"/> + <seeInCurrentUrl url="checkout/cart" stepKey="seeInCurrentUrl"/> + <waitForPageLoad time="30" stepKey="waitForCartPageIsLoaded"/> + </actionGroup> + <actionGroup name="assertOneProductNameInMiniCart"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeInMiniCart"/> + </actionGroup> + + <!--Remove an item from the cart using minicart--> + <actionGroup name="removeProductFromMiniCart"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForMiniCartOpen"/> + <click selector="{{StorefrontMinicartSection.deleteMiniCartItemByName(productName)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{StoreFrontRemoveItemModalSection.message}}" stepKey="waitForConfirmationModal"/> + <see selector="{{StoreFrontRemoveItemModalSection.message}}" userInput="Are you sure you would like to remove this item from the shopping cart?" stepKey="seeDeleteConfirmationMessage"/> + <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + </actionGroup> + + <!--Check that the minicart is empty--> + <actionGroup name="AssertMiniCartEmpty"> + <annotations> + <description>Validates that the provided Product Count appears in the Storefront Header next to the Shopping Cart icon. Clicks on the Mini Shopping Cart icon. Validates that the 'No Items' message is present and correct in the Storefront Mini Shopping Cart.</description> + </annotations> + + <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> + <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartFromMinicartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartFromMinicartActionGroup.xml new file mode 100644 index 0000000000000..91665f8aafcd5 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartFromMinicartActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- Open the Cart from Minicart--> + <actionGroup name="StorefrontOpenCartFromMinicartActionGroup"> + <waitForElement selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForShowMinicart" /> + <waitForElement selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForCartLink" /> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickShowMinicart" /> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="clickCart" /> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCategoryPageAndAddProductToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCategoryPageAndAddProductToCartActionGroup.xml new file mode 100644 index 0000000000000..60ac789939a32 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCategoryPageAndAddProductToCartActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenCategoryPageAndAddProductToCartActionGroup" extends="StorefrontAddProductToCartFromCategoryPageActionGroup"> + <annotations> + <description>EXTENDS: StorefrontAddProductToCartFromCategoryPageActionGroup. Opens the Category page. Adds the Product to the shopping cart.</description> + </annotations> + <arguments> + <argument name="categoryUrlKey" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontCategoryPage.url(categoryUrlKey)}}" before="scrollToProduct" stepKey="goToCategoryPage"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.ProductInfoByName(productName)}}" after="goToCategoryPage" stepKey="waitForProductVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml new file mode 100644 index 0000000000000..7afcb070f3f06 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenMiniCartActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenMiniCartActionGroup"> + <waitForElementVisible selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForElementToBeVisible"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontViewAndEditCartFromMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontViewAndEditCartFromMiniCartActionGroup.xml new file mode 100644 index 0000000000000..54742221e0522 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontViewAndEditCartFromMiniCartActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontViewAndEditCartFromMiniCartActionGroup"> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="viewAndEditCart"/> + <seeInCurrentUrl url="checkout/cart" stepKey="seeInCurrentUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..055facbc72e8a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- Free shipping --> + <entity name="EnableFreeShippingConfigData"> + <data key="path">carriers/freeshipping/active</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="EnableFreeShippingToSpecificCountriesConfigData"> + <data key="path">carriers/freeshipping/sallowspecific</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Specific Countries</data> + <data key="value">1</data> + </entity> + <entity name="EnableFreeShippingToAfghanistanConfigData"> + <data key="path">carriers/freeshipping/specificcountry</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Afghanistan</data> + <data key="value">AF</data> + </entity> + <entity name="EnableFreeShippingToAllAllowedCountriesConfigData"> + <data key="path">carriers/freeshipping/sallowspecific</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">All Allowed Countries</data> + <data key="value">0</data> + </entity> + <entity name="DisableFreeShippingConfigData"> + <data key="path">carriers/freeshipping/active</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + + <!-- Flat Rate shipping --> + <entity name="EnableFlatRateConfigData"> + <data key="path">carriers/flatrate/active</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="EnableFlatRateToSpecificCountriesConfigData"> + <data key="path">carriers/flatrate/sallowspecific</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Specific Countries</data> + <data key="value">1</data> + </entity> + <entity name="EnableFlatRateToAfghanistanConfigData"> + <data key="path">carriers/flatrate/specificcountry</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">Afghanistan</data> + <data key="value">AF</data> + </entity> + <entity name="EnableFlatRateToAllAllowedCountriesConfigData"> + <data key="path">carriers/flatrate/sallowspecific</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">All Allowed Countries</data> + <data key="value">0</data> + </entity> + <entity name="DisableFlatRateConfigData"> + <data key="path">carriers/flatrate/active</data> + <data key="scope">carriers</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + + <!-- Checkout Config: Display Cart Summary --> + <entity name="DisplayItemsQuantities"> + <data key="path">checkout/cart_link/use_qty</data> + <data key="label">Display items quantities</data> + <data key="value">1</data> + </entity> + <entity name="DisplayUniqueItems"> + <data key="path">checkout/cart_link/use_qty</data> + <data key="label">Display number of items in cart</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml new file mode 100644 index 0000000000000..1703d7255e1f8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConstData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <!-- @TODO: Get rid off this workaround and its usages after MQE-498 is implemented --> + <entity name="CONST" type="CONST"> + <data key="successGuestCheckoutOrderNumberMessage">Your order # is:</data> + <data key="successCheckoutOrderNumberMessage">Your order number is:</data> + <data key="successCheckoutEmailYouMessage">We'll email you an order confirmation with details and tracking info.</data> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml new file mode 100644 index 0000000000000..fcbbf537c5d7b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="TopDestinationCountries" type="countryArray"> + <array key="country"> + <item>Bahamas</item> + </array> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml new file mode 100644 index 0000000000000..36dea5a521a04 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EstimateAddressCalifornia"> + <data key="country">United States</data> + <data key="state">California</data> + <data key="zipCode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/LICENSE.txt b/app/code/Magento/Checkout/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Checkout/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 <insert your license name here>" 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/app/code/Magento/Checkout/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Checkout/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Checkout/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 <insert your license name here>" 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/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml new file mode 100644 index 0000000000000..37079e2bcf144 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CheckoutCartPage" url="/checkout/cart" area="storefront" module="Magento_Checkout"> + <section name="CheckoutCartProductSection"/> + <section name="StorefrontCheckoutCartSummarySection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml new file mode 100644 index 0000000000000..0c6a1682a7719 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CheckoutPage" url="/checkout" area="storefront" module="Magento_Checkout"> + <section name="CheckoutShippingSection"/> + <section name="CheckoutShippingMethodsSection"/> + <section name="CheckoutOrderSummarySection"/> + <section name="CheckoutSuccessMainSection"/> + <section name="CheckoutPaymentSection"/> + <section name="CheckoutGuestShippingInfoSection"/> + <section name="CheckoutAddressPopupSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml new file mode 100644 index 0000000000000..a118482357d4a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CheckoutShippingPage" url="/checkout/#shipping" module="Checkout" area="storefront"> + <section name="CheckoutShippingGuestInfoSection"/> + <section name="CheckoutShippingSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml new file mode 100644 index 0000000000000..1c3293267e2ab --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutSuccessPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CheckoutSuccessPage" url="/checkout/onepage/success/" area="storefront" module="Magento_Checkout"> + <section name="CheckoutSuccessMainSection"/> + <section name="CheckoutSuccessRegisterSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutPage.xml new file mode 100644 index 0000000000000..15a63d0b97a50 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="GuestCheckoutPage" url="/checkout" area="storefront" module="Checkout"> + <section name="GuestCheckoutShippingSection"/> + <section name="GuestCheckoutPaymentSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml new file mode 100644 index 0000000000000..0b2776bf9bef9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/GuestCheckoutReviewAndPaymentsPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="GuestCheckoutPage" url="/checkout/#payment" area="storefront" module="Checkout"> + <section name="PaymentMethodSection"/> + <section name="ShipToSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/README.md b/app/code/Magento/Checkout/Test/Mftf/README.md new file mode 100644 index 0000000000000..ec43eb9c6c3ef --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Checkout Functional Tests + +The Functional Test Module for **Magento Checkout** module. diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml new file mode 100644 index 0000000000000..de866fbb50c80 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCheckoutPaymentSection"> + <element name="checkBillingMethodByName" type="radio" selector="//div[@id='order-billing_method']//dl[@class='admin__payment-methods']//dt//label[contains(., '{{methodName}}')]/..//input" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml new file mode 100644 index 0000000000000..9e8adab362a88 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutAddressPopupSection"> + <element name="newAddressModalPopup" type="block" selector=".modal-popup.modal-slide._inner-scroll"/> + <element name="closeAddressModalPopup" type="button" selector=".action-hide-popup"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml new file mode 100644 index 0000000000000..a2dd524abd99f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutCartProductSection"> + <element name="itemOptionsBlock" type="block" + selector=".item:nth-of-type({{numElement}}) .item-options" + parameterized="true"/> + <element name="editItemParametersButton" type="block" + selector=".item:nth-of-type({{numElement}}) .action-edit" + parameterized="true"/> + <element name="productQuantityByName" type="input" + selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'qty')]//input[contains(@class, 'qty')]" + parameterized="true"/> + <element name="productSubtotalByName" type="input" + selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'subtotal')]//span[@class='price']" + parameterized="true"/> + <element name="updateShoppingCartButton" type="button" + selector="#form-validate button[type='submit'].update" + timeout="30"/> + <element name="productLinkByName" type="button" + selector="//main//table[@id='shopping-cart-table']//tbody//tr//strong[contains(@class, 'product-item-name')]//a[contains(text(), '{{var1}}')]" + parameterized="true"/> + <element name="productOptionByNameAndAttribute" type="input" + selector="//main//table[@id='shopping-cart-table']//tbody//tr[.//strong[contains(@class, 'product-item-name')]//a[contains(text(), '{{var1}}')]]//dl[@class='item-options']//dt[.='{{var2}}']/following-sibling::dd[1]" + parameterized="true"/> + <element name="removeItem" type="button" + selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> + <element name="productPriceByName" type="text" + selector="//table[@id='shopping-cart-table']//tbody//tr[//a/text()='{{var1}}']//ancestor::td[contains(@class,'price')]//span[@class='price']" + parameterized="true"/> + <element name="productName" type="text" selector="#shopping-cart-table tbody strong.product-item-name"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml new file mode 100644 index 0000000000000..04fbd97797e6b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutHeaderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CheckoutHeaderSection"> + <element name="shippingMethodStep" type="text" selector=".opc-progress-bar-item:nth-of-type(1)"/> + <element name="reviewAndPaymentsStep" type="text" selector=".opc-progress-bar-item:nth-of-type(2)"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml new file mode 100644 index 0000000000000..fdccab659fa85 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CheckoutOrderSummarySection"> + <element name="miniCartTab" type="button" selector=".title[role='tab']"/> + <element name="productItemName" type="text" selector=".product-item-name"/> + <element name="productItemQty" type="text" selector=".value"/> + <element name="productItemPrice" type="text" selector=".price"/> + <element name="shippingAddress" type="block" selector=".box-address-shipping address"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml new file mode 100644 index 0000000000000..614855be46061 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutPaymentSection"> + <element name="isPaymentSection" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Review & Payments')]]"/> + <element name="availablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div:nth-child(2)>div.payment-method-title.field.choice"/> + <element name="notAvailablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div.payment-method._active>div.payment-method-title.field.choice"/> + <element name="billingNewAddressForm" type="text" selector="[data-form='billing-new-address']"/> + <element name="placeOrderDisabled" type="button" selector="#checkout-payment-method-load button.disabled"/> + <element name="update" type="button" selector=".payment-method-billing-address .action.action-update"/> + <element name="guestFirstName" type="input" selector=".billing-address-form input[name*='firstname']"/> + <element name="guestLastName" type="input" selector=".billing-address-form input[name*='lastname']"/> + <element name="guestStreet" type="input" selector=".billing-address-form input[name*='street[0]']"/> + <element name="guestCity" type="input" selector=".billing-address-form input[name*='city']"/> + <element name="guestRegion" type="select" selector=".billing-address-form select[name*='region_id']"/> + <element name="guestPostcode" type="input" selector=".billing-address-form input[name*='postcode']"/> + <element name="guestTelephone" type="input" selector=".billing-address-form input[name*='telephone']"/> + <element name="country" type="select" selector=".billing-address-form select[name*='country_id']"/> + <element name="cartItems" type="text" selector=".minicart-items"/> + <element name="billingAddress" type="text" selector="div.billing-address-details"/> + <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> + <element name="placeOrderNoWait" type="button" selector=".payment-method._active button.action.primary.checkout"/> + <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> + <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> + <element name="productItemPriceByName" type="text" selector="//div[@class='product-item-details'][contains(., '{{ProductName}}')]//span[@class='price']" parameterized="true"/> + <element name="productOptionsActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true" /> + <element name="productOptionsByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true" /> + <element name="cartItemsArea" type="button" selector=".items-in-cart"/> + <element name="cartItemsAreaActive" type="textarea" selector=".items-in-cart.active" timeout="30"/> + <element name="paymentSectionTitle" type="text" selector="#checkout-payment-method-load .step-title" /> + <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> + <element name="checkMoneyOrderPayment" type="radio" selector="#checkmo.radio" timeout="30"/> + <element name="editAddressButton" type="button" selector=".payment-method._active .action.action-edit-address"/> + <element name="newAddressSelect" type="select" selector=".payment-method._active select[name*='billing_address_id']"/> + <element name="goToShipping" type="button" selector="#checkout>ul>li.opc-progress-bar-item._complete>span"/> + <element name="orderSummarySubtotal" type="text" selector=".totals.sub span" /> + <element name="billingAddressSameAsShipping" type="checkbox" selector=".payment-method._active [name='billing-address-same-as-shipping']"/> + <element name="orderSummaryTotal" type="text" selector="tr.grand.totals span.price" /> + <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[contains(., '{{paymentName}}')]/..//input[@type='radio']" parameterized="true"/> + <element name="orderSummaryShippingTotal" type="text" selector=".totals.shipping.excl span.price"/> + <element name="noPaymentMethods" type="text" selector=".no-quotes-block"/> + <element name="billingAddressSelectShared" type="select" selector=".checkout-billing-address select[name='billing_address_id']"/> + <element name="billingAddressSameAsShippingShared" type="checkbox" selector="#billing-address-same-as-shipping-shared"/> + <element name="addressAction" type="button" selector="//div[@class='actions-toolbar']//span[text()='{{action}}']" parameterized="true"/> + <element name="discount" type="block" selector="tr.totals.discount"/> + <element name="discountPrice" type="text" selector=".discount .price"/> + <element name="shippingMethodInformation" type="text" selector=".ship-via .shipping-information-content"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml new file mode 100644 index 0000000000000..cd11d016aa025 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutShippingGuestInfoSection"> + <element name="email" type="input" selector="#customer-email"/> + <element name="firstName" type="input" selector="input[name=firstname]"/> + <element name="lastName" type="input" selector="input[name=lastname]"/> + <element name="street" type="input" selector="input[name='street[0]']"/> + <element name="street2" type="input" selector="input[name='street[1]']"/> + <element name="city" type="input" selector="input[name=city]"/> + <element name="region" type="select" selector="select[name=region_id]"/> + <element name="regionInput" type="input" selector="input[name=region]"/> + <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="telephone" type="input" selector="input[name=telephone]"/> + <element name="country" type="select" selector="select[name=country_id]"/> + <element name="company" type="input" selector="input[name=company]"/> + + <!--Order Summary--> + <element name="itemInCart" type="button" selector="//div[@class='title']"/> + <element name="productName" type="text" selector="//strong[@class='product-item-name']"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml new file mode 100644 index 0000000000000..a8485ca026b22 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CheckoutShippingMethodsSection"> + <element name="next" type="button" selector="button.button.action.continue.primary"/> + <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> + <element name="shippingMethodRow" type="text" selector=".form.methods-shipping table tbody tr"/> + <element name="checkShippingMethodByName" type="radio" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/..//input" parameterized="true"/> + <element name="shippingMethodRowByName" type="text" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/.." parameterized="true"/> + <element name="flatRate" type="radio" selector="#label_carrier_flatrate_flatrate"/> + <element name="noQuotesMsg" type="text" selector="#checkout-step-shipping_method div"/> + <element name="freeShippingShippingMethod" type="input" selector="#s_method_freeshipping_freeshipping" timeout="30"/> + <element name="shippingMethodFreeShipping" type="radio" selector="#checkout-shipping-method-load input[value='freeshipping_freeshipping']"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml new file mode 100644 index 0000000000000..2ae138a76d42a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutShippingSection"> + <element name="isShippingStep" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Shipping')]]"/> + <element name="email" type="input" selector="#customer-email"/> + <element name="firstName" type="input" selector="input[name=firstname]"/> + <element name="lastName" type="input" selector="input[name=lastname]"/> + <element name="street" type="input" selector="input[name='street[0]']"/> + <element name="city" type="input" selector="input[name=city]"/> + <element name="region" type="select" selector="select[name=region_id]"/> + <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="country" type="select" selector="select[name=country_id]"/> + <element name="telephone" type="input" selector="input[name=telephone]"/> + <element name="firstShippingMethod" type="radio" selector="#checkout-shipping-method-load input[type='radio']" timeout="30"/> + <element name="selectedShippingAddress" type="text" selector=".shipping-address-item.selected-item"/> + <element name="newAddressButton" type="button" selector="#checkout-step-shipping button"/> + <element name="next" type="button" selector="[data-role='opc-continue']"/> + <element name="stateInput" type="input" selector="input[name=region]"/> + <element name="newAdress" type="button" selector="button.action.action-show-popup" timeout="30"/> + <element name="addStreet" type="input" selector="#shipping-new-address-form input[name='street[0]']"/> + <element name="addCity" type="input" selector="#shipping-new-address-form input[name='city']"/> + <element name="addState" type="select" selector="#shipping-new-address-form select[name='region_id']"/> + <element name="addPostcode" type="input" selector="#shipping-new-address-form input[name='postcode']"/> + <element name="addTelephone" type="input" selector="#shipping-new-address-form input[name='telephone']"/> + <element name="addCountry" type="select" selector="#shipping-new-address-form select[name='country_id']"/> + <element name="addSaveButton" type="button" selector=".action.primary.action-save-address"/> + <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> + <element name="namePrefix" type="select" selector="select[name=prefix]"/> + <element name="namePrefixOption" type="select" selector="select[name=prefix] option[value='{{optionValue}}']" parameterized="true"/> + <element name="nameSuffix" type="select" selector="[name='suffix']"/> + <element name="nameSuffixOption" type="text" selector="select[name='suffix'] option[value='{{optionValue}}']" parameterized="true" timeout="30"/> + <element name="password" type="input" selector="#customer-password"/> + <element name="loginButton" type="button" selector=".action.login" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml new file mode 100644 index 0000000000000..3f92d170832a7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CheckoutSuccessMainSection"> + <element name="successTitle" type="text" selector=".page-title"/> + <element name="success" type="text" selector="div.checkout-success"/> + <element name="orderNumber" type="text" selector="div.checkout-success > p:nth-child(1) > span"/> + <element name="orderNumberLink" type="text" selector="div.checkout-success > p:nth-child(1) > a"/> + <element name="orderNumber22" type="text" selector=".order-number>strong"/> + <element name="orderLink" type="text" selector="a[href*=order_id].order-number"/> + <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> + <element name="continueShoppingButton" type="button" selector=".action.primary.continue"/> + <element name="printLink" type="button" selector=".print"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml new file mode 100644 index 0000000000000..eb3d16db7a2cc --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CheckoutSuccessRegisterSection"> + <element name="registerMessage" type="text" selector="#registration p:nth-child(1)"/> + <element name="customerEmail" type="text" selector="#registration p:nth-child(2)"/> + <element name="createAccountButton" type="button" selector="#registration form input[type='submit']"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutPaymentSection.xml new file mode 100644 index 0000000000000..210bfcd2e7a74 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutPaymentSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="GuestCheckoutPaymentSection"> + <element name="cartItemsArea" type="textarea" selector=".items-in-cart"/> + <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active"/> + <element name="cartItems" type="text" selector=".minicart-items"/> + <element name="billingAddress" type="text" selector="div.billing-address-details"/> + <element name="placeOrder" type="button" selector="button.action.primary.checkout" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutShippingSection.xml new file mode 100644 index 0000000000000..722165e2e8381 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/GuestCheckoutShippingSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="GuestCheckoutShippingSection"> + <element name="email" type="input" selector="#customer-email"/> + <element name="firstName" type="input" selector="input[name=firstname]"/> + <element name="lastName" type="input" selector="input[name=lastname]"/> + <element name="street" type="input" selector="input[name='street[0]']"/> + <element name="city" type="input" selector="input[name=city]"/> + <element name="region" type="select" selector="select[name=region_id]"/> + <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="telephone" type="input" selector="input[name=telephone]"/> + <element name="next" type="button" selector="button.button.action.continue.primary"/> + <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/PaymentMethodSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/PaymentMethodSection.xml new file mode 100644 index 0000000000000..05681a5068190 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/PaymentMethodSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="PaymentMethodSection"> + <element name="billingAddress" type="text" selector=".checkout-billing-address"/> + <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[@class='payment-method']//label//span[contains(., '{{methodName}}')]/../..//input" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/ShipToSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/ShipToSection.xml new file mode 100644 index 0000000000000..ba23de306e94e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/ShipToSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="ShipToSection"> + <element name="shippingInformation" type="text" selector=".shipping-information-content"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml new file mode 100644 index 0000000000000..c7599deab9644 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryProductSection"> + <element name="ProductAddToCartByNumber" type="button" selector="//main//li[{{var1}}]//button[contains(@class, 'tocart')]" parameterized="true"/> + <element name="productAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//button[contains(@class, 'tocart')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml new file mode 100644 index 0000000000000..dc523a597a405 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCartSummarySection"> + <element name="subtotal" type="text" selector="#cart-totals tr.totals.sub span.price"/> + <element name="proceedToCheckout" type="button" selector=".action.primary.checkout span" timeout="30"/> + <element name="total" type="text" selector=".amount[data-th='Order Total'] span"/> + <element name="country" type="select" selector="#block-summary select[name='country_id']" timeout="30"/> + <element name="region" type="select" selector="#block-summary select[name='region_id']" timeout="30"/> + <element name="stateProvinceInput" type="input" selector="input[name='region']"/> + <element name="postcode" type="text" selector="#block-summary input[name='postcode']" timeout="30"/> + <element name="estimateShippingAndTax" type="text" selector="#block-shipping-heading" timeout="5"/> + <element name="flatRateShippingMethod" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="itemDiscount" type="text" selector="tr[class='totals'] td.amount > span"/> + <element name="countryParameterized" type="select" selector="select[name='country_id'] > option:nth-child({{var}})" timeout="10" parameterized="true"/> + <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> + <element name="blockSummary" type="button" selector="#block-summary"/> + <element name="discountAmount" type="text" selector="td[data-th='Discount']"/> + <element name="totalsElementByPosition" type="text" selector=".data.table.totals > tbody tr:nth-of-type({{value}}) > th" parameterized="true"/> + <element name="tableTotals" type="text" selector="#cart-totals .data.table.totals"/> + <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> + <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> + <element name="methodName" type="text" selector="#co-shipping-method-form label"/> + <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml new file mode 100644 index 0000000000000..174b91434931e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCheckoutCustomerLoginSection"> + <element name="email" type="input" selector="form[data-role='email-with-possible-login'] input[name='username']"/> + <element name="emailErrorMessage" type="text" selector="#customer-email-error"/> + <element name="emailTooltipButton" type="button" selector="#customer-email-fieldset .field-tooltip-action"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontIdentityOfDefaultBillingAndShippingAddressSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontIdentityOfDefaultBillingAndShippingAddressSection.xml new file mode 100644 index 0000000000000..748db42af3bfe --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontIdentityOfDefaultBillingAndShippingAddressSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontIdentityOfDefaultBillingAndShippingAddressSection"> + <element name="shippingAddress" type="textarea" selector=".box.box-billing-address address"/> + <element name="billingAddress" type="textarea" selector=".box.box-shipping-address address"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml new file mode 100644 index 0000000000000..6a015a5fd4f05 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontMessagesSection"> + <!-- @TODO: Use general message selector after MQE-694 is fixed --> + <element name="messageProductAddedToCart" type="text" + selector="//*[@data-ui-id='message-success' and normalize-space(.)='You added {{var1}} to your shopping cart.']" + parameterized="true" + /> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml new file mode 100644 index 0000000000000..ad62605a30f7e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontMinicartSection"> + <element name="blockMiniCart" type="block" selector=".block-minicart" timeout="30"/> + <element name="productCount" type="text" selector="//header//div[contains(@class, 'minicart-wrapper')]//a[contains(@class, 'showcart')]//span[@class='counter-number']"/> + <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> + <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> + <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> + <element name="showCart" type="button" selector="a.showcart"/> + <element name="quantity" type="button" selector="span.counter-number"/> + <element name="miniCartOpened" type="button" selector="a.showcart.active"/> + <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> + <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> + <element name="miniCartItemsText" type="text" selector=".minicart-items"/> + <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> + <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> + <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> + <element name="emptyCart" type="text" selector=".counter.qty.empty"/> + <element name="minicartContent" type="block" selector="#minicart-content-wrapper"/> + <element name="visibleItemsCountText" type="text" selector="//div[@class='items-total']"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml new file mode 100644 index 0000000000000..335750cf9a760 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductCompareMainSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontProductCompareMainSection"> + <element name="productAddToCartByName" type="button" selector="//*[@id='product-comparison']//td[.//strong[@class='product-item-name']/a[contains(text(), '{{var1}}')]]//button[contains(@class, 'tocart')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..1ff5d2c874459 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="AddToCart" type="button" selector="#product-addtocart-button"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml new file mode 100644 index 0000000000000..e211a90a95c63 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AddressStateFieldShouldNotAcceptJustIntegerValuesTest"> + <annotations> + <features value="Checkout"/> + <title value="Guest Checkout"/> + <description value="Address State field should not allow just integer values"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93291"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{UK_Address.country_id}}" stepKey="selectCounty"/> + <waitForPageLoad stepKey="waitFormToReload"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput="1" stepKey="enterStateAsIntegerValue"/> + <waitForPageLoad stepKey="waitforFormValidation"/> + <see userInput="First character must be letter." stepKey="seeTheErrorMessageDisplayed"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput=" 1" stepKey="enterStateAsIntegerValue1"/> + <waitForPageLoad stepKey="waitforFormValidation1"/> + <see userInput="First character must be letter." stepKey="seeTheErrorMessageDisplayed1"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput="ABC1" stepKey="enterStateAsIntegerValue2"/> + <waitForPageLoad stepKey="waitforFormValidation2"/> + <dontSee userInput="First character must be letter." stepKey="seeTheErrorMessageIsNotDisplayed"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml new file mode 100644 index 0000000000000..c86261be4f374 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckConfigsChangesAreNotAffectedStartedCheckoutProcessTest"> + <annotations> + <features value="Checkout"/> + <stories value="Changes in configs are not affecting checkout process"/> + <title value="Admin check configs changes are not affected started checkout process test"/> + <description value="Changes in admin for shipping rates are not affecting checkout process that has been started"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8893"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!-- Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + + <!-- Disable flat rate method --> + <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + </before> + <after> + <!-- Roll back configuration --> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + + <!-- Delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Add product to cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Proceed to Checkout from mini shopping cart --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckout"/> + + <!-- Fill all required fields --> + <actionGroup ref="GuestCheckoutFillNewShippingAddressActionGroup" stepKey="fillNewShippingAddress"> + <argument name="customer" value="Simple_Customer_Without_Address"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Assert Free Shipping checkbox --> + <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.shippingMethodFreeShipping}}" stepKey="freeShippingMethodCheckboxIsChecked"/> + + <!-- Click Next button --> + <click selector="{{GuestCheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + + <!-- Payment step is opened --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + + <!-- Order Summary block contains information about shipping --> + <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckShippingMethod"> + <argument name="shippingMethod" value="freeTitleDefault.value"/> + </actionGroup> + + <!-- Open new browser's window and login as Admin --> + <openNewTab stepKey="openNewTab"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Go to Store > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Enable "Flat Rate" --> + <actionGroup ref="AdminChangeFlatRateShippingMethodStatusActionGroup" stepKey="enableFlatRateShippingStatus"/> + + <!-- Flush cache --> + <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + + <!-- Back to the Checkout and refresh the page --> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitPageReload"/> + + <!-- Payment step is opened after refreshing --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSection"/> + + <!-- Order Summary block contains information about free shipping --> + <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckFreeShippingMethod"> + <argument name="shippingMethod" value="freeTitleDefault.value"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminZeroSubtotalOrdersWithProcessingStatusTest.xml new file mode 100644 index 0000000000000..e603cf4fde4a3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminZeroSubtotalOrdersWithProcessingStatusTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Checking status of Zero Subtotal Orders with 'Processing' New Order Status"/> + <description value="Created order should be in Processing status"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95994"/> + <useCaseId value="MAGETWO-72877"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create entities--> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + <createData entity="SaleRule50PercentDiscountNoCoupon" stepKey="createCartPriceRule"> + <field key="discount_amount">100</field> + <field key="coupon_type">SPECIFIC_COUPON</field> + </createData> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <createData entity="ZeroSubtotalCheckoutPaymentMethodConfig" stepKey="enableZeroSubtotalCheckout"/> + <!--Login to Admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete entities--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <createData entity="ZeroSubtotalCheckoutPaymentMethodDefault" stepKey="disableZeroSubtotalCheckout"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <!--Clear filters--> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAdminOrdersGridFilters"/> + <!--Logout from Admin page--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> + + <!--Add product to shopping cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Navigate to shopping cart page--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="navigateToCartPage"/> + + <!--Apply Cart Rule On Storefront--> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCartRule"> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountAmountVisible"/> + + <!--Navigate to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> + + <!--Place Order with Free Shipping--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <see selector="{{CheckoutPaymentSection.notAvailablePaymentSolutions}}" userInput="No Payment Information Required" stepKey="seePaymentInformation"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!--Verify that Created order is in Processing status--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml new file mode 100644 index 0000000000000..e09736e1f437a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="CheckCheckoutSuccessPageAsRegisterCustomer"> + <annotations> + <features value="Checkout"/> + <stories value="Success page elements are presented for placed order as Customer"/> + <title value="Customer Checkout"/> + <description value="To be sure that other elements of Success page are shown for placed order as registered Customer."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77616"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleOne" stepKey="createSimpleProduct"/> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <field key="group_id">1</field> + </createData> + </before> + + <after> + <!--Logout from customer account--> + <amOnPage url="customer/account/logout/" stepKey="logoutCustomerOne"/> + <waitForPageLoad stepKey="waitLogoutCustomerOne"/> + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="signUpNewUser"> + <argument name="customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest3"/> + + <!--Click Place Order button--> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order number is: " stepKey="seeOrderNumber"/> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="We'll email you an order confirmation with details and tracking info." stepKey="seeSuccessNotify"/> + + <click selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="clickOrderLink"/> + <waitForPageLoad stepKey="waitPageOrder"/> + <seeInCurrentUrl url="{{OrderDetailsPage.url}}" stepKey="seeMyOrderPage"/> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton2"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest4"/> + + <!--Click Place Order button--> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle2"/> + <click selector="{{CheckoutSuccessMainSection.continueShoppingButton}}" stepKey="clickContinueShoppingButton"/> + <waitForPageLoad stepKey="waitHomePage"/> + <see userInput="Home Page" selector="{{StorefrontHeaderSection.mainTitle}}" stepKey="seeHomePageTitle"/> + <seeCurrentUrlEquals url="{{_ENV.MAGENTO_BASE_URL}}" stepKey="seeHomePageUrl"/> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage3"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad3"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage3"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart3"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod3"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton3"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext3"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest5"/> + + <!--Click Place Order button--> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear3"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder3"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle3"/> + + <!--Check "Print Receipt" button is presented (desktop only)--> + <seeElement selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="seeVisiblePrint"/> + <resizeWindow width="600" height="800" stepKey="resizeWindow"/> + <waitForElementNotVisible selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="waitInvisiblePrint"/> + <dontSeeElement selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="seeInvisiblePrint"/> + <resizeWindow width="1360" height="1020" stepKey="maximizeWindowKey1"/> + <waitForElementVisible selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="waitVisiblePrint"/> + <seeElement selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="seeVisiblePrint2" /> + + <!--See print page--> + <click selector="{{CheckoutSuccessMainSection.printLink}}" stepKey="clickPrintLink"/> + <waitForPageLoad stepKey="waitPrintPage"/> + <switchToWindow stepKey="switchToWindow"/> + <switchToNextTab stepKey="switchToTab"/> + <seeInCurrentUrl url="sales/order/print/order_id" stepKey="seePrintPage"/> + <seeElement selector="{{StorefrontCustomerOrderViewSection.orderTitle}}" stepKey="seeOrderTitleOnPrint"/> + <switchToWindow stepKey="switchToWindow2"/> + </test> + <test name="CheckCheckoutSuccessPageAsGuest"> + <annotations> + <features value="Checkout"/> + <stories value="Success page elements are presented for placed order as Guest"/> + <title value="Customer Checkout"/> + <description value="To be sure that other elements of Success page are presented for placed order as Guest."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77614"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleOne" stepKey="createSimpleProduct"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + </after> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!--Click Place Order button--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + <!--Check register section--> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="We'll email you an order confirmation with details and tracking info." stepKey="seeSuccessNotify"/> + <see selector="{{CheckoutSuccessRegisterSection.registerMessage}}" userInput="You can track your order status by creating an account." stepKey="seeRegisterMessage"/> + <see selector="{{CheckoutSuccessRegisterSection.customerEmail}}" userInput="Email Address: {{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> + <seeElement selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="seeVisibleCreateAccountButton"/> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <waitForPageLoad stepKey="waitAccountPage"/> + <seeInCurrentUrl url="{{StorefrontCustomerCreatePage.url}}" stepKey="seeHomePageUrl"/> + <see userInput="Create New Customer Account" selector="{{StorefrontHeaderSection.mainTitle}}" stepKey="seeHomePageTitle"/> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection2"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!--Click Place Order button--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle2"/> + <click selector="{{CheckoutSuccessMainSection.continueShoppingButton}}" stepKey="clickContinueShoppingButton2"/> + <waitForPageLoad stepKey="waitHomePage2"/> + <seeCurrentUrlEquals url="{{_ENV.MAGENTO_BASE_URL}}" stepKey="seeHomePageUrl2"/> + <see userInput="Home Page" selector="{{StorefrontHeaderSection.mainTitle}}" stepKey="seeHomePageTitle2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml new file mode 100644 index 0000000000000..4b4ca1935fd78 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckNotVisibleProductInMinicartTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-96422: Hidden Products are absent in Storefront Mini-Cart" /> + <title value="Not visible individually product in mini-shopping cart."/> + <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <!--Create simple product1 and simple product2--> + <createData entity="SimpleTwo" stepKey="createSimpleProduct1"/> + <createData entity="SimpleTwo" stepKey="createSimpleProduct2"/> + + <!--Go to simple product1 page--> + <amOnPage url="$$createSimpleProduct1.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage1"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add simple product1 to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage1"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Make simple product1 not visible individually--> + <updateData entity="SetProductVisibilityHidden" createDataKey="createSimpleProduct1" stepKey="updateSimpleProduct1"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </updateData> + + <!--Go to simple product2 page--> + <amOnPage url="$$createSimpleProduct2.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> + + <!--Add simple product2 to Shopping Cart for updating cart items--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check hidden simple product 1 in minicart" stepKey="commentCheckHiddenSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertHiddenProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product2 in minicart--> + <comment userInput="Check hidden simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct2NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Delete simple product1 and simple product2--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml new file mode 100644 index 0000000000000..8b52c438a6d50 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml @@ -0,0 +1,255 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ConfiguringInstantPurchaseFunctionalityTest"> + <annotations> + <features value="One Page Checkout"/> + <stories value="Configuring instant purchase"/> + <title value="Configuring instant purchase functionality test"/> + <description value="Configuring instant purchase"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-7093"/> + <group value="checkout"/> + <skip> + <issueId value="MQE-1576"/> + </skip> + </annotations> + <before> + <!-- Configure Braintree Vault-enabled payment method --> + <createData entity="SandboxBraintreeConfig" stepKey="braintreeConfigurationData"/> + <createData entity="CustomBraintreeConfigurationData" stepKey="enableBraintree"/> + + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Create product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Set configs to default --> + <createData entity="DefaultBraintreeConfig" stepKey="defaultBraintreeConfig"/> + <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="rollBackCustomBraintreeConfigurationData"/> + + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Admin logout --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Create store views --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!-- Login to Frontend --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!-- Customer placed order with payment method save --> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + + <!-- Fill Braintree cart data --> + <waitForElementVisible selector="{{StorefrontBraintreePaymentConfigurationSection.creditCardBraintreePaymentMethod}}" stepKey="waitForBraintreePaymentMethodVisible"/> + <actionGroup ref="StorefrontBraintreeSelectPaymentMethodActionGroup" stepKey="selectBraintreePaymentMethod"/> + <actionGroup ref="StorefrontBraintreeFillCardDataActionGroup" stepKey="fillCartData"/> + <waitForPageLoad stepKey="waitForFillCartData"/> + + <!-- Place order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!-- Go to Configuration > Sales --> + <actionGroup ref="AdminOpenInstantPurchaseConfigPageActionGroup" stepKey="openInstantPurchaseConfigPage"/> + + <!-- Enable Instant Purchase --> + <actionGroup ref="AdminChangeInstantPurchaseStatusActionGroup" stepKey="enableInstantPurchase"/> + + <!-- Switch to specific store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToSpecificStoreView"> + <argument name="scopeName" value="customStoreEN.name"/> + </actionGroup> + + <!-- Change button text on a single store view --> + <actionGroup ref="AdminChangeInstantPurchaseButtonTextActionGroup" stepKey="changeInstantPurchaseButtonText"> + <argument name="buttonText" value="Quick Buy"/> + </actionGroup> + + <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address on given store view: + 1. Go to Storefront page --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> + + <!-- 2. Switch store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchStoreView"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + + <!-- 3. Assert customer is logged with saved card with address --> + <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openStorefrontCustomerStoredPaymentMethodsPage"/> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod"/> + <actionGroup ref="StorefrontCustomerAccountCheckTab" stepKey="goToAddressBook"> + <argument name="tabName" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="checkShippingAddress"/> + + <!-- Open product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Quick Buy button shows up. Clicking it opens review popup --> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeQuickBuyButton"> + <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> + <argument name="userInput" value="Quick Buy"/> + </actionGroup> + <click selector="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}" stepKey="clickQuickBuyButton"/> + <waitForElementVisible selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" stepKey="waitForPopUpTitleVisible"/> + <see selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" userInput="Instant Purchase Confirmation" stepKey="seeReviewPopUp"/> + <click selector="{{StorefrontInstantPurchasePopupSection.cancel}}" stepKey="closeModalPopup"/> + <waitForPageLoad stepKey="waitForClosing"/> + + <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address on a store view for which description was not changed + 1. New customer session should be started to verify changes --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- 2. Switch store view which description was not changed --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchToSecondStoreView"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <!-- 3. Assert customer is logged with saved card and address --> + <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openStorefrontCustomerPaymentMethodsPage"/> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertPaymentMethod"/> + <actionGroup ref="StorefrontCustomerAccountCheckTab" stepKey="openAddressBook"> + <argument name="tabName" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="seeShippingAddress"/> + + <!-- Open product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openCreatedProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Instant Purchase button shows up. Clicking it opens review popup. --> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeInstantPurchaseButton"> + <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> + <argument name="userInput" value="Instant Purchase"/> + </actionGroup> + <click selector="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}" stepKey="clickInstantPurchaseButton"/> + <waitForElementVisible selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" stepKey="waitForPopUpVisible"/> + <see selector="{{StorefrontInstantPurchasePopupSection.modalTitle}}" userInput="Instant Purchase Confirmation" stepKey="seeReviewPopUpTitle"/> + <click selector="{{StorefrontInstantPurchasePopupSection.cancel}}" stepKey="closeModalPopUp"/> + <waitForPageLoad stepKey="waitForModalClosing"/> + + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="createdCustomerLogout"/> + + <!-- Return to configuration and disable Instant Purchase in a specific store view --> + <actionGroup ref="AdminOpenInstantPurchaseConfigPageActionGroup" stepKey="openInstantPurchasePage"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToFirstSpecificStoreView"> + <argument name="scopeName" value="customStoreEN.name"/> + </actionGroup> + <actionGroup ref="AdminChangeInstantPurchaseStatusActionGroup" stepKey="disableInstantPurchase"> + <argument name="status" value="0"/> + </actionGroup> + + <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address in the specific store view + 1. New customer session should be started to verify changes --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCreatedCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- 2. Switch store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + + <!-- 3. Assert customer is logged with saved card and address --> + <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="openCustomerPaymentMethodsPage"/> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCartPaymentMethod"/> + <actionGroup ref="StorefrontCustomerAccountCheckTab" stepKey="goToAddressBookPage"> + <argument name="tabName" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="assertCustomerShippingAddress"/> + + <!-- Open product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductIndexPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Quick Buy button is not visible --> + <actionGroup ref="AssertStorefrontElementInvisibleActionGroup" stepKey="dontSeeQuickBuyButton"> + <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> + <argument name="userInput" value="Quick Buy"/> + </actionGroup> + + <!-- Verify changes on the front-end by opening a simple product as a logged in customer with saved card and address in the other store view + 1. New customer session should be started to verify changes --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLoginToStorefront"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- 2. Switch store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToSecondStoreView"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <!-- 3. Assert customer is logged with saved card and address --> + <actionGroup ref="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup" stepKey="goToStorefrontCustomerPaymentMethodsPage"/> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCardPaymentMethod"/> + <actionGroup ref="StorefrontCustomerAccountCheckTab" stepKey="openAddressBookPage"> + <argument name="tabName" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="seeCustomerShippingAddress"/> + + <!-- Open product page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Instant Purchase button is still visible --> + <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeInstantPurchaseBtn"> + <argument name="selector" value="{{StorefrontInstantPurchaseSection.instantPurchaseButton}}"/> + <argument name="userInput" value="Instant Purchase"/> + </actionGroup> + + <!-- Delete store views --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml new file mode 100644 index 0000000000000..256c59ae05c20 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DefaultBillingAddressShouldBeCheckedOnPaymentPageTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="The default billing address should be used on checkout"/> + <description value="Default billing address should be preselected on payments page on checkout if it exist"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98974"/> + <useCaseId value="MAGETWO-72961"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefrontAccount"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout from customer account--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + </after> + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="US_Address_CA"/> + </actionGroup> + <dontSeeElement selector="{{CheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + <!--Select Shipping Rate "Flat Rate" and click "Next" button--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <waitForLoadingMaskToDisappear stepKey="waitForShippingMethodMaskDisappear"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> + <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressSameAsShipping}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml new file mode 100644 index 0000000000000..b72f238565fd6 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="IdentityOfDefaultBillingAndShippingAddressTest"> + <annotations> + <features value="Customer"/> + <title value="Checking assignment of default billing address after placing an order"/> + <description value="In 'Address book' field 'Default Billing Address' should be the same as 'Default Shipping Address'"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94336"/> + <stories value="MAGETWO-73604: New address is not marked as 'Default Billing'"/> + <group value="customer"/> + </annotations> + + <before> + <!--Create product--> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + + <!--Go to Storefront--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + + <!-- Fill out form for a new user with address --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="Simple_US_NY_Customer"/> + </actionGroup> + + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <!--Proceed to shipment--> + <amOnPage url="{{CheckoutPage.url}}/" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForShippingSection"/> + + <!--Fill shipment form--> + <actionGroup ref="LoggedInUserCheckoutFillingShippingSectionActionGroup" stepKey="checkoutFillingShippingSection" > + <argument name="customerVar" value="Simple_US_NY_Customer" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + + <!--Fill cart data--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrderPayment" /> + + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeorder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <!--Go To My Account--> + <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="goToMyAccountPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!--Assert That Shipping And Billing Address are the same--> + <actionGroup ref="AssertThatShippingAndBillingAddressTheSame" stepKey="assertThatShippingAndBillingAddressTheSame"/> + + <after> + <!--Delete created Product--> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <!--Delete created customer--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="RemoveCustomerFromAdminActionGroup" stepKey="removeCreatedCustomer"> + <argument name="customer" value="Simple_US_NY_Customer"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml new file mode 100644 index 0000000000000..7563df33b471e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontApplyPromoCodeDuringCheckoutTest"> + <annotations> + <features value="OnePageCheckout"/> + <stories value="OnePageCheckout with Promo Code"/> + <title value="Storefront apply promo code during checkout test"/> + <description value="Apply promo code during checkout for physical product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8316"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct3" stepKey="createProduct"> + <field key="price">10.00</field> + </createData> + + <!-- Create cart price rule --> + <createData entity="ActiveSalesRuleForNotLoggedIn" stepKey="createCartPriceRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + </before> + <after> + <!-- Delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Delete sales rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + + <!-- Admin log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Storefront as Guest and add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!-- Fill all required fields with valid data and select Flat Rate, price = 5, shipping --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!-- Click Apply Discount Code: section is expanded. Input promo code, apply and see success message --> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCoupon"> + <argument name="discountCode" value="$$createCouponForCartPriceRule.code$$"/> + </actionGroup> + + <!-- Apply button is disappeared --> + <dontSeeElement selector="{{StorefrontDiscountSection.applyCodeBtn}}" stepKey="dontSeeApplyButton"/> + + <!-- Cancel coupon button is appeared --> + <seeElement selector="{{StorefrontDiscountSection.CancelCouponBtn}}" stepKey="seeCancelCouponButton"/> + + <!-- Order summary contains information about applied code --> + <seeElement selector="{{CheckoutPaymentSection.discount}}" stepKey="seeDiscountCouponInSummaryBlock"/> + <see selector="{{CheckoutPaymentSection.discountPrice}}" userInput="-$5.00" stepKey="seeDiscountPrice"/> + + <!-- Select payment solution --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="clickCheckMoneyOrderPayment"/> + + <!-- Place Order: order is successfully placed --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Verify total on order page --> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForAdminOrderPageLoad"/> + <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$$createProduct.price$$" stepKey="checkTotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest.xml new file mode 100644 index 0000000000000..693d5c532e56b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCartItemsCountDisplayItemsQuantities"> + <annotations> + <stories value="Checkout order summary has wrong item count"/> + <title value="Checkout order summary has wrong item count - display items quantities"/> + <description value="Items count in shopping cart and on checkout page should be consistent with settings 'checkout/cart_link/use_qty'"/> + <testCaseId value="MC-18286"/> + <severity value="CRITICAL"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Set Display Cart Summary to display items quantities--> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="setDisplayCartSummary"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="category1"/> + <!--Create product1--> + <createData entity="_defaultProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <!--Create product2--> + <createData entity="_defaultProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="resetDisplayCartSummary"/> + </after> + + <!-- Add simpleProduct1 to cart --> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct1.custom_attributes[url_key]$)}}" stepKey="amOnProduct1Page"/> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addProduct1ToCart"> + <argument name="productName" value="$$simpleProduct1.name$$"/> + <argument name="quantity" value="2"/> + </actionGroup> + <!-- Add simpleProduct2 to cart --> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct2.custom_attributes[url_key]$)}}" stepKey="amOnProduct2Page"/> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addProduct2ToCart"> + <argument name="productName" value="$$simpleProduct2.name$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + + <!-- Open Mini Cart --> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> + + <!-- Assert Products Count in Mini Cart --> + <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> + <argument name="productCount" value="3"/> + <argument name="productCountText" value="3 Items in Cart"/> + </actionGroup> + <!-- Assert Products Count on checkout page --> + <actionGroup ref="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup" stepKey="assertProductCountOnCheckoutPage"> + <argument name="itemsText" value="3 Items in Cart"/> + </actionGroup> + </test> + <test name="StorefrontCartItemsCountDisplayUniqueItems" extends="StorefrontCartItemsCountDisplayItemsQuantities"> + <annotations> + <stories value="Checkout order summary has wrong item count"/> + <title value="Checkout order summary has wrong item count - display unique items"/> + <description value="Items count in shopping cart and on checkout page should be consistent with settings 'checkout/cart_link/use_qty'"/> + <testCaseId value="MC-18286"/> + <severity value="CRITICAL"/> + <group value="checkout"/> + </annotations> + + <!-- Assert Products Count in Mini Cart --> + <actionGroup ref="StorefrontAssertMiniCartItemCountActionGroup" stepKey="assertProductCountAndTextInMiniCart"> + <argument name="productCount" value="2"/> + <argument name="productCountText" value="2 Items in Cart"/> + </actionGroup> + <!-- Assert Products Count on checkout page --> + <actionGroup ref="StorefrontCheckoutAndAssertOrderSummaryDisplayActionGroup" stepKey="assertProductCountOnCheckoutPage"> + <argument name="itemsText" value="2 Items in Cart"/> + </actionGroup> + + <before> + <!--Set Display Cart Summary to display items quantities--> + <magentoCLI command="config:set {{DisplayUniqueItems.path}} {{DisplayUniqueItems.value}}" stepKey="setDisplayCartSummary"/> + </before> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml new file mode 100644 index 0000000000000..32cc254543bca --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckCustomerInfoCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check order customer information created by guest"/> + <title value="Check Order Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13839"/> + <useCaseId value="MAGETWO-95182"/> + <group value="checkout"/> + <group value="customer"/> + <group value="sales"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + <deleteData createDataKey="createCategory" stepKey="deleteCategory" /> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutFromStorefront" /> + </after> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="navigateToProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="typePassword"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="typeConfirmationPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="Thank you for registering" stepKey="verifyAccountCreated"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <amOnPage url="{{AdminOrderDetailsPage.url('$grabOrderNumber')}}" stepKey="navigateToOrderPage"/> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminOrderDetailsInformationSection.customerName}}" stepKey="seeCustomerName"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutSpecificDestinationsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutSpecificDestinationsTest.xml new file mode 100644 index 0000000000000..69f63d3548b32 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutSpecificDestinationsTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCheckoutSpecificDestinationsTest"> + <annotations> + <title value="Check that top destinations can be removed after a selection was previously saved"/> + <stories value="MAGETWO-87971: Top destinations cannot be removed after a selection was previously saved"/> + <description value="Check that top destinations can be removed after a selection was previously saved"/> + <features value="Checkout"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-94906"/> + <group value="Checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Select top destinations countries--> + <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> + <argument name="countries" value="TopDestinationCountries"/> + </actionGroup> + + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!--Add product to cart--> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + + <!--Verify country options in checkout top destination section--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> + <argument name="country" value="Bahamas"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <!--Unselect top destinations countries--> + <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> + <argument name="countries" value="TopDestinationCountries"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad2"/> + + <!--Verify country options is shown by default--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> + <argument name="country" value="Afghanistan"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml new file mode 100644 index 0000000000000..11aae024d2e0a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutTotalsSortOrderInCartTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckoutTotalsSortOrderInCartTest"> + <annotations> + <title value="Checkout Totals Sort Order configuration and displaying in cart"/> + <stories value="MAGETWO-89397: Wrong Checkout Totals Sort Order in cart"/> + <description value="Checkout Totals Sort Order configuration and displaying in cart"/> + <features value="Checkout"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-96635"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SaleRule50PercentDiscountNoCoupon" stepKey="createCartRule"/> + <createData entity="CheckoutShippingTotalsSortOrder" stepKey="setConfigShippingTotalsSortOrder"/> + </before> + + <after> + <deleteData createDataKey="createCartRule" stepKey="deleteCartRule"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <createData entity="DefaultTotalsSortOrder" stepKey="setDefaultTotalsSortOrder"/> + </after> + + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <actionGroup ref="VerifyDiscountAmount" stepKey="verifyDiscountAmount"> + <argument name="expectedDiscount" value="-$100"/> + </actionGroup> + + <see userInput="Shipping (Flat Rate - Fixed)" selector="{{StorefrontCheckoutCartSummarySection.totalsElementByPosition('3')}}" stepKey="assertElementPosition"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml new file mode 100644 index 0000000000000..374ce8dbb425c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Admin"/> + <title value="Customer Checkout"/> + <description value="Should be able to place an order as a customer."/> + <severity value="CRITICAL"/> + <testCaseId value="#"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="simpleproduct1"> + <requiredEntity createDataKey="simplecategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleuscustomer" stepKey="deleteCustomer"/> + </after> + + <amOnPage stepKey="s1" url="customer/account/login/"/> + <fillField stepKey="s3" userInput="$$simpleuscustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="s5" userInput="$$simpleuscustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="s7" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <waitForPageLoad stepKey="s9"/> + + <amOnPage stepKey="s11" url="/$$simplecategory.name$$.html" /> + <moveMouseOver stepKey="s15" selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" /> + <click stepKey="s17" selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" /> + <waitForElementVisible stepKey="s21" selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" /> + <see stepKey="s23" selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$simpleproduct1.name$$ to your shopping cart."/> + <see stepKey="s25" selector="{{StorefrontMiniCartSection.quantity}}" userInput="1" /> + <click stepKey="s27" selector="{{StorefrontMiniCartSection.show}}" /> + <click stepKey="s31" selector="{{StorefrontMiniCartSection.goToCheckout}}" /> + <waitForPageLoad stepKey="s33"/> + <waitForLoadingMaskToDisappear stepKey="s34"/> + <click stepKey="s35" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> + <waitForElement stepKey="s36" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> + <click stepKey="s37" selector="{{CheckoutShippingMethodsSection.next}}" /> + <waitForPageLoad stepKey="s39"/> + <waitForElement stepKey="s41" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> + <see stepKey="s47" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" /> + <click stepKey="s49" selector="{{CheckoutPaymentSection.placeOrder}}" /> + <waitForPageLoad stepKey="s51"/> + <grabTextFrom stepKey="s53" selector="{{CheckoutSuccessMainSection.orderNumber22}}"/> + <see stepKey="s55" selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" /> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <amOnPage stepKey="s67" url="{{AdminOrdersPage.url}}"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <fillField stepKey="s77" selector="{{OrdersGridSection.search}}" userInput="{$s53}" /> + <waitForPageLoad stepKey="s78"/> + + <click stepKey="s81" selector="{{OrdersGridSection.submitSearch22}}" /> + <waitForPageLoad stepKey="s831"/> + <click stepKey="s84" selector="{{OrdersGridSection.firstRow}}" /> + <see stepKey="s85" selector="{{OrderDetailsInformationSection.orderStatus}}" userInput="Pending" /> + <see stepKey="s87" selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="Customer" /> + <see stepKey="s89" selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="$$simpleuscustomer.email$$" /> + <see stepKey="s91" selector="{{OrderDetailsInformationSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" /> + <see stepKey="s93" selector="{{OrderDetailsInformationSection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}}" /> + <see stepKey="s95" selector="{{OrderDetailsInformationSection.itemsOrdered}}" userInput="$$simpleproduct1.name$$" /> + <amOnPage stepKey="s96" url="{{AdminCustomerPage.url}}"/> + <waitForPageLoad stepKey="s97"/> + <waitForElementVisible selector="{{AdminCustomerFiltersSection.filtersButton}}" time="30" stepKey="waitFiltersButton"/> + <click stepKey="s98" selector="{{AdminCustomerFiltersSection.filtersButton}}"/> + <fillField stepKey="s99" selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$simpleuscustomer.email$$"/> + <click stepKey="s100" selector="{{AdminCustomerFiltersSection.apply}}"/> + <click stepKey="s101" selector="{{AdminCustomerGridSection.firstRowEditLink}}"/> + <click stepKey="s102" selector="{{AdminEditCustomerInformationSection.orders}}"/> + <see stepKey="s103" selector="{{AdminEditCustomerOrdersSection.orderGrid}}" userInput="{$s53}" /> + <see stepKey="s104" selector="{{AdminEditCustomerOrdersSection.orderGrid}}" userInput="$$simpleuscustomer.firstname$$ $$simpleuscustomer.lastname$$" /> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml new file mode 100644 index 0000000000000..fef6b9a203735 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest" + extends="StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest"> + <annotations> + <stories value="Checkout via the Storefront"/> + <title value="Checkout via Customer Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Customer with restricted countries for payment."/> + <testCaseId value="MC-10831"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"/> + </before> + <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + </after> + + <remove keyForRemoval="guestCheckoutFillingShippingSection"/> + <remove keyForRemoval="guestCheckoutFillingShippingSectionUK"/> + <remove keyForRemoval="guestPlaceOrder"/> + + <!-- Login as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" before="goToProductPage" stepKey="customerLogin"> + <argument name="customer" value="$$createSimpleUsCustomer$$" /> + </actionGroup> + + <!-- Select address and go to payments page--> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{US_Address_TX.state}}" after="shippingStepIsOpened" stepKey="seeRegion" /> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" after="seeRegion" stepKey="waitNextButton"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" after="waitNextButton" stepKey="selectShippingMethod"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" after="selectShippingMethod" stepKey="clickNextButton" /> + <waitForPageLoad after="clickNextButton" stepKey="waitForPaymentStep"/> + <selectOption selector="{{CheckoutPaymentSection.billingAddressSelectShared}}" userInput="New Address" after="uncheckBillingAddressSameAsShippingCheckCheckBox" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitBillingAddressForm"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceorder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxForVirtulQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxForVirtulQuoteTest.xml new file mode 100644 index 0000000000000..677146f5da05d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxForVirtulQuoteTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerCheckoutWithTaxForVirtulQuoteTest"> + <annotations> + <features value="Tax rules creation"/> + <stories value="Create a Tax rules via Admin"/> + <title value="Tax for virtual quote is recalculated according to inputted data on Checkout flow for Customer"/> + <description value="Tax for virtual quote is recalculated according to inputted data on Checkout flow for Customer"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78902"/> + <group value="recalculatedTaxVirtual"/> + <group value="checkoutTax"/> + </annotations> + <before> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <createData entity="TaxConfig" stepKey="configTaxSettings"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <field key="price">40.00</field> + </createData> + <createData entity="Simple_US_NY_Customer" stepKey="simpleUsNyCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + </before> + <after> + <createData entity="DefaultTaxConfig" stepKey="defaultTaxSettings"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> + <deleteData createDataKey="simpleUsNyCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$simpleUsNyCustomer$$" /> + </actionGroup> + <!-- Step 2: Add virtual product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="viewProduct"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddVirtualProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicartActionGroup"/> + <seeElement selector="{{CheckoutPaymentSection.isPaymentSection}}" stepKey="seePaymentStep"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="seeCartSubtotalCa" /> + <see userInput="$3.35" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTaxInfo" /> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButton" /> + <see userInput="US-NY-*-Rate 1 (8.375%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfoRatesNy" /> + <see userInput="$43.35" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeTotalInclTaxNy"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeTotalExclTaxNy"/> + <!-- Step 4: Select payment --> + <!-- Step 5: Click Edit for and select new address value --> + <click selector="{{CheckoutPaymentSection.editAddressButton}}" stepKey="clickEditButton"/> + <selectOption selector="{{CheckoutPaymentSection.newAddressSelect}}" userInput="New Address" stepKey="selectNewAddress"/> + <!-- Step 6: Fill form with valid data and set: California --> + <actionGroup ref="LoggedInUserCheckoutFillNewBillingAddressActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="US_Address_CA" /> + </actionGroup> + <!-- Step 7: Click update --> + <click selector="{{CheckoutPaymentSection.update}}" stepKey="clickUpdateButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading4" /> + <see userInput="California" selector="{{CheckoutPaymentSection.billingAddress}}" stepKey="seeBillingCa"/> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButtonCa" /> + <see userInput="US-CA-*-Rate 1 (8.25%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfoRatesCa" /> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml new file mode 100644 index 0000000000000..7dba25812f0dd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerCheckoutWithTaxTest"> + <annotations> + <features value="TaxIsRecalculatedAccordingToInputtedDataOnCheckoutFlowForGuestTest"/> + <stories value="Create order in store front with taxes"/> + <title value="Tax is recalculated according to inputted data on Checkout flow for Customer"/> + <description value="Tax is recalculated according to inputted data on Checkout flow for Customer"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78903"/> + <group value="recalculatedTax"/> + <group value="checkoutTax"/> + </annotations> + <before> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <createData entity="TaxConfig" stepKey="createConf"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <field key="price">10.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_NY_Customer" stepKey="simpleUsNyCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + </before> + <after> + <createData entity="DefaultTaxConfig" stepKey="defaultConf"/> + <deleteData createDataKey="createCategory" stepKey="deleteMyNewCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="simpleUsNyCustomer" stepKey="deleteMyNewCustomer"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$simpleUsNyCustomer$$" /> + </actionGroup> + <!-- Step 2: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="viewProduct"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicartActionGroup"/> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened"/> + <see stepKey="seeRegion" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="New York"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <!-- Step 4: Select Flat Rate as shipping --> + <click selector="{{CheckoutShippingMethodsSection.flatRate}}" stepKey="selectShippingMethod"/> + <!-- Step 5: Go Next --> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitPaymentStep"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="paymentStepIsOpened"/> + <see userInput="$0.84" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTax"/> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButton" /> + <see userInput="US-NY-*-Rate 1 (8.375%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeOrderTaxRate"/> + <see userInput="$15.84" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeOrderPriceIncl"/> + <see userInput="$15.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeOrderPriceExcl"/> + <!-- Step 6: Go to the previous step - Shipping --> + <click selector="{{CheckoutPaymentSection.goToShipping}}" stepKey="clickGoToShipping" /> + <waitForPageLoad stepKey="waitForPage"/> + <!-- Step 7: Click New Address button --> + <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="clickAddNewAddress"/> + <!-- Step 8: Fill form with valid data and set: California --> + <!-- Step 9: Click Save Address --> + <actionGroup ref="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="US_Address_CA"/> + </actionGroup> + <!-- Step 10: Go next --> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <waitForPageLoad stepKey="waitPaymentStep2"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="payment2StepIsOpened"/> + <see userInput="$0.83" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTaxPrice2"/> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButton2" /> + <see userInput="US-CA-*-Rate 1 (8.25%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeOrderTaxRate2"/> + <see userInput="$15.83" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeOrderPriceIncl2"/> + <see userInput="$15.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeOrderPriceExcl2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml new file mode 100644 index 0000000000000..cc22fe796faa6 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerLoginDuringCheckoutTest"> + <annotations> + <features value="OnePageCheckout"/> + <stories value="Customer Login during checkout"/> + <title value="Storefront customer login during checkout test"/> + <description value="Logging during checkout for customer without addresses in address book"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8120"/> + <group value="OnePageCheckout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Customer log out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + + <!-- Delete customer --> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="CustomerEntityOne.email"/> + </actionGroup> + + <!-- Logout admin --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Go to Storefront as Guest and create new account --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createNewCustomerAccount"/> + + <!-- Sign Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + + <!-- Add simple product to cart as Guest --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Go to Checkout page --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + + <!-- Input in field email and password for newly created customer; click Login button --> + <actionGroup ref="StorefrontLoginCustomerOnCheckoutPageActionGroup" stepKey="customerLogin"> + <argument name="customerEmail" value="{{CustomerEntityOne.email}}"/> + <argument name="customerPassword" value="{{CustomerEntityOne.password}}"/> + </actionGroup> + + <!-- Shipping form is pre-filed with first name and last name --> + <seeInField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstNameInField"/> + <seeInField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastNameInField"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml new file mode 100644 index 0000000000000..f2c41e0a08763 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Customer Checkout"/> + <description value="Customer can place order with new addresses."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77181"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!--Go to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> + <argument name="customerAddress" value="UK_Default_Address"/> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Click "Save Addresses" --> + <click selector="{{CheckoutShippingSection.addSaveButton}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <dontSeeElement selector="{{CheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + + <!--Select Shipping Rate "Flat Rate"--> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + + <!--Click "Edit" for the new address and clear required fields--> + <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> + <actionGroup ref="clearCheckoutAddressPopupFieldsActionGroup" stepKey="clearRequiredFields"> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Close Popup and click next--> + <click selector="{{CheckoutAddressPopupSection.closeAddressModalPopup}}" stepKey="closePopup"/> + <waitForPageLoad stepKey="waitForPopupClosed"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + + <!--Refresh Page and Place Order--> + <reloadPage stepKey="reloadPage"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="grabOrderNumber"/> + + <!--Verify New addresses in Customer's Address Book--> + <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToCustomerAddressBook"/> + <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddresses"/> + <!--Order review page has address that was created during checkout--> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> + <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml new file mode 100644 index 0000000000000..bebb7be6c9b15 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutDataPersistTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-95067: Checkout data (shipping address etc) not persistant after cart update"/> + <title value="Check that checkout data persist after cart update"/> + <description value="Checkout data should be persist after updating cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-96670"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Fill shipping address --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <seeInField selector="{{GuestCheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="assertGuestEmail"/> + <seeInField selector="{{GuestCheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="assertGuestFirstName"/> + <seeInField selector="{{GuestCheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="assertGuestLastName"/> + <seeInField selector="{{GuestCheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="assertGuestStreet"/> + <seeInField selector="{{GuestCheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="assertGuestCity"/> + <seeInField selector="{{GuestCheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="assertGuestRegion"/> + <seeInField selector="{{GuestCheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="assertGuestPostcode"/> + <seeInField selector="{{GuestCheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="assertGuestTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml new file mode 100644 index 0000000000000..f6a428ab08bfb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutForSpecificCountriesTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutForSpecificCountriesTest"> + <annotations> + <features value="One Page Checkout"/> + <stories value="Checkout for Specific Countries"/> + <title value="Storefront guest checkout for specific countries test"/> + <description value="Checkout flow if shipping rates are not available"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8190"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!-- Enable free shipping to specific country - Afghanistan --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFreeShippingToSpecificCountriesConfigData.path}} {{EnableFreeShippingToSpecificCountriesConfigData.value}}" stepKey="allowFreeShippingSpecificCountries"/> + <magentoCLI command="config:set {{EnableFreeShippingToAfghanistanConfigData.path}} {{EnableFreeShippingToAfghanistanConfigData.value}}" stepKey="enableFreeShippingToAfghanistan"/> + + <!-- Enable flat rate shipping to specific country - Afghanistan --> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateToSpecificCountriesConfigData.path}} {{EnableFlatRateToSpecificCountriesConfigData.value}}" stepKey="allowFlatRateSpecificCountries"/> + <magentoCLI command="config:set {{EnableFlatRateToAfghanistanConfigData.path}} {{EnableFlatRateToAfghanistanConfigData.value}}" stepKey="enableFlatRateToAfghanistan"/> + </before> + <after> + <!-- Rollback all configurations --> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + <magentoCLI command="config:set {{EnableFreeShippingToAllAllowedCountriesConfigData.path}} {{EnableFreeShippingToAllAllowedCountriesConfigData.value}}" stepKey="allowFreeShippingToAllCountries"/> + <magentoCLI command="config:set {{EnableFlatRateToAllAllowedCountriesConfigData.path}} {{EnableFlatRateToAllAllowedCountriesConfigData.value}}" stepKey="allowFlatRateToAllCountries"/> + + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Go to Storefront as Guest. Check that "Log out" button absent on page --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <waitForPageLoad stepKey="waitForPage"/> + <dontSeeElement selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="dontSeeLogOutButton"/> + + <!-- Add product to cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> + + <!-- Assert shipping methods are unavailable --> + <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontSeeFlatRateShippingMethod"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontFreeShippingMethod"/> + + <!-- Assert no quotes message --> + <actionGroup ref="AssertStoreFrontNoQuotesMessageActionGroup" stepKey="assertNoQuotesMessage"/> + + <!-- Assert Next button --> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="dontSeeNextButton"/> + + <!-- Fill form with valid data for US > California --> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectCountry"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{US_Address_CA.state}}" stepKey="selectState"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="fillPostcode"/> + + <!-- Assert shipping methods are unavailable for US > California --> + <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontSeeFlatRateShippingMtd"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="AssertStoreFrontShippingMethodUnavailableActionGroup" stepKey="dontFreeShippingMtd"/> + + <!-- Assert no quotes message for US > California --> + <actionGroup ref="AssertStoreFrontNoQuotesMessageActionGroup" stepKey="assertNoQuotesMsg"/> + + <!-- Assert Next button for US > California --> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="dontSeeNextBtn"/> + + <!-- Fill form for specific country - Afghanistan --> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="Afghanistan" stepKey="selectSpecificCountry"/> + + <!-- Assert shipping methods are available --> + <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="seeFlatRateShippingMethod"/> + <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="seeFreeShippingMethod"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + + <!-- Assert Next button is available --> + <seeElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="seeNextButton"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml new file mode 100644 index 0000000000000..249c6dafae1a3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Guest Checkout"/> + <description value="Should be able to place an order as a Guest."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-72094"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Clear filter in orders grid--> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="ordersGridClearFilters"/> + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add simple product to cart --> + <actionGroup stepKey="addProductToCart1" ref="AddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <see selector="{{StorefrontMiniCartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <!-- Navigate to checkout --> + <actionGroup stepKey="addProductNavigateToCheckout" ref="NavigateToCheckoutActionGroup"/> + <fillField selector="{{GuestCheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{GuestCheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{GuestCheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{GuestCheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{GuestCheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{GuestCheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{GuestCheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{GuestCheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{GuestCheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{GuestCheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{GuestCheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="checkPaymentMethod"/> + <waitForElement selector="{{GuestCheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <conditionalClick selector="{{GuestCheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{GuestCheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="exposeMiniCart"/> + <see selector="{{GuestCheckoutPaymentSection.cartItems}}" userInput="{{_defaultProduct.name}}" stepKey="seeProductInCart"/> + <see selector="{{GuestCheckoutPaymentSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAddress"/> + <click selector="{{GuestCheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order # is:" stepKey="seeOrderNumber"/> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="We'll email you an order confirmation with details and tracking info." stepKey="seeEmailYou"/> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <!--Navigate to orders index page and filter orders--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> + <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <see selector="{{OrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeAdminOrderStatus"/> + <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.fullname}}" stepKey="seeAdminOrderGuest"/> + <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAdminOrderEmail"/> + <see selector="{{OrderDetailsInformationSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderBillingAddress"/> + <see selector="{{OrderDetailsInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderShippingAddress"/> + <see selector="{{OrderDetailsInformationSection.itemsOrdered}}" userInput="{{_defaultProduct.name}}" stepKey="seeAdminOrderProduct"/> + </test> + <test name="StorefrontGuestCheckoutWithSidebarDisabledTest" extends="StorefrontGuestCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Guest Checkout when Cart sidebar disabled"/> + <description value="Should be able to place an order as a Guest when Cart sidebar is disabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97155"/> + <group value="checkout"/> + </annotations> + <before> + <magentoCLI command="config:set checkout/sidebar/display 0" stepKey="disableSidebar" /> + </before> + <after> + <magentoCLI command="config:set checkout/sidebar/display 1" stepKey="enableSidebar" /> + </after> + <remove keyForRemoval="addProductNavigateToCheckout" /> + <actionGroup ref="GoToCheckoutFromCartActionGroup" stepKey="guestGoToCheckoutFromCart" after="seeCartQuantity" /> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml new file mode 100644 index 0000000000000..ab04763a448bd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Checkout via Guest Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Guest with restricted countries for payment."/> + <severity value="MAJOR"/> + <testCaseId value="MC-8243"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set checkout/options/display_billing_address_on 1" stepKey="setShowBillingAddressOnPaymentPage" /> + <magentoCLI command="config:set payment/checkmo/allowspecific" arguments="1" stepKey="setAllowSpecificCountiesValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry" arguments="GB" stepKey="setSpecificCountryValue" /> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="unsetAllowSpecificCountiesValue"/> + <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="unsetSpecificCountryValue" /> + <magentoCLI command="config:set checkout/options/display_billing_address_on 0" stepKey="setDisplayBillingAddressOnPaymentMethod" /> + </after> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$)}}" stepKey="goToProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <!-- Fill US Address and verify that no payment available --> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutPaymentsActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <waitForElementVisible selector="{{CheckoutPaymentSection.noPaymentMethods}}" stepKey="waitMessage"/> + <see userInput="No Payment method available." stepKey="checkMessage"/> + + <!-- Fill UK Address and verify that payment available and checkout successful --> + <uncheckOption selector="{{CheckoutPaymentSection.billingAddressSameAsShippingShared}}" stepKey="uncheckBillingAddressSameAsShippingCheckCheckBox"/> + <waitForPageLoad stepKey="waitNewAddressBillingForm"/> + <actionGroup ref="GuestCheckoutFillNewBillingAddressWithoutEmailActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="UK_Default_Address" /> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Update')}}" stepKey="clickUpdateBillingAddressButton" /> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxForVirtulQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxForVirtulQuoteTest.xml new file mode 100644 index 0000000000000..0a808f8015722 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxForVirtulQuoteTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutWithTaxForVirtulQuoteTest"> + <annotations> + <features value="Tax rules creation"/> + <stories value="Create a Tax rules via Admin"/> + <title value="Tax for virtual quote is recalculated according to inputted data on Checkout flow for Guest"/> + <description value="Tax for virtual quote is recalculated according to inputted data on Checkout flow for Guest"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78901"/> + <group value="recalculatedTaxVirtual"/> + <group value="checkoutTax"/> + </annotations> + <before> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <createData entity="TaxConfig" stepKey="createConf"/> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <field key="price">40.00</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + </before> + <after> + <createData entity="DefaultTaxConfig" stepKey="defaultConf"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deletePreReqVirtual"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Step 1: Go to Storefront as Guest --> + <!-- Step 2: Add virtual product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="viewProduct"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddVirtualProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicartActionGroup"/> + <seeElement selector="{{CheckoutPaymentSection.isPaymentSection}}" stepKey="seePaymentStep"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="seeCartSubtotalCa" /> + <see userInput="$3.30" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTaxInfo" /> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButton" /> + <see userInput="US-CA-*-Rate 1 (8.25%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfoRatesCa" /> + <see userInput="$43.30" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeTotalInclTaxCa"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeTotalExclTaxCa"/> + <!-- Step 4: Select payment --> + <!-- Step 5: Fill required fields with valid data and click Update --> + <actionGroup ref="GuestCheckoutFillNewBillingAddressActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + <!-- Step 6: Click Update --> + <click selector="{{CheckoutPaymentSection.update}}" stepKey="clickUpdateButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading4" /> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="New York" stepKey="seeBillingNy" /> + <seeElement selector="{{CheckoutPaymentSection.editAddressButton}}" stepKey="seeEditButton"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="seeCartSubtotalNy" /> + <see userInput="$3.35" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTax" /> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButtonNy" /> + <see userInput="US-NY-*-Rate 1 (8.375%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfoRatesNy" /> + <see userInput="$43.35" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeTotalInclTaxNy"/> + <see userInput="$40.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeTotalExclTaxNy"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxTest.xml new file mode 100644 index 0000000000000..27101a29ed4d3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithTaxTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutWithTaxTest"> + <annotations> + <features value="TaxIsRecalculatedAccordingToInputtedDataOnCheckoutFlowForGuestTest"/> + <stories value="Create order in store front with taxes"/> + <title value="Tax is recalculated according to inputted data on Checkout flow for Guest"/> + <description value="Tax is recalculated according to inputted data on Checkout flow for Guest"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78904"/> + <group value="recalculatedTax"/> + <group value="checkoutTax"/> + </annotations> + <before> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <createData entity="TaxConfig" stepKey="createConf"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <field key="price">10.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> + </before> + <after> + <createData entity="DefaultTaxConfig" stepKey="defaultConf"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteNewCategory"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Step 1: Go to Storefront as Guest --> + <!-- Step 2: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="viewProduct"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicartActionGroup"/> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened" /> + <!-- Step 4: Fill all required fields with valid data --> + <!-- Step 5: Select Flat Rate as shipping(price = 5 by default) --> + <click selector="{{CheckoutShippingMethodsSection.flatRate}}" stepKey="selectShippingMethod"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="US_Address_CA" /> + </actionGroup> + <!-- Step 6: Go Next --> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="paymentStepIsOpened"/> + <see userInput="$0.83" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTax" /> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButtonCa" /> + <see userInput="US-CA-*-Rate 1 (8.25%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfoRatesCa" /> + <see userInput="$15.83" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeTaxPrice"/> + <see userInput="$15.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeOrderPriceExcl"/> + <!-- Step 7: Go to the previous step - Shipping --> + <click selector="{{CheckoutPaymentSection.goToShipping}}" stepKey="clickGoToShipping" /> + <!-- Step 8: Change data for:State/Province = New York --> + <selectOption selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{US_Address_NY.state}}" stepKey="selectRegion"/> + <!-- Step 9: Go Next --> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForNextButtonToBeClear"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPagePlaceOrder"/> + <see userInput="$0.84" selector="{{CheckoutPaymentSection.tax}}" stepKey="seeTaxInf"/> + <click selector="{{CheckoutPaymentSection.taxRateTab}}" stepKey="clickTaxTabsButtonNy"/> + <see userInput="US-NY-*-Rate 1 (8.375%)" selector="{{CheckoutPaymentSection.taxRate}}" stepKey="seeTaxInfRatesNy" /> + <see userInput="$15.84" selector="{{CheckoutPaymentSection.orderTotalInclTax}}" stepKey="seeTaxPriceNy"/> + <see userInput="$15.00" selector="{{CheckoutPaymentSection.orderTotalExclTax}}" stepKey="seeOrderPriceExclNy"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml new file mode 100644 index 0000000000000..093435b8c8f26 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontOnePageCheckoutDataWhenChangeQtyTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="One page Checkout Customer data when changing Product Qty"/> + <description value="One page Checkout Customer data when changing Product Qty"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13609"/> + <useCaseId value="MAGETWO-96711"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Add product to cart and checkout--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!--Grab customer data to check it--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> + + <!--Select shipping method and finalize checkout--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + + <!--Go to cart page, update qty and proceed to checkout--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <see userInput="Shopping Cart" stepKey="seeCartPageIsOpened"/> + <fillField selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" userInput="2" stepKey="updateProductQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCart"/> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQty"/> + <assertEquals expected="2" actual="$grabQty" stepKey="assertQty"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + + <!--Check that form is filled with customer data--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail1"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet1"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity1"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion1"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode1"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone1"/> + + <assertEquals expected="$grabEmail" actual="$grabEmail1" stepKey="assertEmail"/> + <assertEquals expected="$grabFirstName" actual="$grabFirstName1" stepKey="assertFirstName"/> + <assertEquals expected="$grabLastName" actual="$grabLastName1" stepKey="assertLastName"/> + <assertEquals expected="$grabStreet" actual="$grabStreet1" stepKey="assertStreet"/> + <assertEquals expected="$grabCity" actual="$grabCity1" stepKey="assertCity"/> + <assertEquals expected="$grabRegion" actual="$grabRegion1" stepKey="assertRegion"/> + <assertEquals expected="$grabPostcode" actual="$grabPostcode1" stepKey="assertPostcode"/> + <assertEquals expected="$grabTelephone" actual="$grabTelephone1" stepKey="assertTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml new file mode 100644 index 0000000000000..553d50c0bae64 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Persistent Data for Guest Customer with physical quote"/> + <description value="One can use Persistent Data for Guest Customer with physical quote"/> + <severity value="MAJOR"/> + <testCaseId value="MC-8176"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createProduct"> + <field key="price">10</field> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + </after> + <!-- 1. Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- 2. Go to Shopping Cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage"/> + <!-- 3. Open "Estimate Shipping and Tax" section and input data --> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection"/> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFlatRateIsPresentInCart"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFreeShippingIsPresentInCart"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 4. Select Flat Rate as shipping --> + <checkOption selector="{{StorefrontCheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterFlatRateSelection"/> + <see selector="{{StorefrontCheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> + <!-- 5. Refresh browser page (F5) --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> + <argument name="carrierCode" value="flatrate"/> + <argument name="methodCode" value="flatrate"/> + </actionGroup> + <!-- 6. Go to Checkout --> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutEstimateShippingInformationActionGroup" stepKey="assertCheckoutEstimateShippingInformationAfterGoingToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsCheckedAfterGoingToCheckout"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- 7. Change persisted data --> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United Kingdom" stepKey="changeCountryField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="KW1 7NQ" stepKey="changeZipPostalCodeField"/> + <!-- 8. Change shipping rate, select Free Shipping --> + <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="checkFreeShippingAsShippingMethod"/> + <!-- 9. Fill other fields --> + <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> + <!-- 10. Refresh browser page(F5) --> + <reloadPage stepKey="reloadCheckoutPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 11. Go back to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage1"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterGoingBackToShoppingCart"> + <argument name="customerData" value="Simple_UK_Customer_For_Shipment"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsCheckedAfterGoingBackToShoppingCart"> + <argument name="carrierCode" value="freeshipping"/> + <argument name="methodCode" value="freeshipping"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..736b5e66558d0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> + <annotations> + <features value="Checkout"/> + <title value="Checking Product name with custom store views"/> + <description value="Checking Product name in Minicart and on Checkout page with custom store views"/> + <stories value="Checkout via Guest Checkout"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14944"/> + <useCaseId value="MAGETWO-95904"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> + <!--Delete product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Delete store view--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <!--Logout from admin--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Go to created product page--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> + + <!--Switch to second store view and change the product name--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="customStore"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrolToShowNameField"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + + <!--Check simple product in minicart--> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProductNameInMiniCart1"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Switch to second store view--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!--Check simple product in minicart--> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProductNameInMiniCart2"> + <argument name="productName" value="$$createProduct.name$$-new"/> + </actionGroup> + + <!--Go to Shopping Cart--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <seeElement selector="{{CheckoutCartProductSection.productLinkByName($$createProduct.name$$-new)}}" stepKey="assertProductName"/> + + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutPage"/> + <conditionalClick selector="{{CheckoutOrderSummarySection.miniCartTab}}" dependentSelector="{{CheckoutOrderSummarySection.miniCartTab}}" visible="true" stepKey="clickItemsInCart"/> + <waitForElementVisible selector="{{CheckoutOrderSummarySection.productItemName}}" stepKey="waitForProduct"/> + <see selector="{{CheckoutOrderSummarySection.productItemName}}" userInput="$$createProduct.name$$-new" stepKey="seeProductNameAtCheckout"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml new file mode 100644 index 0000000000000..3c7a6433cf818 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRefreshPageDuringGuestCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Guest checkout"/> + <title value="Storefront refresh page during guest checkout test"/> + <description value="Address is not lost for guest checkout if guest refresh page during checkout"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8549"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create simple product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + </before> + <after> + <!-- Delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Logout admin --> + <actionGroup ref="logout" stepKey="logoutAsAdmin"/> + </after> + <!-- Add simple product to cart as Guest --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Go to Checkout page --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + + <!-- Fill email field and addresses form and go next --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!-- Refresh page --> + <reloadPage stepKey="refreshPage"/> + + <!-- Click Place Order and assert order is placed --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!-- Order review page has address that was created during checkout --> + <amOnPage url="{{AdminOrderDetailsPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage"/> + <waitForPageLoad stepKey="waitForCreatedOrderPage"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}} {{CustomerAddressSimple.city}}, {{CustomerAddressSimple.state}}, {{CustomerAddressSimple.postcode}}" stepKey="checkShippingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml new file mode 100644 index 0000000000000..7750ce0e1686a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <description value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-78596"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!-- Steps --> + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Step 2: Add virtual product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$)}}" stepKey="amOnPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Shopping Cart --> + <actionGroup ref="StorefrontViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingcart"/> + <!-- Step 4: Open Estimate Tax section --> + <click selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCountry"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> + <scrollTo selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="scrollToPostCodeField"/> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml new file mode 100644 index 0000000000000..cda8039e505a8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdatePriceInShoppingCartAfterProductSaveTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Update price in shopping cart after product save"/> + <description value="Price in shopping cart should be updated after product save with changed price"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77832"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"> + <field key="price">100</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setCustomerDataLifetime"> + <argument name="minutes" value="1"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindex"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check price--> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice"/> + + <!--Edit product price via admin panel--> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <fillField userInput="120" selector="{{AdminProductFormSection.productPrice}}" stepKey="setNewPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <closeTab stepKey="closeTab"/> + + <!--Check price--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml new file mode 100644 index 0000000000000..b0a2c0bfb7e13 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateQtyInShoppingCartAfterUpdateInMinicartTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Check updating shopping cart while updating items from minicart"/> + <description value="Check updating shopping cart while updating items from minicart"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-13626"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete product--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Delete category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!--Open Product Page--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openProductPage"/> + <!--Add product to cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Go to Shopping cart--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> + <!--Check quantity in Shopping cart--> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyFromShoppingCart"/> + <assertEquals expected="1" actual="$grabQtyFromShoppingCart" stepKey="assertQtyInShoppingCart"/> + + <!--Open minicart--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="waitForItemQuantity"/> + <pressKey selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="clearQtyField"/> + <fillField selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" userInput="5" stepKey="fillQtyField"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="waitForUpdateButton"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="clickUpdateButton"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <!--Check quantity in shopping cart after updating--> + <grabValueFrom selector="{{CheckoutCartProductSection.productQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyFromShoppingCart1"/> + <assertEquals expected="5" actual="$grabQtyFromShoppingCart1" stepKey="assertQtyInShoppingCart1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml new file mode 100644 index 0000000000000..4c690eba6c87b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectCheckout"> + <annotations> + <features value="Checkout"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Checkout Pages"/> + <description value="Verify that the Secure URL configuration applies to the Checkout pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15717"/> + <group value="checkout"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="moveMouseOverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCartButton"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForAddedToCartSuccessMessage"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$product.name$$ to your shopping cart." stepKey="seeAddedToCartSuccessMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/checkout" stepKey="goToUnsecureCheckoutURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/checkout" stepKey="seeSecureCheckoutURL"/> + <amOnUrl url="http://{$hostname}/checkout/sidebar" stepKey="goToUnsecureCheckoutSidebarURL"/> + <seeCurrentUrlEquals url="http://{$hostname}/checkout/sidebar" stepKey="seeUnsecureCheckoutSidebarURL"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/AbstractCartTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/AbstractCartTest.php index aecaf0ec9f039..1a9c5555c91c0 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/AbstractCartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/AbstractCartTest.php @@ -124,6 +124,9 @@ public function testGetTotalsCache($expectedResult, $isVirtual) $this->assertEquals($expectedResult, $model->getTotalsCache()); } + /** + * @return array + */ public function getTotalsCacheDataProvider() { return [ diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/Item/RendererTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/Item/RendererTest.php index d963fa2d76e6b..38c1288069d38 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/Item/RendererTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/Item/RendererTest.php @@ -5,8 +5,11 @@ */ namespace Magento\Checkout\Test\Unit\Block\Cart\Item; +use Magento\Catalog\Block\Product\Image; +use Magento\Catalog\Model\Product; use Magento\Checkout\Block\Cart\Item\Renderer; use Magento\Quote\Model\Quote\Item; +use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -16,17 +19,22 @@ class RendererTest extends \PHPUnit\Framework\TestCase /** * @var Renderer */ - protected $_renderer; + private $renderer; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $layout; + private $layout; /** * @var \Magento\Catalog\Block\Product\ImageBuilder|\PHPUnit_Framework_MockObject_MockObject */ - protected $imageBuilder; + private $imageBuilder; + + /** + * @var ItemResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemResolver; protected function setUp() { @@ -39,17 +47,20 @@ protected function setUp() ->getMock(); $context->expects($this->once()) ->method('getLayout') - ->will($this->returnValue($this->layout)); + ->willReturn($this->layout); $this->imageBuilder = $this->getMockBuilder(\Magento\Catalog\Block\Product\ImageBuilder::class) ->disableOriginalConstructor() ->getMock(); - $this->_renderer = $objectManagerHelper->getObject( + $this->itemResolver = $this->createMock(ItemResolverInterface::class); + + $this->renderer = $objectManagerHelper->getObject( \Magento\Checkout\Block\Cart\Item\Renderer::class, [ 'context' => $context, 'imageBuilder' => $this->imageBuilder, + 'itemResolver' => $this->itemResolver, ] ); } @@ -57,29 +68,34 @@ protected function setUp() public function testGetProductForThumbnail() { $product = $this->_initProduct(); - $productForThumbnail = $this->_renderer->getProductForThumbnail(); + $productForThumbnail = $this->renderer->getProductForThumbnail(); $this->assertEquals($product->getName(), $productForThumbnail->getName(), 'Invalid product was returned.'); } /** * Initialize product. * - * @return \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject + * @return Product|\PHPUnit_Framework_MockObject_MockObject */ protected function _initProduct() { - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + Product::class, ['getName', '__wakeup', 'getIdentities'] ); - $product->expects($this->any())->method('getName')->will($this->returnValue('Parent Product')); + $product->expects($this->any())->method('getName')->willReturn('Parent Product'); /** @var Item|\PHPUnit_Framework_MockObject_MockObject $item */ $item = $this->createMock(\Magento\Quote\Model\Quote\Item::class); - $item->expects($this->any())->method('getProduct')->will($this->returnValue($product)); + $item->expects($this->any())->method('getProduct')->willReturn($product); + + $this->itemResolver->expects($this->any()) + ->method('getFinalProduct') + ->with($item) + ->willReturn($product); - $this->_renderer->setItem($item); + $this->renderer->setItem($item); return $product; } @@ -89,14 +105,14 @@ public function testGetIdentities() $identities = [1 => 1, 2 => 2, 3 => 3]; $product->expects($this->exactly(2)) ->method('getIdentities') - ->will($this->returnValue($identities)); + ->willReturn($identities); - $this->assertEquals($product->getIdentities(), $this->_renderer->getIdentities()); + $this->assertEquals($product->getIdentities(), $this->renderer->getIdentities()); } public function testGetIdentitiesFromEmptyItem() { - $this->assertEmpty($this->_renderer->getIdentities()); + $this->assertEmpty($this->renderer->getIdentities()); } /** @@ -106,7 +122,7 @@ public function testGetIdentitiesFromEmptyItem() public function testGetProductPriceHtml() { $priceHtml = 'some price html'; - $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); @@ -117,7 +133,7 @@ public function testGetProductPriceHtml() $this->layout->expects($this->atLeastOnce()) ->method('getBlock') ->with('product.price.render.default') - ->will($this->returnValue($priceRender)); + ->willReturn($priceRender); $priceRender->expects($this->once()) ->method('render') @@ -129,9 +145,9 @@ public function testGetProductPriceHtml() 'display_minimal_price' => true, 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST ] - )->will($this->returnValue($priceHtml)); + )->willReturn($priceHtml); - $this->assertEquals($priceHtml, $this->_renderer->getProductPriceHtml($product)); + $this->assertEquals($priceHtml, $this->renderer->getProductPriceHtml($product)); } public function testGetActions() @@ -148,7 +164,7 @@ public function testGetActions() $this->layout->expects($this->once()) ->method('getChildName') - ->with($this->_renderer->getNameInLayout(), 'actions') + ->with($this->renderer->getNameInLayout(), 'actions') ->willReturn($blockNameInLayout); $this->layout->expects($this->once()) ->method('getBlock') @@ -169,14 +185,14 @@ public function testGetActions() ->method('toHtml') ->willReturn($blockHtml); - $this->assertEquals($blockHtml, $this->_renderer->getActions($itemMock)); + $this->assertEquals($blockHtml, $this->renderer->getActions($itemMock)); } public function testGetActionsWithNoBlock() { $this->layout->expects($this->once()) ->method('getChildName') - ->with($this->_renderer->getNameInLayout(), 'actions') + ->with($this->renderer->getNameInLayout(), 'actions') ->willReturn(false); /** @@ -186,25 +202,19 @@ public function testGetActionsWithNoBlock() ->disableOriginalConstructor() ->getMock(); - $this->assertEquals('', $this->_renderer->getActions($itemMock)); + $this->assertEquals('', $this->renderer->getActions($itemMock)); } public function testGetImage() { $imageId = 'test_image_id'; $attributes = []; - - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->getMock(); - - $imageMock = $this->getMockBuilder(\Magento\Catalog\Block\Product\Image::class) - ->disableOriginalConstructor() - ->getMock(); + $product = $this->createMock(Product::class); + $imageMock = $this->createMock(Image::class); $this->imageBuilder->expects($this->once()) ->method('setProduct') - ->with($productMock) + ->with($product) ->willReturnSelf(); $this->imageBuilder->expects($this->once()) ->method('setImageId') @@ -219,8 +229,8 @@ public function testGetImage() ->willReturn($imageMock); $this->assertInstanceOf( - \Magento\Catalog\Block\Product\Image::class, - $this->_renderer->getImage($productMock, $imageId, $attributes) + Image::class, + $this->renderer->getImage($product, $imageId, $attributes) ); } } diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/LinkTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/LinkTest.php index 2478270e0aec6..417c1e4295ea1 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/LinkTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/LinkTest.php @@ -82,6 +82,9 @@ public function testGetLabel($productCount, $label) $this->assertSame($label, (string)$block->getLabel()); } + /** + * @return array + */ public function getLabelDataProvider() { return [[1, 'My Cart (1 item)'], [2, 'My Cart (2 items)'], [0, 'My Cart']]; diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/ShippingTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/ShippingTest.php index e419a1535207e..6a2ffd87b1885 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/ShippingTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/ShippingTest.php @@ -98,10 +98,7 @@ public function testGetJsLayout() ->method('process') ->with($this->layout) ->willReturn($layoutProcessed); - - $this->serializer->expects($this->once())->method('serialize')->will( - $this->returnValue($jsonLayoutProcessed) - ); + $this->assertEquals( $jsonLayoutProcessed, $this->model->getJsLayout() @@ -121,9 +118,6 @@ public function testGetSerializedCheckoutConfig() { $checkoutConfig = ['checkout', 'config']; $this->configProvider->expects($this->once())->method('getConfig')->willReturn($checkoutConfig); - $this->serializer->expects($this->once())->method('serialize')->will( - $this->returnValue(json_encode($checkoutConfig)) - ); $this->assertEquals(json_encode($checkoutConfig), $this->model->getSerializedCheckoutConfig()); } diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index 88751b899d7c9..015d8ccbe928f 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Test\Unit\Block\Cart; /** + * Unit tests for Magento\Checkout\Block\Cart\Sidebar. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SidebarTest extends \PHPUnit\Framework\TestCase @@ -123,6 +125,11 @@ public function testGetTotalsHtml() $this->assertEquals($totalsHtml, $this->model->getTotalsHtml()); } + /** + * Unit test for getConfig method. + * + * @return void + */ public function testGetConfig() { $websiteId = 100; @@ -144,14 +151,15 @@ public function testGetConfig() 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, 'websiteId' => 100, - 'maxItemsToDisplay' => 8 + 'maxItemsToDisplay' => 8, + 'storeId' => null, ]; $valueMap = [ ['checkout/cart', [], $shoppingCartUrl], ['checkout', [], $checkoutUrl], ['checkout/sidebar/updateItemQty', ['_secure' => false], $updateItemQtyUrl], - ['checkout/sidebar/removeItem', ['_secure' => false], $removeItemUrl] + ['checkout/sidebar/removeItem', ['_secure' => false], $removeItemUrl], ]; $this->requestMock->expects($this->any()) @@ -161,7 +169,7 @@ public function testGetConfig() $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); - $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); $this->imageHelper->expects($this->once())->method('getFrame')->willReturn(false); diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php new file mode 100644 index 0000000000000..bff2243f30d03 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Block\Checkout; + +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Checkout\Block\Checkout\AttributeMerger; + +class AttributeMergerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CustomerRepository + */ + private $customerRepositoryMock; + + /** + * @var CustomerSession + */ + private $customerSessionMock; + + /** + * @var AddressHelper + */ + private $addressHelperMock; + + /** + * @var DirectoryHelper + */ + private $directoryHelperMock; + + /** + * @var AttributeMerger + */ + private $attributeMerger; + + /** + * @inheritdoc + */ + protected function setUp() + { + + $this->customerRepositoryMock = $this->createMock(CustomerRepository::class); + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->addressHelperMock = $this->createMock(AddressHelper::class); + $this->directoryHelperMock = $this->createMock(DirectoryHelper::class); + + $this->attributeMerger = new AttributeMerger( + $this->addressHelperMock, + $this->customerSessionMock, + $this->customerRepositoryMock, + $this->directoryHelperMock + ); + } + + /** + * Tests of element attributes merging. + * + * @param string $validationRule + * @param string $expectedValidation + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testMerge($validationRule, $expectedValidation) + { + $elements = [ + 'field' => [ + 'visible' => true, + 'formElement' => 'input', + 'label' => __('City'), + 'value' => null, + 'sortOrder' => 1, + 'validation' => [ + 'input_validation' => $validationRule, + ], + ] + ]; + + $actualResult = $this->attributeMerger->merge( + $elements, + 'provider', + 'dataScope', + [ + 'field' => + [ + 'validation' => ['length' => true], + ], + ] + ); + + $expectedResult = [ + $expectedValidation => true, + 'length' => true, + ]; + + $this->assertEquals($expectedResult, $actualResult['field']['validation']); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'email2'], + ['length', 'validate-length'], + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/LinkTest.php b/app/code/Magento/Checkout/Test/Unit/Block/LinkTest.php index 24065c1f54eb3..7db5d7ecb19fd 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/LinkTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/LinkTest.php @@ -68,6 +68,9 @@ public function testToHtml($canOnepageCheckout, $isOutputEnabled) $this->assertEquals('', $block->toHtml()); } + /** + * @return array + */ public function toHtmlDataProvider() { return [[false, true], [true, false], [false, false]]; diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Onepage/SuccessTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Onepage/SuccessTest.php index 18281494029b6..36d37d07ef752 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Onepage/SuccessTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Onepage/SuccessTest.php @@ -153,6 +153,9 @@ public function testToHtmlOrderVisibleOnFront(array $invisibleStatuses, $expecte $this->assertEquals($expectedResult, $this->block->getIsOrderVisible()); } + /** + * @return array + */ public function invisibleStatusesProvider() { return [ diff --git a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php index e47fac06d8057..b54339aa2c1d8 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php @@ -35,7 +35,7 @@ class OnepageTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $serializer; + private $serializerMock; protected function setUp() { @@ -49,7 +49,7 @@ protected function setUp() \Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class ); - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); $this->model = new \Magento\Checkout\Block\Onepage( $contextMock, @@ -57,7 +57,8 @@ protected function setUp() $this->configProviderMock, [$this->layoutProcessorMock], [], - $this->serializer + $this->serializerMock, + $this->serializerMock ); } @@ -93,9 +94,7 @@ public function testGetJsLayout() $processedLayout = ['layout' => ['processed' => true]]; $jsonLayout = '{"layout":{"processed":true}}'; $this->layoutProcessorMock->expects($this->once())->method('process')->with([])->willReturn($processedLayout); - $this->serializer->expects($this->once())->method('serialize')->will( - $this->returnValue(json_encode($processedLayout)) - ); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn($jsonLayout); $this->assertEquals($jsonLayout, $this->model->getJsLayout()); } @@ -104,9 +103,7 @@ public function testGetSerializedCheckoutConfig() { $checkoutConfig = ['checkout', 'config']; $this->configProviderMock->expects($this->once())->method('getConfig')->willReturn($checkoutConfig); - $this->serializer->expects($this->once())->method('serialize')->will( - $this->returnValue(json_encode($checkoutConfig)) - ); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn(json_encode($checkoutConfig)); $this->assertEquals(json_encode($checkoutConfig), $this->model->getSerializedCheckoutConfig()); } diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php new file mode 100644 index 0000000000000..f7721bbc58f18 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Controller\Cart; + +use Magento\Checkout\Controller\Cart\Add; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Controller\Result\RedirectFactory; +use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class AddTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var Validator|\PHPUnit_Framework_MockObject_MockObject + */ + private $formKeyValidator; + + /** + * @var RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactory; + + /** + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $messageManager; + + /** + * @var Add|\PHPUnit_Framework_MockObject_MockObject + */ + private $cartAdd; + + /** + * Init mocks for tests. + * + * @return void + */ + public function setUp() + { + $this->formKeyValidator = $this->getMockBuilder(Validator::class) + ->disableOriginalConstructor()->getMock(); + $this->resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) + ->disableOriginalConstructor()->getMock(); + $this->request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); + $this->messageManager = $this->getMockBuilder(ManagerInterface::class) + ->disableOriginalConstructor()->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->cartAdd = $this->objectManagerHelper->getObject( + Add::class, + [ + '_formKeyValidator' => $this->formKeyValidator, + 'resultRedirectFactory' => $this->resultRedirectFactory, + '_request' => $this->request, + 'messageManager' => $this->messageManager, + ] + ); + } + + /** + * Test for method execute. + * + * @return void + */ + public function testExecute() + { + $redirect = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $path = '*/*/'; + + $this->formKeyValidator->expects($this->once())->method('validate')->with($this->request)->willReturn(false); + $this->messageManager->expects($this->once())->method('addErrorMessage'); + $this->resultRedirectFactory->expects($this->once())->method('create')->willReturn($redirect); + $redirect->expects($this->once())->method('setPath')->with($path)->willReturnSelf(); + $this->assertEquals($redirect, $this->cartAdd->execute()); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php index b8f46feab0a48..93f6a3895aed1 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php @@ -6,6 +6,7 @@ namespace Magento\Checkout\Test\Unit\Controller\Cart; use Magento\Checkout\Controller\Cart\Index; +use Magento\Framework\Data\Form\FormKey\Validator; /** * Class IndexTest @@ -79,12 +80,18 @@ class CouponPostTest extends \PHPUnit\Framework\TestCase */ private $redirectFactory; + /** + * @var Validator|\PHPUnit_Framework_MockObject_MockObject + */ + private $formKeyValidatorMock; + /** * @return void */ protected function setUp() { $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); $this->quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, [ 'setCouponCode', @@ -148,6 +155,8 @@ protected function setUp() ->setMethods(['create']) ->getMock(); $this->quoteRepository = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); + $this->formKeyValidatorMock = $this->createMock(Validator::class); + $this->formKeyValidatorMock->expects($this->once())->method('validate')->willReturn(true); $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -158,22 +167,20 @@ protected function setUp() 'checkoutSession' => $this->checkoutSession, 'cart' => $this->cart, 'couponFactory' => $this->couponFactory, - 'quoteRepository' => $this->quoteRepository + 'quoteRepository' => $this->quoteRepository, + 'formKeyValidator' => $this->formKeyValidatorMock, ] ); } public function testExecuteWithEmptyCoupon() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn(''); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, ''], + ] + ); $this->cart->expects($this->once()) ->method('getQuote') @@ -184,15 +191,12 @@ public function testExecuteWithEmptyCoupon() public function testExecuteWithGoodCouponAndItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -236,7 +240,7 @@ public function testExecuteWithGoodCouponAndItems() ->willReturn('CODE'); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) @@ -248,15 +252,12 @@ public function testExecuteWithGoodCouponAndItems() public function testExecuteWithGoodCouponAndNoItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -290,7 +291,7 @@ public function testExecuteWithGoodCouponAndNoItems() ->willReturnSelf(); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) @@ -302,15 +303,12 @@ public function testExecuteWithGoodCouponAndNoItems() public function testExecuteWithBadCouponAndItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn(''); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, ''], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -344,7 +342,7 @@ public function testExecuteWithBadCouponAndItems() ->willReturnSelf(); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('You canceled the coupon code.') ->willReturnSelf(); @@ -353,15 +351,12 @@ public function testExecuteWithBadCouponAndItems() public function testExecuteWithBadCouponAndNoItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -386,7 +381,7 @@ public function testExecuteWithBadCouponAndNoItems() ->willReturn($coupon); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Index/IndexTest.php index 8d105f25465e4..04723c5894f8f 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Index/IndexTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Index/IndexTest.php @@ -1,14 +1,23 @@ <?php /** - * Test for \Magento\Checkout\Controller\Index\Index - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Test\Unit\Controller\Index; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManager; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Request\Http; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_Builder_InvocationMocker as InvocationMocker; +use PHPUnit_Framework_MockObject_Matcher_InvokedCount as InvokedCount; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Checkout\Helper\Data; +use Magento\Quote\Model\Quote; +use Magento\Framework\View\Result\Page; +use Magento\Checkout\Controller\Index\Index; +use Magento\Framework\ObjectManagerInterface; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -17,108 +26,111 @@ class IndexTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var ObjectManager */ private $objectManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $objectManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - private $dataMock; + private $data; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - private $quoteMock; + private $quote; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $contextMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $sessionMock; + private $session; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $onepageMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $layoutMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ - private $requestMock; + private $request; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $responseMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ private $redirectMock; /** - * @var \Magento\Checkout\Controller\Index\Index + * @var Index */ private $model; /** - * @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject + * @var Page|MockObject */ - protected $resultPageMock; + private $resultPage; /** * @var \Magento\Framework\View\Page\Config */ - protected $pageConfigMock; + private $pageConfigMock; /** * @var \Magento\Framework\View\Page\Title */ - protected $titleMock; + private $titleMock; /** * @var \Magento\Framework\UrlInterface */ - protected $url; + private $url; /** - * @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Controller\Result\Redirect|MockObject */ - protected $resultRedirectMock; + private $resultRedirectMock; protected function setUp() { // mock objects $this->objectManager = new ObjectManager($this); - $this->objectManagerMock = $this->basicMock(\Magento\Framework\ObjectManagerInterface::class); - $this->dataMock = $this->basicMock(\Magento\Checkout\Helper\Data::class); - $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + $this->objectManagerMock = $this->basicMock(ObjectManagerInterface::class); + $this->data = $this->basicMock(Data::class); + $this->quote = $this->createPartialMock( + Quote::class, ['getHasError', 'hasItems', 'validateMinimumAmount', 'hasError'] ); $this->contextMock = $this->basicMock(\Magento\Framework\App\Action\Context::class); - $this->sessionMock = $this->basicMock(\Magento\Customer\Model\Session::class); + $this->session = $this->basicMock(Session::class); $this->onepageMock = $this->basicMock(\Magento\Checkout\Model\Type\Onepage::class); $this->layoutMock = $this->basicMock(\Magento\Framework\View\Layout::class); - $this->requestMock = $this->basicMock(\Magento\Framework\App\RequestInterface::class); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->setMethods(['isSecure', 'getHeader']) + ->getMock(); $this->responseMock = $this->basicMock(\Magento\Framework\App\ResponseInterface::class); $this->redirectMock = $this->basicMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->resultPageMock = $this->basicMock(\Magento\Framework\View\Result\Page::class); + $this->resultPage = $this->basicMock(Page::class); $this->pageConfigMock = $this->basicMock(\Magento\Framework\View\Page\Config::class); $this->titleMock = $this->basicMock(\Magento\Framework\View\Page\Title::class); $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); @@ -130,7 +142,7 @@ protected function setUp() ->getMock(); $resultPageFactoryMock->expects($this->any()) ->method('create') - ->willReturn($this->resultPageMock); + ->willReturn($this->resultPage); $resultRedirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) ->disableOriginalConstructor() @@ -141,21 +153,21 @@ protected function setUp() ->willReturn($this->resultRedirectMock); // stubs - $this->basicStub($this->onepageMock, 'getQuote')->willReturn($this->quoteMock); - $this->basicStub($this->resultPageMock, 'getLayout')->willReturn($this->layoutMock); + $this->basicStub($this->onepageMock, 'getQuote')->willReturn($this->quote); + $this->basicStub($this->resultPage, 'getLayout')->willReturn($this->layoutMock); $this->basicStub($this->layoutMock, 'getBlock') ->willReturn($this->basicMock(\Magento\Theme\Block\Html\Header::class)); - $this->basicStub($this->resultPageMock, 'getConfig')->willReturn($this->pageConfigMock); + $this->basicStub($this->resultPage, 'getConfig')->willReturn($this->pageConfigMock); $this->basicStub($this->pageConfigMock, 'getTitle')->willReturn($this->titleMock); $this->basicStub($this->titleMock, 'set')->willReturn($this->titleMock); // objectManagerMock $objectManagerReturns = [ - [\Magento\Checkout\Helper\Data::class, $this->dataMock], + [Data::class, $this->data], [\Magento\Checkout\Model\Type\Onepage::class, $this->onepageMock], [\Magento\Checkout\Model\Session::class, $this->basicMock(\Magento\Checkout\Model\Session::class)], - [\Magento\Customer\Model\Session::class, $this->basicMock(\Magento\Customer\Model\Session::class)], + [Session::class, $this->basicMock(Session::class)], ]; $this->objectManagerMock->expects($this->any()) @@ -165,7 +177,7 @@ protected function setUp() ->willReturn($this->basicMock(\Magento\Framework\UrlInterface::class)); // context stubs $this->basicStub($this->contextMock, 'getObjectManager')->willReturn($this->objectManagerMock); - $this->basicStub($this->contextMock, 'getRequest')->willReturn($this->requestMock); + $this->basicStub($this->contextMock, 'getRequest')->willReturn($this->request); $this->basicStub($this->contextMock, 'getResponse')->willReturn($this->responseMock); $this->basicStub($this->contextMock, 'getMessageManager') ->willReturn($this->basicMock(\Magento\Framework\Message\ManagerInterface::class)); @@ -175,33 +187,82 @@ protected function setUp() // SUT $this->model = $this->objectManager->getObject( - \Magento\Checkout\Controller\Index\Index::class, + Index::class, [ 'context' => $this->contextMock, - 'customerSession' => $this->sessionMock, + 'customerSession' => $this->session, 'resultPageFactory' => $resultPageFactoryMock, 'resultRedirectFactory' => $resultRedirectFactoryMock ] ); } - public function testRegenerateSessionIdOnExecute() + /** + * Checks a case when session should be or not regenerated during the request. + * + * @param bool $secure + * @param string $referer + * @param InvokedCount $expectedCall + * @dataProvider sessionRegenerationDataProvider + */ + public function testRegenerateSessionIdOnExecute(bool $secure, string $referer, InvokedCount $expectedCall) + { + $this->data->method('canOnepageCheckout') + ->willReturn(true); + $this->quote->method('hasItems') + ->willReturn(true); + $this->quote->method('getHasError') + ->willReturn(false); + $this->quote->method('validateMinimumAmount') + ->willReturn(true); + $this->session->method('isLoggedIn') + ->willReturn(true); + $this->request->method('isSecure') + ->willReturn($secure); + $this->request->method('getHeader') + ->with('referer') + ->willReturn($referer); + + $this->session->expects($expectedCall) + ->method('regenerateId'); + $this->assertSame($this->resultPage, $this->model->execute()); + } + + /** + * Gets list of variations for generating new session. + * + * @return array + */ + public function sessionRegenerationDataProvider(): array { - //Stubs to control execution flow - $this->basicStub($this->dataMock, 'canOnepageCheckout')->willReturn(true); - $this->basicStub($this->quoteMock, 'hasItems')->willReturn(true); - $this->basicStub($this->quoteMock, 'getHasError')->willReturn(false); - $this->basicStub($this->quoteMock, 'validateMinimumAmount')->willReturn(true); - $this->basicStub($this->sessionMock, 'isLoggedIn')->willReturn(true); - - //Expected outcomes - $this->sessionMock->expects($this->once())->method('regenerateId'); - $this->assertSame($this->resultPageMock, $this->model->execute()); + return [ + [ + 'secure' => false, + 'referer' => 'https://test.domain.com/', + 'expectedCall' => self::once() + ], + [ + 'secure' => true, + 'referer' => false, + 'expectedCall' => self::once() + ], + [ + 'secure' => true, + 'referer' => 'http://test.domain.com/', + 'expectedCall' => self::once() + ], + // This is the only case in which session regeneration can be skipped + [ + 'secure' => true, + 'referer' => 'https://test.domain.com/', + 'expectedCall' => self::never() + ], + ]; } public function testOnepageCheckoutNotAvailable() { - $this->basicStub($this->dataMock, 'canOnepageCheckout')->willReturn(false); + $this->basicStub($this->data, 'canOnepageCheckout')->willReturn(false); $expectedPath = 'checkout/cart'; $this->resultRedirectMock->expects($this->once()) @@ -214,7 +275,7 @@ public function testOnepageCheckoutNotAvailable() public function testInvalidQuote() { - $this->basicStub($this->quoteMock, 'hasError')->willReturn(true); + $this->basicStub($this->quote, 'hasError')->willReturn(true); $expectedPath = 'checkout/cart'; $this->resultRedirectMock->expects($this->once()) @@ -226,23 +287,22 @@ public function testInvalidQuote() } /** - * @param \PHPUnit_Framework_MockObject_MockObject $mock + * @param MockObject $mock * @param string $method * - * @return \PHPUnit\Framework\MockObject_Builder_InvocationMocker + * @return InvocationMocker */ - private function basicStub($mock, $method) + private function basicStub($mock, $method): InvocationMocker { - return $mock->expects($this->any()) - ->method($method) - ->withAnyParameters(); + return $mock->method($method) + ->withAnyParameters(); } /** * @param string $className - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ - private function basicMock($className) + private function basicMock(string $className): MockObject { return $this->getMockBuilder($className) ->disableOriginalConstructor() diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php index 7653a51b2f9b7..3f9a84c1b1763 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php @@ -47,7 +47,11 @@ protected function setUp() $this->sidebarMock = $this->createMock(\Magento\Checkout\Model\Sidebar::class); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); $this->jsonHelperMock = $this->createMock(\Magento\Framework\Json\Helper\Data::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockForAbstractClass( \Magento\Framework\App\ResponseInterface::class, [], diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/UpdateItemQtyTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/UpdateItemQtyTest.php index e2a00c6872542..269536baa9c59 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/UpdateItemQtyTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/UpdateItemQtyTest.php @@ -5,40 +5,46 @@ */ namespace Magento\Checkout\Test\Unit\Controller\Sidebar; +use Magento\Checkout\Controller\Sidebar\UpdateItemQty; +use Magento\Checkout\Model\Sidebar; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Json\Helper\Data; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Psr\Log\LoggerInterface; class UpdateItemQtyTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Checkout\Controller\Sidebar\UpdateItemQty */ - protected $updateItemQty; + /** @var UpdateItemQty */ + private $updateItemQty; /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + private $objectManagerHelper; - /** @var \Magento\Checkout\Model\Sidebar|\PHPUnit_Framework_MockObject_MockObject */ - protected $sidebarMock; + /** @var Sidebar|\PHPUnit_Framework_MockObject_MockObject */ + private $sidebarMock; - /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $loggerMock; + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $loggerMock; - /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $jsonHelperMock; + /** @var Data|\PHPUnit_Framework_MockObject_MockObject */ + private $jsonHelperMock; - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + /** @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $requestMock; - /** @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $responseMock; + /** @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $responseMock; protected function setUp() { - $this->sidebarMock = $this->createMock(\Magento\Checkout\Model\Sidebar::class); - $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->jsonHelperMock = $this->createMock(\Magento\Framework\Json\Helper\Data::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->sidebarMock = $this->createMock(Sidebar::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->jsonHelperMock = $this->createMock(Data::class); + $this->requestMock = $this->createMock(RequestInterface::class); $this->responseMock = $this->getMockForAbstractClass( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, [], '', false, @@ -49,7 +55,7 @@ protected function setUp() $this->objectManagerHelper = new ObjectManagerHelper($this); $this->updateItemQty = $this->objectManagerHelper->getObject( - \Magento\Checkout\Controller\Sidebar\UpdateItemQty::class, + UpdateItemQty::class, [ 'sidebar' => $this->sidebarMock, 'logger' => $this->loggerMock, @@ -60,6 +66,9 @@ protected function setUp() ); } + /** + * Tests execute action. + */ public function testExecute() { $this->requestMock->expects($this->at(0)) @@ -113,6 +122,9 @@ public function testExecute() $this->assertEquals('json represented', $this->updateItemQty->execute()); } + /** + * Tests with localized exception. + */ public function testExecuteWithLocalizedException() { $this->requestMock->expects($this->at(0)) @@ -157,6 +169,9 @@ public function testExecuteWithLocalizedException() $this->assertEquals('json represented', $this->updateItemQty->execute()); } + /** + * Tests with exception. + */ public function testExecuteWithException() { $this->requestMock->expects($this->at(0)) @@ -207,4 +222,31 @@ public function testExecuteWithException() $this->assertEquals('json represented', $this->updateItemQty->execute()); } + + /** + * Tests execute with float item quantity. + */ + public function testExecuteWithFloatItemQty() + { + $itemId = '1'; + $floatItemQty = '2.2'; + + $this->requestMock->expects($this->at(0)) + ->method('getParam') + ->with('item_id', null) + ->willReturn($itemId); + $this->requestMock->expects($this->at(1)) + ->method('getParam') + ->with('item_qty', null) + ->willReturn($floatItemQty); + + $this->sidebarMock->expects($this->once()) + ->method('checkQuoteItem') + ->with($itemId); + $this->sidebarMock->expects($this->once()) + ->method('updateQuoteItem') + ->with($itemId, $floatItemQty); + + $this->updateItemQty->execute(); + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Stub/OnepageStub.php b/app/code/Magento/Checkout/Test/Unit/Controller/Stub/OnepageStub.php index 26771c1531267..1a8fecd8356bb 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Stub/OnepageStub.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Stub/OnepageStub.php @@ -8,6 +8,9 @@ class OnepageStub extends \Magento\Checkout\Controller\Onepage { + /** + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void + */ public function execute() { // Empty method stub for test diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php index 75e181cbabd08..9f718f00b4b9d 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php @@ -7,6 +7,8 @@ namespace Magento\Checkout\Test\Unit\CustomerData; /** + * Unit tests for Magento\Checkout\CustomerData\Cart. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CartTest extends \PHPUnit\Framework\TestCase @@ -78,6 +80,11 @@ public function testIsGuestCheckoutAllowed() $this->assertTrue($this->model->isGuestCheckoutAllowed()); } + /** + * Unit test for getSectionData method. + * + * @return void + */ public function testGetSectionData() { $summaryQty = 100; @@ -113,7 +120,7 @@ public function testGetSectionData() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, @@ -162,6 +169,7 @@ public function testGetSectionData() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null, ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } @@ -199,7 +207,7 @@ public function testGetSectionDataWithCompositeProduct() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); $this->checkoutHelperMock->expects($this->once()) @@ -265,6 +273,7 @@ public function testGetSectionDataWithCompositeProduct() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null, ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php index 8a7c2e951dd72..86825f6ca1da0 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php @@ -5,12 +5,14 @@ */ namespace Magento\Checkout\Test\Unit\CustomerData; +use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; + class DefaultItemTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Checkout\CustomerData\DefaultItem */ - protected $model; + private $model; /** * @var \Magento\Catalog\Helper\Image @@ -22,6 +24,14 @@ class DefaultItemTest extends \PHPUnit\Framework\TestCase */ private $configurationPool; + /** + * @var ItemResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemResolver; + + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -35,12 +45,14 @@ protected function setUp() $checkoutHelper = $this->getMockBuilder(\Magento\Checkout\Helper\Data::class) ->setMethods(['formatPrice'])->disableOriginalConstructor()->getMock(); $checkoutHelper->expects($this->any())->method('formatPrice')->willReturn(5); + $this->itemResolver = $this->createMock(ItemResolverInterface::class); $this->model = $objectManager->getObject( \Magento\Checkout\CustomerData\DefaultItem::class, [ 'imageHelper' => $this->imageHelper, 'configurationPool' => $this->configurationPool, - 'checkoutHelper' => $checkoutHelper + 'checkoutHelper' => $checkoutHelper, + 'itemResolver' => $this->itemResolver, ] ); } @@ -72,6 +84,10 @@ public function testGetItemData() $this->imageHelper->expects($this->any())->method('getWidth')->willReturn(100); $this->imageHelper->expects($this->any())->method('getHeight')->willReturn(100); $this->configurationPool->expects($this->any())->method('getByProductType')->willReturn($product); + $this->itemResolver->expects($this->any()) + ->method('getFinalProduct') + ->with($item) + ->willReturn($product); $itemData = $this->model->getItemData($item); $this->assertArrayHasKey('options', $itemData); diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/CartTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/CartTest.php index 2614499d0916c..e5cb6e4e10ec0 100644 --- a/app/code/Magento/Checkout/Test/Unit/Helper/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Helper/CartTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Checkout\Test\Unit\Helper; use \Magento\Checkout\Helper\Cart; @@ -188,7 +186,10 @@ public function testGetAddUrl() $productEntityId = 1; $storeId = 1; $isRequestSecure = false; - $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getEntityId', 'hasUrlDataObject', 'getUrlDataObject', '__wakeup']); + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getEntityId', 'hasUrlDataObject', 'getUrlDataObject', '__wakeup'] + ); $productMock->expects($this->any())->method('getEntityId')->will($this->returnValue($productEntityId)); $productMock->expects($this->any())->method('hasUrlDataObject')->will($this->returnValue(true)); $productMock->expects($this->any())->method('getUrlDataObject') diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php index 31203b63f854a..f6f9ff78c1cb2 100644 --- a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php @@ -4,12 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Checkout\Test\Unit\Helper; -use \Magento\Checkout\Helper\Data; - +use Magento\Checkout\Helper\Data; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\ScopeInterface; @@ -41,23 +38,21 @@ class DataTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_checkoutSession; + private $_checkoutSession; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_scopeConfig; + private $_scopeConfig; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_collectionFactory; + private $_eventManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - protected $_eventManager; - protected function setUp() { $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -70,59 +65,57 @@ protected function setUp() $this->_scopeConfig = $context->getScopeConfig(); $this->_scopeConfig->expects($this->any()) ->method('getValue') - ->will( - $this->returnValueMap( + ->willReturnMap( + [ + [ + 'checkout/payment_failed/template', + ScopeInterface::SCOPE_STORE, + 8, + 'fixture_email_template_payment_failed', + ], + [ + 'checkout/payment_failed/receiver', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin', + ], + [ + 'trans_email/ident_sysadmin/email', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin@example.com', + ], + [ + 'trans_email/ident_sysadmin/name', + ScopeInterface::SCOPE_STORE, + 8, + 'System Administrator', + ], + [ + 'checkout/payment_failed/identity', + ScopeInterface::SCOPE_STORE, + 8, + 'noreply@example.com', + ], + [ + 'carriers/ground/title', + ScopeInterface::SCOPE_STORE, + null, + 'Ground Shipping', + ], [ - [ - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'fixture_email_template_payment_failed' - ], - [ - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin' - ], - [ - 'trans_email/ident_sysadmin/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin@example.com' - ], - [ - 'trans_email/ident_sysadmin/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'System Administrator' - ], - [ - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'noreply@example.com' - ], - [ - 'carriers/ground/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Ground Shipping' - ], - [ - 'payment/fixture-payment-method/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Check Money Order' - ], - [ - 'checkout/options/onepage_checkout_enabled', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'One Page Checkout' - ] - ] - ) + 'payment/fixture-payment-method/title', + ScopeInterface::SCOPE_STORE, + null, + 'Check Money Order', + ], + [ + 'checkout/options/onepage_checkout_enabled', + ScopeInterface::SCOPE_STORE, + null, + 'One Page Checkout', + ], + ] ); $this->_checkoutSession = $arguments['checkoutSession']; @@ -139,117 +132,16 @@ protected function setUp() /** * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSendPaymentFailedEmail() { - $shippingAddress = new \Magento\Framework\DataObject(['shipping_method' => 'ground_transportation']); - $billingAddress = new \Magento\Framework\DataObject(['street' => 'Fixture St']); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateOptions' - )->with( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateIdentifier' - )->with( - 'fixture_email_template_payment_failed' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setFrom' - )->with( - 'noreply@example.com' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'addTo' - )->with( - 'sysadmin@example.com', - 'System Administrator' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateVars' - )->with( - [ - 'reason' => 'test message', - 'checkoutType' => 'onepage', - 'dateAndTime' => 'Oct 02, 2013', - 'customer' => 'John Doe', - 'customerEmail' => 'john.doe@example.com', - 'billingAddress' => $billingAddress, - 'shippingAddress' => $shippingAddress, - 'shippingMethod' => 'Ground Shipping', - 'paymentMethod' => 'Check Money Order', - 'items' => "Product One x 2 USD 10<br />\nProduct Two x 3 USD 60<br />\n", - 'total' => 'USD 70' - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects($this->once())->method('addBcc')->will($this->returnSelf()); - $this->_transportBuilder->expects( - $this->once() - )->method( - 'getTransport' - )->will( - $this->returnValue($this->createMock(\Magento\Framework\Mail\TransportInterface::class)) - ); - - $this->_translator->expects($this->at(1))->method('suspend'); - $this->_translator->expects($this->at(1))->method('resume'); + $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getId')->willReturn(1); - $productOne = $this->createMock(\Magento\Catalog\Model\Product::class); - $productOne->expects($this->once())->method('getName')->will($this->returnValue('Product One')); - $productOne->expects($this->once())->method('getFinalPrice')->with(2)->will($this->returnValue(10)); - - $productTwo = $this->createMock(\Magento\Catalog\Model\Product::class); - $productTwo->expects($this->once())->method('getName')->will($this->returnValue('Product Two')); - $productTwo->expects($this->once())->method('getFinalPrice')->with(3)->will($this->returnValue(60)); - - $quote = new \Magento\Framework\DataObject( - [ - 'store_id' => 8, - 'store_currency_code' => 'USD', - 'grand_total' => 70, - 'customer_firstname' => 'John', - 'customer_lastname' => 'Doe', - 'customer_email' => 'john.doe@example.com', - 'billing_address' => $billingAddress, - 'shipping_address' => $shippingAddress, - 'payment' => new \Magento\Framework\DataObject(['method' => 'fixture-payment-method']), - 'all_visible_items' => [ - new \Magento\Framework\DataObject(['product' => $productOne, 'qty' => 2]), - new \Magento\Framework\DataObject(['product' => $productTwo, 'qty' => 3]) - ] - ] - ); - $this->assertSame($this->_helper, $this->_helper->sendPaymentFailedEmail($quote, 'test message')); + $this->assertSame($this->_helper, $this->_helper->sendPaymentFailedEmail($quoteMock, 'test message')); } /** @@ -343,7 +235,10 @@ public function testGetPriceInclTaxWithoutTax() 'priceCurrency' => $this->priceCurrency, ] ); - $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getPriceInclTax', 'getQty', 'getTaxAmount', 'getDiscountTaxCompensation', 'getRowTotal', 'getQtyOrdered']); + $itemMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['getPriceInclTax', 'getQty', 'getTaxAmount', 'getDiscountTaxCompensation', 'getRowTotal', 'getQtyOrdered'] + ); $itemMock->expects($this->once())->method('getPriceInclTax')->will($this->returnValue(false)); $itemMock->expects($this->exactly(2))->method('getQty')->will($this->returnValue($qty)); $itemMock->expects($this->never())->method('getQtyOrdered'); @@ -370,7 +265,10 @@ public function testGetSubtotalInclTaxNegative() $discountTaxCompensation = 1; $rowTotal = 15; $expected = 17; - $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getRowTotalInclTax', 'getTaxAmount', 'getDiscountTaxCompensation', 'getRowTotal']); + $itemMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['getRowTotalInclTax', 'getTaxAmount', 'getDiscountTaxCompensation', 'getRowTotal'] + ); $itemMock->expects($this->once())->method('getRowTotalInclTax')->will($this->returnValue(false)); $itemMock->expects($this->once())->method('getTaxAmount')->will($this->returnValue($taxAmount)); $itemMock->expects($this->once()) @@ -416,7 +314,10 @@ public function testGetBasePriceInclTax() public function testGetBaseSubtotalInclTax() { - $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getBaseTaxAmount', 'getBaseDiscountTaxCompensation', 'getBaseRowTotal']); + $itemMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['getBaseTaxAmount', 'getBaseDiscountTaxCompensation', 'getBaseRowTotal'] + ); $itemMock->expects($this->once())->method('getBaseTaxAmount'); $itemMock->expects($this->once())->method('getBaseDiscountTaxCompensation'); $itemMock->expects($this->once())->method('getBaseRowTotal'); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php new file mode 100644 index 0000000000000..14410578b12e4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\Cart; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\RegionInterface; +use Magento\Checkout\Model\Cart\CollectQuote; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\EstimateAddressInterface; +use Magento\Quote\Api\Data\EstimateAddressInterfaceFactory; +use Magento\Quote\Api\ShippingMethodManagementInterface; +use Magento\Quote\Model\Quote; +use PHPUnit\Framework\TestCase; + +/** + * Class CollectQuoteTest + */ +class CollectQuoteTest extends TestCase +{ + /** + * @var CollectQuote + */ + private $model; + + /** + * @var CustomerSession|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepositoryMock; + + /** + * @var EstimateAddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $estimateAddressFactoryMock; + + /** + * @var EstimateAddressInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $estimateAddressMock; + + /** + * @var ShippingMethodManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingMethodManagerMock; + + /** + * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteRepositoryMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerMock; + + /** + * @var AddressInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressMock; + + /** + * Set up + */ + protected function setUp() + { + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->customerRepositoryMock = $this->getMockForAbstractClass( + CustomerRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getById'] + ); + $this->addressRepositoryMock = $this->createMock(AddressRepositoryInterface::class); + $this->estimateAddressMock = $this->createMock(EstimateAddressInterface::class); + $this->estimateAddressFactoryMock = + $this->createPartialMock(EstimateAddressInterfaceFactory::class, ['create']); + $this->shippingMethodManagerMock = $this->createMock(ShippingMethodManagementInterface::class); + $this->quoteRepositoryMock = $this->createMock(CartRepositoryInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->customerMock = $this->createMock(CustomerInterface::class); + $this->addressMock = $this->createMock(AddressInterface::class); + + $this->model = new CollectQuote( + $this->customerSessionMock, + $this->customerRepositoryMock, + $this->addressRepositoryMock, + $this->estimateAddressFactoryMock, + $this->shippingMethodManagerMock, + $this->quoteRepositoryMock + ); + } + + /** + * Test collect method + */ + public function testCollect() + { + $customerId = 1; + $defaultAddressId = 999; + $countryId = 'USA'; + $regionId = 'CA'; + $regionMock = $this->createMock(RegionInterface::class); + + $this->customerSessionMock->expects(self::once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->customerSessionMock->expects(self::once()) + ->method('getCustomerId') + ->willReturn($customerId); + $this->customerRepositoryMock->expects(self::once()) + ->method('getById') + ->willReturn($this->customerMock); + $this->customerMock->expects(self::once()) + ->method('getDefaultShipping') + ->willReturn($defaultAddressId); + $this->addressMock->expects(self::once()) + ->method('getCountryId') + ->willReturn($countryId); + $regionMock->expects(self::once()) + ->method('getRegion') + ->willReturn($regionId); + $this->addressMock->expects(self::once()) + ->method('getRegion') + ->willReturn($regionMock); + $this->addressRepositoryMock->expects(self::once()) + ->method('getById') + ->with($defaultAddressId) + ->willReturn($this->addressMock); + $this->estimateAddressFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($this->estimateAddressMock); + $this->quoteRepositoryMock->expects(self::once()) + ->method('save') + ->with($this->quoteMock); + + $this->model->collect($this->quoteMock); + } + + /** + * Test with a not logged in customer + */ + public function testCollectWhenCustomerIsNotLoggedIn() + { + $this->customerSessionMock->expects(self::once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->customerRepositoryMock->expects(self::never()) + ->method('getById'); + + $this->model->collect($this->quoteMock); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/ImageProviderTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/ImageProviderTest.php index 5330d93b46f6a..993a01d922c1c 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Cart/ImageProviderTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/ImageProviderTest.php @@ -11,25 +11,34 @@ class ImageProviderTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Checkout\Model\Cart\ImageProvider */ - public $model; + private $model; /** * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Quote\Api\CartItemRepositoryInterface */ - protected $itemRepositoryMock; + private $itemRepositoryMock; /** * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Checkout\CustomerData\ItemPoolInterface */ - protected $itemPoolMock; + private $itemPoolMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Checkout\CustomerData\DefaultItem + */ + private $customerItem; protected function setUp() { $this->itemRepositoryMock = $this->createMock(\Magento\Quote\Api\CartItemRepositoryInterface::class); $this->itemPoolMock = $this->createMock(\Magento\Checkout\CustomerData\ItemPoolInterface::class); + $this->customerItem = $this->getMockBuilder(\Magento\Checkout\CustomerData\DefaultItem::class) + ->disableOriginalConstructor() + ->getMock(); $this->model = new \Magento\Checkout\Model\Cart\ImageProvider( $this->itemRepositoryMock, - $this->itemPoolMock + $this->itemPoolMock, + $this->customerItem ); } @@ -44,7 +53,7 @@ public function testGetImages() $expectedResult = [$itemId => $itemData['product_image']]; $this->itemRepositoryMock->expects($this->once())->method('getList')->with($cartId)->willReturn([$itemMock]); - $this->itemPoolMock->expects($this->once())->method('getItemData')->with($itemMock)->willReturn($itemData); + $this->customerItem->expects($this->once())->method('getItemData')->with($itemMock)->willReturn($itemData); $this->assertEquals($expectedResult, $this->model->getImages($cartId)); } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php new file mode 100644 index 0000000000000..daabb080b1c9a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\Cart; + +use Magento\Checkout\Model\Cart\RequestQuantityProcessor; +use Magento\Framework\Locale\ResolverInterface; +use \PHPUnit_Framework_MockObject_MockObject as MockObject; + +class RequestQuantityProcessorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ResolverInterface | MockObject + */ + private $localeResolver; + + /** + * @var RequestQuantityProcessor + */ + private $requestProcessor; + + protected function setUp() + { + $this->localeResolver = $this->getMockBuilder(ResolverInterface::class) + ->getMockForAbstractClass(); + + $this->localeResolver->method('getLocale') + ->willReturn('en_US'); + + $this->requestProcessor = new RequestQuantityProcessor( + $this->localeResolver + ); + } + + /** + * Test of cart data processing. + * + * @param array $cartData + * @param array $expected + * @dataProvider cartDataProvider + */ + public function testProcess($cartData, $expected) + { + $this->assertEquals($this->requestProcessor->process($cartData), $expected); + } + + public function cartDataProvider() + { + return [ + 'empty_array' => [ + 'cartData' => [], + 'expected' => [], + ], + 'strings_array' => [ + 'cartData' => [ + ['qty' => ' 10 '], + ['qty' => ' 0.5 '] + ], + 'expected' => [ + ['qty' => 10], + ['qty' => 0.5] + ], + ], + 'integer_array' => [ + 'cartData' => [ + ['qty' => 1], + ['qty' => 0.002] + ], + 'expected' => [ + ['qty' => 1], + ['qty' => 0.002] + ], + ], + 'array_of arrays' => [ + 'cartData' => [ + ['qty' => [1, 2 ,3]], + ], + 'expected' => [ + ['qty' => [1, 2, 3]], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php index 40de71e28c05e..6bd0bdf258a0a 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php @@ -279,6 +279,9 @@ public function testGetSummaryQty($useQty) $this->assertEquals($itemsCount, $this->cart->getSummaryQty()); } + /** + * @return array + */ public function useQtyDataProvider() { return [ diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index a33649551bdcb..7a731c1c07039 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -5,6 +5,12 @@ */ namespace Magento\Checkout\Test\Unit\Model; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\QuoteIdMask; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -45,6 +51,11 @@ class GuestPaymentInformationManagementTest extends \PHPUnit\Framework\TestCase */ private $loggerMock; + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -62,6 +73,10 @@ protected function setUp() ['create'] ); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( \Magento\Checkout\Model\GuestPaymentInformationManagement::class, [ @@ -69,7 +84,8 @@ protected function setUp() 'paymentMethodManagement' => $this->paymentMethodManagementMock, 'cartManagement' => $this->cartManagementMock, 'cartRepository' => $this->cartRepositoryMock, - 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'connectionPull' => $this->resourceConnectionMock, ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -82,12 +98,30 @@ public function testSavePaymentInformationAndPlaceOrder() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('commit'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('commit'); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); @@ -108,11 +142,30 @@ public function testSavePaymentInformationAndPlaceOrderException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('rollback'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('rollback'); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $exception = new \Exception(__('DB exception')); $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); @@ -126,11 +179,9 @@ public function testSavePaymentInformation() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->assertTrue($this->model->savePaymentInformation($cartId, $email, $paymentMock, $billingAddressMock)); @@ -142,13 +193,13 @@ public function testSavePaymentInformationWithoutBillingAddress() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $this->billingAddressManagementMock->expects($this->never())->method('assign'); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $quoteIdMaskMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMask::class, ['getQuoteId', 'load']); + $quoteIdMaskMock = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); $this->quoteIdMaskFactoryMock->expects($this->once())->method('create')->willReturn($quoteIdMaskMock); $quoteIdMaskMock->expects($this->once())->method('load')->with($cartId, 'masked_id')->willReturnSelf(); $quoteIdMaskMock->expects($this->once())->method('getQuoteId')->willReturn($cartId); @@ -169,11 +220,38 @@ public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $quoteMock = $this->createMock(Quote::class); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); + $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); + + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($quoteIdMask); + $quoteIdMask->method('load')->with($cartId, 'masked_id')->willReturnSelf(); + $quoteIdMask->method('getQuoteId')->willReturn($cartId); + $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('rollback'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('rollback'); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $phrase = new \Magento\Framework\Phrase(__('DB exception')); $exception = new \Magento\Framework\Exception\LocalizedException($phrase); @@ -182,4 +260,60 @@ public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); } + + /** + * @param int $cartId + * @param \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + * @return void + */ + private function getMockForAssignBillingAddress($cartId, $billingAddressMock) + { + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create') + ->willReturn($quoteIdMask); + $quoteIdMask->method('load') + ->with($cartId, 'masked_id') + ->willReturnSelf(); + $quoteIdMask->method('getQuoteId') + ->willReturn($cartId); + + $billingAddressId = 1; + $quote = $this->createMock(Quote::class); + $quoteBillingAddress = $this->createMock(Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); + $quoteShippingAddress = $this->createPartialMock( + Address::class, + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] + ); + $this->cartRepositoryMock->method('getActive') + ->with($cartId) + ->willReturn($quote); + $quote->expects($this->once()) + ->method('getBillingAddress') + ->willReturn($quoteBillingAddress); + $quote->expects($this->once()) + ->method('getShippingAddress') + ->willReturn($quoteShippingAddress); + $quoteBillingAddress->expects($this->once()) + ->method('getId') + ->willReturn($billingAddressId); + $quote->expects($this->once()) + ->method('removeAddress') + ->with($billingAddressId); + $quote->expects($this->once()) + ->method('setBillingAddress') + ->with($billingAddressMock); + $quoteShippingAddress->expects($this->any()) + ->method('getShippingRateByCode') + ->willReturn($shippingRate); + $quote->expects($this->once()) + ->method('setDataChanges') + ->willReturnSelf(); + $quoteShippingAddress->method('getShippingMethod') + ->willReturn('flatrate_flatrate'); + $quoteShippingAddress->expects($this->once()) + ->method('setLimitCarrier') + ->with('flatrate'); + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php index 586ed57076763..350f9954208fa 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Checkout\Test\Unit\Model\Layout; /** @@ -39,7 +37,10 @@ class DepersonalizePluginTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->layoutMock = $this->createMock(\Magento\Framework\View\Layout::class); - $this->checkoutSessionMock = $this->createPartialMock(\Magento\Framework\Session\Generic::class, ['clearStorage', 'setData', 'getData']); + $this->checkoutSessionMock = $this->createPartialMock( + \Magento\Framework\Session\Generic::class, + ['clearStorage', 'setData', 'getData'] + ); $this->checkoutSessionMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['clearStorage']); $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index b40b2b244ac4c..7663c1c3c9645 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -159,6 +159,31 @@ public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); } + /** + * Test for save payment and place order with new billing address + * + * @return void + */ + public function testSavePaymentInformationAndPlaceOrderWithNewBillingAddress() + { + $cartId = 100; + $quoteBillingAddressId = 1; + $customerId = 1; + $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); + + $quoteBillingAddress->method('getCustomerId')->willReturn($customerId); + $quoteMock->method('getBillingAddress')->willReturn($quoteBillingAddress); + $quoteBillingAddress->method('getId')->willReturn($quoteBillingAddressId); + $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); + + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $billingAddressMock->expects($this->once())->method('setCustomerId')->with($customerId); + $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock, $billingAddressMock)); + } + /** * @param int $cartId * @param \PHPUnit_Framework_MockObject_MockObject $billingAddressMock @@ -168,17 +193,21 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $billingAddressId = 1; $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); $quoteShippingAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['setLimitCarrier', 'getShippingMethod'] + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] ); $this->cartRepositoryMock->expects($this->any())->method('getActive')->with($cartId)->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($quoteBillingAddress); + $quoteMock->method('getBillingAddress')->willReturn($quoteBillingAddress); $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($quoteShippingAddress); $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); + $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); $quoteMock->expects($this->once())->method('removeAddress')->with($billingAddressId); $quoteMock->expects($this->once())->method('setBillingAddress')->with($billingAddressMock); $quoteMock->expects($this->once())->method('setDataChanges')->willReturnSelf(); + $quoteShippingAddress->expects($this->any())->method('getShippingRateByCode')->willReturn($shippingRate); $quoteShippingAddress->expects($this->any())->method('getShippingMethod')->willReturn('flatrate_flatrate'); $quoteShippingAddress->expects($this->once())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Session/SuccessValidatorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Session/SuccessValidatorTest.php index fec1d6d4d003f..2751a54d2d2fe 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Session/SuccessValidatorTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Session/SuccessValidatorTest.php @@ -90,6 +90,11 @@ public function testIsValidTrue() $this->assertTrue($this->createSuccessValidator($checkoutSession)->isValid($checkoutSession)); } + /** + * @param \PHPUnit_Framework_MockObject_MockObject $checkoutSession + * + * @return object + */ protected function createSuccessValidator(\PHPUnit_Framework_MockObject_MockObject $checkoutSession) { return $this->objectManagerHelper->getObject( diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php index 8889927730302..8de3ff1a34859 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Test class for \Magento\Checkout\Model\Session */ @@ -146,7 +144,10 @@ public function clearHelperDataDataProvider() */ public function testRestoreQuote($hasOrderId, $hasQuoteId) { - $order = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['getId', 'loadByIncrementId', '__wakeup']); + $order = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + ['getId', 'loadByIncrementId', '__wakeup'] + ); $order->expects($this->once())->method('getId')->will($this->returnValue($hasOrderId ? 'order id' : null)); $orderFactory = $this->createPartialMock(\Magento\Sales\Model\OrderFactory::class, ['create']); $orderFactory->expects($this->once())->method('create')->will($this->returnValue($order)); @@ -178,7 +179,10 @@ public function testRestoreQuote($hasOrderId, $hasQuoteId) if ($hasOrderId) { $order->setQuoteId($quoteId); - $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['setIsActive', 'getId', 'setReservedOrderId', '__wakeup', 'save']); + $quote = $this->createPartialMock( + \Magento\Quote\Model\Quote::class, + ['setIsActive', 'getId', 'setReservedOrderId', '__wakeup', 'save'] + ); if ($hasQuoteId) { $quoteRepository->expects($this->once())->method('get')->with($quoteId)->willReturn($quote); $quote->expects( @@ -291,7 +295,10 @@ public function testReplaceQuote() $storage->expects($this->any()) ->method('setData'); - $quoteIdMaskMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMask::class, ['getMaskedId', 'load', 'setQuoteId', 'save']); + $quoteIdMaskMock = $this->createPartialMock( + \Magento\Quote\Model\QuoteIdMask::class, + ['getMaskedId', 'load', 'setQuoteId', 'save'] + ); $quoteIdMaskMock->expects($this->once())->method('load')->with($replaceQuoteId, 'quote_id')->willReturnSelf(); $quoteIdMaskMock->expects($this->once())->method('getMaskedId')->willReturn(null); $quoteIdMaskMock->expects($this->once())->method('setQuoteId')->with($replaceQuoteId)->willReturnSelf(); @@ -339,9 +346,11 @@ public function testResetCheckout() { /** @var $session \Magento\Checkout\Model\Session */ $session = $this->_helper->getObject( - \Magento\Checkout\Model\Session::class, [ - 'storage' => new \Magento\Framework\Session\Storage() - ]); + \Magento\Checkout\Model\Session::class, + [ + 'storage' => new \Magento\Framework\Session\Storage(), + ] + ); $session->resetCheckout(); $this->assertEquals(\Magento\Checkout\Model\Session::CHECKOUT_STATE_BEGIN, $session->getCheckoutState()); } @@ -356,9 +365,11 @@ public function testGetStepData() ]; /** @var $session \Magento\Checkout\Model\Session */ $session = $this->_helper->getObject( - \Magento\Checkout\Model\Session::class, [ - 'storage' => new \Magento\Framework\Session\Storage() - ]); + \Magento\Checkout\Model\Session::class, + [ + 'storage' => new \Magento\Framework\Session\Storage() + ] + ); $session->setSteps($stepData); $this->assertEquals($stepData, $session->getStepData()); $this->assertFalse($session->getStepData('invalid_key')); @@ -376,9 +387,11 @@ public function testSetStepData() ]; /** @var $session \Magento\Checkout\Model\Session */ $session = $this->_helper->getObject( - \Magento\Checkout\Model\Session::class, [ - 'storage' => new \Magento\Framework\Session\Storage() - ]); + \Magento\Checkout\Model\Session::class, + [ + 'storage' => new \Magento\Framework\Session\Storage() + ] + ); $session->setSteps($stepData); $session->setStepData('complex', 'key2', 'value2'); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php index 5c6314b2a35c8..3999c843f88c3 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php @@ -81,6 +81,11 @@ class ShippingInformationManagementTest extends \PHPUnit\Framework\TestCase */ private $shippingMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $addressValidatorMock; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -140,6 +145,9 @@ protected function setUp() $this->createPartialMock(\Magento\Quote\Api\Data\CartExtensionFactory::class, ['create']); $this->shippingFactoryMock = $this->createPartialMock(\Magento\Quote\Model\ShippingFactory::class, ['create']); + $this->addressValidatorMock = $this->createMock( + \Magento\Quote\Model\QuoteAddressValidator::class + ); $this->model = $this->objectManager->getObject( \Magento\Checkout\Model\ShippingInformationManagement::class, @@ -150,7 +158,8 @@ protected function setUp() 'quoteRepository' => $this->quoteRepositoryMock, 'shippingAssignmentFactory' => $this->shippingAssignmentFactoryMock, 'cartExtensionFactory' => $this->cartExtensionFactoryMock, - 'shippingFactory' => $this->shippingFactoryMock + 'shippingFactory' => $this->shippingFactoryMock, + 'addressValidator' => $this->addressValidatorMock, ] ); } @@ -162,22 +171,8 @@ protected function setUp() public function testSaveAddressInformationIfCartIsEmpty() { $cartId = 100; - $carrierCode = 'carrier_code'; - $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); - $billingAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $addressInformationMock->expects($this->once()) - ->method('getShippingAddress') - ->willReturn($this->shippingAddressMock); - $addressInformationMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddress); - $addressInformationMock->expects($this->once())->method('getShippingCarrierCode')->willReturn($carrierCode); - $addressInformationMock->expects($this->once())->method('getShippingMethodCode')->willReturn($shippingMethod); - - $this->shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn('USA'); - - $this->setShippingAssignmentsMocks($carrierCode . '_' . $shippingMethod); - $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(0); $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') @@ -243,21 +238,19 @@ private function setShippingAssignmentsMocks($shippingMethod) public function testSaveAddressInformationIfShippingAddressNotSet() { $cartId = 100; - $carrierCode = 'carrier_code'; - $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); - $addressInformationMock->expects($this->once()) ->method('getShippingAddress') ->willReturn($this->shippingAddressMock); - $addressInformationMock->expects($this->once())->method('getShippingCarrierCode')->willReturn($carrierCode); - $addressInformationMock->expects($this->once())->method('getShippingMethodCode')->willReturn($shippingMethod); - - $billingAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $addressInformationMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddress); $this->shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(null); + $this->quoteRepositoryMock->expects($this->once()) + ->method('getActive') + ->with($cartId) + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(100); + $this->model->saveAddressInformation($cartId, $addressInformationMock); } @@ -272,6 +265,9 @@ public function testSaveAddressInformationIfCanNotSaveQuote() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) @@ -313,6 +309,9 @@ public function testSaveAddressInformationIfCarrierCodeIsInvalid() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) @@ -354,6 +353,9 @@ public function testSaveAddressInformation() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php index 29537e8ec0526..1ec1cf632490a 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php @@ -45,6 +45,9 @@ public function testGetResponseData($error, $result) $this->assertEquals($result, $this->sidebar->getResponseData($error)); } + /** + * @return array + */ public function dataProviderGetResponseData() { return [ @@ -94,7 +97,7 @@ public function testCheckQuoteItem() /** * @expectedException \Magento\Framework\Exception\LocalizedException - * @exceptedExceptionMessage We can't find the quote item. + * @expectedExceptionMessage We can't find the quote item. */ public function testCheckQuoteItemWithException() { diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Type/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Type/OnepageTest.php index 684ad9264f77c..eb6a5623d9df9 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Type/OnepageTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Type/OnepageTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Checkout\Test\Unit\Model\Type; use \Magento\Checkout\Model\Type\Onepage; @@ -122,8 +120,14 @@ protected function setUp() $this->checkoutHelperMock = $this->createMock(\Magento\Checkout\Helper\Data::class); $this->customerUrlMock = $this->createMock(\Magento\Customer\Model\Url::class); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->checkoutSessionMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['getLastOrderId', 'getQuote', 'setStepData', 'getStepData']); - $this->customerSessionMock = $this->createPartialMock(\Magento\Customer\Model\Session::class, ['getCustomerDataObject', 'isLoggedIn']); + $this->checkoutSessionMock = $this->createPartialMock( + \Magento\Checkout\Model\Session::class, + ['getLastOrderId', 'getQuote', 'setStepData', 'getStepData'] + ); + $this->customerSessionMock = $this->createPartialMock( + \Magento\Customer\Model\Session::class, + ['getCustomerDataObject', 'isLoggedIn'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); @@ -135,7 +139,10 @@ protected function setUp() $this->copyMock = $this->createMock(\Magento\Framework\DataObject\Copy::class); $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->customerFormFactoryMock = $this->createPartialMock(\Magento\Customer\Model\FormFactory::class, ['create']); + $this->customerFormFactoryMock = $this->createPartialMock( + \Magento\Customer\Model\FormFactory::class, + ['create'] + ); $this->customerDataFactoryMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); @@ -264,6 +271,9 @@ public function testInitCheckout($stepData, $isLoggedIn, $isSetStepDataCalled) $this->onepage->initCheckout(); } + /** + * @return array + */ public function initCheckoutDataProvider() { return [ @@ -296,6 +306,9 @@ public function testGetCheckoutMethod($isLoggedIn, $quoteCheckoutMethod, $isAllo $this->assertEquals($expected, $this->onepage->getCheckoutMethod()); } + /** + * @return array + */ public function getCheckoutMethodDataProvider() { return [ @@ -325,7 +338,10 @@ public function testGetLastOrderId() $orderId = 1; $this->checkoutSessionMock->expects($this->once())->method('getLastOrderId') ->will($this->returnValue($orderId)); - $orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['load', 'getIncrementId', '__wakeup']); + $orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + ['load', 'getIncrementId', '__wakeup'] + ); $orderMock->expects($this->once())->method('load')->with($orderId)->will($this->returnSelf()); $orderMock->expects($this->once())->method('getIncrementId')->will($this->returnValue($orderIncrementId)); $this->orderFactoryMock->expects($this->once())->method('create')->will($this->returnValue($orderMock)); diff --git a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php index 6070bb5d424c1..dabaf173d90b3 100644 --- a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php @@ -30,13 +30,14 @@ protected function setUp() public function testSalesQuoteSaveAfter() { + $quoteId = 7; $observer = $this->createMock(\Magento\Framework\Event\Observer::class); $observer->expects($this->once())->method('getEvent')->will( $this->returnValue(new \Magento\Framework\DataObject( - ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => 7])] + ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => $quoteId])] )) ); - $this->checkoutSession->expects($this->once())->method('getQuoteId')->with(7); + $this->checkoutSession->expects($this->once())->method('setQuoteId')->with($quoteId); $this->object->execute($observer); } diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 7f6b1dcaec01e..c1af6407c81f1 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -2,10 +2,9 @@ "name": "magento/module-checkout", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-sales": "101.0.*", - "magento/module-backend": "100.2.*", "magento/module-catalog-inventory": "100.2.*", "magento/module-config": "101.0.*", "magento/module-customer": "101.0.*", @@ -27,7 +26,7 @@ "magento/module-cookie": "100.2.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Checkout/etc/adminhtml/routes.xml b/app/code/Magento/Checkout/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..e537861059870 --- /dev/null +++ b/app/code/Magento/Checkout/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="checkout" frontName="checkout"> + <module name="Magento_Checkout" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 889689e6c0d16..8f35fe9f37abf 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -39,7 +39,7 @@ </type> <preference for="Magento\Checkout\CustomerData\ItemPoolInterface" type="Magento\Checkout\CustomerData\ItemPool"/> - <type name="Magento\Checkout\CustomerData\ItemPoolInterface"> + <type name="Magento\Checkout\CustomerData\ItemPool"> <arguments> <argument name="defaultItemId" xsi:type="string">Magento\Checkout\CustomerData\DefaultItem</argument> </arguments> @@ -59,6 +59,7 @@ <item name="totalsSortOrder" xsi:type="object">Magento\Checkout\Block\Checkout\TotalsProcessor</item> <item name="directoryData" xsi:type="object">Magento\Checkout\Block\Checkout\DirectoryDataProcessor</item> </argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\JsonHexTag</argument> </arguments> </type> <type name="Magento\Checkout\Block\Cart\Totals"> @@ -83,4 +84,19 @@ </argument> </arguments> </type> + <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> + <arguments> + <argument name="configurationsMap" xsi:type="array"> + <item name="addCartSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Checkout::messages/addCartSuccessMessage.phtml</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 35733a6119a25..90c2878f501cf 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -46,7 +46,6 @@ </action> <action name="rest/*/V1/guest-carts/*/payment-information"> <section name="cart"/> - <section name="checkout-data"/> </action> <action name="rest/*/V1/guest-carts/*/selected-payment-method"> <section name="cart"/> diff --git a/app/code/Magento/Checkout/etc/webapi.xml b/app/code/Magento/Checkout/etc/webapi.xml index 7b435db200f19..26c601a4e9f38 100644 --- a/app/code/Magento/Checkout/etc/webapi.xml +++ b/app/code/Magento/Checkout/etc/webapi.xml @@ -104,7 +104,7 @@ <resource ref="anonymous" /> </resources> </route> - <!-- Managing My shipping information --> + <!-- Managing My payment information --> <route url="/V1/carts/mine/set-payment-information" method="POST"> <service class="Magento\Checkout\Api\PaymentInformationManagementInterface" method="savePaymentInformation"/> <resources> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 8d297c4060abd..bacfe169c1bff 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -176,4 +176,9 @@ Payment,Payment "Not yet calculated","Not yet calculated" "We received your order!","We received your order!" "Thank you for your purchase!","Thank you for your purchase!" -"optional", "optional" +"Password", "Password" +"Something went wrong while saving the page. Please refresh the page and try again.","Something went wrong while saving the page. Please refresh the page and try again." +"Item in Cart","Item in Cart" +"Items in Cart","Items in Cart" +"Close","Close" +"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." diff --git a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html index fb55f9b601dc9..6d2b27fd0e293 100644 --- a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html +++ b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html @@ -6,13 +6,13 @@ --> <!--@subject {{trans "Payment Transaction Failed Reminder"}} @--> <!--@vars { -"var billingAddress.format('html')|raw":"Billing Address", +"var billingAddressHtml|raw":"Billing Address", "var checkoutType":"Checkout Type", "var customerEmail":"Customer Email", "var customer":"Customer Name", "var dateAndTime":"Date and Time of Transaction", "var paymentMethod":"Payment Method", -"var shippingAddress.format('html')|raw":"Shipping Address", +"var shippingAddressHtml|raw":"Shipping Address", "var shippingMethod":"Shipping Method", "var items|raw":"Shopping Cart Items", "var total":"Total", @@ -23,43 +23,43 @@ <h1>{{trans "Payment Transaction Failed"}}</h1> <ul> <li> - <b>{{trans "Reason"}}</b><br /> + <strong>{{trans "Reason"}}</strong><br /> {{var reason}} </li> <li> - <b>{{trans "Checkout Type"}}</b><br /> + <strong>{{trans "Checkout Type"}}</strong><br /> {{var checkoutType}} </li> <li> - <b>{{trans "Customer:"}}</b><br /> + <strong>{{trans "Customer:"}}</strong><br /> <a href="mailto:{{var customerEmail}}">{{var customer}}</a> <{{var customerEmail}}> </li> <li> - <b>{{trans "Items"}}</b><br /> + <strong>{{trans "Items"}}</strong><br /> {{var items|raw}} </li> <li> - <b>{{trans "Total:"}}</b><br /> + <strong>{{trans "Total:"}}</strong><br /> {{var total}} </li> <li> - <b>{{trans "Billing Address:"}}</b><br /> - {{var billingAddress.format('html')|raw}} + <strong>{{trans "Billing Address:"}}</strong><br /> + {{var billingAddressHtml|raw}} </li> <li> - <b>{{trans "Shipping Address:"}}</b><br /> - {{var shippingAddress.format('html')|raw}} + <strong>{{trans "Shipping Address:"}}</strong><br /> + {{var shippingAddressHtml|raw}} </li> <li> - <b>{{trans "Shipping Method:"}}</b><br /> + <strong>{{trans "Shipping Method:"}}</strong><br /> {{var shippingMethod}} </li> <li> - <b>{{trans "Payment Method:"}}</b><br /> + <strong>{{trans "Payment Method:"}}</strong><br /> {{var paymentMethod}} </li> <li> - <b>{{trans "Date & Time:"}}</b><br /> + <strong>{{trans "Date & Time:"}}</strong><br /> {{var dateAndTime}} </li> </ul> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index 6c562f0b9027b..642fb5664ddc0 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -404,6 +404,10 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/subtotal</item> <item name="displayArea" xsi:type="string">after_details</item> </item> + <item name="message" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/message</item> + <item name="displayArea" xsi:type="string">item_message</item> + </item> </item> </item> </item> diff --git a/app/code/Magento/Checkout/view/frontend/templates/button.phtml b/app/code/Magento/Checkout/view/frontend/templates/button.phtml index c3edfe30f8bdd..b0087794ea850 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/button.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/button.phtml @@ -3,15 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> <?php if ($block->getCanViewOrder() && $block->getCanPrintOrder()) :?> - <a href="<?= /* @escapeNotVerified */ $block->getPrintUrl() ?>" target="_blank" class="print"> - <?= /* @escapeNotVerified */ __('Print receipt') ?> + <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" target="_blank" class="print"> + <?= $block->escapeHtml(__('Print receipt')) ?> </a> <?= $block->getChildHtml() ?> <?php endif;?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart.phtml index 929f053febfc7..e71ea8c66288c 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart.phtml @@ -12,7 +12,9 @@ */ if ($block->getItemsCount()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $block->getChildHtml('with-items'); } else { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $block->getChildHtml('no-items'); } diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml index cb92e62f1f0c8..050a256ff7930 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/additional/info.phtml @@ -4,18 +4,19 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** * Shopping cart additional info * @var $block \Magento\Framework\View\Element\Template */ + +// phpcs:disable PSR2.Files.ClosingTag ?> <?php $name = $block->getNameInLayout(); foreach ($block->getChildNames($name) as $childName) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $block->getChildBlock($childName)->setItem($block->getItem())->toHtml(); } ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml index b224c96f07e9b..9f334605ec95e 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml @@ -4,16 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> -<div class="block discount" id="block-discount" data-mage-init='{"collapsible":{"openedState": "active", "saveState": false}}'> +<div class="block discount" + id="block-discount" + data-mage-init='{"collapsible":{"openedState": "active", "saveState": false}}' +> <div class="title" data-role="title"> - <strong id="block-discount-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Apply Discount Code') ?></strong> + <strong id="block-discount-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Apply Discount Code')) ?></strong> </div> <div class="content" data-role="content" aria-labelledby="block-discount-heading"> <form id="discount-coupon-form" - action="<?= /* @escapeNotVerified */ $block->getUrl('checkout/cart/couponPost') ?>" + action="<?= $block->escapeUrl($block->getUrl('checkout/cart/couponPost')) ?>" method="post" data-mage-init='{"discountCode":{"couponCodeSelector": "#coupon_code", "removeCouponSelector": "#remove-coupon", @@ -22,21 +23,31 @@ <div class="fieldset coupon<?= strlen($block->getCouponCode()) ? ' applied' : '' ?>"> <input type="hidden" name="remove" id="remove-coupon" value="0" /> <div class="field"> - <label for="coupon_code" class="label"><span><?= /* @escapeNotVerified */ __('Enter discount code') ?></span></label> + <label for="coupon_code" class="label"><span><?= $block->escapeHtml(__('Enter discount code')) ?></span></label> <div class="control"> - <input type="text" class="input-text" id="coupon_code" name="coupon_code" value="<?= $block->escapeHtml($block->getCouponCode()) ?>" placeholder="<?= $block->escapeHtml(__('Enter discount code')) ?>" /> + <input type="text" + class="input-text" + id="coupon_code" + name="coupon_code" + value="<?= $block->escapeHtmlAttr($block->getCouponCode()) ?>" + placeholder="<?= $block->escapeHtmlAttr(__('Enter discount code')) ?>" + <?php if (strlen($block->getCouponCode())) :?> + disabled="disabled" + <?php endif; ?> + /> </div> </div> <div class="actions-toolbar"> - <?php if (!strlen($block->getCouponCode())): ?> + <?= $block->getBlockHtml('formkey') ?> + <?php if (!strlen($block->getCouponCode())) : ?> <div class="primary"> - <button class="action apply primary" type="button" value="<?= /* @escapeNotVerified */ __('Apply Discount') ?>"> - <span><?= /* @escapeNotVerified */ __('Apply Discount') ?></span> + <button class="action apply primary" type="button" value="<?= $block->escapeHtmlAttr(__('Apply Discount')) ?>"> + <span><?= $block->escapeHtml(__('Apply Discount')) ?></span> </button> </div> - <?php else: ?> + <?php else :?> <div class="primary"> - <button type="button" class="action cancel primary" value="<?= /* @escapeNotVerified */ __('Cancel Coupon') ?>"><span><?= /* @escapeNotVerified */ __('Cancel Coupon') ?></span></button> + <button type="button" class="action cancel primary" value="<?= $block->escapeHtmlAttr(__('Cancel Coupon')) ?>"><span><?= $block->escapeHtml(__('Cancel Coupon')) ?></span></button> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 4940cd7f26747..e1ab036c7d889 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -4,49 +4,56 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Cart\Grid */ ?> -<?php $mergedCells = ($this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> +<?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> <?= $block->getChildHtml('form_before') ?> -<form action="<?= /* @escapeNotVerified */ $block->getUrl('checkout/cart/updatePost') ?>" +<form action="<?= $block->escapeUrl($block->getUrl('checkout/cart/updatePost')) ?>" method="post" id="form-validate" - data-mage-init='{"validation":{}}' + data-mage-init='{"Magento_Checkout/js/action/update-shopping-cart": + {"validationURL" : "<?= $block->escapeUrl($block->getUrl('checkout/cart/updateItemQty')) ?>", + "updateCartActionContainer": "#update_cart_action_container"} + }' class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> <div class="cart table-wrapper<?= $mergedCells == 2 ? ' detailed' : '' ?>"> - <?php if ($block->getPagerHtml()): ?> - <div class="cart-products-toolbar cart-products-toolbar-top toolbar" data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?></div> + <?php if ($block->getPagerHtml()) :?> + <div class="cart-products-toolbar cart-products-toolbar-top toolbar" + data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?> + </div> <?php endif ?> <table id="shopping-cart-table" class="cart items data table" - data-mage-init='{"shoppingCart":{"emptyCartButton": "action.clear", + data-mage-init='{"shoppingCart":{"emptyCartButton": ".action.clear", "updateCartActionContainer": "#update_cart_action_container"}}'> - <caption role="heading" aria-level="2" class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('Shopping Cart Items')) ?></caption> <thead> <tr> - <th class="col item" scope="col"><span><?= /* @escapeNotVerified */ __('Item') ?></span></th> - <th class="col price" scope="col"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col qty" scope="col"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col subtotal" scope="col"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> + <th class="col item" scope="col"><span><?= $block->escapeHtml(__('Item')) ?></span></th> + <th class="col price" scope="col"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col qty" scope="col"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col subtotal" scope="col"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> </tr> </thead> - <?php foreach ($block->getItems() as $_item): ?> + <?php foreach ($block->getItems() as $_item) :?> <?= $block->getItemHtml($_item) ?> <?php endforeach ?> </table> - <?php if ($block->getPagerHtml()): ?> - <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?></div> + <?php if ($block->getPagerHtml()) :?> + <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" + data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?> + </div> <?php endif ?> </div> <div class="cart main actions"> - <?php if ($block->getContinueShoppingUrl()): ?> + <?php if ($block->getContinueShoppingUrl()) :?> <a class="action continue" href="<?= $block->escapeUrl($block->getContinueShoppingUrl()) ?>" title="<?= $block->escapeHtml(__('Continue Shopping')) ?>"> - <span><?= /* @escapeNotVerified */ __('Continue Shopping') ?></span> + <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> </a> <?php endif; ?> <button type="submit" @@ -55,7 +62,7 @@ value="empty_cart" title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" class="action clear" id="empty_cart_button"> - <span><?= /* @escapeNotVerified */ __('Clear Shopping Cart') ?></span> + <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> </button> <button type="submit" name="update_cart_action" @@ -63,7 +70,7 @@ value="update_qty" title="<?= $block->escapeHtml(__('Update Shopping Cart')) ?>" class="action update"> - <span><?= /* @escapeNotVerified */ __('Update Shopping Cart') ?></span> + <span><?= $block->escapeHtml(__('Update Shopping Cart')) ?></span> </button> <input type="hidden" value="" id="update_cart_action_container" data-cart-item-update=""/> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml index b41d548e95b99..3b09512eb505b 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml @@ -4,35 +4,34 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Catalog\Block\Product\View */ ?> <?php $_product = $block->getProduct(); ?> <?php $buttonTitle = __('Update Cart'); ?> -<?php if ($_product->isSaleable()): ?> +<?php if ($_product->isSaleable()) :?> <div class="box-tocart update"> <fieldset class="fieldset"> - <?php if ($block->shouldRenderQuantity()): ?> + <?php if ($block->shouldRenderQuantity()) :?> <div class="field qty"> - <label class="label" for="qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></label> + <label class="label" for="qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> <input type="number" name="qty" id="qty" + min="0" value="" - title="<?= /* @escapeNotVerified */ __('Qty') ?>" + title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" class="input-text qty" - data-validate="{'required-number':true,digits:true}"/> + data-validate="<?= $block->escapeHtml(json_encode($block->getQuantityValidators())) ?>"/> </div> </div> <?php endif; ?> <div class="actions"> <button type="submit" - title="<?= /* @escapeNotVerified */ $buttonTitle ?>" + title="<?= $block->escapeHtmlAttr($buttonTitle) ?>" class="action primary tocart" id="product-updatecart-button"> - <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> + <span><?= $block->escapeHtml($buttonTitle) ?></span> </button> <?= $block->getChildHtml('', true) ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index 02c969f849074..77dde1eab482a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -4,7 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Magento2.Files.LineLength.MaxExceeded /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer */ @@ -12,108 +13,118 @@ $_item = $block->getItem(); $product = $_item->getProduct(); $isVisibleProduct = $product->isVisibleInSiteVisibility(); /** @var \Magento\Msrp\Helper\Data $helper */ -$helper = $this->helper('Magento\Msrp\Helper\Data'); +$helper = $this->helper(Magento\Msrp\Helper\Data::class); $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinimalPriceLessMsrp($product); ?> <tbody class="cart item"> <tr class="item-info"> <td data-th="<?= $block->escapeHtml(__('Item')) ?>" class="col item"> - <?php if ($block->hasProductUrl()):?> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl() ?>" + <?php if ($block->hasProductUrl()) :?> + <a href="<?= $block->escapeUrl($block->getProductUrl()) ?>" title="<?= $block->escapeHtml($block->getProductName()) ?>" tabindex="-1" class="product-item-photo"> - <?php else:?> + <?php else :?> <span class="product-item-photo"> <?php endif;?> <?= $block->getImage($block->getProductForThumbnail(), 'cart_page_product_thumbnail')->toHtml() ?> - <?php if ($block->hasProductUrl()):?> + <?php if ($block->hasProductUrl()) :?> </a> - <?php else: ?> + <?php else :?> </span> <?php endif; ?> <div class="product-item-details"> <strong class="product-item-name"> - <?php if ($block->hasProductUrl()):?> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl() ?>"><?= $block->escapeHtml($block->getProductName()) ?></a> - <?php else: ?> + <?php if ($block->hasProductUrl()) :?> + <a href="<?= $block->escapeUrl($block->getProductUrl()) ?>"><?= $block->escapeHtml($block->getProductName()) ?></a> + <?php else :?> <?= $block->escapeHtml($block->getProductName()) ?> <?php endif; ?> </strong> - <?php if ($_options = $block->getOptionList()):?> + <?php if ($_options = $block->getOptionList()) :?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option) :?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> - <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> + <?php if (isset($_formatedOptionValue['full_view'])) :?> + <?= $block->escapeHtml($_formatedOptionValue['full_view']) ?> + <?php else :?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> <?php endif; ?> </dd> <?php endforeach; ?> </dl> <?php endif;?> - <?php if ($messages = $block->getMessages()): ?> - <?php foreach ($messages as $message): ?> - <div class="cart item message <?= /* @escapeNotVerified */ $message['type'] ?>"><div><?= $block->escapeHtml($message['text']) ?></div></div> + <?php if ($messages = $block->getMessages()) :?> + <?php foreach ($messages as $message) :?> + <div class= "cart item message <?= $block->escapeHtmlAttr($message['type']) ?>"> + <div><?= $block->escapeHtml($message['text']) ?></div> + </div> <?php endforeach; ?> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock): ?> + <?php if ($addInfoBlock) :?> <?= $addInfoBlock->setItem($_item)->toHtml() ?> <?php endif;?> </div> </td> - <?php if ($canApplyMsrp): ?> + <?php if ($canApplyMsrp) :?> <td class="col msrp" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <span class="pricing msrp"> - <span class="msrp notice"><?= /* @escapeNotVerified */ __('See price before order confirmation.') ?></span> + <span class="msrp notice"><?= $block->escapeHtml(__('See price before order confirmation.')) ?></span> <?php $helpLinkId = 'cart-msrp-help-' . $_item->getId(); ?> - <a href="#" class="action help map" id="<?= /* @escapeNotVerified */ ($helpLinkId) ?>" data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @escapeNotVerified */ $helpLinkId ?>","productName": "<?= /* @escapeNotVerified */ $product->getName() ?>","showAddToCart": false}}'> - <span><?= /* @escapeNotVerified */ __("What's this?") ?></span> + <a href="#" class="action help map" + id="<?= ($block->escapeHtmlAttr($helpLinkId)) ?>" + data-mage-init='{"addToCart":{ + "helpLinkId": "#<?= $block->escapeJs($block->escapeHtml($helpLinkId)) ?>", + "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", + "showAddToCart": false + } + }' + > + <span><?= $block->escapeHtml(__("What's this?")) ?></span> </a> </span> </td> - <?php else: ?> + <?php else :?> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getUnitPriceHtml($_item) ?> </td> <?php endif; ?> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <div class="field qty"> - <label class="label" for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> - <span><?= /* @escapeNotVerified */ __('Qty') ?></span> - </label> <div class="control qty"> - <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" - name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" - data-cart-item-id="<?= /* @escapeNotVerified */ $_item->getSku() ?>" - value="<?= /* @escapeNotVerified */ $block->getQty() ?>" - type="number" - size="4" - title="<?= $block->escapeHtml(__('Qty')) ?>" - class="input-text qty" - data-validate="{required:true,'validate-greater-than-zero':true}" - data-role="cart-item-qty"/> + <label for="cart-<?= $block->escapeHtmlAttr($_item->getId()) ?>-qty"> + <span class="label"><?= $block->escapeHtml(__('Qty')) ?></span> + <input id="cart-<?= $block->escapeHtmlAttr($_item->getId()) ?>-qty" + name="cart[<?= $block->escapeHtmlAttr($_item->getId()) ?>][qty]" + data-cart-item-id="<?= $block->escapeHtmlAttr($_item->getSku()) ?>" + value="<?= $block->escapeHtmlAttr($block->getQty()) ?>" + type="number" + size="4" + title="<?= $block->escapeHtmlAttr(__('Qty')) ?>" + class="input-text qty" + data-validate="{required:true,'validate-greater-than-zero':true}" + data-role="cart-item-qty"/> + </label> </div> </div> </td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if ($canApplyMsrp): ?> + <?php if ($canApplyMsrp) :?> <span class="cart msrp subtotal">--</span> - <?php else: ?> + <?php else :?> <?= $block->getRowTotalHtml($_item) ?> <?php endif; ?> </td> </tr> <tr class="item-actions"> - <td colspan="100"> + <td colspan="4"> <div class="actions-toolbar"> - <?= /* @escapeNotVerified */ $block->getActions($_item) ?> + <?= /* @noEscape */ $block->getActions($_item) ?> </div> </td> </tr> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml index d7a625695b476..1d30cfc8dc45d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/price/sidebar.phtml @@ -4,12 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ ?> <?php $_item = $block->getItem() ?> <div class="price-container"> - <span class="price-label"><?= /* @escapeNotVerified */ __('Price') ?></span> - <span class="price-wrapper"><?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?></span> + <span class="price-label"><?= $block->escapeHtml(__('Price')) ?></span> + <span class="price-wrapper"> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), + ['span'] + ) ?> + </span> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml index 1876cf2edb786..357bbf27772b5 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml @@ -4,16 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer\Actions\Edit */ ?> -<?php if ($block->isProductVisibleInSiteVisibility()): ?> +<?php if ($block->isProductVisibleInSiteVisibility()) :?> <a class="action action-edit" - href="<?= /* @escapeNotVerified */ $block->getConfigureUrl() ?>" - title="<?= $block->escapeHtml(__('Edit item parameters')) ?>"> - <span> - <?= /* @escapeNotVerified */ __('Edit') ?> - </span> - </a> + href="<?= $block->escapeUrl($block->getConfigureUrl()) ?>" + title="<?= $block->escapeHtmlAttr(__('Edit item parameters')) ?>"> + <span><?= $block->escapeHtml(__('Edit')) ?></span> + </a> <?php endif ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml index 445721ca5d0c2..8b72b6c55e821 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/remove.phtml @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Checkout\Block\Cart\Item\Renderer\Actions\Remove */ ?> <a href="#" title="<?= $block->escapeHtml(__('Remove item')) ?>" class="action action-delete" - data-post='<?= /* @escapeNotVerified */ $block->getDeletePostJson() ?>'> + data-post='<?= /* @noEscape */ $block->getDeletePostJson() ?>'> <span> - <?= /* @escapeNotVerified */ __('Remove item') ?> + <?= $block->escapeHtml(__('Remove item')) ?> </span> </a> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml index 5530f7661bb1b..b045f4ec98ff7 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/methods.phtml @@ -4,19 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Checkout\Block\Cart */ ?> -<?php if (!$block->hasError()): ?> -<?php $methods = $block->getMethods('methods') ?: $block->getMethods('top_methods') ?> -<ul class="checkout methods items checkout-methods-items"> -<?php foreach ($methods as $method): ?> - <?php if ($methodHtml = $block->getMethodHtml($method)): ?> - <li class="item"><?= /* @escapeNotVerified */ $methodHtml ?></li> - <?php endif; ?> -<?php endforeach; ?> -</ul> +<?php if (!$block->hasError()) :?> + <?php $methods = $block->getMethods('methods') ?: $block->getMethods('top_methods') ?> + <ul class="checkout methods items checkout-methods-items"> + <?php foreach ($methods as $method) :?> + <?php $methodHtml = $block->getMethodHtml($method); ?> + <?php if (trim($methodHtml) !== '') :?> + <li class="item"><?= /* @noEscape */ $methodHtml ?></li> + <?php endif; ?> + <?php endforeach; ?> + </ul> <?php endif; ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml index af652e6f556b9..28275e0223936 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Checkout\Block\Cart\Sidebar */ ?> <div data-block="minicart" class="minicart-wrapper"> - <a class="action showcart" href="<?= /* @escapeNotVerified */ $block->getShoppingCartUrl() ?>" + <a class="action showcart" href="<?= $block->escapeUrl($block->getShoppingCartUrl()) ?>" data-bind="scope: 'minicart_content'"> - <span class="text"><?= /* @escapeNotVerified */ __('My Cart') ?></span> + <span class="text"><?= $block->escapeHtml(__('My Cart')) ?></span> <span class="counter qty empty" - data-bind="css: { empty: !!getCartParam('summary_count') == false }, blockLoader: isLoading"> + data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, blockLoader: isLoading"> <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span> <span class="counter-label"> <!-- ko if: getCartParam('summary_count') --> @@ -24,8 +22,8 @@ </span> </span> </a> - <?php if ($block->getIsNeedToDisplaySideBar()): ?> - <div class="block block-minicart empty" + <?php if ($block->getIsNeedToDisplaySideBar()) :?> + <div class="block block-minicart" data-role="dropdownDialog" data-mage-init='{"dropdownDialog":{ "appendTo":"[data-block=minicart]", @@ -41,17 +39,27 @@ </div> <?= $block->getChildHtml('minicart.addons') ?> </div> + <?php else :?> + <script> + require(['jquery'], function ($) { + $('a.action.showcart').click(function() { + $(document.body).trigger('processStart'); + }); + }); + </script> <?php endif ?> <script> - window.checkout = <?= /* @escapeNotVerified */ $block->getSerializedConfig() ?>; + window.checkout = <?= /* @noEscape */ $block->getSerializedConfig() ?>; </script> <script type="text/x-magento-init"> { "[data-block='minicart']": { - "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> + "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> }, "*": { - "Magento_Ui/js/block-loader": "<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" + "Magento_Ui/js/block-loader": "<?= $block->escapeJs( + $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) + ) ?>" } } </script> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml index 1c0c221a550cd..39289c711aa8c 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml @@ -3,13 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile + /** @var $block \Magento\Checkout\Block\Cart */ ?> <div class="cart-empty"> <?= $block->getChildHtml('checkout_cart_empty_widget') ?> - <p><?= /* @escapeNotVerified */ __('You have no items in your shopping cart.') ?></p> - <p><?php /* @escapeNotVerified */ echo __('Click <a href="%1">here</a> to continue shopping.', - $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> + <p><?= $block->escapeHtml(__('You have no items in your shopping cart.')) ?></p> + <p><?= $block->escapeHtml( + __( + 'Click <a href="%1">here</a> to continue shopping.', + $block->escapeUrl($block->getContinueShoppingUrl()) + ), + ['a'] + ) ?> + </p> <?= $block->getChildHtml('shopping.cart.table.after') ?> </div> +<script type="text/x-magento-init"> +{ + "*": { + "Magento_Checkout/js/empty-cart": {} + } +} +</script> \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml index b5ddb8446ba05..a44d37dccfdc5 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml @@ -4,36 +4,47 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Checkout\Block\Cart\Shipping */ ?> -<div id="block-shipping" class="block shipping" data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}'> +<div id="block-shipping" + class="block shipping" + data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}' +> <div class="title" data-role="title"> <strong id="block-shipping-heading" role="heading" aria-level="2"> - <?= /* @escapeNotVerified */ $block->getQuote()->isVirtual() ? __('Estimate Tax') : __('Estimate Shipping and Tax') ?> + <?= $block->getQuote()->isVirtual() + ? $block->escapeHtml(__('Estimate Tax')) + : $block->escapeHtml(__('Estimate Shipping and Tax')) + ?> </strong> </div> - <div id="block-summary" data-bind="scope:'block-summary'" class="content" data-role="content" aria-labelledby="block-shipping-heading"> + <div id="block-summary" + data-bind="scope:'block-summary'" + class="content" + data-role="content" + aria-labelledby="block-shipping-heading" + > <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> { "#block-summary": { - "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> + "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> } } </script> <script> - window.checkoutConfig = <?= /* @escapeNotVerified */ $block->getSerializedCheckoutConfig() ?>; + window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; window.customerData = window.checkoutConfig.customerData; window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; require([ 'mage/url', 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { - blockLoader("<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"); - return url.setBaseUrl('<?= /* @escapeNotVerified */ $block->getBaseUrl() ?>'); + blockLoader( + "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" + ); + return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); }) </script> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml index f39b70df98424..784c4c39076e6 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/totals.phtml @@ -15,7 +15,7 @@ <script type="text/x-magento-init"> { "#cart-totals": { - "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> + "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> } } </script> diff --git a/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml b/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml index 37a945b238d30..533d75b6ae843 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/item/price/row.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ @@ -12,6 +12,9 @@ $_item = $block->getItem(); ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getRowTotal()) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getRowTotal()), + ['span'] + ) ?> </span> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml b/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml index 45a6ef48e36d6..fafbe9c7c969d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/item/price/unit.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ @@ -12,6 +12,9 @@ $_item = $block->getItem(); ?> <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), + ['span'] + ) ?> </span> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml b/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml new file mode 100644 index 0000000000000..a6686444d2ed5 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +?> + +<?= $block->escapeHtml(__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + $block->getData('product_name'), + $block->getData('cart_url') +), ['a']); diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml index 47a56e8f333bc..55f7039f33344 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var $block \Magento\Checkout\Block\Onepage */ ?> <div id="checkout" data-bind="scope:'checkout'" class="checkout-container"> <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"checkoutLoader": {}}'> <div class="loader"> - <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" - alt="<?= /* @escapeNotVerified */ __('Loading...') ?>" + <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) ?>" + alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>" style="position: absolute;"> </div> </div> @@ -18,12 +18,12 @@ <script type="text/x-magento-init"> { "#checkout": { - "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?> + "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> } } </script> <script> - window.checkoutConfig = <?= /* @escapeNotVerified */ $block->getSerializedCheckoutConfig() ?>; + window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; // Create aliases for customer.js model from customer module window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; window.customerData = window.checkoutConfig.customerData; @@ -33,8 +33,8 @@ 'mage/url', 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { - blockLoader("<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"); - return url.setBaseUrl('<?= /* @escapeNotVerified */ $block->getBaseUrl() ?>'); + blockLoader("<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>"); + return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); }) </script> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml index 43791ef496745..ace2b0464171c 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/failure.phtml @@ -4,9 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Checkout\Block\Onepage\Failure */ ?> -<?php if ($block->getRealOrderId()) : ?><p><?= /* @escapeNotVerified */ __('Order #') . $block->getRealOrderId() ?></p><?php endif ?> -<?php if ($error = $block->getErrorMessage()) : ?><p><?= /* @escapeNotVerified */ $error ?></p><?php endif ?> -<p><?= /* @escapeNotVerified */ __('Click <a href="%1">here</a> to continue shopping.', $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> +<?php if ($block->getRealOrderId()) :?> + <p><?= $block->escapeHtml(__('Order #') . $block->getRealOrderId()) ?></p> +<?php endif ?> +<?php if ($error = $block->getErrorMessage()) :?> + <p><?= $block->escapeHtml($error) ?></p> +<?php endif ?> +<p><?= $block->escapeHtml( + __('Click <a href="%1">here</a> to continue shopping.', $block->escapeUrl($block->getContinueShoppingUrl())), + ['a'] +) ?> +</p> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml index 53a1fe8783509..b667764ac7bba 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/link.phtml @@ -4,15 +4,21 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var $block \Magento\Checkout\Block\Onepage\Link */ ?> -<?php if ($block->isPossibleOnepageCheckout()):?> +<?php if ($block->isPossibleOnepageCheckout()) :?> <button type="button" data-role="proceed-to-checkout" - title="<?= /* @escapeNotVerified */ __('Proceed to Checkout') ?>" - data-mage-init='{"Magento_Checkout/js/proceed-to-checkout":{"checkoutUrl":"<?= /* @escapeNotVerified */ $block->getCheckoutUrl() ?>"}}' + title="<?= $block->escapeHtmlAttr(__('Proceed to Checkout')) ?>" + data-mage-init='{ + "Magento_Checkout/js/proceed-to-checkout":{ + "checkoutUrl":"<?= $block->escapeJs($block->escapeUrl($block->getCheckoutUrl())) ?>" + } + }' class="action primary checkout<?= ($block->isDisabled()) ? ' disabled' : '' ?>" - <?php if ($block->isDisabled()):?>disabled="disabled"<?php endif; ?>> - <span><?= /* @escapeNotVerified */ __('Proceed to Checkout') ?></span> + <?php if ($block->isDisabled()) :?> + disabled="disabled" + <?php endif; ?>> + <span><?= $block->escapeHtml(__('Proceed to Checkout')) ?></span> </button> <?php endif?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml index 2428cc010779d..2a7ccc38e9d83 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item.phtml @@ -4,11 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block Magento\Checkout\Block\Cart\Item\Renderer */ $_item = $block->getItem(); +$taxDataHelper = $this->helper(Magento\Tax\Helper\Data::class); ?> <tbody class="cart item"> <tr> @@ -17,47 +18,53 @@ $_item = $block->getItem(); <?= $block->getImage($block->getProductForThumbnail(), 'cart_page_product_thumbnail')->toHtml() ?> </span> <div class="product-item-details"> - <strong class="product name product-item-name"><?= $block->escapeHtml($block->getProductName()) ?></strong> - <?php if ($_options = $block->getOptionList()):?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> - <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php endif; ?> - </dd> - <?php endforeach; ?> - </dl> + <strong class="product name product-item-name"> + <?= $block->escapeHtml($block->getProductName()) ?> + </strong> + <?php if ($_options = $block->getOptionList()) :?> + <dl class="item-options"> + <?php foreach ($_options as $_option) :?> + <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dd> + <?php if (isset($_formatedOptionValue['full_view'])) :?> + <?= $block->escapeHtml($_formatedOptionValue['full_view']) ?> + <?php else :?> + <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?php endif; ?> + </dd> + <?php endforeach; ?> + </dl> <?php endif;?> - <?php if ($addtInfoBlock = $block->getProductAdditionalInformationBlock()):?> + <?php if ($addtInfoBlock = $block->getProductAdditionalInformationBlock()) :?> <?= $addtInfoBlock->setItem($_item)->toHtml() ?> <?php endif;?> </div> </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + <?php if ($taxDataHelper->displayCartPriceInclTax() || $taxDataHelper->displayCartBothPrices()) :?> <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')) ?>"> <?= $block->getUnitPriceInclTaxHtml($_item) ?> </span> <?php endif; ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + <?php if ($taxDataHelper->displayCartPriceExclTax() || $taxDataHelper->displayCartBothPrices()) :?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <?= $block->getUnitPriceExclTaxHtml($_item) ?> </span> <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"><span class="qty"><?= /* @escapeNotVerified */ $_item->getQty() ?></span></td> + <td class="col qty" + data-th="<?= $block->escapeHtml(__('Qty')) ?>" + > + <span class="qty"><?= $block->escapeHtml($_item->getQty()) ?></span> + </td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + <?php if ($taxDataHelper->displayCartPriceInclTax() || $taxDataHelper->displayCartBothPrices()) :?> <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')) ?>"> <?= $block->getRowTotalInclTaxHtml($_item) ?> </span> <?php endif; ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + <?php if ($taxDataHelper->displayCartPriceExclTax() || $taxDataHelper->displayCartBothPrices()) :?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <?= $block->getRowTotalExclTaxHtml($_item) ?> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml index 7ee3e416b9ade..64eefd0dad011 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_excl_tax.phtml @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ $_item = $block->getItem(); ?> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getRowTotal()) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getRowTotal()), + ['span'] + ) ?> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml index 2f364aafbbcc0..14deb07640e6d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/row_incl_tax.phtml @@ -4,13 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ $_item = $block->getItem(); ?> -<?php $_incl = $this->helper('Magento\Checkout\Helper\Data')->getSubtotalInclTax($_item); ?> +<?php $_incl = $this->helper(Magento\Checkout\Helper\Data::class)->getSubtotalInclTax($_item); ?> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_incl) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_incl), + ['span'] + ) ?> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml index a1ec004c2a886..a65a5a50ae1a1 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_excl_tax.phtml @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ $_item = $block->getItem(); ?> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_item->getCalculationPrice()) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_item->getCalculationPrice()), + ['span'] + ) ?> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml index 0ed3c05ee6d1f..b623e1f1c3fd9 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage/review/item/price/unit_incl_tax.phtml @@ -4,13 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Checkout\Block\Item\Price\Renderer */ $_item = $block->getItem(); ?> -<?php $_incl = $this->helper('Magento\Checkout\Helper\Data')->getPriceInclTax($_item); ?> +<?php $_incl = $this->helper(Magento\Checkout\Helper\Data::class)->getPriceInclTax($_item); ?> <span class="cart-price"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($_incl) ?> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($_incl), + ['span'] + ) ?> </span> diff --git a/app/code/Magento/Checkout/view/frontend/templates/registration.phtml b/app/code/Magento/Checkout/view/frontend/templates/registration.phtml index f239fbd47dec2..da36b4b61d656 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/registration.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/registration.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var $block \Magento\Checkout\Block\Registration */ ?> <div id="registration" data-bind="scope:'registration'"> <br /> @@ -17,8 +17,9 @@ "registration": { "component": "Magento_Checkout/js/view/registration", "config": { - "registrationUrl": "<?= /* @escapeNotVerified */ $block->getCreateAccountUrl() ?>", - "email": "<?= /* @escapeNotVerified */ $block->getEmailAddress() ?>" + "registrationUrl": + "<?= $block->escapeJs($block->escapeUrl($block->getCreateAccountUrl())) ?>", + "email": "<?= $block->escapeJs($block->getEmailAddress()) ?>" }, "children": { "errors": { diff --git a/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml b/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml index 892b7926525f3..6aa883bc6b042 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/shipping/price.phtml @@ -4,10 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +?> +<?php +// phpcs:disable PSR2.Files.ClosingTag +/** @var $block \Magento\Checkout\Block\Shipping\Price */ ?> -<?php /** @var $block \Magento\Checkout\Block\Shipping\Price */ ?> <?php $shippingPrice = $block->getShippingPrice(); ?> -<?= /* @escapeNotVerified */ $shippingPrice ?> +<?= $block->escapeHtml($shippingPrice) ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/success.phtml b/app/code/Magento/Checkout/view/frontend/templates/success.phtml index b3517eab8a5d3..828b4eb86c330 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/success.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/success.phtml @@ -4,25 +4,23 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> <div class="checkout-success"> - <?php if ($block->getOrderId()):?> + <?php if ($block->getOrderId()) :?> <?php if ($block->getCanViewOrder()) :?> - <p><?= __('Your order number is: %1.', sprintf('<a href="%s" class="order-number"><strong>%s</strong></a>', $block->escapeHtml($block->getViewOrderUrl()), $block->escapeHtml($block->getOrderId()))) ?></p> + <p><?= $block->escapeHtml(__('Your order number is: %1.', sprintf('<a href="%s" class="order-number"><strong>%s</strong></a>', $block->escapeUrl($block->getViewOrderUrl()), $block->getOrderId())), ['a', 'strong']) ?></p> <?php else :?> - <p><?= __('Your order # is: <span>%1</span>.', $block->escapeHtml($block->getOrderId())) ?></p> + <p><?= $block->escapeHtml(__('Your order # is: <span>%1</span>.', $block->getOrderId()), ['span']) ?></p> <?php endif;?> - <p><?= /* @escapeNotVerified */ __('We\'ll email you an order confirmation with details and tracking info.') ?></p> + <p><?= $block->escapeHtml(__('We\'ll email you an order confirmation with details and tracking info.')) ?></p> <?php endif;?> <?= $block->getAdditionalInfoHtml() ?> <div class="actions-toolbar"> <div class="primary"> - <a class="action primary continue" href="<?= /* @escapeNotVerified */ $block->getContinueUrl() ?>"><span><?= /* @escapeNotVerified */ __('Continue Shopping') ?></span></a> + <a class="action primary continue" href="<?= $block->escapeUrl($block->getContinueUrl()) ?>"><span><?= $block->escapeHtml(__('Continue Shopping')) ?></span></a> </div> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml index 2ea1cdd7f53f5..0d9da171c11a8 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml @@ -4,18 +4,40 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var $block \Magento\Checkout\Block\Total\DefaultTotal */ ?> <tr class="totals"> - <th colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>" style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="mark" scope="row"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> - <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> + <th + colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" + style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" + class="mark" scope="row" + > + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <strong> + <?php endif; ?> + <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + </strong> + <?php endif; ?> </th> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="amount" data-th="<?= $block->escapeHtml($block->getTotal()->getTitle()) ?>"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> - <span><?= /* @escapeNotVerified */ $this->helper('Magento\Checkout\Helper\Data')->formatPrice($block->getTotal()->getValue()) ?></span> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> + <td + style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" + class="amount" + data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>" + > + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <strong> + <?php endif; ?> + <span> + <?= $block->escapeHtml( + $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()), + ['span'] + ) ?> + </span> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + </strong> + <?php endif; ?> </td> </tr> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js b/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js index 702df47526715..34f1700749794 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/select-payment-method.js @@ -12,6 +12,11 @@ define([ 'use strict'; return function (paymentMethod) { + if (paymentMethod) { + paymentMethod.__disableTmpl = { + title: true + }; + } quote.paymentMethod(paymentMethod); }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js new file mode 100644 index 0000000000000..9de8a93905c99 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -0,0 +1,79 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/url-builder', + 'mage/storage', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Customer/js/model/customer', + 'Magento_Checkout/js/action/get-totals', + 'Magento_Checkout/js/model/full-screen-loader', + 'underscore' +], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader, _) { + 'use strict'; + + /** + * Filter template data. + * + * @param {Object|Array} data + */ + var filterTemplateData = function (data) { + return _.each(data, function (value, key, list) { + if (_.isArray(value) || _.isObject(value)) { + list[key] = filterTemplateData(value); + } + + if (key === '__disableTmpl') { + delete list[key]; + } + }); + }; + + return function (messageContainer, paymentData, skipBilling) { + var serviceUrl, + payload; + + paymentData = filterTemplateData(paymentData); + skipBilling = skipBilling || false; + payload = { + cartId: quote.getQuoteId(), + paymentMethod: paymentData + }; + + /** + * Checkout for guest and registered customer. + */ + if (!customer.isLoggedIn()) { + serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { + cartId: quote.getQuoteId() + }); + payload.email = quote.guestEmail; + } else { + serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); + } + + if (skipBilling === false) { + payload.billingAddress = quote.billingAddress(); + } + + fullScreenLoader.startLoader(); + + return storage.post( + serviceUrl, JSON.stringify(payload) + ).fail( + function (response) { + errorProcessor.process(response, messageContainer); + } + ).always( + function () { + fullScreenLoader.stopLoader(); + } + ); + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js index 997b60503a2b3..d5261c976a725 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js @@ -7,54 +7,13 @@ * @api */ define([ - 'Magento_Checkout/js/model/quote', - 'Magento_Checkout/js/model/url-builder', - 'mage/storage', - 'Magento_Checkout/js/model/error-processor', - 'Magento_Customer/js/model/customer', - 'Magento_Checkout/js/action/get-totals', - 'Magento_Checkout/js/model/full-screen-loader' -], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'Magento_Checkout/js/action/set-payment-information-extended' + +], function (setPaymentInformationExtended) { 'use strict'; return function (messageContainer, paymentData) { - var serviceUrl, - payload; - - /** - * Checkout for guest and registered customer. - */ - if (!customer.isLoggedIn()) { - serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { - cartId: quote.getQuoteId() - }); - payload = { - cartId: quote.getQuoteId(), - email: quote.guestEmail, - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } else { - serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); - payload = { - cartId: quote.getQuoteId(), - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } - - fullScreenLoader.startLoader(); - return storage.post( - serviceUrl, JSON.stringify(payload) - ).fail( - function (response) { - errorProcessor.process(response, messageContainer); - } - ).always( - function () { - fullScreenLoader.stopLoader(); - } - ); + return setPaymentInformationExtended(messageContainer, paymentData, false); }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js new file mode 100644 index 0000000000000..1920bc4d7ac41 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/modal/alert', + 'jquery', + 'jquery/ui', + 'mage/validation' +], function (alert, $) { + 'use strict'; + + $.widget('mage.updateShoppingCart', { + options: { + validationURL: '', + eventName: 'updateCartItemQty', + updateCartActionContainer: '' + }, + + /** @inheritdoc */ + _create: function () { + this._on(this.element, { + 'submit': this.onSubmit + }); + }, + + /** + * Prevents default submit action and calls form validator. + * + * @param {Event} event + * @return {Boolean} + */ + onSubmit: function (event) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { + return true; + } + + if (this.isValid()) { + event.preventDefault(); + this.validateItems(this.options.validationURL, this.element.serialize()); + } + + return false; + }, + + /** + * Validates requested form. + * + * @return {Boolean} + */ + isValid: function () { + return this.element.validation() && this.element.validation('isValid'); + }, + + /** + * Validates updated shopping cart data. + * + * @param {String} url - request url + * @param {Object} data - post data for ajax call + */ + validateItems: function (url, data) { + $.extend(data, { + 'form_key': $.mage.cookies.get('form_key') + }); + + $.ajax({ + url: url, + data: data, + type: 'post', + dataType: 'json', + context: this, + + /** @inheritdoc */ + beforeSend: function () { + $(document.body).trigger('processStart'); + }, + + /** @inheritdoc */ + complete: function () { + $(document.body).trigger('processStop'); + } + }) + .done(function (response) { + if (response.success) { + this.onSuccess(); + } else { + this.onError(response); + } + }) + .fail(function () { + this.submitForm(); + }); + }, + + /** + * Form validation succeed. + */ + onSuccess: function () { + $(document).trigger('ajax:' + this.options.eventName); + this.submitForm(); + }, + + /** + * Form validation failed. + */ + onError: function (response) { + if (response['error_message']) { + alert({ + content: response['error_message'] + }); + } else { + this.submitForm(); + } + }, + + /** + * Real submit of validated form. + */ + submitForm: function () { + this.element + .off('submit', this.onSubmit) + .submit(); + } + }); + + return $.mage.updateShoppingCart; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js index 22b37b2da0b2f..1858ce946fb07 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js @@ -10,7 +10,8 @@ */ define([ 'jquery', - 'Magento_Customer/js/customer-data' + 'Magento_Customer/js/customer-data', + 'jquery/jquery-storageapi' ], function ($, storage) { 'use strict'; @@ -23,6 +24,22 @@ define([ storage.set(cacheKey, data); }, + /** + * @return {*} + */ + initData = function () { + return { + 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage + 'shippingAddressFromData': null, //Shipping address pulled from persistence storage + 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer + 'selectedShippingRate': null, //Shipping rate pulled from persistence storage + 'selectedPaymentMethod': null, //Payment method pulled from persistence storage + 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage + 'billingAddressFromData': null, //Billing address pulled from persistence storage + 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer + }; + }, + /** * @return {*} */ @@ -30,17 +47,12 @@ define([ var data = storage.get(cacheKey)(); if ($.isEmptyObject(data)) { - data = { - 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage - 'shippingAddressFromData': null, //Shipping address pulled from persistence storage - 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer - 'selectedShippingRate': null, //Shipping rate pulled from persistence storage - 'selectedPaymentMethod': null, //Payment method pulled from persistence storage - 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage - 'billingAddressFromData': null, //Billing address pulled from persistence storage - 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer - }; - saveData(data); + data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); + + if ($.isEmptyObject(data)) { + data = initData(); + saveData(data); + } } return data; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js new file mode 100644 index 0000000000000..27d38697afe39 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Customer/js/customer-data' +], function (customerData) { + 'use strict'; + + customerData.reload(['cart'], false); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 6e689091d091f..9b20a782c38d9 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -58,6 +58,16 @@ define([ } delete addressData['region_id']; + if (addressData['custom_attributes']) { + addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) + .map(function (customAttribute) { + return { + 'attribute_code': customAttribute[0], + 'value': customAttribute[1] + }; + }); + } + return address(addressData); }, @@ -72,20 +82,20 @@ define([ output = {}, streetObject; + $.each(addrs, function (key) { + if (addrs.hasOwnProperty(key) && !$.isFunction(addrs[key])) { + output[self.toUnderscore(key)] = addrs[key]; + } + }); + if ($.isArray(addrs.street)) { streetObject = {}; addrs.street.forEach(function (value, index) { streetObject[index] = value; }); - addrs.street = streetObject; + output.street = streetObject; } - $.each(addrs, function (key) { - if (addrs.hasOwnProperty(key) && !$.isFunction(addrs[key])) { - output[self.toUnderscore(key)] = addrs[key]; - } - }); - return output; }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 76e3d911e7d3f..54e496131972e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -14,55 +14,71 @@ define([ 'use strict'; var rateProcessors = [], - totalsProcessors = []; + totalsProcessors = [], - quote.shippingAddress.subscribe(function () { - var type = quote.shippingAddress().getType(); + /** + * Estimate totals for shipping address and update shipping rates. + */ + estimateTotalsAndUpdateRates = function () { + var type = quote.shippingAddress().getType(); - if ( - quote.isVirtual() || - window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 - ) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.shippingAddress()) : - totalsProcessors['default'].estimateTotals(quote.shippingAddress()); - } else { - // check if user data not changed -> load rates from cache - if (!cartCache.isChanged('address', quote.shippingAddress()) && - !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && - cartCache.get('rates') + if ( + quote.isVirtual() || + window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 ) { - shippingService.setShippingRates(cartCache.get('rates')); + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.shippingAddress()) : + totalsProcessors['default'].estimateTotals(quote.shippingAddress()); + } else { + // check if user data not changed -> load rates from cache + if (!cartCache.isChanged('address', quote.shippingAddress()) && + !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + cartCache.get('rates') + ) { + shippingService.setShippingRates(cartCache.get('rates')); - return; + return; + } + + // update rates list when estimated address was set + rateProcessors['default'] = defaultProcessor; + rateProcessors[type] ? + rateProcessors[type].getRates(quote.shippingAddress()) : + rateProcessors['default'].getRates(quote.shippingAddress()); + + // save rates to cache after load + shippingService.getShippingRates().subscribe(function (rates) { + cartCache.set('rates', rates); + }); } + }, - // update rates list when estimated address was set - rateProcessors['default'] = defaultProcessor; - rateProcessors[type] ? - rateProcessors[type].getRates(quote.shippingAddress()) : - rateProcessors['default'].getRates(quote.shippingAddress()); + /** + * Estimate totals for shipping address. + */ + estimateTotalsShipping = function () { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + }, - // save rates to cache after load - shippingService.getShippingRates().subscribe(function (rates) { - cartCache.set('rates', rates); - }); - } - }); - quote.shippingMethod.subscribe(function () { - totalsDefaultProvider.estimateTotals(quote.shippingAddress()); - }); - quote.billingAddress.subscribe(function () { - var type = quote.billingAddress().getType(); + /** + * Estimate totals for billing address. + */ + estimateTotalsBilling = function () { + var type = quote.billingAddress().getType(); + + if (quote.isVirtual()) { + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.billingAddress()) : + totalsProcessors['default'].estimateTotals(quote.billingAddress()); + } + }; - if (quote.isVirtual()) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.billingAddress()) : - totalsProcessors['default'].estimateTotals(quote.billingAddress()); - } - }); + quote.shippingAddress.subscribe(estimateTotalsAndUpdateRates); + quote.shippingMethod.subscribe(estimateTotalsShipping); + quote.billingAddress.subscribe(estimateTotalsBilling); + customerData.get('cart').subscribe(estimateTotalsShipping); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js index e269462047748..d8fea6ea329e8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/totals-processor/default.js @@ -38,7 +38,7 @@ define([ payload.addressInformation['shipping_carrier_code'] = quote.shippingMethod()['carrier_code']; } - storage.post( + return storage.post( serviceUrl, JSON.stringify(payload), false ).done(function (result) { var data = { @@ -87,17 +87,7 @@ define([ data.shippingCarrierCode = quote.shippingMethod()['carrier_code']; } - if (!cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && - !cartCache.isChanged('shippingMethodCode', data.shippingMethodCode) && - !cartCache.isChanged('shippingCarrierCode', data.shippingCarrierCode) && - !cartCache.isChanged('address', address) && - cartCache.get('totals') && - !cartCache.isChanged('subtotal', parseFloat(quote.totals().subtotal)) - ) { - quote.setTotals(cartCache.get('totals')); - } else { - loadFromServer(address); - } + return loadFromServer(address); } }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 73f4df567903c..e54f464f24d02 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -60,14 +60,21 @@ define([ this.resolveBillingAddress(); } } - }, /** * Resolve shipping address. Used local storage */ resolveShippingAddress: function () { - var newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); + var newCustomerShippingAddress; + + if (!checkoutData.getShippingAddressFromData() && + window.checkoutConfig.shippingAddressFromData + ) { + checkoutData.setShippingAddressFromData(window.checkoutConfig.shippingAddressFromData); + } + + newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); if (newCustomerShippingAddress) { createShippingAddress(newCustomerShippingAddress); @@ -196,8 +203,17 @@ define([ * Resolve billing address. Used local storage */ resolveBillingAddress: function () { - var selectedBillingAddress = checkoutData.getSelectedBillingAddress(), - newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); + var selectedBillingAddress, + newCustomerBillingAddressData; + + if (!checkoutData.getBillingAddressFromData() && + window.checkoutConfig.billingAddressFromData + ) { + checkoutData.setBillingAddressFromData(window.checkoutConfig.billingAddressFromData); + } + + selectedBillingAddress = checkoutData.getSelectedBillingAddress(); + newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); if (selectedBillingAddress) { if (selectedBillingAddress == 'new-customer-address' && newCustomerBillingAddressData) { //eslint-disable-line @@ -218,16 +234,31 @@ define([ * Apply resolved billing address to quote */ applyBillingAddress: function () { - var shippingAddress; + var shippingAddress, + isBillingAddressInitialized; if (quote.billingAddress()) { selectBillingAddress(quote.billingAddress()); return; } + + if (quote.isVirtual() || !quote.billingAddress()) { + isBillingAddressInitialized = addressList.some(function (addrs) { + if (addrs.isDefaultBilling()) { + selectBillingAddress(addrs); + + return true; + } + + return false; + }); + } + shippingAddress = quote.shippingAddress(); - if (shippingAddress && + if (!isBillingAddressInitialized && + shippingAddress && shippingAddress.canUseForBilling() && (shippingAddress.isDefaultShipping() || !quote.isVirtual()) ) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js index 848a7daf71e1b..f60312d223561 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js @@ -8,8 +8,10 @@ */ define([ 'mage/url', - 'Magento_Ui/js/model/messageList' -], function (url, globalMessageList) { + 'Magento_Ui/js/model/messageList', + 'consoleLogger', + 'mage/translate' +], function (url, globalMessageList, consoleLogger, $t) { 'use strict'; return { @@ -25,7 +27,14 @@ define([ if (response.status == 401) { //eslint-disable-line eqeqeq window.location.replace(url.build('customer/account/login/')); } else { - error = JSON.parse(response.responseText); + try { + error = JSON.parse(response.responseText); + } catch (e) { + consoleLogger.error(e); + error = { + message: $t('Something went wrong with your request. Please try again later.') + }; + } messageContainer.addErrorMessage(error); } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js index 3bc5911946fda..7b858c92bee34 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js @@ -17,18 +17,28 @@ define([ */ return function (addressData) { var identifier = Date.now(), + countryId, regionId; - if (addressData.region && addressData.region['region_id']) { - regionId = addressData.region['region_id']; - } else if (addressData['country_id'] && addressData['country_id'] == window.checkoutConfig.defaultCountryId) { //eslint-disable-line - regionId = window.checkoutConfig.defaultRegionId || undefined; + countryId = addressData['country_id'] || addressData.countryId; + + if (countryId) { + if (addressData.region && addressData.region['region_id']) { + regionId = addressData.region['region_id']; + } else if (!addressData['region_id']) { + regionId = undefined; + } else if (countryId === window.checkoutConfig.defaultCountryId) { + regionId = window.checkoutConfig.defaultRegionId; + } + } else { + countryId = window.checkoutConfig.defaultCountryId; + regionId = window.checkoutConfig.defaultRegionId; } return { email: addressData.email, - countryId: addressData['country_id'] || addressData.countryId || window.checkoutConfig.defaultCountryId, - regionId: regionId || addressData.regionId, + countryId: countryId, + regionId: regionId, regionCode: addressData.region ? addressData.region['region_code'] : null, region: addressData.region ? addressData.region.region : null, customerId: addressData['customer_id'] || addressData.customerId, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js index c3c5b9d68cec0..c07878fcaea92 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js @@ -9,9 +9,10 @@ define( [ 'mage/storage', 'Magento_Checkout/js/model/error-processor', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Customer/js/customer-data' ], - function (storage, errorProcessor, fullScreenLoader) { + function (storage, errorProcessor, fullScreenLoader, customerData) { 'use strict'; return function (serviceUrl, payload, messageContainer) { @@ -23,6 +24,23 @@ define( function (response) { errorProcessor.process(response, messageContainer); } + ).success( + function (response) { + var clearData = { + 'selectedShippingAddress': null, + 'shippingAddressFromData': null, + 'newCustomerShippingAddress': null, + 'selectedShippingRate': null, + 'selectedPaymentMethod': null, + 'selectedBillingAddress': null, + 'billingAddressFromData': null, + 'newCustomerBillingAddress': null + }; + + if (response.responseType !== 'error') { + customerData.set('checkout-data', clearData); + } + } ).always( function () { fullScreenLoader.stopLoader(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js index a95471d90dab8..0a5334a42c7e5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js @@ -14,11 +14,13 @@ define([ /** * @param {*} postCode * @param {*} countryId + * @param {Array} postCodesPatterns * @return {Boolean} */ - validate: function (postCode, countryId) { - var patterns = window.checkoutConfig.postCodes[countryId], - pattern, regex; + validate: function (postCode, countryId, postCodesPatterns) { + var pattern, regex, + patterns = postCodesPatterns ? postCodesPatterns[countryId] : + window.checkoutConfig.postCodes[countryId]; this.validatedPostCodeExample = []; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js index 2510d1aced3d3..3486a92736617 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js @@ -7,7 +7,8 @@ */ define([ 'ko', - 'underscore' + 'underscore', + 'domReady!' ], function (ko, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index d31c0dca38116..8b07c02e4d380 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -35,13 +35,14 @@ define([ var checkoutConfig = window.checkoutConfig, validators = [], observedElements = [], - postcodeElement = null, + postcodeElements = [], postcodeElementName = 'postcode'; validators.push(defaultValidator); return { validateAddressTimeout: 0, + validateZipCodeTimeout: 0, validateDelay: 2000, /** @@ -101,7 +102,7 @@ define([ if (element.index === postcodeElementName) { this.bindHandler(element, delay); - postcodeElement = element; + postcodeElements.push(element); } }, @@ -133,10 +134,20 @@ define([ }); } else { element.on('value', function () { + clearTimeout(self.validateZipCodeTimeout); + self.validateZipCodeTimeout = setTimeout(function () { + if (element.index === postcodeElementName) { + self.postcodeValidation(element); + } else { + $.each(postcodeElements, function (index, elem) { + self.postcodeValidation(elem); + }); + } + }, delay); + if (!formPopUpState.isVisible()) { clearTimeout(self.validateAddressTimeout); self.validateAddressTimeout = setTimeout(function () { - self.postcodeValidation(); self.validateFields(); }, delay); } @@ -148,8 +159,8 @@ define([ /** * @return {*} */ - postcodeValidation: function () { - var countryId = $('select[name="country_id"]').val(), + postcodeValidation: function (postcodeElement) { + var countryId = $('select[name="country_id"]:visible').val(), validationResult, warnMessage; @@ -178,8 +189,8 @@ define([ */ validateFields: function () { var addressFlat = addressConverter.formDataProviderToFlatData( - this.collectObservedData(), - 'shippingAddress' + this.collectObservedData(), + 'shippingAddress' ), address; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js index 3a6574bff8948..e34f861f7714f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js @@ -35,7 +35,7 @@ define([ saveShippingInformation: function () { var payload; - if (!quote.billingAddress()) { + if (!quote.billingAddress() && quote.shippingAddress().canUseForBilling()) { selectBillingAddressAction(quote.shippingAddress()); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/payload-extender.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/payload-extender.js index 9a082a056a382..dcd4340774af4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/payload-extender.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/payload-extender.js @@ -1,5 +1,5 @@ /** - * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ define(function () { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js index bfcd0d02585bb..64e751d0d56d0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js @@ -66,7 +66,7 @@ define([ * @param {*} sortOrder */ registerStep: function (code, alias, title, isVisible, navigate, sortOrder) { - var hash; + var hash, active; if ($.inArray(code, this.validCodes) !== -1) { throw new DOMException('Step code [' + code + '] already registered in step navigator'); @@ -87,6 +87,12 @@ define([ navigate: navigate, sortOrder: sortOrder }); + active = this.getActiveItemIndex(); + steps.each(function (elem, index) { + if (active !== index) { + elem.isVisible(false); + } + }); this.stepCodes.push(code); hash = window.location.hash.replace('#', ''); @@ -111,10 +117,14 @@ define([ getActiveItemIndex: function () { var activeIndex = 0; - steps.sort(this.sortItems).forEach(function (element, index) { + steps.sort(this.sortItems).some(function (element, index) { if (element.isVisible()) { activeIndex = index; + + return true; } + + return false; }); return activeIndex; @@ -172,6 +182,15 @@ define([ }); }, + /** + * Sets window location hash. + * + * @param {String} hash + */ + setHash: function (hash) { + window.location.hash = hash; + }, + /** * Next step. */ @@ -189,7 +208,7 @@ define([ if (steps().length > activeIndex + 1) { code = steps()[activeIndex + 1].code; steps()[activeIndex + 1].isVisible(true); - window.location = window.checkoutConfig.checkoutUrl + '#' + code; + this.setHash(code); document.body.scrollTop = document.documentElement.scrollTop = 0; } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js index f0679c657ab90..0bb0a53ce0a6b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/proceed-to-checkout.js @@ -22,6 +22,7 @@ define([ return false; } + $(element).attr('disabled', true); location.href = config.checkoutUrl; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index 79050ca087740..cf2a59cdba427 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -188,6 +188,8 @@ define([ if (!this.options.optionalRegionAllowed) { //eslint-disable-line max-depth regionList.attr('disabled', 'disabled'); + } else { + regionList.removeAttr('disabled'); } } @@ -195,6 +197,8 @@ define([ regionInput.hide(); label.attr('for', regionList.attr('id')); } else { + this._removeSelectOptions(regionList); + if (this.options.isRegionRequired) { regionInput.addClass('required-entry').removeAttr('disabled'); requiredLabel.addClass('required'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 399321bd2f67d..8935242724f3e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,7 +14,11 @@ define([ _create: function () { var items, i; - $(this.options.emptyCartButton).on('click', $.proxy(function () { + $(this.options.emptyCartButton).on('click', $.proxy(function (event) { + if (event.detail === 0) { + return; + } + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index 13a2b524e5186..e66c66006246c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -9,11 +9,12 @@ define([ 'Magento_Customer/js/customer-data', 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/modal/confirm', + 'underscore', 'jquery/ui', 'mage/decorate', 'mage/collapsible', 'mage/cookies' -], function ($, authenticationPopup, customerData, alert, confirm) { +], function ($, authenticationPopup, customerData, alert, confirm, _) { 'use strict'; $.widget('mage.sidebar', { @@ -24,6 +25,7 @@ define([ } }, scrollHeight: 0, + shoppingCartUrl: window.checkout.shoppingCartUrl, /** * Create sidebar. @@ -60,13 +62,15 @@ define([ }; events['click ' + this.options.button.checkout] = $.proxy(function () { var cart = customerData.get('cart'), - customer = customerData.get('customer'); + customer = customerData.get('customer'), + element = $(this.options.button.checkout); if (!customer().firstname && cart().isGuestCheckoutAllowed === false) { // set URL for redirect on successful login/registration. It's postprocessed on backend. $.cookie('login_redirect', this.options.url.checkout); if (this.options.url.isRedirectRequired) { + element.prop('disabled', true); location.href = this.options.url.loginUrl; } else { authenticationPopup.showModal(); @@ -74,6 +78,7 @@ define([ return false; } + element.prop('disabled', true); location.href = this.options.url.checkout; }, this); @@ -105,6 +110,13 @@ define([ self._showItemButton($(event.target)); }; + /** + * @param {jQuery.Event} event + */ + events['change ' + this.options.item.qty] = function (event) { + self._showItemButton($(event.target)); + }; + /** * @param {jQuery.Event} event */ @@ -212,6 +224,15 @@ define([ * @param {HTMLElement} elem */ _updateItemQtyAfter: function (elem) { + var productData = this._getProductById(Number(elem.data('cart-item'))); + + if (!_.isUndefined(productData)) { + $(document).trigger('ajax:updateCartItemQty'); + + if (window.location.href === this.shoppingCartUrl) { + window.location.reload(false); + } + } this._hideItemButton(elem); }, @@ -234,11 +255,26 @@ define([ * @private */ _removeItemAfter: function (elem) { - var productData = customerData.get('cart')().items.find(function (item) { - return Number(elem.data('cart-item')) === Number(item['item_id']); - }); + var productData = this._getProductById(Number(elem.data('cart-item'))); + + if (!_.isUndefined(productData)) { + $(document).trigger('ajax:removeFromCart', { + productIds: [productData['product_id']] + }); + } + }, - $(document).trigger('ajax:removeFromCart', productData['product_sku']); + /** + * Retrieves product data by Id. + * + * @param {Number} productId - product Id + * @returns {Object|undefined} + * @private + */ + _getProductById: function (productId) { + return _.find(customerData.get('cart')().items, function (item) { + return productId === Number(item['item_id']); + }); }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/authentication.js b/app/code/Magento/Checkout/view/frontend/web/js/view/authentication.js index e7480b25aa791..796b502cab54d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/authentication.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/authentication.js @@ -18,7 +18,6 @@ define([ return Component.extend({ isGuestCheckoutAllowed: checkoutConfig.isGuestCheckoutAllowed, - isCustomerLoginRequired: checkoutConfig.isCustomerLoginRequired, registerUrl: checkoutConfig.registerUrl, forgotPasswordUrl: checkoutConfig.forgotPasswordUrl, autocomplete: checkoutConfig.autocomplete, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 6b5d08c2641cc..d68b0682eb511 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -17,7 +17,8 @@ define([ 'Magento_Customer/js/customer-data', 'Magento_Checkout/js/action/set-billing-address', 'Magento_Ui/js/model/messageList', - 'mage/translate' + 'mage/translate', + 'Magento_Checkout/js/model/shipping-rates-validator' ], function ( ko, @@ -33,7 +34,8 @@ function ( customerData, setBillingAddressAction, globalMessageList, - $t + $t, + shippingRatesValidator ) { 'use strict'; @@ -71,6 +73,7 @@ function ( quote.paymentMethod.subscribe(function () { checkoutDataResolver.resolveBillingAddress(); }, this); + shippingRatesValidator.initFields(this.get('name') + '.form-fields'); }, /** @@ -199,6 +202,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js index 6cc5d3f6679a9..a709ff0aacee1 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js @@ -47,6 +47,12 @@ define( checkoutDataResolver.resolveEstimationAddress(); address = quote.isVirtual() ? quote.billingAddress() : quote.shippingAddress(); + if (!address && quote.isVirtual()) { + address = addressConverter.formAddressDataToQuoteAddress( + checkoutData.getSelectedBillingAddress() + ); + } + if (address) { estimatedAddress = address.isEditable() ? addressConverter.quoteAddressToFormAddressData(address) : diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/configure/product-customer-data.js b/app/code/Magento/Checkout/view/frontend/web/js/view/configure/product-customer-data.js index a612b5e2dc6b7..0e2fc6bbfc8fe 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/configure/product-customer-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/configure/product-customer-data.js @@ -1,15 +1,19 @@ require([ 'jquery', - 'Magento_Customer/js/customer-data' -], function ($, customerData) { + 'Magento_Customer/js/customer-data', + 'underscore', + 'domReady!' +], function ($, customerData, _) { 'use strict'; var selectors = { qtySelector: '#product_addtocart_form [name="qty"]', - productIdSelector: '#product_addtocart_form [name="product"]' + productIdSelector: '#product_addtocart_form [name="product"]', + itemIdSelector: '#product_addtocart_form [name="item"]' }, cartData = customerData.get('cart'), productId = $(selectors.productIdSelector).val(), + itemId = $(selectors.itemIdSelector).val(), productQty, productQtyInput, @@ -38,9 +42,11 @@ require([ if (!(data && data.items && data.items.length && productId)) { return; } - product = data.items.find(function (item) { - return item['product_id'] === productId || - item['item_id'] === productId; + product = _.find(data.items, function (item) { + if (item['item_id'] === itemId) { + return item['product_id'] === productId || + item['item_id'] === productId; + } }); if (!product) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js index 90ad07da0ae37..a195037394085 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -17,7 +17,16 @@ define([ ], function ($, Component, ko, customer, checkEmailAvailability, loginAction, quote, checkoutData, fullScreenLoader) { 'use strict'; - var validatedEmail = checkoutData.getValidatedEmailValue(); + var validatedEmail; + + if (!checkoutData.getValidatedEmailValue() && + window.checkoutConfig.validatedEmailValue + ) { + checkoutData.setInputFieldEmailValue(window.checkoutConfig.validatedEmailValue); + checkoutData.setValidatedEmailValue(window.checkoutConfig.validatedEmailValue); + } + + validatedEmail = checkoutData.getValidatedEmailValue(); if (validatedEmail && !customer.isLoggedIn()) { quote.guestEmail = validatedEmail; @@ -33,6 +42,9 @@ define([ listens: { email: 'emailHasChanged', emailFocused: 'validateEmail' + }, + ignoreTmpls: { + email: true } }, checkDelay: 2000, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index 5ac062a12180c..c1e93cac86fb7 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -10,7 +10,8 @@ define([ 'ko', 'underscore', 'sidebar', - 'mage/translate' + 'mage/translate', + 'mage/dropdown' ], function (Component, customerData, $, ko, _) { 'use strict'; @@ -80,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -100,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId && + cartData().website_id !== undefined + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js index c17e5e40d5c98..e8994c61b7221 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js @@ -66,9 +66,21 @@ define([ navigate: function () { var self = this; - getPaymentInformation().done(function () { - self.isVisible(true); - }); + if (!self.hasShippingMethod()) { + this.isVisible(false); + stepNavigator.setHash('shipping'); + } else { + getPaymentInformation().done(function () { + self.isVisible(true); + }); + } + }, + + /** + * @return {Boolean} + */ + hasShippingMethod: function () { + return window.checkoutConfig.selectedShippingMethod !== null; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js index 7b200860c4d55..1b5463c0770a3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/default.js @@ -133,15 +133,14 @@ define([ event.preventDefault(); } - if (this.validate() && additionalValidators.validate()) { + if (this.validate() && + additionalValidators.validate() && + this.isPlaceOrderActionAllowed() === true + ) { this.isPlaceOrderActionAllowed(false); this.getPlaceOrderDeferredObject() - .fail( - function () { - self.isPlaceOrderActionAllowed(true); - } - ).done( + .done( function () { self.afterPlaceOrder(); @@ -149,6 +148,10 @@ define([ redirectOnSuccessAction.execute(); } } + ).always( + function () { + self.isPlaceOrderActionAllowed(true); + } ); return true; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js index 0cbc16ef72bc3..db49683129f2b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js @@ -24,8 +24,19 @@ define([ /** @inheritdoc */ initialize: function () { + var stepsValue; + this._super(); $(window).hashchange(_.bind(stepNavigator.handleHash, stepNavigator)); + + if (!window.location.hash) { + stepsValue = stepNavigator.steps(); + + if (stepsValue.length) { + stepNavigator.setHash(stepsValue.sort(stepNavigator.sortItems)[0].code); + } + } + stepNavigator.handleHash(); }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js b/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js index c715b5c4d45ce..a7b3e18c06088 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js @@ -38,7 +38,16 @@ define([ }, /** - * Create new user account + * @return String + */ + getUrl: function () { + return this.registrationUrl; + }, + + /** + * Create new user account. + * + * @deprecated */ createAccount: function () { this.creationStarted(true); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 619de95e467f0..b4997f9664c81 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -247,6 +247,8 @@ define([ */ setShippingInformation: function () { if (this.validateShippingInformation()) { + quote.billingAddress(null); + checkoutDataResolver.resolveBillingAddress(); setShippingInformationAction().done( function () { stepNavigator.next(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js index 488bcfd67594c..003fef9f15d3f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/cart-items.js @@ -12,6 +12,8 @@ define([ ], function (ko, totals, Component, stepNavigator, quote) { 'use strict'; + var useQty = window.checkoutConfig.useQty; + return Component.extend({ defaults: { template: 'Magento_Checkout/summary/cart-items' @@ -44,6 +46,15 @@ define([ return parseInt(totals.getItems()().length, 10); }, + /** + * Returns shopping cart items summary (includes config settings) + * + * @returns {Number} + */ + getCartSummaryItemsCount: function () { + return useQty ? this.getItemsQty() : this.getCartLineItemsCount(); + }, + /** * @inheritdoc */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js new file mode 100644 index 0000000000000..ed41fd26c47ec --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['uiComponent'], function (Component) { + 'use strict'; + + var quoteMessages = window.checkoutConfig.quoteMessages; + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/summary/item/details/message' + }, + displayArea: 'item_message', + quoteMessages: quoteMessages, + + /** + * @param {Object} item + * @return {null} + */ + getMessage: function (item) { + if (this.quoteMessages[item['item_id']]) { + return this.quoteMessages[item['item_id']]; + } + + return null; + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js index 3fda260339254..ae4388f282c95 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js @@ -21,14 +21,19 @@ define([ * @return {*} */ getShippingMethodTitle: function () { - var shippingMethod; + var shippingMethod, + shippingMethodTitle = ''; if (!this.isCalculated()) { return ''; } shippingMethod = quote.shippingMethod(); - return shippingMethod ? shippingMethod['carrier_title'] + ' - ' + shippingMethod['method_title'] : ''; + if (typeof shippingMethod['method_title'] !== 'undefined') { + shippingMethodTitle = ' - ' + shippingMethod['method_title']; + } + + return shippingMethod ? shippingMethod['carrier_title'] + shippingMethodTitle : ''; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa9..63edb5057b933 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -22,7 +22,7 @@ <button class="action action-update" type="button" data-bind="click: updateAddress"> <span data-bind="i18n: 'Update'"></span> </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> + <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit, visible: canUseCancelBillingAddress()"> <span data-bind="i18n: 'Cancel'"></span> </button> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index f2baf5d50030e..a0827d17d6622 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -4,28 +4,35 @@ * See COPYING.txt for license details. */ --> -<div class="billing-address-details" data-bind="if: isAddressDetailsVisible() && currentBillingAddress()"> - <!-- ko text: currentBillingAddress().prefix --><!-- /ko --> <!-- ko text: currentBillingAddress().firstname --><!-- /ko --> <!-- ko text: currentBillingAddress().middlename --><!-- /ko --> - <!-- ko text: currentBillingAddress().lastname --><!-- /ko --> <!-- ko text: currentBillingAddress().suffix --><!-- /ko --><br/> - <!-- ko text: currentBillingAddress().street --><!-- /ko --><br/> - <!-- ko text: currentBillingAddress().city --><!-- /ko -->, <span data-bind="html: currentBillingAddress().region"></span> <!-- ko text: currentBillingAddress().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(currentBillingAddress().countryId) --><!-- /ko --><br/> - <!-- ko if: (currentBillingAddress().telephone) --> - <a data-bind="text: currentBillingAddress().telephone, attr: {'href': 'tel:' + currentBillingAddress().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: currentBillingAddress().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> - <button type="button" +<div if="isAddressDetailsVisible() && currentBillingAddress()" class="billing-address-details"> + <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> + <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> + <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> + <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> + <text args="getCountryName(currentBillingAddress().countryId)"/><br/> + <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> + + <each args="data: currentBillingAddress().customAttributes, as: 'element'"> + <if args="typeof element === 'object'"> + <if args="element.label"> + <text args="element.label"/> + </if> + <ifnot args="element.label"> + <if args="element.value"> + <text args="element.value"/> + </if> + </ifnot> + </if> + <if args="typeof element === 'string'"> + <text args="element"/> + </if><br/> + </each> + + <button visible="!isAddressSameAsShipping()" + type="button" class="action action-edit-address" - data-bind="visible: !isAddressSameAsShipping(), click: editAddress"> - <span data-bind="i18n: 'Edit'"></span> + click="editAddress"> + <span translate="'Edit'"></span> </button> </div> + diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html index 1be754934042b..54fe9a1f59394 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html @@ -9,14 +9,14 @@ <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> <form data-bind="attr: {'data-hasrequired': $t('* Required Fields')}"> - <fieldset id="billing-new-address-form" class="fieldset address"> + <fieldset class="fieldset address" data-form="billing-new-address"> <!-- ko foreach: getRegion('additional-fieldsets') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> <!-- ko if: (isCustomerLoggedIn && customerHasAddresses) --> <div class="choice field"> - <input type="checkbox" class="checkbox" id="billing-save-in-address-book" data-bind="checked: saveInAddressBook" /> - <label class="label" for="billing-save-in-address-book"> + <input type="checkbox" class="checkbox" data-bind="checked: saveInAddressBook, attr: {id: 'billing-save-in-address-book-' + getCode($parent)}" /> + <label class="label" data-bind="attr: {for: 'billing-save-in-address-book-' + getCode($parent)}" > <span data-bind="i18n: 'Save in address book'"></span> </label> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html index 0dee6cb0708e6..8d6142e07fcf0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html @@ -22,7 +22,8 @@ type="email" data-bind=" textInput: email, - hasFocus: emailFocused" + hasFocus: emailFocused, + mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" id="customer-email" /> @@ -41,7 +42,7 @@ <input class="input-text" data-bind=" attr: { - placeholder: $t('optional'), + placeholder: $t('Password'), }" type="password" name="password" diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 8bf1a87d34e6e..0719a7d01ec70 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -29,9 +29,13 @@ <div class="items-total"> <span class="count" if="maxItemsToDisplay < getCartLineItemsCount()" text="maxItemsToDisplay"/> <translate args="'of'" if="maxItemsToDisplay < getCartLineItemsCount()"/> - <span class="count" text="getCartLineItemsCount()"/> - <translate args="'Item in Cart'" if="getCartLineItemsCount() === 1"/> - <translate args="'Items in Cart'" if="getCartLineItemsCount() > 1"/> + <span class="count" text="getCartParam('summary_count')"/> + <!-- ko if: (getCartLineItemsCount() === 1) --> + <span translate="'Item in Cart'"/> + <!--/ko--> + <!-- ko if: (getCartLineItemsCount() > 1) --> + <span translate="'Items in Cart'"/> + <!--/ko--> </div> <each args="getRegion('subtotalContainer')" render=""/> @@ -93,7 +97,7 @@ </div> </div> - <div id="minicart-widgets" class="minicart-widgets"> + <div id="minicart-widgets" class="minicart-widgets" if="getRegion('promotion').length"> <each args="getRegion('promotion')" render=""/> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 8d32adb75308f..41d442a76d510 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -24,7 +24,7 @@ <div class="product-item-details"> <strong class="product-item-name"> <!-- ko if: product_has_url --> - <a data-bind="attr: {href: product_url}, text: product_name"></a> + <a data-bind="attr: {href: product_url}, html: product_name"></a> <!-- /ko --> <!-- ko ifnot: product_has_url --> <!-- ko text: product_name --><!-- /ko --> @@ -45,7 +45,7 @@ <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> <!-- ko ifnot: Array.isArray(option.value) --> - <span data-bind="html: option.value"></span> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/registration.html b/app/code/Magento/Checkout/view/frontend/web/template/registration.html index 256fc1968abfc..ea94726e5443e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/registration.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/registration.html @@ -11,11 +11,8 @@ <!-- ko if: isFormVisible --> <p data-bind="i18n: 'You can track your order status by creating an account.'"></p> <p><span data-bind="i18n: 'Email Address'"></span>: <span data-bind="text: getEmailAddress()"></span></p> - <form method="post" data-bind="submit: createAccount"> - <input type="submit" class="action primary" data-bind="value: $t('Create an Account'), disable: creationStarted" /> + <form method="get" data-bind="attr: { action: getUrl() }"> + <input type="submit" class="action primary" data-bind="value: $t('Create an Account')" /> </form> - <!-- /ko --> - <!-- ko if: accountCreated --> - <p data-bind="i18n: 'A letter with further instructions will be sent to your email.'"></p> - <!-- /ko --> + <!--/ko--> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html index 2e268461d1eea..2a5dc27328a43 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html @@ -4,33 +4,38 @@ * See COPYING.txt for license details. */ --> -<div class="shipping-address-item" data-bind="css: isSelected() ? 'selected-item' : 'not-selected-item'"> - <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> - <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> - <!-- ko text: _.values(address().street).join(", ") --><!-- /ko --><br/> - <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> - <!-- ko if: (address().telephone) --> - <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> - <!-- ko if: (address().isEditable()) --> - <button type="button" +<div class="shipping-address-item" css="'selected-item' : isSelected() , 'not-selected-item':!isSelected()"> + <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> + <text args="address().lastname"/> <text args="address().suffix"/><br/> + <text args="_.values(address().street).join(', ')"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> + <text args="getCountryName(address().countryId)"/><br/> + <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> + + <each args="data: address().customAttributes, as: 'element'"> + <each args="data: Object.keys(element), as: 'attribute'"> + <if args="typeof element[attribute] === 'object'"> + <if args="element[attribute].label"> + <text args="element[attribute].label"/> + </if> + <ifnot args="element[attribute].label"> + <if args="element[attribute].value"> + <text args="element[attribute].value"/> + </if> + </ifnot> + </if> + <if args="typeof element[attribute] === 'string'"> + <text args="element[attribute]"/> + </if><br/> + </each> + </each> + + <button visible="address().isEditable()" type="button" class="action edit-address-link" - data-bind="click: editAddress, visible: address().isEditable()"> - <span data-bind="i18n: 'Edit'"></span> + click="editAddress"> + <span translate="'Edit'"></span> </button> - <!-- /ko --> - <button type="button" data-bind="click: selectAddress" class="action action-select-shipping-item"> - <span data-bind="i18n: 'Ship Here'"></span> + <button type="button" click="selectAddress" class="action action-select-shipping-item"> + <span translate="'Ship Here'"></span> </button> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html index ec41cae0bdc5e..75e061426d816 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html @@ -4,23 +4,27 @@ * See COPYING.txt for license details. */ --> -<!-- ko if: (visible()) --> - <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> - <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> - <!-- ko text: address().street --><!-- /ko --><br/> - <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> - <!-- ko if: (address().telephone) --> - <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> -<!-- /ko --> +<if args="visible()"> + <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> + <text args="address().lastname"/> <text args="address().suffix"/><br/> + <text args="_.values(address().street).join(', ')"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> + <text args="getCountryName(address().countryId)"/><br/> + <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> + + <each args="data: address().customAttributes, as: 'element'"> + <if args="typeof element === 'object'"> + <if args="element.label"> + <text args="element.label"/> + </if> + <ifnot args="element.label"> + <if args="element.value"> + <text args="element.value"/> + </if> + </ifnot> + </if> + <if args="typeof element === 'string'"> + <text args="element"/> + </if><br/> + </each> +</if> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 34ec91aa43c72..fa4ab2cdefbc6 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -9,7 +9,7 @@ <strong role="heading"> <translate args="maxCartItemsToDisplay" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> <translate args="'of'" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> - <span data-bind="text: getCartLineItemsCount()"></span> + <span data-bind="text: getCartSummaryItemsCount()"></span> <translate args="'Item in Cart'" if="getCartLineItemsCount() === 1"/> <translate args="'Items in Cart'" if="getCartLineItemsCount() > 1"/> </strong> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html index daa37cbe2dc89..9c0621099060b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html @@ -12,7 +12,7 @@ <div class="product-item-inner"> <div class="product-item-name-block"> - <strong class="product-item-name" data-bind="text: $parent.name"></strong> + <strong class="product-item-name" data-bind="html: $parent.name"></strong> <div class="details-qty"> <span class="label"><!-- ko i18n: 'Qty' --><!-- /ko --></span> <span class="value" data-bind="text: $parent.qty"></span> @@ -35,7 +35,7 @@ <dd class="values" data-bind="html: full_view"></dd> <!-- /ko --> <!-- ko ifnot: ($data.full_view)--> - <dd class="values" data-bind="html: value"></dd> + <dd class="values" data-bind="text: value"></dd> <!-- /ko --> <!-- /ko --> </dl> @@ -43,3 +43,6 @@ </div> <!-- /ko --> </div> +<!-- ko foreach: getRegion('item_message') --> + <!-- ko template: getTemplate() --><!-- /ko --> +<!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html new file mode 100644 index 0000000000000..ea8f58cccd595 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="cart item message notice" if="getMessage($parents[1])"> + <div data-bind="text: getMessage($parents[1])"></div> +</div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html index 981541e7251e7..eb218bbee9941 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html @@ -8,6 +8,6 @@ data-bind="attr: {'style': 'height: ' + getHeight($parents[1]) + 'px; width: ' + getWidth($parents[1]) + 'px;' }"> <span class="product-image-wrapper"> <img - data-bind="attr: {'src': getSrc($parents[1]), 'width': getWidth($parents[1]), 'height': getHeight($parents[1]), 'alt': getAlt($parents[1]) }"/> + data-bind="attr: {'src': getSrc($parents[1]), 'width': getWidth($parents[1]), 'height': getHeight($parents[1]), 'alt': getAlt($parents[1]), 'title': getAlt($parents[1]) }"/> </span> </span> diff --git a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php index ed9ecc642e16b..4a35a58a41ff9 100644 --- a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php +++ b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php @@ -5,27 +5,42 @@ */ namespace Magento\CheckoutAgreements\Block\Adminhtml\Agreement; +use Magento\Framework\App\ObjectManager; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement\Grid\CollectionFactory as GridCollectionFactory; + class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { /** * @var \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory + * @deprecated */ protected $_collectionFactory; + /** + * @param GridCollectionFactory + */ + private $gridCollectionFactory; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory $collectionFactory * @param array $data + * @param GridCollectionFactory $gridColFactory * @codeCoverageIgnore */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory $collectionFactory, - array $data = [] + array $data = [], + GridCollectionFactory $gridColFactory = null ) { + $this->_collectionFactory = $collectionFactory; + $this->gridCollectionFactory = $gridColFactory + ? : ObjectManager::getInstance()->get(GridCollectionFactory::class); + parent::__construct($context, $backendHelper, $data); } @@ -47,7 +62,7 @@ protected function _construct() */ protected function _prepareCollection() { - $this->setCollection($this->_collectionFactory->create()); + $this->setCollection($this->gridCollectionFactory->create()); return parent::_prepareCollection(); } diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement.php index 13130a4491eb2..aa6f461fc5ee2 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement.php @@ -5,7 +5,11 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml; -abstract class Agreement extends \Magento\Backend\App\Action +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Registry; + +abstract class Agreement extends Action { /** * Authorization level of a basic admin session @@ -22,12 +26,14 @@ abstract class Agreement extends \Magento\Backend\App\Action protected $_coreRegistry = null; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry + * @param Context $context + * @param Registry $coreRegistry * @codeCoverageIgnore */ - public function __construct(\Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry) - { + public function __construct( + Context $context, + Registry $coreRegistry + ) { $this->_coreRegistry = $coreRegistry; parent::__construct($context); } diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php index 65aca6205caa4..447689c95dfd0 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php @@ -6,27 +6,59 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; -class Delete extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement +use Magento\CheckoutAgreements\Api\CheckoutAgreementsRepositoryInterface; +use Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Registry; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; + +class Delete extends Agreement { + /** + * @var CheckoutAgreementsRepositoryInterface + */ + private $agreementRepository; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param CheckoutAgreementsRepositoryInterface $agreementRepository + */ + public function __construct( + Context $context, + Registry $coreRegistry, + CheckoutAgreementsRepositoryInterface $agreementRepository = null + ) { + $this->agreementRepository = $agreementRepository ?: + ObjectManager::getInstance()->get(CheckoutAgreementsRepositoryInterface::class); + parent::__construct($context, $coreRegistry); + } /** * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $id = (int)$this->getRequest()->getParam('id'); - $model = $this->_objectManager->get(\Magento\CheckoutAgreements\Model\Agreement::class)->load($id); - if (!$model->getId()) { + $agreement = $this->agreementRepository->get($id); + if (!$agreement->getAgreementId()) { $this->messageManager->addError(__('This condition no longer exists.')); $this->_redirect('checkout/*/'); return; } try { - $model->delete(); + $this->agreementRepository->delete($agreement); $this->messageManager->addSuccess(__('You deleted the condition.')); $this->_redirect('checkout/*/'); return; - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addError($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addError(__('Something went wrong while deleting this condition.')); diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php index 73ac129bc993c..8bec3b581cd54 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php @@ -6,8 +6,34 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; -class Edit extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement +use Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; +use Magento\CheckoutAgreements\Model\AgreementFactory; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Registry; +use Magento\Framework\App\ObjectManager; +use Magento\CheckoutAgreements\Block\Adminhtml\Agreement\Edit as BlockEdit; + +class Edit extends Agreement { + /** + * @var AgreementFactory + */ + private $agreementFactory; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param AgreementFactory $agreementFactory + */ + public function __construct( + Context $context, + Registry $coreRegistry, + AgreementFactory $agreementFactory = null + ) { + $this->agreementFactory = $agreementFactory ?: + ObjectManager::getInstance()->get(AgreementFactory::class); + parent::__construct($context, $coreRegistry); + } /** * @return void * @SuppressWarnings(PHPMD.NPathComplexity) @@ -15,7 +41,7 @@ class Edit extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement public function execute() { $id = $this->getRequest()->getParam('id'); - $agreementModel = $this->_objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + $agreementModel = $this->agreementFactory->create(); if ($id) { $agreementModel->load($id); @@ -26,7 +52,7 @@ public function execute() } } - $data = $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getAgreementData(true); + $data = $this->_session->getAgreementData(true); if (!empty($data)) { $agreementModel->setData($data); } @@ -38,7 +64,7 @@ public function execute() $id ? __('Edit Condition') : __('New Condition') )->_addContent( $this->_view->getLayout()->createBlock( - \Magento\CheckoutAgreements\Block\Adminhtml\Agreement\Edit::class + BlockEdit::class )->setData( 'action', $this->getUrl('checkout/*/save') diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php index 25c034203620b..05a16d3dd4264 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php @@ -6,8 +6,35 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; -class Save extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement +use Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; +use Magento\CheckoutAgreements\Model\AgreementFactory; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Registry; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; + +class Save extends Agreement { + /** + * @var AgreementFactory + */ + private $agreementFactory; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param AgreementFactory $agreementFactory + */ + public function __construct( + Context $context, + Registry $coreRegistry, + AgreementFactory $agreementFactory = null + ) { + $this->agreementFactory = $agreementFactory ?: + ObjectManager::getInstance()->get(AgreementFactory::class); + parent::__construct($context, $coreRegistry); + } /** * @return void */ @@ -15,11 +42,11 @@ public function execute() { $postData = $this->getRequest()->getPostValue(); if ($postData) { - $model = $this->_objectManager->get(\Magento\CheckoutAgreements\Model\Agreement::class); + $model = $this->agreementFactory->create(); $model->setData($postData); try { - $validationResult = $model->validateData(new \Magento\Framework\DataObject($postData)); + $validationResult = $model->validateData(new DataObject($postData)); if ($validationResult !== true) { foreach ($validationResult as $message) { $this->messageManager->addError($message); @@ -30,13 +57,13 @@ public function execute() $this->_redirect('checkout/*/'); return; } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addError($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addError(__('Something went wrong while saving this condition.')); } - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setAgreementData($postData); + $this->_session->setAgreementData($postData); $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); } } diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 331307292e40a..61326207d24ec 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -45,17 +45,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $agreements = []; $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); + return $agreements; } /** - * Returns agreements config + * Returns agreements config. * * @return array */ @@ -75,7 +76,7 @@ protected function getAgreementsConfig() 'content' => $agreement->getIsHtml() ? $agreement->getContent() : nl2br($this->escaper->escapeHtml($agreement->getContent())), - 'checkboxText' => $agreement->getCheckboxText(), + 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), 'agreementId' => $agreement->getAgreementId() ]; diff --git a/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php new file mode 100644 index 0000000000000..2cce918c5edd4 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CheckoutAgreements\Model\ResourceModel\Agreement\Grid; + +/** + * CheckoutAgreement Grid Collection + */ +class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\Collection +{ + + /** + * {@inheritdoc} + */ + public function load($printQuery = false, $logQuery = false) + { + if ($this->isLoaded()) { + return $this; + } + + parent::load($printQuery, $logQuery); + + $this->addStoresToResult(); + + return $this; + } + + /** + * @return void + */ + private function addStoresToResult() + { + $stores = $this->getStoresForAgreements(); + + if (!empty($stores)) { + $storesByAgreementId = []; + + foreach ($stores as $storeData) { + $storesByAgreementId[$storeData['agreement_id']][] = $storeData['store_id']; + } + + foreach ($this as $item) { + $agreementId = $item->getData('agreement_id'); + + if (!isset($storesByAgreementId[$agreementId])) { + continue; + } + + $item->setData('stores', $storesByAgreementId[$agreementId]); + } + } + } + + /** + * @return array + */ + private function getStoresForAgreements() + { + $agreementId = $this->getColumnValues('agreement_id'); + + if (!empty($agreementId)) { + $select = $this->getConnection()->select()->from( + ['agreement_store' => $this->getResource()->getTable('checkout_agreement_store')] + )->where( + 'agreement_store.agreement_id IN (?)', + $agreementId + ); + + return $this->getConnection()->fetchAll($select); + } + + return []; + } +} diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/LICENSE.txt b/app/code/Magento/CheckoutAgreements/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/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 <insert your license name here>" 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/app/code/Magento/CheckoutAgreements/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CheckoutAgreements/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/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 <insert your license name here>" 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/app/code/Magento/CheckoutAgreements/Test/Mftf/README.md b/app/code/Magento/CheckoutAgreements/Test/Mftf/README.md new file mode 100644 index 0000000000000..593e89f08b5b5 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Checkout Agreements Functional Tests + +The Functional Test Module for **Magento Checkout Agreements** module. diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index eae347e27aa11..cacc1c1226cff 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -8,6 +8,9 @@ use Magento\CheckoutAgreements\Model\AgreementsProvider; use Magento\Store\Model\ScopeInterface; +/** + * Tests for AgreementsConfigProvider. + */ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase { /** @@ -30,6 +33,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase */ protected $escaperMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -45,10 +51,16 @@ protected function setUp() ); } + /** + * Test for getConfig if content is HTML. + * + * @return void + */ public function testGetConfigIfContentIsHtml() { $content = 'content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -57,12 +69,12 @@ public function testGetConfigIfContentIsHtml() 'agreements' => [ [ 'content' => $content, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -71,8 +83,12 @@ public function testGetConfigIfContentIsHtml() ->willReturn(true); $agreement = $this->createMock(\Magento\CheckoutAgreements\Api\Data\AgreementInterface::class); - $this->agreementsRepositoryMock->expects($this->any())->method('getList')->willReturn([$agreement]); + $this->agreementsRepositoryMock->expects($this->once())->method('getList')->willReturn([$agreement]); + $this->escaperMock->expects($this->once()) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); @@ -82,11 +98,17 @@ public function testGetConfigIfContentIsHtml() $this->assertEquals($expectedResult, $this->model->getConfig()); } + /** + * Test for getConfig if content is not HTML. + * + * @return void + */ public function testGetConfigIfContentIsNotHtml() { $content = 'content'; $escapedContent = 'escaped_content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -95,12 +117,12 @@ public function testGetConfigIfContentIsNotHtml() 'agreements' => [ [ 'content' => $escapedContent, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -109,9 +131,13 @@ public function testGetConfigIfContentIsNotHtml() ->willReturn(true); $agreement = $this->createMock(\Magento\CheckoutAgreements\Api\Data\AgreementInterface::class); - $this->agreementsRepositoryMock->expects($this->any())->method('getList')->willReturn([$agreement]); - $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->agreementsRepositoryMock->expects($this->once())->method('getList')->willReturn([$agreement]); + $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); diff --git a/app/code/Magento/CheckoutAgreements/composer.json b/app/code/Magento/CheckoutAgreements/composer.json index b8fa41ea7c724..f7b1427f25234 100644 --- a/app/code/Magento/CheckoutAgreements/composer.json +++ b/app/code/Magento/CheckoutAgreements/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-checkout-agreements", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-checkout": "100.2.*", "magento/module-quote": "101.0.*", "magento/module-store": "100.2.*", @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml b/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml index 1249ea44b991e..5a708f49a7034 100644 --- a/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml +++ b/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router id="admin"> - <route id="checkout" frontName="checkout"> + <route id="checkout"> <module name="Magento_CheckoutAgreements" before="Magento_Backend" /> </route> </router> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml index 3f742de0177da..122160f1a10cd 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout_overview"> - <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> + <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml index 663cae4debcfa..7486b6d870565 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/additional_agreements.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\CheckoutAgreements\Block\Agreements */ diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml index b0c6384bcc9fe..5cb256090c196 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Files.LineLength ?> <?php @@ -17,30 +17,42 @@ } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement): ?> + <?php foreach ($block->getAgreements() as $agreement) :?> <li class="item"> - <div class="checkout-agreement-item-content"<?= ($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()):?> - <?= /* @escapeNotVerified */ $agreement->getContent() ?> - <?php else:?> - <?= nl2br($block->escapeHtml($agreement->getContent())) ?> + <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getContent() ?> + <?php else :?> + <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <form id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree required"> - <?php if($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL): ?> + <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> + <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> <input type="checkbox" - id="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" - name="agreement[<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>]" + id="agreement-<?= (int) $agreement->getAgreementId() ?>" + name="agreement[<?= (int) $agreement->getAgreementId() ?>]" value="1" title="<?= $block->escapeHtml($agreement->getCheckboxText()) ?>" class="checkbox" data-validate="{required:true}"/> - <label class="label" for="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>"> - <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> + <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> + <span> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getCheckboxText() ?> + <?php else :?> + <?= $block->escapeHtml($agreement->getCheckboxText()) ?> + <?php endif; ?> + </span> </label> - <?php elseif($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO): ?> - <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree"> - <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> + <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> + <span> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getCheckboxText() ?> + <?php else :?> + <?= $block->escapeHtml($agreement->getCheckboxText()) ?> + <?php endif; ?> + </span> </div> <?php endif; ?> </form> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index 3400770f5cee8..fb2d5168d21de 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,7 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// @deprecated +// phpcs:disable Magento2.Files.LineLength ?> <?php @@ -17,31 +18,43 @@ } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement): ?> + <?php foreach ($block->getAgreements() as $agreement) :?> <li class="item"> - <div class="checkout-agreement-item-content"<?= ($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()):?> - <?= /* @escapeNotVerified */ $agreement->getContent() ?> - <?php else:?> - <?= nl2br($block->escapeHtml($agreement->getContent())) ?> + <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getContent() ?> + <?php else :?> + <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <?php if($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL): ?> - <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree required"> + <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> + <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> <input type="checkbox" - id="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" - name="agreement[<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>]" + id="agreement-<?= (int) $agreement->getAgreementId() ?>" + name="agreement[<?= (int) $agreement->getAgreementId() ?>]" value="1" title="<?= $block->escapeHtml($agreement->getCheckboxText()) ?>" class="checkbox" data-validate="{required:true}"/> - <label class="label" for="agreement-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>"> - <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> + <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> + <span> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getCheckboxText() ?> + <?php else :?> + <?= $block->escapeHtml($agreement->getCheckboxText()) ?> + <?php endif; ?> + </span> </label> </div> - <?php elseif($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO): ?> - <div id="checkout-agreements-form-<?= /* @escapeNotVerified */ $agreement->getAgreementId() ?>" class="field choice agree"> - <span><?= $agreement->getIsHtml() ? $agreement->getCheckboxText() : $block->escapeHtml($agreement->getCheckboxText()) ?></span> + <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> + <span> + <?php if ($agreement->getIsHtml()) :?> + <?= /* @noEscape */ $agreement->getCheckboxText() ?> + <?php else :?> + <?= $block->escapeHtml($agreement->getCheckboxText()) ?> + <?php endif; ?> + </span> </div> <?php endif; ?> </li> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e83..4b1a68624e547 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Api/Data/PageInterface.php b/app/code/Magento/Cms/Api/Data/PageInterface.php index 032f4916a85e4..7a31ab1b9a94f 100644 --- a/app/code/Magento/Cms/Api/Data/PageInterface.php +++ b/app/code/Magento/Cms/Api/Data/PageInterface.php @@ -125,6 +125,7 @@ public function getSortOrder(); * Get layout update xml * * @return string|null + * @deprecated Existing updates are applied, new are not accepted. */ public function getLayoutUpdateXml(); @@ -145,6 +146,8 @@ public function getCustomRootTemplate(); /** * Get custom layout update xml * + * @deprecated Existing updates are applied, new are not accepted. + * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface * @return string|null */ public function getCustomLayoutUpdateXml(); @@ -272,6 +275,7 @@ public function setSortOrder($sortOrder); * * @param string $layoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface + * @deprecated Existing updates are applied, new are not accepted. */ public function setLayoutUpdateXml($layoutUpdateXml); @@ -296,6 +300,8 @@ public function setCustomRootTemplate($customRootTemplate); * * @param string $customLayoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface + * @deprecated Existing updates are applied, new are not accepted. + * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface */ public function setCustomLayoutUpdateXml($customLayoutUpdateXml); diff --git a/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php new file mode 100644 index 0000000000000..c6bf4c8404701 --- /dev/null +++ b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Cms\Api; + +/** + * Utility Cms Pages + * + * @api + */ +interface GetUtilityPageIdentifiersInterface +{ + /** + * Get List Page Identifiers + * @return array + */ + public function execute(); +} diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php index a7410cac64d76..926886316f16a 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php index 1fc599e4c856a..b11c2fc4163b2 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content/Uploader.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content/Uploader.php index cf0fc34b217e4..883629e463cb5 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content/Uploader.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content/Uploader.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content; /** diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index d0d75ea691195..86cf059525e1e 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -13,6 +13,11 @@ */ class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface { + /** + * Prefix for cache key of CMS block + */ + const CACHE_KEY_PREFIX = 'CMS_BLOCK_'; + /** * @var \Magento\Cms\Model\Template\FilterProvider */ @@ -84,4 +89,14 @@ public function getIdentities() { return [\Magento\Cms\Model\Block::CACHE_TAG . '_' . $this->getBlockId()]; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + $cacheKeyInfo[] = $this->_storeManager->getStore()->getId(); + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php index 76b6aeb013285..22672e57ee6ab 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php @@ -12,9 +12,14 @@ class Delete extends \Magento\Cms\Controller\Adminhtml\Block * Delete action * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); // check if we know what should be deleted @@ -26,18 +31,18 @@ public function execute() $model->load($id); $model->delete(); // display success message - $this->messageManager->addSuccess(__('You deleted the block.')); + $this->messageManager->addSuccessMessage(__('You deleted the block.')); // go to grid return $resultRedirect->setPath('*/*/'); } catch (\Exception $e) { // display error message - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); // go back to edit form return $resultRedirect->setPath('*/*/edit', ['block_id' => $id]); } } // display error message - $this->messageManager->addError(__('We can\'t find a block to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a block to delete.')); // go to grid return $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php index d4c0517621144..8756089063237 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php @@ -42,7 +42,7 @@ public function execute() if ($id) { $model->load($id); if (!$model->getId()) { - $this->messageManager->addError(__('This block no longer exists.')); + $this->messageManager->addErrorMessage(__('This block no longer exists.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/InlineEdit.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/InlineEdit.php index e267c568fa9e5..3a7e73fbe5eaa 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/InlineEdit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/InlineEdit.php @@ -46,6 +46,7 @@ public function __construct( /** * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php index cff9c8a39b746..ef0fa937dbd5c 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php @@ -49,10 +49,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); @@ -60,7 +64,7 @@ public function execute() $block->delete(); } - $this->messageManager->addSuccess(__('A total of %1 record(s) have been deleted.', $collectionSize)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) have been deleted.', $collectionSize)); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php index 3eb790c83ad69..4332e2edd7057 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php @@ -7,10 +7,12 @@ namespace Magento\Cms\Controller\Adminhtml\Block; use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\BlockRepositoryInterface; use Magento\Cms\Model\Block; +use Magento\Cms\Model\BlockFactory; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\TestFramework\Inspection\Exception; +use Magento\Framework\Registry; class Save extends \Magento\Cms\Controller\Adminhtml\Block { @@ -19,17 +21,35 @@ class Save extends \Magento\Cms\Controller\Adminhtml\Block */ protected $dataPersistor; + /** + * @var BlockFactory + */ + private $blockFactory; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + /** * @param Context $context - * @param \Magento\Framework\Registry $coreRegistry + * @param Registry $coreRegistry * @param DataPersistorInterface $dataPersistor + * @param BlockFactory|null $blockFactory + * @param BlockRepositoryInterface|null $blockRepository */ public function __construct( Context $context, - \Magento\Framework\Registry $coreRegistry, - DataPersistorInterface $dataPersistor + Registry $coreRegistry, + DataPersistorInterface $dataPersistor, + BlockFactory $blockFactory = null, + BlockRepositoryInterface $blockRepository = null ) { $this->dataPersistor = $dataPersistor; + $this->blockFactory = $blockFactory + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(BlockFactory::class); + $this->blockRepository = $blockRepository + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(BlockRepositoryInterface::class); parent::__construct($context, $coreRegistry); } @@ -45,8 +65,6 @@ public function execute() $resultRedirect = $this->resultRedirectFactory->create(); $data = $this->getRequest()->getPostValue(); if ($data) { - $id = $this->getRequest()->getParam('block_id'); - if (isset($data['is_active']) && $data['is_active'] === 'true') { $data['is_active'] = Block::STATUS_ENABLED; } @@ -55,27 +73,32 @@ public function execute() } /** @var \Magento\Cms\Model\Block $model */ - $model = $this->_objectManager->create(\Magento\Cms\Model\Block::class)->load($id); - if (!$model->getId() && $id) { - $this->messageManager->addError(__('This block no longer exists.')); - return $resultRedirect->setPath('*/*/'); + $model = $this->blockFactory->create(); + + $id = $this->getRequest()->getParam('block_id'); + if ($id) { + try { + $model = $this->blockRepository->getById($id); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage(__('This block no longer exists.')); + return $resultRedirect->setPath('*/*/'); + } } $model->setData($data); try { - $model->save(); - $this->messageManager->addSuccess(__('You saved the block.')); + $this->blockRepository->save($model); + $this->messageManager->addSuccessMessage(__('You saved the block.')); $this->dataPersistor->clear('cms_block'); - if ($this->getRequest()->getParam('back')) { return $resultRedirect->setPath('*/*/edit', ['block_id' => $model->getId()]); } return $resultRedirect->setPath('*/*/'); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving the block.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the block.')); } $this->dataPersistor->set('cms_block', $data); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php index c604e683f9aee..78753aee66cef 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -19,28 +18,38 @@ class Delete extends \Magento\Backend\App\Action * Delete action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + // check if we know what should be deleted $id = $this->getRequest()->getParam('page_id'); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); + if ($id) { $title = ""; try { // init model and delete $model = $this->_objectManager->create(\Magento\Cms\Model\Page::class); $model->load($id); + $title = $model->getTitle(); $model->delete(); + // display success message - $this->messageManager->addSuccess(__('The page has been deleted.')); + $this->messageManager->addSuccessMessage(__('The page has been deleted.')); + // go to grid - $this->_eventManager->dispatch( - 'adminhtml_cmspage_on_delete', - ['title' => $title, 'status' => 'success'] - ); + $this->_eventManager->dispatch('adminhtml_cmspage_on_delete', [ + 'title' => $title, + 'status' => 'success' + ]); + return $resultRedirect->setPath('*/*/'); } catch (\Exception $e) { $this->_eventManager->dispatch( @@ -48,13 +57,15 @@ public function execute() ['title' => $title, 'status' => 'fail'] ); // display error message - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); // go back to edit form return $resultRedirect->setPath('*/*/edit', ['page_id' => $id]); } } + // display error message - $this->messageManager->addError(__('We can\'t find a page to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a page to delete.')); + // go to grid return $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php index ca50a935ce915..6d51c28b6aca7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php @@ -76,7 +76,7 @@ public function execute() if ($id) { $model->load($id); if (!$model->getId()) { - $this->messageManager->addError(__('This page no longer exists.')); + $this->messageManager->addErrorMessage(__('This page no longer exists.')); /** \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php index 6d75f490d42dc..b2e36c27a413c 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/InlineEdit.php @@ -56,7 +56,10 @@ public function __construct( } /** + * Process the request + * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { @@ -67,10 +70,12 @@ public function execute() $postItems = $this->getRequest()->getParam('items', []); if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { - return $resultJson->setData([ - 'messages' => [__('Please correct the data sent.')], - 'error' => true, - ]); + return $resultJson->setData( + [ + 'messages' => [__('Please correct the data sent.')], + 'error' => true, + ] + ); } foreach (array_keys($postItems) as $pageId) { @@ -97,10 +102,12 @@ public function execute() } } - return $resultJson->setData([ - 'messages' => $messages, - 'error' => $error - ]); + return $resultJson->setData( + [ + 'messages' => $messages, + 'error' => $error + ] + ); } /** @@ -130,7 +137,7 @@ protected function filterPost($postData = []) */ protected function validatePost(array $pageData, \Magento\Cms\Model\Page $page, &$error, array &$messages) { - if (!($this->dataProcessor->validate($pageData) && $this->dataProcessor->validateRequireEntry($pageData))) { + if (!$this->dataProcessor->validateRequireEntry($pageData)) { $error = true; foreach ($this->messageManager->getMessages(true)->getItems() as $error) { $messages[] = $this->getErrorWithPageId($page, $error->getText()); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php index a711f20d65639..6d8fda918689d 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); @@ -59,10 +63,11 @@ public function execute() $page->delete(); } - $this->messageManager->addSuccess(__('A total of %1 record(s) have been deleted.', $collectionSize)); + $this->messageManager->addSuccessMessage(__('A total of %1 record(s) have been deleted.', $collectionSize)); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + return $resultRedirect->setPath('*/*/'); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php index e39c2115f961e..b36cf087ed9d6 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { @@ -59,7 +63,9 @@ public function execute() $item->save(); } - $this->messageManager->addSuccess(__('A total of %1 record(s) have been disabled.', $collection->getSize())); + $this->messageManager->addSuccessMessage( + __('A total of %1 record(s) have been disabled.', $collection->getSize()) + ); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php index 8278c28a1e696..5ab7ae246a76f 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { @@ -59,7 +63,9 @@ public function execute() $item->save(); } - $this->messageManager->addSuccess(__('A total of %1 record(s) have been enabled.', $collection->getSize())); + $this->messageManager->addSuccessMessage( + __('A total of %1 record(s) have been enabled.', $collection->getSize()) + ); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php index 57f92a713ecb0..fa893e05c799b 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php @@ -12,8 +12,7 @@ use Magento\Framework\Config\Dom\ValidationSchemaException; /** - * Class PostDataProcessor - * @package Magento\Cms\Controller\Adminhtml\Page + * Controller helper for user input. */ class PostDataProcessor { @@ -80,6 +79,7 @@ public function filter($data) * * @param array $data * @return bool Return FALSE if some item is invalid + * @deprecated */ public function validate($data) { @@ -119,7 +119,7 @@ public function validateRequireEntry(array $data) foreach ($data as $field => $value) { if (in_array($field, array_keys($requiredFields)) && $value == '') { $errorNo = false; - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('To apply changes you should fill in hidden required "%1" field', $requiredFields[$field]) ); } @@ -140,6 +140,7 @@ private function validateData($data, $layoutXmlValidator) if (!empty($data['layout_update_xml']) && !$layoutXmlValidator->isValid($data['layout_update_xml'])) { return false; } + if (!empty($data['custom_layout_update_xml']) && !$layoutXmlValidator->isValid($data['custom_layout_update_xml']) ) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php index 5644e25dd4c4a..5a5fa8f39e175 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,9 +7,15 @@ use Magento\Backend\App\Action; use Magento\Cms\Model\Page; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Exception\LocalizedException; +/** + * Save CMS page action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Save extends \Magento\Backend\App\Action { /** @@ -44,9 +49,8 @@ class Save extends \Magento\Backend\App\Action * @param Action\Context $context * @param PostDataProcessor $dataProcessor * @param DataPersistorInterface $dataPersistor - * @param \Magento\Cms\Model\PageFactory $pageFactory - * @param \Magento\Cms\Api\PageRepositoryInterface $pageRepository - * + * @param \Magento\Cms\Model\PageFactory|null $pageFactory + * @param \Magento\Cms\Api\PageRepositoryInterface|null $pageRepository */ public function __construct( Action\Context $context, @@ -57,11 +61,9 @@ public function __construct( ) { $this->dataProcessor = $dataProcessor; $this->dataPersistor = $dataPersistor; - $this->pageFactory = $pageFactory - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Cms\Model\PageFactory::class); + $this->pageFactory = $pageFactory ?: ObjectManager::getInstance()->get(\Magento\Cms\Model\PageFactory::class); $this->pageRepository = $pageRepository - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Cms\Api\PageRepositoryInterface::class); + ?: ObjectManager::getInstance()->get(\Magento\Cms\Api\PageRepositoryInterface::class); parent::__construct($context); } @@ -90,27 +92,24 @@ public function execute() $id = $this->getRequest()->getParam('page_id'); if ($id) { - $model->load($id); - if (!$model->getId()) { + try { + $model = $this->pageRepository->getById($id); + } catch (LocalizedException $e) { $this->messageManager->addErrorMessage(__('This page no longer exists.')); - /** \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); } } + $data['layout_update_xml'] = $model->getLayoutUpdateXml(); + $data['custom_layout_update_xml'] = $model->getCustomLayoutUpdateXml(); $model->setData($data); - $this->_eventManager->dispatch( - 'cms_page_prepare_save', - ['page' => $model, 'request' => $this->getRequest()] - ); - - if (!$this->dataProcessor->validate($data)) { - return $resultRedirect->setPath('*/*/edit', ['page_id' => $model->getId(), '_current' => true]); - } - try { + $this->_eventManager->dispatch( + 'cms_page_prepare_save', + ['page' => $model, 'request' => $this->getRequest()] + ); + $this->pageRepository->save($model); $this->messageManager->addSuccessMessage(__('You saved the page.')); $this->dataPersistor->clear('cms_page'); @@ -119,8 +118,8 @@ public function execute() } return $resultRedirect->setPath('*/*/'); } catch (LocalizedException $e) { - $this->messageManager->addExceptionMessage($e->getPrevious() ?:$e); - } catch (\Exception $e) { + $this->messageManager->addExceptionMessage($e->getPrevious() ?: $e); + } catch (\Throwable $e) { $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the page.')); } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php index 807bdcb015ad6..7aa513dc2f759 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php @@ -7,8 +7,13 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg; use Magento\Backend\App\Action; +use Magento\Cms\Model\Template\Filter; +use Magento\Cms\Model\Wysiwyg\Config; -class Directive extends \Magento\Backend\App\Action +/** + * Process template text for wysiwyg editor. + */ +class Directive extends Action { /** @@ -52,24 +57,28 @@ public function execute() { $directive = $this->getRequest()->getParam('___directive'); $directive = $this->urlDecoder->decode($directive); - $imagePath = $this->_objectManager->create(\Magento\Cms\Model\Template\Filter::class)->filter($directive); - /** @var \Magento\Framework\Image\Adapter\AdapterInterface $image */ - $image = $this->_objectManager->get(\Magento\Framework\Image\AdapterFactory::class)->create(); - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); + try { + /** @var Filter $filter */ + $filter = $this->_objectManager->create(Filter::class); + $imagePath = $filter->filter($directive); + /** @var \Magento\Framework\Image\Adapter\AdapterInterface $image */ + $image = $this->_objectManager->get(\Magento\Framework\Image\AdapterFactory::class)->create(); + /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ + $resultRaw = $this->resultRawFactory->create(); $image->open($imagePath); $resultRaw->setHeader('Content-Type', $image->getMimeType()); $resultRaw->setContents($image->getImage()); } catch (\Exception $e) { - $imagePath = $this->_objectManager->get( - \Magento\Cms\Model\Wysiwyg\Config::class - )->getSkinImagePlaceholderPath(); + /** @var Config $config */ + $config = $this->_objectManager->get(Config::class); + $imagePath = $config->getSkinImagePlaceholderPath(); $image->open($imagePath); $resultRaw->setHeader('Content-Type', $image->getMimeType()); $resultRaw->setContents($image->getImage()); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } + return $resultRaw; } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php index 19dc989620b89..f7970b0a93ca9 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php @@ -7,6 +7,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; +/** + * Delete image files. + */ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { /** @@ -19,6 +22,11 @@ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images */ protected $resultRawFactory; + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver + */ + private $directoryResolver; + /** * Constructor * @@ -26,22 +34,28 @@ class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, + \Magento\Framework\App\Filesystem\DirectoryResolver $directoryResolver = null ) { + parent::__construct($context, $coreRegistry); + $this->resultRawFactory = $resultRawFactory; $this->resultJsonFactory = $resultJsonFactory; - parent::__construct($context, $coreRegistry); + $this->directoryResolver = $directoryResolver + ?: $this->_objectManager->get(\Magento\Framework\App\Filesystem\DirectoryResolver::class); } /** - * Delete file from media storage + * Delete file from media storage. * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { @@ -54,21 +68,28 @@ public function execute() /** @var $helper \Magento\Cms\Helper\Wysiwyg\Images */ $helper = $this->_objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class); $path = $this->getStorage()->getSession()->getCurrentPath(); + if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Directory %1 is not under storage root path.', $path) + ); + } foreach ($files as $file) { $file = $helper->idDecode($file); /** @var \Magento\Framework\Filesystem $filesystem */ $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); $dir = $filesystem->getDirectoryRead(DirectoryList::MEDIA); $filePath = $path . '/' . \Magento\Framework\File\Uploader::getCorrectFileName($file); - if ($dir->isFile($dir->getRelativePath($filePath))) { + if ($dir->isFile($dir->getRelativePath($filePath)) && !preg_match('/^\.htaccess$/', $file)) { $this->getStorage()->deleteFile($filePath); } } + return $this->resultRawFactory->create(); } catch (\Exception $e) { $result = ['error' => true, 'message' => $e->getMessage()]; /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index 8a89de87a6f85..81ae1affb5e00 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -6,6 +6,12 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NotFoundException; + +/** + * Delete image folder. + */ class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { /** @@ -18,38 +24,59 @@ class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images */ protected $resultRawFactory; + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver + */ + private $directoryResolver; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, + \Magento\Framework\App\Filesystem\DirectoryResolver $directoryResolver = null ) { + parent::__construct($context, $coreRegistry); $this->resultRawFactory = $resultRawFactory; $this->resultJsonFactory = $resultJsonFactory; - parent::__construct($context, $coreRegistry); + $this->directoryResolver = $directoryResolver + ?: $this->_objectManager->get(\Magento\Framework\App\Filesystem\DirectoryResolver::class); } /** - * Delete folder action + * Delete folder action. * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $path = $this->getStorage()->getCmsWysiwygImages()->getCurrentPath(); + if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Directory %1 is not under storage root path.', $path) + ); + } $this->getStorage()->deleteDirectory($path); + return $this->resultRawFactory->create(); } catch (\Exception $e) { $result = ['error' => true, 'message' => $e->getMessage()]; /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Index.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Index.php index 525fd31052db8..13765e9faca04 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Index.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Index.php @@ -39,7 +39,7 @@ public function execute() try { $this->_objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class)->getCurrentPath(); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } $this->_initAction(); /** @var \Magento\Framework\View\Result\Layout $resultLayout */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index 2124bdabe6009..5171430e67371 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -6,6 +6,12 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NotFoundException; + +/** + * Creates new folder. + */ class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { /** @@ -13,37 +19,57 @@ class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images */ protected $resultJsonFactory; + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver + */ + private $directoryResolver; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Framework\App\Filesystem\DirectoryResolver $directoryResolver = null ) { - $this->resultJsonFactory = $resultJsonFactory; parent::__construct($context, $coreRegistry); + $this->resultJsonFactory = $resultJsonFactory; + $this->directoryResolver = $directoryResolver + ?: $this->_objectManager->get(\Magento\Framework\App\Filesystem\DirectoryResolver::class); } /** - * New folder action + * New folder action. * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $this->_initAction(); $name = $this->getRequest()->getPost('name'); $path = $this->getStorage()->getSession()->getCurrentPath(); + if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Directory %1 is not under storage root path.', $path) + ); + } $result = $this->getStorage()->createDirectory($name, $path); } catch (\Exception $e) { $result = ['error' => true, 'message' => $e->getMessage()]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php index 2daaf39d58d14..dda3940cd9ba5 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php @@ -35,7 +35,7 @@ public function __construct( public function execute() { $helper = $this->_objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class); - $storeId = $this->getRequest()->getParam('store'); + $storeId = (int)$this->getRequest()->getParam('store'); $filename = $this->getRequest()->getParam('filename'); $filename = $helper->idDecode($filename); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 7a94c4ab6aa12..b25ad695ba008 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -6,6 +6,11 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Filesystem\DirectoryList; + +/** + * Upload image. + */ class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images { /** @@ -13,36 +18,56 @@ class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images */ protected $resultJsonFactory; + /** + * @var \Magento\Framework\App\Filesystem\DirectoryResolver + */ + private $directoryResolver; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Framework\App\Filesystem\DirectoryResolver $directoryResolver = null ) { - $this->resultJsonFactory = $resultJsonFactory; parent::__construct($context, $coreRegistry); + $this->resultJsonFactory = $resultJsonFactory; + $this->directoryResolver = $directoryResolver + ?: $this->_objectManager->get(\Magento\Framework\App\Filesystem\DirectoryResolver::class); } /** - * Files upload processing + * Files upload processing. * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new \Exception('Wrong request.'); + } + $this->_initAction(); - $targetPath = $this->getStorage()->getSession()->getCurrentPath(); - $result = $this->getStorage()->uploadFile($targetPath, $this->getRequest()->getParam('type')); + $path = $this->getStorage()->getSession()->getCurrentPath(); + if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Directory %1 is not under storage root path.', $path) + ); + } + $result = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Index/Index.php b/app/code/Magento/Cms/Controller/Index/Index.php index 8e20feb0f058f..c027bd1a2b717 100644 --- a/app/code/Magento/Cms/Controller/Index/Index.php +++ b/app/code/Magento/Cms/Controller/Index/Index.php @@ -1,27 +1,55 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Index; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Controller\Result\Forward; +use Magento\Framework\Controller\Result\ForwardFactory; +use Magento\Framework\View\Result\Page as ResultPage; +use Magento\Cms\Helper\Page; +use Magento\Store\Model\ScopeInterface; + class Index extends \Magento\Framework\App\Action\Action { /** - * @var \Magento\Framework\Controller\Result\ForwardFactory + * @var ForwardFactory */ protected $resultForwardFactory; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Page + */ + private $page; + + /** + * Index constructor. + * + * @param Context $context + * @param ForwardFactory $resultForwardFactory + * @param ScopeConfigInterface|null $scopeConfig + * @param Page|null $page */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory + Context $context, + ForwardFactory $resultForwardFactory, + ScopeConfigInterface $scopeConfig = null, + Page $page = null ) { $this->resultForwardFactory = $resultForwardFactory; + $this->scopeConfig = $scopeConfig ? : ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->page = $page ? : ObjectManager::getInstance()->get(Page::class); parent::__construct($context); } @@ -29,20 +57,17 @@ public function __construct( * Renders CMS Home page * * @param string|null $coreRoute - * @return \Magento\Framework\Controller\Result\Forward + * + * @return bool|ResponseInterface|Forward|ResultInterface|ResultPage + * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($coreRoute = null) { - $pageId = $this->_objectManager->get( - \Magento\Framework\App\Config\ScopeConfigInterface::class - )->getValue( - \Magento\Cms\Helper\Page::XML_PATH_HOME_PAGE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $resultPage = $this->_objectManager->get(\Magento\Cms\Helper\Page::class)->prepareResultPage($this, $pageId); + $pageId = $this->scopeConfig->getValue(Page::XML_PATH_HOME_PAGE, ScopeInterface::SCOPE_STORE); + $resultPage = $this->page->prepareResultPage($this, $pageId); if (!$resultPage) { - /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ + /** @var Forward $resultForward */ $resultForward = $this->resultForwardFactory->create(); $resultForward->forward('defaultIndex'); return $resultForward; diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index bf51c5f0210e9..195193f9ce850 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -5,7 +5,12 @@ */ namespace Magento\Cms\Helper; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; +use Magento\Cms\Model\Page\CustomLayoutRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; use Magento\Framework\App\Action\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; /** * CMS Page Helper @@ -77,8 +82,21 @@ class Page extends \Magento\Framework\App\Helper\AbstractHelper protected $resultPageFactory; /** - * Constructor - * + * @var CustomLayoutManagerInterface + */ + private $customLayoutManager; + + /** + * @var CustomLayoutRepositoryInterface + */ + private $customLayoutRepo; + + /** + * @var IdentityMap + */ + private $identityMap; + + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param \Magento\Cms\Model\Page $page @@ -88,6 +106,9 @@ class Page extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\Escaper $escaper * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param CustomLayoutManagerInterface|null $customLayoutManager + * @param CustomLayoutRepositoryInterface|null $customLayoutRepo + * @param IdentityMap|null $identityMap * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +120,10 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Escaper $escaper, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + CustomLayoutManagerInterface $customLayoutManager = null, + CustomLayoutRepositoryInterface $customLayoutRepo = null, + IdentityMap $identityMap = null ) { $this->messageManager = $messageManager; $this->_page = $page; @@ -109,6 +133,11 @@ public function __construct( $this->_localeDate = $localeDate; $this->_escaper = $escaper; $this->resultPageFactory = $resultPageFactory; + $this->customLayoutManager = $customLayoutManager + ?? ObjectManager::getInstance()->get(CustomLayoutManagerInterface::class); + $this->customLayoutRepo = $customLayoutRepo + ?? ObjectManager::getInstance()->get(CustomLayoutRepositoryInterface::class); + $this->identityMap = $identityMap ?? ObjectManager::getInstance()->get(IdentityMap::class); parent::__construct($context); } @@ -136,6 +165,7 @@ public function prepareResultPage(Action $action, $pageId = null) if (!$this->_page->getId()) { return false; } + $this->identityMap->add($this->_page); $inRange = $this->_localeDate->isScopeDateInInterval( null, @@ -152,7 +182,19 @@ public function prepareResultPage(Action $action, $pageId = null) $resultPage = $this->resultPageFactory->create(); $this->setLayoutType($inRange, $resultPage); $resultPage->addHandle('cms_page_view'); - $resultPage->addPageLayoutHandles(['id' => str_replace('/', '_', $this->_page->getIdentifier())]); + $pageHandles = ['id' => str_replace('/', '_', $this->_page->getIdentifier())]; + //Selected custom updates. + try { + $this->customLayoutManager->applyUpdate( + $resultPage, + $this->customLayoutRepo->getFor($this->_page->getId()) + ); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (NoSuchEntityException $exception) { + //No custom layout selected + } + + $resultPage->addPageLayoutHandles($pageHandles); $this->_eventManager->dispatch( 'cms_page_render', diff --git a/app/code/Magento/Cms/Helper/Wysiwyg/Images.php b/app/code/Magento/Cms/Helper/Wysiwyg/Images.php index a557e045c5ef2..1133372f8f4e9 100644 --- a/app/code/Magento/Cms/Helper/Wysiwyg/Images.php +++ b/app/code/Magento/Cms/Helper/Wysiwyg/Images.php @@ -8,7 +8,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** - * Wysiwyg Images Helper + * Wysiwyg Images Helper. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Images extends \Magento\Framework\App\Helper\AbstractHelper { @@ -25,11 +27,11 @@ class Images extends \Magento\Framework\App\Helper\AbstractHelper protected $_currentUrl; /** - * Currenty selected store ID if applicable + * Currently selected store ID if applicable * * @var int */ - protected $_storeId = null; + protected $_storeId; /** * @var \Magento\Framework\Filesystem\Directory\Write @@ -69,7 +71,7 @@ public function __construct( $this->_storeManager = $storeManager; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); - $this->_directory->create(\Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY); + $this->_directory->create($this->getStorageRoot()); } /** @@ -91,7 +93,17 @@ public function setStoreId($store) */ public function getStorageRoot() { - return $this->_directory->getAbsolutePath(\Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY); + return $this->_directory->getAbsolutePath($this->getStorageRootSubpath()); + } + + /** + * Get image storage root subpath. User is unable to traverse outside of this subpath in media gallery + * + * @return string + */ + public function getStorageRootSubpath() + { + return ''; } /** @@ -127,17 +139,23 @@ public function convertPathToId($path) } /** - * Decode HTML element id + * Decode HTML element id. * * @param string $id * @return string + * @throws \InvalidArgumentException When path contains restricted symbols. */ public function convertIdToPath($id) { if ($id === \Magento\Theme\Helper\Storage::NODE_ROOT) { return $this->getStorageRoot(); } else { - return $this->getStorageRoot() . $this->idDecode($id); + $path = $this->getStorageRoot() . $this->idDecode($id); + if (preg_match('/\.\.(\\\|\/)/', $path)) { + throw new \InvalidArgumentException('Path is invalid'); + } + + return $path; } } @@ -148,7 +166,7 @@ public function convertIdToPath($id) */ public function isUsingStaticUrlsAllowed() { - $checkResult = new \StdClass(); + $checkResult = new \stdClass(); $checkResult->isAllowed = false; $this->_eventManager->dispatch( 'cms_wysiwyg_images_static_urls_allowed', @@ -200,7 +218,7 @@ public function getImageHtmlDeclaration($filename, $renderAsTag = false) public function getCurrentPath() { if (!$this->_currentPath) { - $currentPath = $this->_directory->getAbsolutePath() . \Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY; + $currentPath = $this->getStorageRoot(); $path = $this->_getRequest()->getParam($this->getTreeNodeName()); if ($path) { $path = $this->convertIdToPath($path); @@ -231,12 +249,8 @@ public function getCurrentUrl() { if (!$this->_currentUrl) { $path = $this->getCurrentPath(); - $mediaUrl = $this->_storeManager->getStore( - $this->_storeId - )->getBaseUrl( - \Magento\Framework\UrlInterface::URL_TYPE_MEDIA - ); - $this->_currentUrl = $mediaUrl . $this->_directory->getRelativePath($path) . '/'; + $mediaUrl = $this->_storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA); + $this->_currentUrl = rtrim($mediaUrl . $this->_directory->getRelativePath($path), '/') . '/'; } return $this->_currentUrl; } @@ -253,7 +267,7 @@ public function idEncode($string) } /** - * Revert opration to idEncode + * Revert operation to idEncode * * @param string $string * @return string diff --git a/app/code/Magento/Cms/Model/Block.php b/app/code/Magento/Cms/Model/Block.php index fc369971054d7..a8b8661bfb913 100644 --- a/app/code/Magento/Cms/Model/Block.php +++ b/app/code/Magento/Cms/Model/Block.php @@ -6,15 +6,14 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\Data\BlockInterface; -use Magento\Cms\Model\ResourceModel\Block as ResourceCmsBlock; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Model\AbstractModel; /** * CMS block model * - * @method Block setStoreId(array $storeId) - * @method array getStoreId() + * @method Block setStoreId(int $storeId) + * @method int getStoreId() */ class Block extends AbstractModel implements BlockInterface, IdentityInterface { @@ -42,6 +41,8 @@ class Block extends AbstractModel implements BlockInterface, IdentityInterface protected $_eventPrefix = 'cms_block'; /** + * Construct. + * * @return void */ protected function _construct() @@ -58,6 +59,11 @@ protected function _construct() public function beforeSave() { $needle = 'block_id="' . $this->getId() . '"'; + + if ($this->hasDataChanges()) { + $this->setUpdateTime(null); + } + if (false == strstr($this->getContent(), $needle)) { return parent::beforeSave(); } diff --git a/app/code/Magento/Cms/Model/Config/Source/Block.php b/app/code/Magento/Cms/Model/Config/Source/Block.php new file mode 100644 index 0000000000000..e89daf381d94a --- /dev/null +++ b/app/code/Magento/Cms/Model/Config/Source/Block.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Cms\Model\Config\Source; + +use Magento\Cms\Model\ResourceModel\Block\CollectionFactory; + +/** + * Class Block + */ +class Block implements \Magento\Framework\Option\ArrayInterface +{ + /** + * @var array + */ + private $options; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * To option array + * + * @return array + */ + public function toOptionArray() + { + if (!$this->options) { + $this->options = $this->collectionFactory->create()->toOptionIdArray(); + } + return $this->options; + } +} diff --git a/app/code/Magento/Cms/Model/GetUtilityPageIdentifiers.php b/app/code/Magento/Cms/Model/GetUtilityPageIdentifiers.php new file mode 100644 index 0000000000000..09c68ee9cf82d --- /dev/null +++ b/app/code/Magento/Cms/Model/GetUtilityPageIdentifiers.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Cms\Model; + +use Magento\Cms\Api\GetUtilityPageIdentifiersInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Utility Cms Pages + */ +class GetUtilityPageIdentifiers implements GetUtilityPageIdentifiersInterface +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * UtilityCmsPage constructor. + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get List Page Identifiers + * @return array + */ + public function execute() + { + $homePageIdentifier = $this->scopeConfig->getValue( + 'web/default/cms_home_page', + ScopeInterface::SCOPE_STORE + ); + $noRouteIdentifier = $this->scopeConfig->getValue( + 'web/default/cms_no_route', + ScopeInterface::SCOPE_STORE + ); + + $noCookieIdentifier = $this->scopeConfig->getValue( + 'web/default/cms_no_cookies', + ScopeInterface::SCOPE_STORE + ); + + return [$homePageIdentifier, $noRouteIdentifier, $noCookieIdentifier]; + } +} diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php index 591f8d93fcdc6..f4ebe356f5b97 100644 --- a/app/code/Magento/Cms/Model/Page.php +++ b/app/code/Magento/Cms/Model/Page.php @@ -6,19 +6,20 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\Data\PageInterface; -use Magento\Cms\Model\ResourceModel\Page as ResourceCmsPage; +use Magento\Cms\Helper\Page as PageHelper; +use Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; -use Magento\Cms\Helper\Page as PageHelper; /** * Cms Page Model * * @api - * @method Page setStoreId(array $storeId) - * @method array getStoreId() + * @method Page setStoreId(int $storeId) + * @method int getStoreId() * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @since 100.0.2 */ @@ -58,6 +59,32 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface */ private $scopeConfig; + /** + * @var CustomLayoutRepository + */ + private $customLayoutRepository; + + /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data + * @param CustomLayoutRepository|null $customLayoutRepository + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [], + CustomLayoutRepository $customLayoutRepository = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); + $this->customLayoutRepository = $customLayoutRepository + ?? ObjectManager::getInstance()->get(CustomLayoutRepository::class); + } + /** * Initialize resource model * @@ -104,8 +131,7 @@ public function getStores() } /** - * Check if page identifier exist for specific store - * return page id if page exists + * Check if page identifier exist for specific store return page id if page exists * * @param string $identifier * @param int $storeId @@ -117,8 +143,7 @@ public function checkIdentifier($identifier, $storeId) } /** - * Prepare page's statuses. - * Available event cms_page_get_available_statuses to customize statuses. + * Prepare page's statuses, available event cms_page_get_available_statuses to customize statuses. * * @return array */ @@ -539,35 +564,63 @@ public function setIsActive($isActive) } /** - * {@inheritdoc} - * @since 101.0.0 + * Validate identifier before saving the entity. + * + * @return void + * @throws LocalizedException */ - public function beforeSave() + private function validateNewIdentifier() { $originalIdentifier = $this->getOrigData('identifier'); $currentIdentifier = $this->getIdentifier(); + if ($this->getId() && $originalIdentifier !== $currentIdentifier) { + switch ($originalIdentifier) { + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_ROUTE_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS No Route Page" in configuration.') + ); + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_HOME_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS Home Page" in configuration.') + ); + case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_COOKIES_PAGE): + throw new LocalizedException( + __('This identifier is reserved for "CMS No Cookies Page" in configuration.') + ); + } + } + } - if (!$this->getId() || $originalIdentifier === $currentIdentifier) { - return parent::beforeSave(); + /** + * @inheritdoc + * @since 101.0.0 + */ + public function beforeSave() + { + if ($this->hasDataChanges()) { + $this->setUpdateTime(null); } - switch ($originalIdentifier) { - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_ROUTE_PAGE): - throw new LocalizedException( - __('This identifier is reserved for "CMS No Route Page" in configuration.') - ); - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_HOME_PAGE): - throw new LocalizedException(__('This identifier is reserved for "CMS Home Page" in configuration.')); - case $this->getScopeConfig()->getValue(PageHelper::XML_PATH_NO_COOKIES_PAGE): - throw new LocalizedException( - __('This identifier is reserved for "CMS No Cookies Page" in configuration.') - ); + $this->validateNewIdentifier(); + + //Removing deprecated custom layout update if a new value is provided + $layoutUpdate = $this->getData('layout_update_selected'); + if ($layoutUpdate === '_no_update_' || ($layoutUpdate && $layoutUpdate !== '_existing_')) { + $this->setCustomLayoutUpdateXml(null); + $this->setLayoutUpdateXml(null); } + if ($layoutUpdate === '_no_update_' || $layoutUpdate === '_existing_') { + $layoutUpdate = null; + } + $this->setData('layout_update_selected', $layoutUpdate); + $this->customLayoutRepository->validateLayoutSelectedFor($this); return parent::beforeSave(); } /** + * Returns scope config. + * * @return ScopeConfigInterface */ private function getScopeConfig() diff --git a/app/code/Magento/Cms/Model/Page/Authorization.php b/app/code/Magento/Cms/Model/Page/Authorization.php new file mode 100644 index 0000000000000..259a54307198a --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/Authorization.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\View\Model\PageLayout\Config\BuilderInterface as PageLayoutBuilder; + +/** + * Authorization for saving a page. + */ +class Authorization +{ + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var PageLayoutBuilder + */ + private $pageLayoutBuilder; + + /** + * @param PageRepositoryInterface $pageRepository + * @param AuthorizationInterface $authorization + * @param PageLayoutBuilder $pageLayoutBuilder + */ + public function __construct( + PageRepositoryInterface $pageRepository, + AuthorizationInterface $authorization, + PageLayoutBuilder $pageLayoutBuilder + ) { + $this->pageRepository = $pageRepository; + $this->authorization = $authorization; + $this->pageLayoutBuilder = $pageLayoutBuilder; + } + + /** + * Check whether the design fields have been changed. + * + * @param PageInterface $page + * @param PageInterface|null $oldPage + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function hasPageChanged(PageInterface $page, $oldPage): bool + { + if (!$oldPage) { + //Finding default page layout value. + $oldPageLayout = array_keys($this->pageLayoutBuilder->getPageLayoutsConfig()->getPageLayouts())[0]; + if ($page->getPageLayout() && $page->getPageLayout() !== $oldPageLayout) { + //If page layout is set and it's not a default value - design attributes are changed. + return true; + } + //Otherwise page layout is empty and is OK to save. + $oldPageLayout = $page->getPageLayout(); + } else { + //Compare page layout to saved value. + $oldPageLayout = $oldPage->getPageLayout(); + } + //Compare new values to saved values or require them to be empty + $oldUpdateXml = $oldPage ? $oldPage->getLayoutUpdateXml() : null; + $oldCustomTheme = $oldPage ? $oldPage->getCustomTheme() : null; + $oldLayoutUpdate = $oldPage ? $oldPage->getCustomLayoutUpdateXml() : null; + $oldThemeFrom = $oldPage ? $oldPage->getCustomThemeFrom() : null; + $oldThemeTo = $oldPage ? $oldPage->getCustomThemeTo() : null; + $oldLayoutSelected = null; + if ($oldPage instanceof \Magento\Cms\Model\Page) { + $oldLayoutSelected = $oldPage->getData('layout_update_selected'); + } + $newLayoutSelected = null; + if ($page instanceof \Magento\Cms\Model\Page) { + $newLayoutSelected = $page->getData('layout_update_selected'); + } + + if ($page->getLayoutUpdateXml() != $oldUpdateXml + || $page->getPageLayout() != $oldPageLayout + || $page->getCustomTheme() != $oldCustomTheme + || $page->getCustomLayoutUpdateXml() != $oldLayoutUpdate + || $page->getCustomThemeFrom() != $oldThemeFrom + || $page->getCustomThemeTo() != $oldThemeTo + || $newLayoutSelected != $oldLayoutSelected + ) { + return true; + } + + return false; + } + + /** + * Authorize user before updating a page. + * + * @param PageInterface $page + * @return void + * @throws AuthorizationException + * @throws \Magento\Framework\Exception\LocalizedException When it is impossible to perform authorization. + */ + public function authorizeFor(PageInterface $page) + { + //Validate design changes. + if (!$this->authorization->isAllowed('Magento_Cms::save_design')) { + $oldPage = null; + if ($page->getId()) { + $oldPage = $this->pageRepository->getById($page->getId()); + } + if ($this->hasPageChanged($page, $oldPage)) { + throw new AuthorizationException( + __('You are not allowed to change CMS pages design settings') + ); + } + } + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php new file mode 100644 index 0000000000000..f838f27245e40 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutManager.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; +use Magento\Cms\Model\Page\IdentityMap; +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Result\Page as PageLayout; +use Magento\Framework\View\Model\Layout\Merge as LayoutProcessor; +use Magento\Framework\View\Model\Layout\MergeFactory as LayoutProcessorFactory; + +/** + * @inheritDoc + */ +class CustomLayoutManager implements CustomLayoutManagerInterface +{ + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var DesignInterface + */ + private $design; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var LayoutProcessorFactory + */ + private $layoutProcessorFactory; + + /** + * @var LayoutProcessor|null + */ + private $layoutProcessor; + + /** + * @var IdentityMap + */ + private $identityMap; + + /** + * @param FlyweightFactory $themeFactory + * @param DesignInterface $design + * @param PageRepositoryInterface $pageRepository + * @param LayoutProcessorFactory $layoutProcessorFactory + * @param IdentityMap $identityMap + */ + public function __construct( + FlyweightFactory $themeFactory, + DesignInterface $design, + PageRepositoryInterface $pageRepository, + LayoutProcessorFactory $layoutProcessorFactory, + IdentityMap $identityMap + ) { + $this->themeFactory = $themeFactory; + $this->design = $design; + $this->pageRepository = $pageRepository; + $this->layoutProcessorFactory = $layoutProcessorFactory; + $this->identityMap = $identityMap; + } + + /** + * Adopt page's identifier to be used as layout handle. + * + * @param PageInterface $page + * @return string + */ + private function sanitizeIdentifier(PageInterface $page): string + { + return str_replace('/', '_', $page->getIdentifier()); + } + + /** + * Get the processor instance. + * + * @return LayoutProcessor + */ + private function getLayoutProcessor(): LayoutProcessor + { + if (!$this->layoutProcessor) { + $this->layoutProcessor = $this->layoutProcessorFactory->create( + [ + 'theme' => $this->themeFactory->create( + $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND) + ) + ] + ); + $this->themeFactory = null; + $this->design = null; + } + + return $this->layoutProcessor; + } + + /** + * @inheritDoc + */ + public function fetchAvailableFiles(PageInterface $page): array + { + $identifier = $this->sanitizeIdentifier($page); + $handles = $this->getLayoutProcessor()->getAvailableHandles(); + + return array_filter( + array_map( + function (string $handle) use ($identifier) { + preg_match( + '/^cms\_page\_view\_selectable\_' .preg_quote($identifier) .'\_([a-z0-9]+)/i', + $handle, + $selectable + ); + if (!empty($selectable[1])) { + return $selectable[1]; + } + + return null; + }, + $handles + ) + ); + } + + /** + * @inheritDoc + */ + public function applyUpdate(PageLayout $layout, CustomLayoutSelectedInterface $layoutSelected) + { + $page = $this->identityMap->get($layoutSelected->getPageId()); + if (!$page) { + $page = $this->pageRepository->getById($layoutSelected->getPageId()); + } + + $layout->addPageLayoutHandles( + ['selectable' => $this->sanitizeIdentifier($page) .'_' .$layoutSelected->getLayoutFileId()] + ); + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php new file mode 100644 index 0000000000000..505e3fd46cd3a --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/CustomLayoutRepository.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout; + +use Magento\Cms\Model\Page as PageModel; +use Magento\Cms\Model\PageFactory as PageModelFactory; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelected; +use Magento\Cms\Model\Page\CustomLayoutRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; +use Magento\Cms\Model\ResourceModel\Page; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; + +/** + * @inheritDoc + */ +class CustomLayoutRepository implements CustomLayoutRepositoryInterface +{ + /** + * @var Page + */ + private $pageRepository; + + /** + * @var PageModelFactory; + */ + private $pageFactory; + + /** + * @var IdentityMap + */ + private $identityMap; + + /** + * @var CustomLayoutManagerInterface + */ + private $manager; + + /** + * @param Page $pageRepository + * @param PageModelFactory $factory + * @param IdentityMap $identityMap + * @param CustomLayoutManagerInterface $manager + */ + public function __construct( + Page $pageRepository, + PageModelFactory $factory, + IdentityMap $identityMap, + CustomLayoutManagerInterface $manager + ) { + $this->pageRepository = $pageRepository; + $this->pageFactory = $factory; + $this->identityMap = $identityMap; + $this->manager = $manager; + } + + /** + * Find page model by ID. + * + * @param int $id + * @return PageModel + * @throws NoSuchEntityException + */ + private function findPage(int $id): PageModel + { + if (!$page = $this->identityMap->get($id)) { + /** @var PageModel $page */ + $this->pageRepository->load($page = $this->pageFactory->create(), $id); + if (!$page->getIdentifier()) { + throw NoSuchEntityException::singleField('id', $id); + } + } + + return $page; + } + + /** + * Check whether the page can use this layout. + * + * @param PageModel $page + * @param string $layoutFile + * @return bool + */ + private function isLayoutValidFor(PageModel $page, string $layoutFile): bool + { + return in_array($layoutFile, $this->manager->fetchAvailableFiles($page), true); + } + + /** + * Save new custom layout file value for a page. + * + * @param int $pageId + * @param string|null $layoutFile + * @throws LocalizedException + * @throws \InvalidArgumentException When invalid file was selected. + * @throws NoSuchEntityException + */ + private function saveLayout(int $pageId, $layoutFile) + { + $page = $this->findPage($pageId); + if ($layoutFile !== null && !$this->isLayoutValidFor($page, $layoutFile)) { + throw new \InvalidArgumentException( + $layoutFile .' is not available for page #' .$pageId + ); + } + + if ($page->getData('layout_update_selected') != $layoutFile) { + $page->setData('layout_update_selected', $layoutFile); + $this->pageRepository->save($page); + } + } + + /** + * @inheritDoc + */ + public function save(CustomLayoutSelectedInterface $layout) + { + $this->saveLayout($layout->getPageId(), $layout->getLayoutFileId()); + } + + /** + * Validate layout update of given page model. + * + * @param PageModel $page + * @return void + * @throws LocalizedException + */ + public function validateLayoutSelectedFor(PageModel $page) + { + $layoutFile = $page->getData('layout_update_selected'); + if ($layoutFile && (!$page->getId() || !$this->isLayoutValidFor($page, $layoutFile))) { + throw new LocalizedException(__('Invalid Custom Layout Update selected')); + } + } + + /** + * @inheritDoc + */ + public function deleteFor(int $pageId) + { + $this->saveLayout($pageId, null); + } + + /** + * @inheritDoc + */ + public function getFor(int $pageId): CustomLayoutSelectedInterface + { + $page = $this->findPage($pageId); + if (!$page['layout_update_selected']) { + throw new NoSuchEntityException( + __('Page "%id" doesn\'t have custom layout assigned', ['id' => $page->getIdentifier()]) + ); + } + + return new CustomLayoutSelected($pageId, $page['layout_update_selected']); + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php new file mode 100644 index 0000000000000..fc29ebd72e802 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelected.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout\Data; + +/** + * @inheritDoc + */ +class CustomLayoutSelected implements CustomLayoutSelectedInterface +{ + /** + * @var int + */ + private $pageId; + + /** + * @var string + */ + private $layoutFile; + + /** + * @param int $pageId + * @param string $layoutFile + */ + public function __construct(int $pageId, string $layoutFile) + { + $this->pageId = $pageId; + $this->layoutFile = $layoutFile; + } + + /** + * @inheritDoc + */ + public function getPageId(): int + { + return $this->pageId; + } + + /** + * @inheritDoc + */ + public function getLayoutFileId(): string + { + return $this->layoutFile; + } +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php new file mode 100644 index 0000000000000..68bac57e98d56 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayout/Data/CustomLayoutSelectedInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page\CustomLayout\Data; + +/** + * Custom layout update file to be used for the specific CMS page. + */ +interface CustomLayoutSelectedInterface +{ + /** + * CMS page ID. + * + * @return int + */ + public function getPageId(): int; + + /** + * Custom layout file ID (layout update handle value). + * + * @return string + */ + public function getLayoutFileId(): string; +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php new file mode 100644 index 0000000000000..e82bbddb4da7c --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayoutManagerInterface.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Framework\View\Result\Page as View; + +/** + * Manage custom layout files for CMS pages. + */ +interface CustomLayoutManagerInterface +{ + /** + * List of available custom files for the given page. + * + * @param PageInterface $page + * @return string[] + */ + public function fetchAvailableFiles(PageInterface $page): array; + + /** + * Apply the page's layout settings. + * + * @param View $layout + * @param CustomLayoutSelectedInterface $layoutSelected + * @return void + */ + public function applyUpdate(View $layout, CustomLayoutSelectedInterface $layoutSelected); +} diff --git a/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php b/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php new file mode 100644 index 0000000000000..20342af1d9239 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/CustomLayoutRepositoryInterface.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Access to "custom layout" page property. + */ +interface CustomLayoutRepositoryInterface +{ + + /** + * Save layout file to be used when rendering given page. + * + * @throws LocalizedException When failed to save new value. + * @throws \InvalidArgumentException When invalid file was selected. + * @throws NoSuchEntityException When given page is not found. + * @param CustomLayoutSelectedInterface $layout + * @return void + */ + public function save(CustomLayoutSelectedInterface $layout); + + /** + * Do not use custom layout update when rendering the page. + * + * @throws NoSuchEntityException When given page is not found. + * @throws LocalizedException When failed to remove existing value. + * @param int $pageId + * @return void + */ + public function deleteFor(int $pageId); + + /** + * Find custom layout settings for a page. + * + * @param int $pageId + * @return CustomLayoutSelectedInterface + * @throws NoSuchEntityException When either the page or any settings are found. + */ + public function getFor(int $pageId): CustomLayoutSelectedInterface; +} diff --git a/app/code/Magento/Cms/Model/Page/DataProvider.php b/app/code/Magento/Cms/Model/Page/DataProvider.php index 67035846024e8..39012965d3905 100644 --- a/app/code/Magento/Cms/Model/Page/DataProvider.php +++ b/app/code/Magento/Cms/Model/Page/DataProvider.php @@ -5,8 +5,12 @@ */ namespace Magento\Cms\Model\Page; +use Magento\Cms\Model\Page; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; /** * Class DataProvider @@ -28,6 +32,26 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ protected $loadedData; + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var CustomLayoutManagerInterface + */ + private $customLayoutManager; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + /** * @param string $name * @param string $primaryFieldName @@ -36,6 +60,10 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param DataPersistorInterface $dataPersistor * @param array $meta * @param array $data + * @param AuthorizationInterface|null $authorization + * @param RequestInterface|null $request + * @param CustomLayoutManagerInterface|null $customLayoutManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -44,12 +72,36 @@ public function __construct( CollectionFactory $pageCollectionFactory, DataPersistorInterface $dataPersistor, array $meta = [], - array $data = [] + array $data = [], + AuthorizationInterface $authorization = null, + RequestInterface $request = null, + CustomLayoutManagerInterface $customLayoutManager = null ) { $this->collection = $pageCollectionFactory->create(); + $this->collectionFactory = $pageCollectionFactory; $this->dataPersistor = $dataPersistor; parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + $this->authorization = $authorization ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); $this->meta = $this->prepareMeta($this->meta); + $this->request = $request ?? ObjectManager::getInstance()->get(RequestInterface::class); + $this->customLayoutManager = $customLayoutManager + ?? ObjectManager::getInstance()->get(CustomLayoutManagerInterface::class); + } + + /** + * Find requested page. + * + * @return Page|null + */ + private function findCurrentPage() + { + if ($this->getRequestFieldName() && ($pageId = (int)$this->request->getParam($this->getRequestFieldName()))) { + //Loading data for the collection. + $this->getData(); + return $this->collection->getItemById($pageId); + } + + return null; } /** @@ -73,10 +125,15 @@ public function getData() if (isset($this->loadedData)) { return $this->loadedData; } + $this->collection = $this->collectionFactory->create(); $items = $this->collection->getItems(); /** @var $page \Magento\Cms\Model\Page */ foreach ($items as $page) { $this->loadedData[$page->getId()] = $page->getData(); + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + //Deprecated layout update exists. + $this->loadedData[$page->getId()]['layout_update_selected'] = '_existing_'; + } } $data = $this->dataPersistor->get('cms_page'); @@ -84,9 +141,66 @@ public function getData() $page = $this->collection->getNewEmptyItem(); $page->setData($data); $this->loadedData[$page->getId()] = $page->getData(); + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + $this->loadedData[$page->getId()]['layout_update_selected'] = '_existing_'; + } $this->dataPersistor->clear('cms_page'); } return $this->loadedData; } + + /** + * @inheritDoc + */ + public function getMeta() + { + $meta = parent::getMeta(); + + if (!$this->authorization->isAllowed('Magento_Cms::save_design')) { + $designMeta = []; + $designFieldSets = ['design', 'custom_design_update']; + + foreach ($designFieldSets as $fieldSet) { + $designMeta[$fieldSet] = [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'disabled' => true, + ], + ], + ], + ]; + } + + $meta = array_merge_recursive($meta, $designMeta); + } + + //List of custom layout files available for current page. + $options = [['label' => 'No update', 'value' => '_no_update_']]; + if ($page = $this->findCurrentPage()) { + //We must have a specific page selected. + //If custom layout XML is set then displaying this special option. + if ($page->getCustomLayoutUpdateXml() || $page->getLayoutUpdateXml()) { + $options[] = ['label' => 'Use existing layout update XML', 'value' => '_existing_']; + } + foreach ($this->customLayoutManager->fetchAvailableFiles($page) as $layoutFile) { + $options[] = ['label' => $layoutFile, 'value' => $layoutFile]; + } + } + $customLayoutMeta = [ + 'design' => [ + 'children' => [ + 'custom_layout_update_select' => [ + 'arguments' => [ + 'data' => ['options' => $options] + ] + ] + ] + ] + ]; + $meta = array_merge_recursive($meta, $customLayoutMeta); + + return $meta; + } } diff --git a/app/code/Magento/Cms/Model/Page/IdentityMap.php b/app/code/Magento/Cms/Model/Page/IdentityMap.php new file mode 100644 index 0000000000000..2a11edb9d1129 --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/IdentityMap.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Cms\Model\Page; + +/** + * Identity map of loaded pages. + */ +class IdentityMap +{ + /** + * @var Page[] + */ + private $pages = []; + + /** + * Add a page to the list. + * + * @param Page $page + * @throws \InvalidArgumentException When page doesn't have an ID. + * @return void + */ + public function add(Page $page) + { + if (!$page->getId()) { + throw new \InvalidArgumentException('Cannot add non-persisted page to identity map'); + } + $this->pages[$page->getId()] = $page; + } + + /** + * Find a loaded page by ID. + * + * @param int $id + * @return Page|null + */ + public function get(int $id) + { + if (array_key_exists($id, $this->pages)) { + return $this->pages[$id]; + } + + return null; + } + + /** + * Remove the page from the list. + * + * @param int $id + * @return void + */ + public function remove(int $id) + { + unset($this->pages[$id]); + } + + /** + * Clear the list. + * + * @return void + */ + public function clear() + { + $this->pages = []; + } +} diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php index fb759348759b2..23a452c0fe58c 100644 --- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php +++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php @@ -20,6 +20,7 @@ class PageLayout implements OptionSourceInterface /** * @var array + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $options; @@ -34,16 +35,10 @@ public function __construct(BuilderInterface $pageLayoutBuilder) } /** - * Get options - * - * @return array + * @inheritdoc */ public function toOptionArray() { - if ($this->options !== null) { - return $this->options; - } - $configOptions = $this->pageLayoutBuilder->getPageLayoutsConfig()->getOptions(); $options = []; foreach ($configOptions as $key => $value) { @@ -54,6 +49,6 @@ public function toOptionArray() } $this->options = $options; - return $this->options; + return $options; } } diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 9c9e18211aa86..fdae9b4fcc016 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Cms\Model; use Magento\Cms\Api\Data; use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\IdentityMap; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; @@ -68,6 +71,11 @@ class PageRepository implements PageRepositoryInterface */ private $collectionProcessor; + /** + * @var IdentityMap + */ + private $identityMap; + /** * @param ResourcePage $resource * @param PageFactory $pageFactory @@ -78,6 +86,8 @@ class PageRepository implements PageRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor + * @param IdentityMap|null $identityMap + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourcePage $resource, @@ -88,7 +98,8 @@ public function __construct( DataObjectHelper $dataObjectHelper, DataObjectProcessor $dataObjectProcessor, StoreManagerInterface $storeManager, - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor = null, + IdentityMap $identityMap = null ) { $this->resource = $resource; $this->pageFactory = $pageFactory; @@ -99,23 +110,50 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->identityMap = $identityMap ?? ObjectManager::getInstance()->get(IdentityMap::class); + } + + /** + * Validate new layout update values. + * + * @param Data\PageInterface $page + * @return void + * @throws \InvalidArgumentException + */ + private function validateLayoutUpdate(Data\PageInterface $page) + { + //Persisted data + $savedPage = $page->getId() ? $this->getById($page->getId()) : null; + //Custom layout update can be removed or kept as is. + if ($page->getCustomLayoutUpdateXml() + && (!$savedPage || $page->getCustomLayoutUpdateXml() !== $savedPage->getCustomLayoutUpdateXml()) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); + } + if ($page->getLayoutUpdateXml() + && (!$savedPage || $page->getLayoutUpdateXml() !== $savedPage->getLayoutUpdateXml()) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); + } } /** * Save Page data * - * @param \Magento\Cms\Api\Data\PageInterface $page + * @param \Magento\Cms\Api\Data\PageInterface|Page $page * @return Page * @throws CouldNotSaveException */ public function save(\Magento\Cms\Api\Data\PageInterface $page) { - if (empty($page->getStoreId())) { + if ($page->getStoreId() === null) { $storeId = $this->storeManager->getStore()->getId(); $page->setStoreId($storeId); } try { + $this->validateLayoutUpdate($page); $this->resource->save($page); + $this->identityMap->add($page); } catch (\Exception $exception) { throw new CouldNotSaveException( __('Could not save the page: %1', $exception->getMessage()), @@ -139,6 +177,8 @@ public function getById($pageId) if (!$page->getId()) { throw new NoSuchEntityException(__('CMS Page with id "%1" does not exist.', $pageId)); } + $this->identityMap->add($page); + return $page; } @@ -176,6 +216,7 @@ public function delete(\Magento\Cms\Api\Data\PageInterface $page) { try { $this->resource->delete($page); + $this->identityMap->remove($page->getId()); } catch (\Exception $exception) { throw new CouldNotDeleteException(__( 'Could not delete the page: %1', @@ -208,7 +249,7 @@ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor' + \Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor::class ); } return $this->collectionProcessor; diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php new file mode 100644 index 0000000000000..9fd94d4c11e1c --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaInterface; + +/** + * Validates and saves a page + */ +class ValidationComposite implements PageRepositoryInterface +{ + /** + * @var PageRepositoryInterface + */ + private $repository; + + /** + * @var array + */ + private $validators; + + /** + * @param PageRepositoryInterface $repository + * @param ValidatorInterface[] $validators + */ + public function __construct( + PageRepositoryInterface $repository, + array $validators = [] + ) { + foreach ($validators as $validator) { + if (!$validator instanceof ValidatorInterface) { + throw new \InvalidArgumentException( + sprintf('Supplied validator does not implement %s', ValidatorInterface::class) + ); + } + } + $this->repository = $repository; + $this->validators = $validators; + } + + /** + * @inheritdoc + */ + public function save(PageInterface $page) + { + foreach ($this->validators as $validator) { + $validator->validate($page); + } + + return $this->repository->save($page); + } + + /** + * @inheritdoc + */ + public function getById($pageId) + { + return $this->repository->getById($pageId); + } + + /** + * @inheritdoc + */ + public function getList(SearchCriteriaInterface $searchCriteria) + { + return $this->repository->getList($searchCriteria); + } + + /** + * @inheritdoc + */ + public function delete(PageInterface $page) + { + return $this->repository->delete($page); + } + + /** + * @inheritdoc + */ + public function deleteById($pageId) + { + return $this->repository->deleteById($pageId); + } +} diff --git a/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php new file mode 100644 index 0000000000000..b4a5da8db73d0 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/Validator/LayoutUpdateValidator.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository\Validator; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository\ValidatorInterface; +use Magento\Framework\Config\Dom\ValidationException; +use Magento\Framework\Config\Dom\ValidationSchemaException; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Model\Layout\Update\Validator; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; + +/** + * Validate a given page + */ +class LayoutUpdateValidator implements ValidatorInterface +{ + /** + * @var ValidatorFactory + */ + private $validatorFactory; + + /** + * @var ValidationStateInterface + */ + private $validationState; + + /** + * @param ValidatorFactory $validatorFactory + * @param ValidationStateInterface $validationState + */ + public function __construct( + ValidatorFactory $validatorFactory, + ValidationStateInterface $validationState + ) { + $this->validatorFactory = $validatorFactory; + $this->validationState = $validationState; + } + + /** + * Validate the data before saving + * + * @param PageInterface $page + * @throws LocalizedException + */ + public function validate(PageInterface $page) + { + $this->validateRequiredFields($page); + $this->validateLayoutUpdate($page); + $this->validateCustomLayoutUpdate($page); + } + + /** + * Validate required fields + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateRequiredFields(PageInterface $page) + { + if (empty($page->getTitle())) { + throw new LocalizedException(__('Required field "%1" is empty.', 'title')); + } + } + + /** + * Validate layout update + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateLayoutUpdate(PageInterface $page) + { + $layoutXmlValidator = $this->getLayoutValidator(); + + try { + if (!empty($page->getLayoutUpdateXml()) + && !$layoutXmlValidator->isValid($page->getLayoutUpdateXml()) + ) { + throw new LocalizedException(__('Layout update is invalid')); + } + } catch (ValidationException $e) { + throw new LocalizedException(__('Layout update is invalid')); + } catch (ValidationSchemaException $e) { + throw new LocalizedException(__('Layout update is invalid')); + } + } + + /** + * Validate custom layout update + * + * @param PageInterface $page + * @throws LocalizedException + */ + private function validateCustomLayoutUpdate(PageInterface $page) + { + $layoutXmlValidator = $this->getLayoutValidator(); + + try { + if (!empty($page->getCustomLayoutUpdateXml()) + && !$layoutXmlValidator->isValid($page->getCustomLayoutUpdateXml()) + ) { + throw new LocalizedException(__('Custom layout update is invalid')); + } + } catch (ValidationException $e) { + throw new LocalizedException(__('Custom layout update is invalid')); + } catch (ValidationSchemaException $e) { + throw new LocalizedException(__('Custom layout update is invalid')); + } + } + + /** + * Return a new validator + * + * @return Validator + */ + private function getLayoutValidator(): Validator + { + return $this->validatorFactory->create( + [ + 'validationState' => $this->validationState, + ] + ); + } +} diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php new file mode 100644 index 0000000000000..3a8632c49da64 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/ValidatorInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Validate a page repository + */ +interface ValidatorInterface +{ + /** + * Assert the given page valid + * + * @param PageInterface $page + * @return void + * @throws LocalizedException + */ + public function validate(PageInterface $page); +} diff --git a/app/code/Magento/Cms/Model/Plugin/Product.php b/app/code/Magento/Cms/Model/Plugin/Product.php new file mode 100644 index 0000000000000..c8456d5cd6bfe --- /dev/null +++ b/app/code/Magento/Cms/Model/Plugin/Product.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Plugin; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Cms\Model\Page; + +/** + * Cleaning no-route page cache for the product details page after enabling product that is not assigned to a category + */ +class Product +{ + /** + * @var Page + */ + private $page; + + /** + * @param Page $page + */ + public function __construct(Page $page) + { + $this->page = $page; + } + + /** + * After get identities + * + * @param CatalogProduct $product + * @param array $identities + * @return array + */ + public function afterGetIdentities(CatalogProduct $product, array $identities) + { + if ($product->getOrigData('status') > $product->getData('status')) { + if (empty($product->getCategoryIds())) { + $noRoutePage = $this->page->load(Page::NOROUTE_PAGE_ID); + $noRoutePageId = $noRoutePage->getId(); + $identities[] = Page::CACHE_TAG . '_' . $noRoutePageId; + } + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/Cms/Model/ResourceModel/AbstractCollection.php b/app/code/Magento/Cms/Model/ResourceModel/AbstractCollection.php index cd5945c2f47cb..2a67378614445 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/AbstractCollection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/AbstractCollection.php @@ -176,4 +176,32 @@ public function getSelectCountSql() return $countSelect; } + + /** + * Returns pairs identifier - title for unique identifiers + * and pairs identifier|entity_id - title for non-unique after first + * + * @return array + */ + public function toOptionIdArray() + { + $res = []; + $existingIdentifiers = []; + foreach ($this as $item) { + $identifier = $item->getData('identifier'); + + $data['value'] = $identifier; + $data['label'] = $item->getData('title'); + + if (in_array($identifier, $existingIdentifiers)) { + $data['value'] .= '|' . $item->getData($this->getIdFieldName()); + } else { + $existingIdentifiers[] = $identifier; + } + + $res[] = $data; + } + + return $res; + } } diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php index d5bae7359fe35..9b4bc5ec3ea11 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php @@ -7,10 +7,10 @@ use Magento\Cms\Api\Data\BlockInterface; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Store\Model\Store; @@ -95,9 +95,11 @@ protected function _beforeSave(AbstractModel $object) } /** + * Get block id. + * * @param AbstractModel $object * @param mixed $value - * @param null $field + * @param string $field * @return bool|int|string * @throws LocalizedException * @throws \Exception @@ -183,10 +185,12 @@ public function getIsUniqueBlockToStores(AbstractModel $object) $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); $linkField = $entityMetadata->getLinkField(); - if ($this->_storeManager->isSingleStoreMode()) { - $stores = [Store::DEFAULT_STORE_ID]; - } else { - $stores = (array)$object->getData('store_id'); + $stores = (array)$object->getData('store_id'); + $isDefaultStore = $this->_storeManager->isSingleStoreMode() + || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; + + if (!$isDefaultStore) { + $stores[] = Store::DEFAULT_STORE_ID; } $select = $this->getConnection()->select() @@ -196,8 +200,11 @@ public function getIsUniqueBlockToStores(AbstractModel $object) 'cb.' . $linkField . ' = cbs.' . $linkField, [] ) - ->where('cb.identifier = ?', $object->getData('identifier')) - ->where('cbs.store_id IN (?)', $stores); + ->where('cb.identifier = ?', $object->getData('identifier')); + + if (!$isDefaultStore) { + $select->where('cbs.store_id IN (?)', $stores); + } if ($object->getId()) { $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); @@ -236,6 +243,8 @@ public function lookupStoreIds($id) } /** + * Save an object. + * * @param AbstractModel $object * @return $this * @throws \Exception diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php index f45d9ee223106..60e87afc61884 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php @@ -6,7 +6,7 @@ namespace Magento\Cms\Model\ResourceModel\Block\Grid; use Magento\Framework\Api\Search\SearchResultInterface; -use Magento\Framework\Search\AggregationInterface; +use Magento\Framework\Api\Search\AggregationInterface; use Magento\Cms\Model\ResourceModel\Block\Collection as BlockCollection; /** @@ -82,6 +82,7 @@ public function getAggregations() public function setAggregations($aggregations) { $this->aggregations = $aggregations; + return $this; } /** diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page.php b/app/code/Magento/Cms/Model/ResourceModel/Page.php index 8e26c8b67fa4b..b836cf199632d 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page.php @@ -6,18 +6,18 @@ namespace Magento\Cms\Model\ResourceModel; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Model\Page as CmsPage; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Stdlib\DateTime; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\EntityManager\EntityManager; -use Magento\Cms\Api\Data\PageInterface; /** * Cms page mysql resource diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php index 98c071890be46..96886a995b1c9 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php @@ -51,34 +51,6 @@ protected function _construct() $this->_map['fields']['store'] = 'store_table.store_id'; } - /** - * Returns pairs identifier - title for unique identifiers - * and pairs identifier|page_id - title for non-unique after first - * - * @return array - */ - public function toOptionIdArray() - { - $res = []; - $existingIdentifiers = []; - foreach ($this as $item) { - $identifier = $item->getData('identifier'); - - $data['value'] = $identifier; - $data['label'] = $item->getData('title'); - - if (in_array($identifier, $existingIdentifiers)) { - $data['value'] .= '|' . $item->getData('page_id'); - } else { - $existingIdentifiers[] = $identifier; - } - - $res[] = $data; - } - - return $res; - } - /** * Set first store flag * @@ -102,7 +74,9 @@ public function addStoreFilter($store, $withAdmin = true) { if (!$this->getFlag('store_filter_added')) { $this->performAddStoreFilter($store, $withAdmin); + $this->setFlag('store_filter_added', true); } + return $this; } diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php index c5c43c3120dcc..19f945e5b4637 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php @@ -83,6 +83,7 @@ public function getAggregations() public function setAggregations($aggregations) { $this->aggregations = $aggregations; + return $this; } /** diff --git a/app/code/Magento/Cms/Model/Template/Filter.php b/app/code/Magento/Cms/Model/Template/Filter.php index 2dcbfbc4ab598..fc1a3e6bfc774 100644 --- a/app/code/Magento/Cms/Model/Template/Filter.php +++ b/app/code/Magento/Cms/Model/Template/Filter.php @@ -34,10 +34,15 @@ public function setUseSessionInUrl($flag) * * @param string[] $construction * @return string + * @throws \InvalidArgumentException */ public function mediaDirective($construction) { $params = $this->getParameters($construction[2]); + if (preg_match('/\.\.(\\\|\/)/', $params['url'])) { + throw new \InvalidArgumentException('Image path must be absolute'); + } + return $this->_storeManager->getStore()->getBaseMediaDir() . '/' . $params['url']; } } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 0688534dc4ad9..ec099efb8e5a3 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg\Images; use Magento\Cms\Helper\Wysiwyg\Images; @@ -241,10 +243,12 @@ protected function getConditionsForExcludeDirs() protected function removeItemFromCollection($collection, $conditions) { $regExp = $conditions['reg_exp'] ? '~' . implode('|', array_keys($conditions['reg_exp'])) . '~i' : null; - $storageRootLength = strlen($this->_cmsWysiwygImages->getStorageRoot()); + $storageRoot = $this->_cmsWysiwygImages->getStorageRoot(); + $storageRootLength = strlen($storageRoot); foreach ($collection as $key => $value) { - $rootChildParts = explode('/', substr($value->getFilename(), $storageRootLength)); + $mediaSubPathname = substr($value->getFilename(), $storageRootLength); + $rootChildParts = explode('/', '/' . ltrim($mediaSubPathname, '/')); if (array_key_exists($rootChildParts[1], $conditions['plain']) || ($regExp && preg_match($regExp, $value->getFilename()))) { @@ -268,7 +272,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); @@ -318,6 +323,8 @@ public function getFilesCollection($path, $type = null) $item->setName($item->getBasename()); $item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename())); $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); + $item->setSize(filesize($item->getFilename())); + $item->setMimeType(\mime_content_type($item->getFilename())); if ($this->isImage($item->getBasename())) { $thumbUrl = $this->getThumbnailUrl($item->getFilename(), true); @@ -409,7 +416,7 @@ public function createDirectory($name, $path) /** * Recursively delete directory from storage * - * @param string $path Target dir + * @param string $path Absolute path to target directory * @return void * @throws \Magento\Framework\Exception\LocalizedException */ @@ -418,12 +425,19 @@ public function deleteDirectory($path) if ($this->_coreFileStorageDb->checkDbUsage()) { $this->_directoryDatabaseFactory->create()->deleteDirectory($path); } + if (!$this->isPathAllowed($path, $this->getConditionsForExcludeDirs())) { + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot delete directory %1.', $this->_getRelativePathToRoot($path)) + ); + } try { $this->_deleteByPath($path); $path = $this->getThumbnailRoot() . $this->_getRelativePathToRoot($path); $this->_deleteByPath($path); } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot delete directory %1.', $path)); + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot delete directory %1.', $this->_getRelativePathToRoot($path)) + ); } } @@ -470,13 +484,18 @@ public function deleteFile($target) /** * Upload and resize new file * - * @param string $targetPath Target directory + * @param string $targetPath Absolute path to target directory * @param string $type Type of storage, e.g. image, media etc. * @return array File info Array * @throws \Magento\Framework\Exception\LocalizedException */ public function uploadFile($targetPath, $type = null) { + if (!$this->isPathAllowed($targetPath, $this->getConditionsForExcludeDirs())) { + throw new \Magento\Framework\Exception\LocalizedException( + __('We can\'t upload the file to current folder right now. Please try another folder.') + ); + } /** @var \Magento\MediaStorage\Model\File\Uploader $uploader */ $uploader = $this->_uploaderFactory->create(['fileId' => 'image']); $allowed = $this->getAllowedExtensions($type); @@ -485,6 +504,9 @@ public function uploadFile($targetPath, $type = null) } $uploader->setAllowRenameFiles(true); $uploader->setFilesDispersion(false); + if (!$uploader->checkMimeType($this->getAllowedMimeTypes($type))) { + throw new \Magento\Framework\Exception\LocalizedException(__('File validation failed.')); + } $result = $uploader->save($targetPath); if (!$result) { @@ -494,14 +516,6 @@ public function uploadFile($targetPath, $type = null) // create thumbnail $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); - $result['cookie'] = [ - 'name' => $this->getSession()->getName(), - 'value' => $this->getSession()->getSessionId(), - 'lifetime' => $this->getSession()->getCookieLifetime(), - 'path' => $this->getSession()->getCookiePath(), - 'domain' => $this->getSession()->getCookieDomain(), - ]; - return $result; } @@ -560,10 +574,10 @@ public function getThumbnailUrl($filePath, $checkFile = false) * Create thumbnail for image and save it to thumbnails directory * * @param string $source Image path to be resized - * @param bool $keepRation Keep aspect ratio or not + * @param bool $keepRatio Keep aspect ratio or not * @return bool|string Resized filepath or false if errors were occurred */ - public function resizeFile($source, $keepRation = true) + public function resizeFile($source, $keepRatio = true) { $realPath = $this->_directory->getRelativePath($source); if (!$this->_directory->isFile($realPath) || !$this->_directory->isExist($realPath)) { @@ -580,7 +594,7 @@ public function resizeFile($source, $keepRation = true) } $image = $this->_imageFactory->create(); $image->open($source); - $image->keepAspectRatio($keepRation); + $image->keepAspectRatio($keepRatio); $image->resize($this->_resizeParameters['width'], $this->_resizeParameters['height']); $dest = $targetDir . '/' . pathinfo($source, PATHINFO_BASENAME); $image->save($dest); @@ -641,11 +655,7 @@ public function getSession() */ public function getAllowedExtensions($type = null) { - if (is_string($type) && array_key_exists("{$type}_allowed", $this->_extensions)) { - $allowed = $this->_extensions["{$type}_allowed"]; - } else { - $allowed = $this->_extensions['allowed']; - } + $allowed = $this->getExtensionsList($type); return array_keys(array_filter($allowed)); } @@ -735,7 +745,7 @@ protected function _validatePath($path) */ protected function _sanitizePath($path) { - return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPath($path)), '/'); + return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPathSafety($path)), '/'); } /** @@ -751,4 +761,58 @@ protected function _getRelativePathToRoot($path) strlen($this->_sanitizePath($this->_cmsWysiwygImages->getStorageRoot())) ); } + + /** + * Prepare mime types config settings. + * + * @param string|null $type Type of storage, e.g. image, media etc. + * @return array Array of allowed file extensions + */ + private function getAllowedMimeTypes($type = null): array + { + $allowed = $this->getExtensionsList($type); + + return array_values(array_filter($allowed)); + } + + /** + * Get list of allowed file extensions with mime type in values. + * + * @param string|null $type + * @return array + */ + private function getExtensionsList($type = null): array + { + if (is_string($type) && array_key_exists("{$type}_allowed", $this->_extensions)) { + $allowed = $this->_extensions["{$type}_allowed"]; + } else { + $allowed = $this->_extensions['allowed']; + } + return $allowed; + } + + /** + * Check if path is not in excluded dirs. + * + * @param string $path Absolute path + * @param array $conditions Exclude conditions + * @return bool + */ + private function isPathAllowed($path, array $conditions): bool + { + $isAllowed = true; + $regExp = $conditions['reg_exp'] ? '~' . implode('|', array_keys($conditions['reg_exp'])) . '~i' : null; + $storageRoot = $this->_cmsWysiwygImages->getStorageRoot(); + $storageRootLength = strlen($storageRoot); + + $mediaSubPathname = substr($path, $storageRootLength); + $rootChildParts = explode('/', '/' . ltrim($mediaSubPathname, '/')); + + if (array_key_exists($rootChildParts[1], $conditions['plain']) + || ($regExp && preg_match($regExp, $path))) { + $isAllowed = false; + } + + return $isAllowed; + } } diff --git a/app/code/Magento/Cms/Observer/PageAclPlugin.php b/app/code/Magento/Cms/Observer/PageAclPlugin.php new file mode 100644 index 0000000000000..c71fe0af396c0 --- /dev/null +++ b/app/code/Magento/Cms/Observer/PageAclPlugin.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Observer; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page\Authorization; + +/** + * Perform additional authorization before saving a page. + */ +class PageAclPlugin +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * Authorize saving before it is executed. + * + * @param PageRepositoryInterface $subject + * @param PageInterface $page + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(PageRepositoryInterface $subject, PageInterface $page): array + { + $this->authorization->authorizeFor($page); + + return [$page]; + } +} diff --git a/app/code/Magento/Cms/Observer/PageValidatorObserver.php b/app/code/Magento/Cms/Observer/PageValidatorObserver.php new file mode 100644 index 0000000000000..b4e5d2bc0e0a7 --- /dev/null +++ b/app/code/Magento/Cms/Observer/PageValidatorObserver.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Observer; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page\Authorization; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Performing additional validation each time a user saves a CMS page. + */ +class PageValidatorObserver implements ObserverInterface +{ + /** + * @var Authorization + */ + private $authorization; + + /** + * @param Authorization $authorization + */ + public function __construct(Authorization $authorization) + { + $this->authorization = $authorization; + } + + /** + * @inheritDoc + * + * @throws LocalizedException + */ + public function execute(Observer $observer) + { + /** @var PageInterface $page */ + $page = $observer->getEvent()->getData('page'); + $this->authorization->authorizeFor($page); + } +} diff --git a/app/code/Magento/Cms/Setup/Recurring.php b/app/code/Magento/Cms/Setup/Recurring.php new file mode 100644 index 0000000000000..a0601d547200b --- /dev/null +++ b/app/code/Magento/Cms/Setup/Recurring.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Setup; + +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\DB\Ddl\Table; + +/** + * @inheritDoc + */ +class Recurring implements InstallSchemaInterface +{ + /** + * @inheritDoc + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $this->addLayoutSelected($setup); + } + + /** + * Add custom layout selected field. + * + * @param SchemaSetupInterface $setup + * @return void + */ + private function addLayoutSelected(SchemaSetupInterface $setup) + { + $table = $setup->getTable('cms_page'); + $connection = $setup->getConnection(); + $column = 'layout_update_selected'; + if (!$connection->tableColumnExists($table, $column)) { + $connection->addColumn( + $table, + $column, + [ + 'type' => Table::TYPE_TEXT, + 'length' => 255, + 'nullable' => true, + 'comment' => 'File containing custom layout update' + ] + ); + } + } +} diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml new file mode 100644 index 0000000000000..597df165f61d1 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillCmsBlockForm"> + <arguments> + <argument name="title" type="string" defaultValue="{{DefaultCmsBlock.title}}"/> + <argument name="identifier" type="string" defaultValue="{{DefaultCmsBlock.identifier}}"/> + <argument name="store" type="string" defaultValue="[All Store View]"/> + <argument name="content" type="string" defaultValue="{{DefaultCmsBlock.content}}"/> + </arguments> + <fillField selector="{{AdminCmsBlockContentSection.title}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <fillField selector="{{AdminCmsBlockContentSection.identifier}}" userInput="{{identifier}}" stepKey="fillFieldIdentifier"/> + <selectOption selector="{{AdminCmsBlockContentSection.storeView}}" parameterArray="{{store}}" stepKey="selectStore" /> + <fillField selector="{{AdminCmsBlockContentSection.content}}" userInput="{{content}}" stepKey="fillContentField"/> + </actionGroup> + <actionGroup name="DeleteCmsBlockActionGroup"> + <arguments> + <argument name="cmsBlockIdentifier" type="string" defaultValue="{{DefaultCmsBlock.identifier}}"/> + </arguments> + <amOnPage url="{{AdminCmsBlockGridPage.url}}" stepKey="navigateToCmsBlockListingPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersBeforeDelete"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openCmsBlockFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" userInput="{{cmsBlockIdentifier}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{CmsPagesPageActionsSection.select(cmsBlockIdentifier)}}" stepKey="clickOnSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(cmsBlockIdentifier)}}" stepKey="clickOnDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirm"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the block." stepKey="verifyBlockIsDeleted"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersAfterDelete"/> + </actionGroup> + <actionGroup name="NavigateToCreateCmsBlockActionGroup"> + <amOnPage url="{{AdminCmsBlockNewPage.url}}" stepKey="navigateToCreateCmsBlockPage"/> + </actionGroup> + <actionGroup name="SaveCmsBlockActionGroup"> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveBlockButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the block." stepKey="verifyMessage"/> + </actionGroup> + <actionGroup name="SaveCmsBlockWithErrorActionGroup" extends="SaveCmsBlockActionGroup"> + <arguments> + <argument name="errorMessage" type="string" defaultValue="A block identifier with the same properties already exists in the selected store."/> + </arguments> + <see selector="{{AdminMessagesSection.error}}" userInput="{{errorMessage}}" stepKey="verifyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml new file mode 100644 index 0000000000000..667dc79d2d6b4 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateNewCmsPageActionGroup"> + <arguments> + <argument name="cmsPage" defaultValue="_defaultCmsPage"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{cmsPage.title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{cmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the page." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + + <actionGroup name="CreateNewPageWithWidgetWithCategoryCondition" extends="CreateNewCmsPageActionGroup"> + <arguments> + <argument name="categoryId" type="string"/> + <argument name="conditionOperator" type="string" defaultValue="is"/> + <argument name="widgetType" type="string" defaultValue="Catalog Products List"/> + </arguments> + <click selector="{{CmsNewPagePageContentSection.header}}" after="fillFieldUrlKey" stepKey="clickExpandContent"/> + <click selector="{{CmsNewPagePageActionsSection.insertWidgetButton}}" after="clickExpandContent" stepKey="clickInsertWidgetButton"/> + + <selectOption selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" userInput="{{widgetType}}" after="clickInsertWidgetButton" stepKey="selectCatalogProductListOption"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.addNewCondition}}" after="selectCatalogProductListOption" stepKey="waitForConditionsElementBecomeAvailable"/> + + <click selector="{{AdminNewWidgetSection.addNewCondition}}" after="waitForConditionsElementBecomeAvailable" stepKey="clickToAddCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCondition}}" after="clickToAddCondition" stepKey="waitForSelectBoxOpened"/> + + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="Category" after="waitForSelectBoxOpened" stepKey="selectCategoryCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.ruleParameter}}" after="selectCategoryCondition" stepKey="seeConditionsAdded"/> + + <click selector="{{AdminNewWidgetSection.conditionOperator}}" after="seeConditionsAdded" stepKey="clickToConditionIs"/> + <selectOption selector="{{AdminNewWidgetSection.conditionOperatorSelect('1')}}" after="clickToConditionIs" userInput="{{conditionOperator}}" stepKey="selectOperator"/> + + <click selector="{{AdminNewWidgetSection.ruleParameter}}" after="selectOperator" stepKey="clickAddConditionItem"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.setRuleParameter}}" after="clickAddConditionItem" stepKey="waitForConditionFieldOpened"/> + + <fillField selector="{{AdminNewWidgetSection.setRuleParameter}}" userInput="{{categoryId}}" after="waitForConditionFieldOpened" stepKey="setCategoryId"/> + <click selector="{{AdminNewWidgetSection.insertWidget}}" after="setCategoryId" stepKey="clickInsertWidget"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" after="clickInsertWidget" stepKey="waitForInsertWidgetSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml new file mode 100644 index 0000000000000..48b10d0ee48be --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateNewPageWithAllValues"> + <arguments> + <argument name="pageTitle" type="string"/> + <argument name="contentHeading" type="string"/> + <argument name="urlKey" type="string"/> + <argument name="selectStoreViewOpt" type="string"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForElementVisible selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" stepKey="waitUntilTitleAppears"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{contentHeading}}" stepKey="fillFieldContentHeading"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimization"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{urlKey}}" stepKey="fillFieldUrlKey"/> + <click selector="{{AdminCmsNewPagePiwSection.header}}" stepKey="clickPageInWebsites"/> + <waitForElementVisible selector="{{AdminCmsNewPagePiwSection.selectStoreView(selectStoreViewOpt)}}" stepKey="waitForStoreGridReload"/> + <clickWithLeftButton selector="{{AdminCmsNewPagePiwSection.selectStoreView(selectStoreViewOpt)}}" stepKey="clickStoreView2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..fd20954dc7a9e --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeletePageByUrlKeyActionGroup"> + <arguments> + <argument name="urlKey" type="string"/> + </arguments> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnCMSPagesIndexPage"/> + <waitForPageLoad time="30" stepKey="waitForCmsPageListingLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersBeforeDelete"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openCmsPageFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" userInput="{{urlKey}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{CmsPagesPageActionsSection.select(urlKey)}}" stepKey="clickSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(urlKey)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The page has been deleted." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersAfterDelete"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml new file mode 100644 index 0000000000000..6a2012b551407 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToCMSPagesActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateToCreatedCMSBlockPage"> + <arguments> + <argument name="cmsBlock"/> + </arguments> + <amOnPage url="{{AdminCmsBlockGridPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickToResetFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{cmsBlock.identifier}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowActionSelect}}" stepKey="clickSelectCreatedCMSBlock" /> + <click selector="{{AdminDataGridTableSection.rowEditAction}}" stepKey="navigateToCreatedCMSBlock" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml new file mode 100644 index 0000000000000..79da00f26ecb9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultCmsBlock" type="cms_block"> + <data key="title">Default Block</data> + <data key="identifier" unique="suffix" >block</data> + <data key="content">Here is a block test. Yeah!</data> + <data key="active">true</data> + </entity> + <entity name="Sales25offBlock" type="cms_block"> + <data key="title" unique="suffix">Sales25off</data> + <data key="identifier" unique="suffix">Sales25off</data> + <data key="content">sales25off everything!</data> + <data key="active">false</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml new file mode 100644 index 0000000000000..67bde89c516d2 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultCmsPage" type="cms_page"> + <data key="title">Test CMS Page</data> + <data key="content_heading">Test Content Heading</data> + <data key="content">Sample page content. Yada yada yada.</data> + <data key="identifier" unique="suffix">test-page-</data> + </entity> + <entity name="simpleCmsPage" type="cms_page"> + <data key="title">Test CMS Page</data> + <data key="content_heading">Test Content Heading</data> + <data key="content">Sample page content. Yada yada yada.</data> + <data key="identifier" unique="suffix">test-page-</data> + </entity> + <entity name="ImageUpload" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento.jpg</data> + <data key="fileName">magento</data> + <data key="content">Image content. Yeah.</data> + <data key="height">1000</data> + </entity> + <entity name="ImageUpload1" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento2.jpg</data> + <data key="fileName">magento2</data> + <data key="content">Image content. Yeah.</data> + <data key="height">1000</data> + </entity> + <entity name="ImageUpload2" type="uploadImage"> + <data key="file">medium.jpg</data> + <data key="fileName">medium</data> + </entity> + <entity name="ImageFolder" type="uploadImage"> + <data key="name" unique="suffix">Test</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml new file mode 100644 index 0000000000000..bc69c94329ac9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="WysiwygEnabledByDefault"> + <data key="path">cms/wysiwyg/enabled</data> + <data key="scope_id">0</data> + <data key="value">enabled</data> + </entity> + <entity name="WysiwygDisabledByDefault"> + <data key="path">cms/wysiwyg/enabled</data> + <data key="scope_id">0</data> + <data key="value">hidden</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/LICENSE.txt b/app/code/Magento/Cms/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Cms/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 <insert your license name here>" 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/app/code/Magento/Cms/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Cms/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Cms/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 <insert your license name here>" 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/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml new file mode 100644 index 0000000000000..60a33c132a6c1 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCmsBlock" dataType="cms_block" type="create" auth="adminOauth" url="/V1/cmsBlock" method="POST"> + <contentType>application/json</contentType> + <object key="block" dataType="cms_block"> + <field key="title">string</field> + <field key="identifier">string</field> + <field key="content">string</field> + <field key="active">string</field> + </object> + </operation> + + <operation name="DeleteCmsBlock" dataType="cms_block" type="delete" auth="adminOauth" url="/V1/cmsBlock/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml new file mode 100644 index 0000000000000..328dc156a38fb --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCmsBlockEditPage" url="/cms/block/edit/block_id/{{blockId}}/" area="admin" module="Magento_Cms" parameterized="true"> + <section name="AdminCmsBlockContentSection" /> + <section name="AdminMediaGallerySection" /> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml new file mode 100644 index 0000000000000..2cabe182714dc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockGridPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCmsBlockGridPage" url="/cms/block/" area="admin" module="Magento_Cms"> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml new file mode 100644 index 0000000000000..2868d832ad762 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCmsBlockNewPage" url="/cms/block/new/" area="admin" module="Magento_Cms"> + <section name="AdminCmsBlockContentSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml new file mode 100644 index 0000000000000..bb8dc9cf9159c --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsNewPagePage.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CmsNewPagePage" url="/cms/page/new" area="admin" module="Magento_Cms"> + <section name="CmsNewPagePageActionsSection"/> + <section name="CmsNewPagePageBasicFieldsSection"/> + <section name="CmsNewPagePageContentSection"/> + <section name="CmsNewPagePageSeoSection"/> + <section name="AdminCmsNewPagePiwSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml new file mode 100644 index 0000000000000..9dcb3d608d04e --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPagesPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CmsPagesPage" url="/cms/page" area="admin" module="Magento_Cms"> + <section name="CmsPagesPageActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml new file mode 100644 index 0000000000000..b2de3a225f8ce --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCmsPage" url="/{{urlKey}}" area="storefront" module="Magento_Cms" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml new file mode 100644 index 0000000000000..5468d08bd4e0b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontHomePage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontHomePage" url="/" module="Magento_Cms" area="storefront"> + <section name="StorefrontHeaderSection"/> + <section name="StorefrontQuickSearchSection"/> + <section name="StorefrontHeaderCurrencySwitcherSection"/> + <section name="StorefrontCmsPageSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/README.md b/app/code/Magento/Cms/Test/Mftf/README.md new file mode 100644 index 0000000000000..5e223390c07cd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Cms Functional Tests + +The Functional Test Module for **Magento Cms** module. diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml new file mode 100644 index 0000000000000..20e55c49ec235 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCmsBlockContentSection"> + <element name="content" type="textarea" selector="#cms_block_form_content"/> + <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin"/> + <element name="title" type="input" selector="input[name=title]"/> + <element name="identifier" type="input" selector="input[name=identifier]"/> + <element name="storeView" type="multiselect" selector="select[name=store_id]"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsNewPagePiwSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsNewPagePiwSection.xml new file mode 100644 index 0000000000000..ce22bae4e2b93 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsNewPagePiwSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCmsNewPagePiwSection"> + <element name="header" type="button" selector="div[data-index=websites]" timeout="30"/> + <element name="selectStoreView" type="select" selector="//option[contains(text(),'{{var1}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminMediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminMediaGallerySection.xml new file mode 100644 index 0000000000000..9d08bc708aef9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminMediaGallerySection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGallerySection"> + <element name="imageSelected" type="text" selector="//small[text()='{{imageName}}']/parent::*[@class='filecnt selected']" parameterized="true"/> + <element name="uploadImage" type="file" selector="input.fileupload" /> + <element name="insertFile" type="text" selector="#insert_files"/> + <element name="imageBlockByName" type="block" selector="//div[@data-row='file'][contains(., '{{imageName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml new file mode 100644 index 0000000000000..740650d6fdfa9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CmsNewPagePageActionsSection"> + <element name="savePage" type="button" selector="#save" timeout="30"/> + <element name="saveAndContinueEdit" type="button" selector="#save_and_continue" timeout="10"/> + <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml new file mode 100644 index 0000000000000..1d9e58e870cfb --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageBasicFieldsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CmsNewPagePageBasicFieldsSection"> + <element name="pageTitle" type="input" selector="input[name=title]"/> + <element name="requiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=title]>.admin__field-label span'), ':after').getPropertyValue('content');"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml new file mode 100644 index 0000000000000..3440805d3814b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageContentSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CmsNewPagePageContentSection"> + <element name="header" type="button" selector="div[data-index=content]"/> + <element name="contentHeading" type="input" selector="input[name=content_heading]"/> + <element name="content" type="input" selector="#cms_page_form_content"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml new file mode 100644 index 0000000000000..0fe9c01d36fcb --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageSeoSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CmsNewPagePageSeoSection"> + <element name="header" type="button" selector="div[data-index=search_engine_optimisation]" timeout="30"/> + <element name="urlKey" type="input" selector="input[name=identifier]"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml new file mode 100644 index 0000000000000..370e7af15d4b2 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CmsPagesPageActionsSection"> + <element name="addNewPage" type="button" selector="#add" timeout="30"/> + <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> + <element name="edit" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Edit']" parameterized="true"/> + <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Preview']" parameterized="true"/> + <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> + <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCmsPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCmsPageSection.xml new file mode 100644 index 0000000000000..3795af36fc004 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCmsPageSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCmsPageSection"> + <element name="imageSource" type="text" selector="img[src*='{{imageName}}']" parameterized="true"/> + <element name="mainContent" type="text" selector="#maincontent"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml new file mode 100644 index 0000000000000..39face57e5c86 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateCmsPageTest"> + <annotations> + <features value="CMS Page Creation"/> + <stories value="Create a CMS Page via the Admin"/> + <title value="Create a CMS Page"/> + <description value="You should be able to create a CMS Page via the Admin."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-25580"/> + <group value="cms"/> + <skip> + <issueId value="MQE-282"/> + </skip> + </annotations> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{CmsPagesPageActionsSection.addNewPage}}" stepKey="clickAddNewPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{_defaultCmsPage.content_heading}}" stepKey="fillFieldContentHeading"/> + <!-- As of 2017/11/15, this test is failing here (Jenkins only, works locally). See MQE-282. --> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{_defaultCmsPage.content}}" stepKey="fillFieldContent"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{_defaultCmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <see userInput="{{_defaultCmsPage.content_heading}}" stepKey="seeContentHeading"/> + <see userInput="{{_defaultCmsPage.content}}" stepKey="seeContent"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminRestrictedUserOnlyAccessCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminRestrictedUserOnlyAccessCmsBlockTest.xml new file mode 100644 index 0000000000000..d0ed330779676 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminRestrictedUserOnlyAccessCmsBlockTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminRestrictedUserOnlyAccessCmsBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="Check access for restricted admin user"/> + <title value="Check: restricted admin with access only to CMS Block"/> + <description value="Check that the system shows information only in Blocks"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13814"/> + <useCaseId value="MAGETWO-88612"/> + <group value="Cms"/> + </annotations> + <before> + <createData entity="restrictedWebUser" stepKey="createRestrictedAdmin"/> + <actionGroup ref="LoginToAdminActionGroup" stepKey="loginToBackend"/> + <actionGroup ref="AdminCreateUserRoleActionGroup" stepKey="createRestrictedAdminRole"> + <argument name="roleName" value="{{RoleTest.roleName}}"/> + <argument name="resourceAccess" value="Custom"/> + <argument name="resource" value="Magento_Cms::block"/> + </actionGroup> + <actionGroup ref="AdminAssignUserRoleActionGroup" stepKey="assignAdminRole"> + <argument name="user_restricted" value="$$createRestrictedAdmin$$"/> + <argument name="roleName" value="{{RoleTest.roleName}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logOut"/> + </before> + <after> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdminWithAllAccess"/> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteRestrictedRole"> + <argument name="roleName" value="{{RoleTest.roleName}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteRestrictedUser"> + <argument name="user_restricted" value="$$createRestrictedAdmin$$"/> + </actionGroup> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--login as restricted user--> + <actionGroup ref="AdminLoginAsAnyUser" stepKey="logAsNewUser"> + <argument name="login" value="$$createRestrictedAdmin.username$$"/> + <argument name="password" value="$$createRestrictedAdmin.password$$"/> + </actionGroup> + + <!--Verify that The system shows information included in "Blocks"--> + <see userInput="Blocks" stepKey="seeBlocksPage"/> + <seeInCurrentUrl url="{{AdminCmsBlockGridPage.url}}" stepKey="assertUrl"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logOut"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.xml new file mode 100644 index 0000000000000..ac1b68269740f --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckCreateStaticBlockOnDuplicateIdentifierTest"> + <annotations> + <features value="Cms"/> + <stories value="Create CMS Block"/> + <title value="Check static blocks: ID should be unique per Store View"/> + <description value="Check static blocks: ID should be unique per Store View"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13912"/> + <useCaseId value="MAGETWO-86215"/> + <group value="cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCmsBlockActionGroup" stepKey="deleteCMSBlockActionGroup"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreateCmsBlockActionGroup" stepKey="navigateToCreateCmsBlock"/> + <actionGroup ref="FillCmsBlockForm" stepKey="fillCmsBlockForm"/> + <actionGroup ref="SaveCmsBlockActionGroup" stepKey="saveCmsBlock"/> + <actionGroup ref="NavigateToCreateCmsBlockActionGroup" stepKey="navigateToCreateDuplicateCmsBlock"/> + <actionGroup ref="FillCmsBlockForm" stepKey="fillDuplicateCmsBlockForm"> + <argument name="store" value="[{{_defaultStore.name}},{{SecondStoreUnique.name}}]"/> + </actionGroup> + <actionGroup ref="SaveCmsBlockWithErrorActionGroup" stepKey="assertErrorMessageOnSave"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Block/Widget/ChooserTest.php b/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Block/Widget/ChooserTest.php index 97988a5676842..a27110ca96b6d 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Block/Widget/ChooserTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Block/Widget/ChooserTest.php @@ -233,6 +233,9 @@ public function testPrepareElementHtml($elementValue, $modelBlockId) $this->assertEquals($this->elementMock, $this->this->prepareElementHtml($this->elementMock)); } + /** + * @return array + */ public function prepareElementHtmlDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Page/Widget/ChooserTest.php b/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Page/Widget/ChooserTest.php index 174e3a68b7c66..7b91d54ec3aa1 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Page/Widget/ChooserTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/Adminhtml/Page/Widget/ChooserTest.php @@ -236,6 +236,9 @@ public function testPrepareElementHtml($elementValue, $cmsPageId) $this->assertEquals($this->elementMock, $this->this->prepareElementHtml($this->elementMock)); } + /** + * @return array + */ public function prepareElementHtmlDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php index ff1ed408eb131..c11c7c3810832 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php @@ -73,7 +73,7 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); $this->blockMock = $this->getMockBuilder(\Magento\Cms\Model\Block::class) @@ -110,6 +110,8 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->deleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Block\Delete::class, [ @@ -134,10 +136,10 @@ public function testDeleteAction() ->with($this->blockId); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You deleted the block.')); $this->messageManagerMock->expects($this->never()) - ->method('addError'); + ->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') @@ -154,10 +156,10 @@ public function testDeleteActionNoId() ->willReturn(null); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t find a block to delete.')); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') @@ -181,10 +183,10 @@ public function testDeleteActionThrowsException() ->willThrowException(new \Exception(__($errorMsg))); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($errorMsg); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/EditTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/EditTest.php index 875dde9fb226b..a28a1b793d943 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/EditTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/EditTest.php @@ -139,7 +139,7 @@ public function testEditActionBlockNoExists() ->willReturn(null); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('This block no longer exists.')); $this->resultRedirectFactoryMock->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php index 2dc14154c85e5..3088e3b62c364 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php @@ -36,12 +36,16 @@ protected function setUp() $this->blockCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Block\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDeleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Block\MassDelete::class, [ 'context' => $this->contextMock, 'filter' => $this->filterMock, - 'collectionFactory' => $this->collectionFactoryMock + 'collectionFactory' => $this->collectionFactoryMock, ] ); } @@ -68,9 +72,9 @@ public function testMassDeleteAction() ->willReturn(new \ArrayIterator($collection)); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) have been deleted.', $deletedBlocksCount)); - $this->messageManagerMock->expects($this->never())->method('addError'); + $this->messageManagerMock->expects($this->never())->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php index f6b709a5c96c9..40ed379e9d7e2 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/SaveTest.php @@ -65,6 +65,16 @@ class SaveTest extends \PHPUnit\Framework\TestCase */ protected $saveController; + /** + * @var \Magento\Cms\Model\BlockFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $blockFactory; + + /** + * @var \Magento\Cms\Api\BlockRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $blockRepository; + /** * @var int */ @@ -129,11 +139,22 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactory); + $this->blockFactory = $this->getMockBuilder(\Magento\Cms\Model\BlockFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->blockRepository = $this->getMockBuilder(\Magento\Cms\Api\BlockRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->saveController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Block\Save::class, [ 'context' => $this->contextMock, 'dataPersistor' => $this->dataPersistorMock, + 'blockFactory' => $this->blockFactory, + 'blockRepository' => $this->blockRepository, ] ); } @@ -158,26 +179,24 @@ public function testSaveAction() ] ); - $this->objectManagerMock->expects($this->atLeastOnce()) + $this->blockFactory->expects($this->atLeastOnce()) ->method('create') - ->with($this->equalTo(\Magento\Cms\Model\Block::class)) ->willReturn($this->blockMock); - $this->blockMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $this->blockMock->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->blockRepository->expects($this->once()) + ->method('getById') + ->with($this->blockId) + ->willReturn($this->blockMock); + $this->blockMock->expects($this->once())->method('setData'); - $this->blockMock->expects($this->once())->method('save'); + $this->blockRepository->expects($this->once())->method('save')->with($this->blockMock); $this->dataPersistorMock->expects($this->any()) ->method('clear') ->with('cms_block'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the block.')); $this->resultRedirect->expects($this->atLeastOnce())->method('setPath')->with('*/*/') ->willReturnSelf(); @@ -204,20 +223,17 @@ public function testSaveActionNoId() ] ); - $this->objectManagerMock->expects($this->atLeastOnce()) + $this->blockFactory->expects($this->atLeastOnce()) ->method('create') - ->with($this->equalTo(\Magento\Cms\Model\Block::class)) ->willReturn($this->blockMock); - $this->blockMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $this->blockMock->expects($this->any()) - ->method('getId') - ->willReturn(false); + $this->blockRepository->expects($this->once()) + ->method('getById') + ->with($this->blockId) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('Error message'))); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('This block no longer exists.')); $this->resultRedirect->expects($this->atLeastOnce())->method('setPath')->with('*/*/') ->willReturnSelf(); @@ -237,22 +253,20 @@ public function testSaveAndContinue() ] ); - $this->objectManagerMock->expects($this->atLeastOnce()) + $this->blockFactory->expects($this->atLeastOnce()) ->method('create') - ->with($this->equalTo(\Magento\Cms\Model\Block::class)) ->willReturn($this->blockMock); - $this->blockMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $this->blockMock->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->blockRepository->expects($this->once()) + ->method('getById') + ->with($this->blockId) + ->willReturn($this->blockMock); + $this->blockMock->expects($this->once())->method('setData'); - $this->blockMock->expects($this->once())->method('save'); + $this->blockRepository->expects($this->once())->method('save')->with($this->blockMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the block.')); $this->dataPersistorMock->expects($this->any()) @@ -279,24 +293,24 @@ public function testSaveActionThrowsException() ] ); - $this->objectManagerMock->expects($this->atLeastOnce()) + $this->blockFactory->expects($this->atLeastOnce()) ->method('create') - ->with($this->equalTo(\Magento\Cms\Model\Block::class)) ->willReturn($this->blockMock); - $this->blockMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $this->blockMock->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->blockRepository->expects($this->once()) + ->method('getById') + ->with($this->blockId) + ->willReturn($this->blockMock); + $this->blockMock->expects($this->once())->method('setData'); - $this->blockMock->expects($this->once())->method('save')->willThrowException(new \Exception('Error message.')); + $this->blockRepository->expects($this->once())->method('save') + ->with($this->blockMock) + ->willThrowException(new \Exception('Error message.')); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->messageManagerMock->expects($this->once()) - ->method('addException'); + ->method('addExceptionMessage'); $this->dataPersistorMock->expects($this->any()) ->method('set') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php index 7f994bf5b3df5..48098242197ae 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php @@ -56,7 +56,7 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); $this->pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) @@ -95,6 +95,8 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->deleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\Delete::class, [ @@ -124,10 +126,10 @@ public function testDeleteAction() ->method('delete'); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('The page has been deleted.')); $this->messageManagerMock->expects($this->never()) - ->method('addError'); + ->method('addErrorMessage'); $this->eventManagerMock->expects($this->once()) ->method('dispatch') @@ -151,10 +153,10 @@ public function testDeleteActionNoId() ->willReturn(null); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t find a page to delete.')); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') @@ -195,10 +197,10 @@ public function testDeleteActionThrowsException() ); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($errorMsg); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/EditTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/EditTest.php index 335abb837523a..5ea5ce5a9fdbb 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/EditTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/EditTest.php @@ -139,7 +139,7 @@ public function testEditActionPageNoExists() ->willReturn(null); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('This page no longer exists.')); $this->resultRedirectFactoryMock->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php index 9d51431b26d8f..7f2ff2086df91 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/InlineEditTest.php @@ -102,10 +102,6 @@ public function prepareMocksForTestExecute() ->method('filter') ->with($postData[1]) ->willReturnArgument(0); - $this->dataProcessor->expects($this->once()) - ->method('validate') - ->with($postData[1]) - ->willReturn(false); $this->messageManager->expects($this->once()) ->method('getMessages') ->with(true) @@ -122,19 +118,23 @@ public function prepareMocksForTestExecute() ->willReturn('1'); $this->cmsPage->expects($this->atLeastOnce()) ->method('getData') - ->willReturn([ - 'layout' => '1column', - 'identifier' => 'test-identifier' - ]); + ->willReturn( + [ + 'layout' => '1column', + 'identifier' => 'test-identifier' + ] + ); $this->cmsPage->expects($this->once()) ->method('setData') - ->with([ - 'layout' => '1column', - 'title' => '404 Not Found', - 'identifier' => 'no-route', - 'custom_theme' => '1', - 'custom_root_template' => '2' - ]); + ->with( + [ + 'layout' => '1column', + 'title' => '404 Not Found', + 'identifier' => 'no-route', + 'custom_theme' => '1', + 'custom_root_template' => '2' + ] + ); $this->jsonFactory->expects($this->once()) ->method('create') ->willReturn($this->resultJson); @@ -149,13 +149,15 @@ public function testExecuteWithLocalizedException() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('LocalizedException'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] LocalizedException' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] LocalizedException' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -170,13 +172,15 @@ public function testExecuteWithRuntimeException() ->willThrowException(new \RuntimeException(__('RuntimeException'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] RuntimeException' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] RuntimeException' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -191,13 +195,15 @@ public function testExecuteWithException() ->willThrowException(new \Exception(__('Exception'))); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - '[Page ID: 1] Error message', - '[Page ID: 1] Something went wrong while saving the page.' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + '[Page ID: 1] Error message', + '[Page ID: 1] Something went wrong while saving the page.' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); @@ -218,12 +224,14 @@ public function testExecuteWithoutData() ); $this->resultJson->expects($this->once()) ->method('setData') - ->with([ - 'messages' => [ - 'Please correct the data sent.' - ], - 'error' => true - ]) + ->with( + [ + 'messages' => [ + 'Please correct the data sent.' + ], + 'error' => true + ] + ) ->willReturnSelf(); $this->assertSame($this->resultJson, $this->controller->execute()); diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php index 8f1a651b0a7e1..c0f3a719091d7 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php @@ -36,12 +36,16 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDeleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassDelete::class, [ 'context' => $this->contextMock, 'filter' => $this->filterMock, - 'collectionFactory' => $this->collectionFactoryMock + 'collectionFactory' => $this->collectionFactoryMock, ] ); } @@ -68,9 +72,9 @@ public function testMassDeleteAction() ->willReturn(new \ArrayIterator($collection)); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) have been deleted.', $deletedPagesCount)); - $this->messageManagerMock->expects($this->never())->method('addError'); + $this->messageManagerMock->expects($this->never())->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php index 0185654434be1..64b47b5a08416 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php @@ -35,6 +35,10 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDisableController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassDisable::class, [ @@ -67,9 +71,9 @@ public function testMassDisableAction() ->willReturn(new \ArrayIterator($collection)); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) have been disabled.', $disabledPagesCount)); - $this->messageManagerMock->expects($this->never())->method('addError'); + $this->messageManagerMock->expects($this->never())->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php index b5907e7b3ffed..a63a81882dfe9 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php @@ -35,6 +35,10 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massEnableController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassEnable::class, [ @@ -67,9 +71,9 @@ public function testMassEnableAction() ->willReturn(new \ArrayIterator($collection)); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('A total of %1 record(s) have been enabled.', $enabledPagesCount)); - $this->messageManagerMock->expects($this->never())->method('addError'); + $this->messageManagerMock->expects($this->never())->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php index 03a8fc0969064..8262f0b39a356 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/SaveTest.php @@ -153,13 +153,9 @@ public function testSaveAction() ->method('create') ->willReturn($page); - $page->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $page->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->pageRepository->expects($this->once())->method('getById')->with($this->pageId)->willReturn($page); $page->expects($this->once())->method('setData'); + $page->method('getId')->willReturn($this->pageId); $this->pageRepository->expects($this->once())->method('save')->with($page); $this->dataPersistorMock->expects($this->any()) @@ -182,6 +178,36 @@ public function testSaveActionWithoutData() $this->assertSame($this->resultRedirect, $this->saveController->execute()); } + public function testSaveActionNoId() + { + $this->requestMock->expects($this->any())->method('getPostValue')->willReturn(['page_id' => 1]); + $this->requestMock->expects($this->atLeastOnce()) + ->method('getParam') + ->willReturnMap( + [ + ['page_id', null, 1], + ['back', null, false], + ] + ); + + $page = $this->getMockBuilder(\Magento\Cms\Model\Page::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->pageFactory->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($page); + $this->pageRepository->expects($this->once()) + ->method('getById') + ->with($this->pageId) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('Error message'))); + $this->messageManagerMock->expects($this->once()) + ->method('addErrorMessage') + ->with(__('This page no longer exists.')); + $this->resultRedirect->expects($this->atLeastOnce())->method('setPath')->with('*/*/') ->willReturnSelf(); + $this->assertSame($this->resultRedirect, $this->saveController->execute()); + } + public function testSaveAndContinue() { $this->requestMock->expects($this->any())->method('getPostValue')->willReturn(['page_id' => $this->pageId]); @@ -204,12 +230,7 @@ public function testSaveAndContinue() ->method('create') ->willReturn($page); - $page->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $page->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->pageRepository->expects($this->once())->method('getById')->with($this->pageId)->willReturn($page); $page->expects($this->once())->method('setData'); $this->pageRepository->expects($this->once())->method('save')->with($page); @@ -247,16 +268,12 @@ public function testSaveActionThrowsException() $page = $this->getMockBuilder(\Magento\Cms\Model\Page::class) ->disableOriginalConstructor() ->getMock(); + $page->method('getId')->willReturn(1); $this->pageFactory->expects($this->atLeastOnce()) ->method('create') ->willReturn($page); - $page->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $page->expects($this->any()) - ->method('getId') - ->willReturn(true); + $this->pageRepository->expects($this->once())->method('getById')->with($this->pageId)->willReturn($page); $page->expects($this->once())->method('setData'); $this->pageRepository->expects($this->once())->method('save')->with($page) ->willThrowException(new \Exception('Error message.')); @@ -268,7 +285,14 @@ public function testSaveActionThrowsException() $this->dataPersistorMock->expects($this->any()) ->method('set') - ->with('cms_page', ['page_id' => $this->pageId]); + ->with( + 'cms_page', + [ + 'page_id' => $this->pageId, + 'layout_update_xml' => null, + 'custom_layout_update_xml' => null + ] + ); $this->resultRedirect->expects($this->atLeastOnce()) ->method('setPath') diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Index/IndexTest.php index 8ff206e8a80fc..d8453bc6ecbce 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Index/IndexTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Index/IndexTest.php @@ -84,7 +84,9 @@ protected function setUp() 'response' => $responseMock, 'objectManager' => $objectManagerMock, 'request' => $this->requestMock, - 'resultForwardFactory' => $this->forwardFactoryMock + 'resultForwardFactory' => $this->forwardFactoryMock, + 'scopeConfig' => $scopeConfigMock, + 'page' => $this->cmsHelperMock ] ); } diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Page/PostDataProcessorTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Page/PostDataProcessorTest.php index 31d99df5f6289..d13dfc628201d 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Page/PostDataProcessorTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Page/PostDataProcessorTest.php @@ -65,7 +65,7 @@ public function testValidateRequireEntry() 'title' => '' ]; $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('To apply changes you should fill in hidden required "%1" field', 'Page Title')); $this->assertFalse($this->postDataProcessor->validateRequireEntry($postData)); diff --git a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php index 8b41f0e3ac0d4..c50f33caa6bc2 100644 --- a/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Helper/PageTest.php @@ -367,6 +367,9 @@ public function testPrepareResultPage( ); } + /** + * @return array + */ public function renderPageExtendedDataProvider() { return [ @@ -467,6 +470,9 @@ public function testGetPageUrl( $this->assertEquals($expectedResult, $this->object->getPageUrl($pageId)); } + /** + * @return array + */ public function getPageUrlDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Helper/Wysiwyg/ImagesTest.php b/app/code/Magento/Cms/Test/Unit/Helper/Wysiwyg/ImagesTest.php index 05dad459f064e..8222f8be6fcd5 100644 --- a/app/code/Magento/Cms/Test/Unit/Helper/Wysiwyg/ImagesTest.php +++ b/app/code/Magento/Cms/Test/Unit/Helper/Wysiwyg/ImagesTest.php @@ -74,7 +74,7 @@ class ImagesTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->path = 'PATH/'; + $this->path = 'PATH'; $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); @@ -105,7 +105,8 @@ protected function setUp() ->willReturnMap( [ [WysiwygConfig::IMAGE_DIRECTORY, null, $this->getAbsolutePath(WysiwygConfig::IMAGE_DIRECTORY)], - [null, null, $this->getAbsolutePath(null)] + [null, null, $this->getAbsolutePath(null)], + ['', null, $this->getAbsolutePath('')], ] ); @@ -170,7 +171,7 @@ public function testSetStoreId() public function testGetStorageRoot() { $this->assertEquals( - $this->getAbsolutePath(WysiwygConfig::IMAGE_DIRECTORY), + $this->getAbsolutePath(''), $this->imagesHelper->getStorageRoot() ); } @@ -194,7 +195,7 @@ public function testGetTreeNodeName() public function testConvertPathToId() { $pathOne = '/test_path'; - $pathTwo = $this->getAbsolutePath(WysiwygConfig::IMAGE_DIRECTORY) . '/test_path'; + $pathTwo = $this->getAbsolutePath('') . '/test_path'; $this->assertEquals( $this->imagesHelper->convertPathToId($pathOne), $this->imagesHelper->convertPathToId($pathTwo) @@ -230,6 +231,15 @@ public function testConvertIdToPathNodeRoot() $this->assertEquals($this->imagesHelper->getStorageRoot(), $this->imagesHelper->convertIdToPath($pathId)); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Path is invalid + */ + public function testConvertIdToPathInvalid() + { + $this->imagesHelper->convertIdToPath('Ly4uLy4uLy4uLy4uLy4uL3dvcms-'); + } + /** * @param string $fileName * @param int $maxLength @@ -293,7 +303,7 @@ protected function generalSettingsIsUsingStaticUrlsAllowed($allowedValue) { $storeId = 1; $this->imagesHelper->setStoreId($storeId); - $checkResult = new \StdClass(); + $checkResult = new \stdClass(); $checkResult->isAllowed = false; $this->eventManagerMock->expects($this->any()) ->method('dispatch') @@ -322,26 +332,30 @@ public function providerIsUsingStaticUrlsAllowed() */ public function testGetCurrentPath($pathId, $expectedPath, $isExist) { - $this->requestMock->expects($this->once()) + $this->requestMock->expects($this->any()) ->method('getParam') - ->willReturn($pathId); + ->willReturnMap( + [ + ['node', null, $pathId], + ] + ); $this->directoryWriteMock->expects($this->any()) ->method('isDirectory') ->willReturnMap( [ - ['/../wysiwyg/test_path', true], - ['/../wysiwyg/my.jpg', false], - ['/../wysiwyg', true] + ['/../test_path', true], + ['/../my.jpg', false], + ['.', true], ] ); $this->directoryWriteMock->expects($this->any()) ->method('getRelativePath') ->willReturnMap( [ - ['PATH/wysiwyg/test_path', '/../wysiwyg/test_path'], - ['PATH/wysiwyg/my.jpg', '/../wysiwyg/my.jpg'], - ['PATH/wysiwyg', '/../wysiwyg'], + ['PATH/test_path', '/../test_path'], + ['PATH/my.jpg', '/../my.jpg'], + ['PATH', '.'], ] ); $this->directoryWriteMock->expects($this->once()) @@ -358,7 +372,7 @@ public function testGetCurrentPathThrowException() { $this->expectException( \Magento\Framework\Exception\LocalizedException::class, - 'The directory PATH/wysiwyg is not writable by server.' + 'The directory PATH is not writable by server.' ); $this->directoryWriteMock->expects($this->once()) @@ -375,15 +389,18 @@ public function testGetCurrentPathThrowException() $this->fail('An expected exception has not been raised.'); } + /** + * @return array + */ public function providerGetCurrentPath() { return [ - ['L3Rlc3RfcGF0aA--', 'PATH/wysiwyg/test_path', true], - ['L215LmpwZw--', 'PATH/wysiwyg', true], - [null, 'PATH/wysiwyg', true], - ['L3Rlc3RfcGF0aA--', 'PATH/wysiwyg/test_path', false], - ['L215LmpwZw--', 'PATH/wysiwyg', false], - [null, 'PATH/wysiwyg', false] + ['L3Rlc3RfcGF0aA--', 'PATH/test_path', true], + ['L215LmpwZw--', 'PATH', true], + [null, 'PATH', true], + ['L3Rlc3RfcGF0aA--', 'PATH/test_path', false], + ['L215LmpwZw--', 'PATH', false], + [null, 'PATH', false], ]; } @@ -422,6 +439,9 @@ public function testGetImageHtmlDeclarationRenderingAsTag($baseUrl, $fileName, $ $this->assertEquals($expectedHtml, $this->imagesHelper->getImageHtmlDeclaration($fileName, true)); } + /** + * @return array + */ public function providerGetImageHtmlDeclarationRenderingAsTag() { return [ @@ -456,6 +476,9 @@ public function testGetImageHtmlDeclaration($baseUrl, $fileName, $isUsingStaticU $this->assertEquals($expectedHtml, $this->imagesHelper->getImageHtmlDeclaration($fileName)); } + /** + * @return array + */ public function providerGetImageHtmlDeclaration() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php b/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php new file mode 100644 index 0000000000000..abfaa8701458c --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/BlockTest.php @@ -0,0 +1,339 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model; + +use Magento\Cms\Model\Block; +use Magento\Cms\Model\ResourceModel\Block as BlockResource; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\Context; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * @covers \Magento\Cms\Model\Block + */ +class BlockTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var Block + */ + private $blockModel; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var BlockResource|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->resourceMock = $this->createMock(BlockResource::class); + $this->eventManagerMock = $this->createMock(ManagerInterface::class); + $this->contextMock = $this->createMock(Context::class); + $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManagerMock); + $this->objectManager = new ObjectManager($this); + $this->blockModel = $this->objectManager->getObject( + Block::class, + [ + 'context' => $this->contextMock, + 'resource' => $this->resourceMock, + ] + ); + } + + /** + * Test beforeSave method + * + * @return void + * + * @throws LocalizedException + */ + public function testBeforeSave() + { + $blockId = 7; + $this->blockModel->setData(Block::BLOCK_ID, $blockId); + $this->blockModel->setData(Block::CONTENT, 'test'); + $this->objectManager->setBackwardCompatibleProperty($this->blockModel, '_hasDataChanges', true); + $this->eventManagerMock->expects($this->atLeastOnce())->method('dispatch'); + + $expected = $this->blockModel; + $actual = $this->blockModel->beforeSave(); + self::assertEquals($expected, $actual); + } + + /** + * Test beforeSave method + * + * @return void + * + * @throws LocalizedException + */ + public function testBeforeSaveWithException() + { + $blockId = 10; + $this->blockModel->setData(Block::BLOCK_ID, $blockId); + $this->blockModel->setData(Block::CONTENT, 'Test block_id="' . $blockId . '".'); + $this->objectManager->setBackwardCompatibleProperty($this->blockModel, '_hasDataChanges', false); + $this->eventManagerMock->expects($this->never())->method('dispatch'); + $this->expectException(LocalizedException::class); + $this->blockModel->beforeSave(); + } + + /** + * Test getIdentities method + * + * @return void + */ + public function testGetIdentities() + { + $result = $this->blockModel->getIdentities(); + self::assertInternalType('array', $result); + } + + /** + * Test getId method + * + * @return void + */ + public function testGetId() + { + $blockId = 12; + $this->blockModel->setData(Block::BLOCK_ID, $blockId); + $expected = $blockId; + $actual = $this->blockModel->getId(); + self::assertEquals($expected, $actual); + } + + /** + * Test getIdentifier method + * + * @return void + */ + public function testGetIdentifier() + { + $identifier = 'test01'; + $this->blockModel->setData(Block::IDENTIFIER, $identifier); + + $expected = $identifier; + $actual = $this->blockModel->getIdentifier(); + self::assertEquals($expected, $actual); + } + + /** + * Test getTitle method + * + * @return void + */ + public function testGetTitle() + { + $title = 'test02'; + $this->blockModel->setData(Block::TITLE, $title); + $expected = $title; + $actual = $this->blockModel->getTitle(); + self::assertEquals($expected, $actual); + } + + /** + * Test getContent method + * + * @return void + */ + public function testGetContent() + { + $content = 'test03'; + $this->blockModel->setData(Block::CONTENT, $content); + $expected = $content; + $actual = $this->blockModel->getContent(); + self::assertEquals($expected, $actual); + } + + /** + * Test getCreationTime method + * + * @return void + */ + public function testGetCreationTime() + { + $creationTime = 'test04'; + $this->blockModel->setData(Block::CREATION_TIME, $creationTime); + $expected = $creationTime; + $actual = $this->blockModel->getCreationTime(); + self::assertEquals($expected, $actual); + } + + /** + * Test getUpdateTime method + * + * @return void + */ + public function testGetUpdateTime() + { + $updateTime = 'test05'; + $this->blockModel->setData(Block::UPDATE_TIME, $updateTime); + $expected = $updateTime; + $actual = $this->blockModel->getUpdateTime(); + self::assertEquals($expected, $actual); + } + + /** + * Test isActive method + * + * @return void + */ + public function testIsActive() + { + $isActive = true; + $this->blockModel->setData(Block::IS_ACTIVE, $isActive); + $result = $this->blockModel->isActive(); + self::assertTrue($result); + } + + /** + * Test setId method + * + * @return void + */ + public function testSetId() + { + $blockId = 15; + $this->blockModel->setId($blockId); + $expected = $blockId; + $actual = $this->blockModel->getData(Block::BLOCK_ID); + self::assertEquals($expected, $actual); + } + + /** + * Test setIdentifier method + * + * @return void + */ + public function testSetIdentifier() + { + $identifier = 'test06'; + $this->blockModel->setIdentifier($identifier); + $expected = $identifier; + $actual = $this->blockModel->getData(Block::IDENTIFIER); + self::assertEquals($expected, $actual); + } + + /** + * Test setTitle method + * + * @return void + */ + public function testSetTitle() + { + $title = 'test07'; + $this->blockModel->setTitle($title); + $expected = $title; + $actual = $this->blockModel->getData(Block::TITLE); + self::assertEquals($expected, $actual); + } + + /** + * Test setContent method + * + * @return void + */ + public function testSetContent() + { + $content = 'test08'; + $this->blockModel->setContent($content); + $expected = $content; + $actual = $this->blockModel->getData(Block::CONTENT); + self::assertEquals($expected, $actual); + } + + /** + * Test setCreationTime method + * + * @return void + */ + public function testSetCreationTime() + { + $creationTime = 'test09'; + $this->blockModel->setCreationTime($creationTime); + $expected = $creationTime; + $actual = $this->blockModel->getData(Block::CREATION_TIME); + self::assertEquals($expected, $actual); + } + + /** + * Test setUpdateTime method + * + * @return void + */ + public function testSetUpdateTime() + { + $updateTime = 'test10'; + $this->blockModel->setUpdateTime($updateTime); + $expected = $updateTime; + $actual = $this->blockModel->getData(Block::UPDATE_TIME); + self::assertEquals($expected, $actual); + } + + /** + * Test setIsActive method + * + * @return void + */ + public function testSetIsActive() + { + $this->blockModel->setIsActive(false); + $result = $this->blockModel->getData(Block::IS_ACTIVE); + self::assertFalse($result); + } + + /** + * Test getStores method + * + * @return void + */ + public function testGetStores() + { + $stores = [1, 4, 9]; + $this->blockModel->setData('stores', $stores); + $expected = $stores; + $actual = $this->blockModel->getStores(); + self::assertEquals($expected, $actual); + } + + /** + * Test getAvailableStatuses method + * + * @return void + */ + public function testGetAvailableStatuses() + { + $result = $this->blockModel->getAvailableStatuses(); + self::assertInternalType('array', $result); + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/Config/Source/BlockTest.php b/app/code/Magento/Cms/Test/Unit/Model/Config/Source/BlockTest.php new file mode 100644 index 0000000000000..7d9162bd56776 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/Config/Source/BlockTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Cms\Test\Unit\Model\Config\Source; + +/** + * Class BlockTest + */ +class BlockTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Cms\Model\ResourceModel\Block\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $collectionFactory; + + /** + * @var \Magento\Cms\Model\Config\Source\Block + */ + protected $block; + + /** + * Set up + * + * @return void + */ + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->collectionFactory = $this->createPartialMock( + \Magento\Cms\Model\ResourceModel\Block\CollectionFactory::class, + ['create'] + ); + + $this->block = $objectManager->getObject( + \Magento\Cms\Model\Config\Source\Block::class, + [ + 'collectionFactory' => $this->collectionFactory, + ] + ); + } + + /** + * Run test toOptionArray method + * + * @return void + */ + public function testToOptionArray() + { + $blockCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Block\Collection::class); + + $this->collectionFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue($blockCollectionMock)); + + $blockCollectionMock->expects($this->once()) + ->method('toOptionIdArray') + ->will($this->returnValue('return-value')); + + $this->assertEquals('return-value', $this->block->toOptionArray()); + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/GetUtilityPageIdentifiersTest.php b/app/code/Magento/Cms/Test/Unit/Model/GetUtilityPageIdentifiersTest.php new file mode 100644 index 0000000000000..0cf923345c083 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/GetUtilityPageIdentifiersTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model; + +use Magento\Cms\Model\GetUtilityPageIdentifiers; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * @covers \Magento\Cms\Model\GetUtilityPageIdentifiers + */ +class GetUtilityPageIdentifiersTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var GetUtilityPageIdentifiers + */ + private $getUtilityPageIdentifiers; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->getUtilityPageIdentifiers = new GetUtilityPageIdentifiers($this->scopeConfigMock); + } + + /** + * Test execute method + * + * @return void + */ + public function testExecute() + { + $homePageIdentifier = 'home'; + $noRouteIdentifier = 'no_route'; + $noCookieIdentifier = 'no_cookie'; + + $this->scopeConfigMock->expects($this->exactly(3))->method('getValue')->willReturnMap([ + ['web/default/cms_home_page', ScopeInterface::SCOPE_STORE, null, $homePageIdentifier], + ['web/default/cms_no_route', ScopeInterface::SCOPE_STORE, null, $noRouteIdentifier], + ['web/default/cms_no_cookies', ScopeInterface::SCOPE_STORE, null, $noCookieIdentifier], + ]); + + $expected = [$homePageIdentifier, $noRouteIdentifier, $noCookieIdentifier]; + $actual = $this->getUtilityPageIdentifiers->execute(); + self::assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php new file mode 100644 index 0000000000000..f73396230a669 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\PageRepository; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\PageRepository\ValidationComposite; +use Magento\Cms\Model\PageRepository\ValidatorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Exception\LocalizedException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Validate behavior of the validation composite + */ +class ValidationCompositeTest extends TestCase +{ + /** + * @var PageRepositoryInterface|MockObject + */ + private $subject; + + protected function setUp() + { + /** @var PageRepositoryInterface subject */ + $this->subject = $this->createMock(PageRepositoryInterface::class); + } + + /** + * @param $validators + * @expectedException \InvalidArgumentException + * @dataProvider constructorArgumentProvider + */ + public function testConstructorValidation($validators) + { + new ValidationComposite($this->subject, $validators); + } + + public function testSaveInvokesValidatorsWithSucess() + { + $validator1 = $this->createMock(ValidatorInterface::class); + $validator2 = $this->createMock(ValidatorInterface::class); + $page = $this->createMock(PageInterface::class); + + // Assert each are called + $validator1 + ->expects($this->once()) + ->method('validate') + ->with($page); + $validator2 + ->expects($this->once()) + ->method('validate') + ->with($page); + + // Assert that the success is called + $this->subject + ->expects($this->once()) + ->method('save') + ->with($page) + ->willReturn('foo'); + + $composite = new ValidationComposite($this->subject, [$validator1, $validator2]); + $result = $composite->save($page); + + self::assertSame('foo', $result); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Oh no. That isn't right. + */ + public function testSaveInvokesValidatorsWithErrors() + { + $validator1 = $this->createMock(ValidatorInterface::class); + $validator2 = $this->createMock(ValidatorInterface::class); + $page = $this->createMock(PageInterface::class); + + // Assert the first is called + $validator1 + ->expects($this->once()) + ->method('validate') + ->with($page) + ->willThrowException(new LocalizedException(__('Oh no. That isn\'t right.'))); + + // Assert the second is NOT called + $validator2 + ->expects($this->never()) + ->method('validate'); + + // Assert that the success is NOT called + $this->subject + ->expects($this->never()) + ->method('save'); + + $composite = new ValidationComposite($this->subject, [$validator1, $validator2]); + $composite->save($page); + } + + /** + * @param $method + * @param $arg + * @dataProvider passthroughMethodDataProvider + */ + public function testPassthroughMethods($method, $arg) + { + $this->subject + ->method($method) + ->with($arg) + ->willReturn('foo'); + + $composite = new ValidationComposite($this->subject, []); + $result = $composite->{$method}($arg); + + self::assertSame('foo', $result); + } + + public function constructorArgumentProvider() + { + return [ + [[null], false], + [[''], false], + [['foo'], false], + [[new \stdClass()], false], + [[$this->createMock(ValidatorInterface::class), 'foo'], false], + ]; + } + + public function passthroughMethodDataProvider() + { + return [ + ['save', $this->createMock(PageInterface::class)], + ['getById', 1], + ['getList', $this->createMock(SearchCriteriaInterface::class)], + ['delete', $this->createMock(PageInterface::class)], + ['deleteById', 1], + ]; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php new file mode 100644 index 0000000000000..487a90bb9a185 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/Validator/LayoutUpdateValidatorTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\PageRepository\Validator; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator; +use Magento\Framework\Config\Dom\ValidationException; +use Magento\Framework\Config\Dom\ValidationSchemaException; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Model\Layout\Update\ValidatorFactory; +use Magento\Framework\View\Model\Layout\Update\Validator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test cases for the layout update validator + */ +class LayoutUpdateValidatorTest extends TestCase +{ + /** + * @var Validator|MockObject + */ + private $layoutValidator; + + /** + * @var LayoutUpdateValidator + */ + private $validator; + + protected function setUp() + { + $layoutValidatorFactory = $this->createMock(ValidatorFactory::class); + $this->layoutValidator = $this->createMock(Validator::class); + $layoutValidatorState = $this->createMock(ValidationStateInterface::class); + + $layoutValidatorFactory + ->method('create') + ->with(['validationState' => $layoutValidatorState]) + ->willReturn($this->layoutValidator); + + $this->validator = new LayoutUpdateValidator($layoutValidatorFactory, $layoutValidatorState); + } + + /** + * @dataProvider validationSetDataProvider + */ + public function testValidate($data, $expectedExceptionMessage, $layoutValidatorException, $isLayoutValid = false) + { + if ($expectedExceptionMessage) { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + } + + if ($layoutValidatorException) { + $this->layoutValidator + ->method('isValid') + ->with($data['getLayoutUpdateXml'] ?? $data['getCustomLayoutUpdateXml']) + ->willThrowException($layoutValidatorException); + } elseif (!empty($data['getLayoutUpdateXml'])) { + $this->layoutValidator + ->method('isValid') + ->with($data['getLayoutUpdateXml']) + ->willReturn($isLayoutValid); + } elseif (!empty($data['getCustomLayoutUpdateXml'])) { + $this->layoutValidator + ->method('isValid') + ->with($data['getCustomLayoutUpdateXml']) + ->willReturn($isLayoutValid); + } + + $page = $this->createMock(PageInterface::class); + foreach ($data as $method => $value) { + $page + ->method($method) + ->willReturn($value); + } + + self::assertNull($this->validator->validate($page)); + } + + public function validationSetDataProvider() + { + $layoutError = 'Layout update is invalid'; + $customLayoutError = 'Custom layout update is invalid'; + $validationException = new ValidationException('Invalid format'); + $schemaException = new ValidationSchemaException(__('Invalid format')); + + return [ + [['getTitle' => ''], 'Required field "title" is empty.', null], + [['getTitle' => null], 'Required field "title" is empty.', null], + [['getTitle' => false], 'Required field "title" is empty.', null], + [['getTitle' => 0], 'Required field "title" is empty.', null], + [['getTitle' => '0'], 'Required field "title" is empty.', null], + [['getTitle' => []], 'Required field "title" is empty.', null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => ''], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => null], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => false], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 0], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => '0'], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => []], null, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, null], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $validationException], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], $layoutError, $schemaException], + [['getTitle' => 'foo', 'getLayoutUpdateXml' => 'foo'], null, null, true], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => ''], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => null], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => false], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 0], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => '0'], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => []], null, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, null], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $validationException], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], $customLayoutError, $schemaException], + [['getTitle' => 'foo', 'getCustomLayoutUpdateXml' => 'foo'], null, null, true], + ]; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php deleted file mode 100644 index 61001794e2a0b..0000000000000 --- a/app/code/Magento/Cms/Test/Unit/Model/PageRepositoryTest.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Cms\Test\Unit\Model; - -use Magento\Cms\Model\PageRepository; -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; - -/** - * Test for Magento\Cms\Model\PageRepository - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class PageRepositoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var PageRepository - */ - protected $repository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\ResourceModel\Page - */ - protected $pageResource; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\Page - */ - protected $page; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Api\Data\PageInterface - */ - protected $pageData; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Api\Data\PageSearchResultsInterface - */ - protected $pageSearchResult; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Api\DataObjectHelper - */ - protected $dataHelper; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Reflection\DataObjectProcessor - */ - protected $dataObjectProcessor; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Cms\Model\ResourceModel\Page\Collection - */ - protected $collection; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface - */ - private $storeManager; - - /** - * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $collectionProcessor; - - /** - * Initialize repository - */ - protected function setUp() - { - $this->pageResource = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page::class) - ->disableOriginalConstructor() - ->getMock(); - $this->dataObjectProcessor = $this->getMockBuilder(\Magento\Framework\Reflection\DataObjectProcessor::class) - ->disableOriginalConstructor() - ->getMock(); - $pageFactory = $this->getMockBuilder(\Magento\Cms\Model\PageFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $pageDataFactory = $this->getMockBuilder(\Magento\Cms\Api\Data\PageInterfaceFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $pageSearchResultFactory = $this->getMockBuilder(\Magento\Cms\Api\Data\PageSearchResultsInterfaceFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $collectionFactory = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store->expects($this->any())->method('getId')->willReturn(0); - $this->storeManager->expects($this->any())->method('getStore')->willReturn($store); - - $this->page = $this->getMockBuilder(\Magento\Cms\Model\Page::class)->disableOriginalConstructor()->getMock(); - $this->pageData = $this->getMockBuilder(\Magento\Cms\Api\Data\PageInterface::class) - ->getMock(); - $this->pageSearchResult = $this->getMockBuilder(\Magento\Cms\Api\Data\PageSearchResultsInterface::class) - ->getMock(); - $this->collection = $this->getMockBuilder(\Magento\Cms\Model\ResourceModel\Page\Collection::class) - ->disableOriginalConstructor() - ->setMethods(['getSize', 'setCurPage', 'setPageSize', 'load', 'addOrder']) - ->getMock(); - - $pageFactory->expects($this->any()) - ->method('create') - ->willReturn($this->page); - $pageDataFactory->expects($this->any()) - ->method('create') - ->willReturn($this->pageData); - $pageSearchResultFactory->expects($this->any()) - ->method('create') - ->willReturn($this->pageSearchResult); - $collectionFactory->expects($this->any()) - ->method('create') - ->willReturn($this->collection); - /** - * @var \Magento\Cms\Model\PageFactory $pageFactory - * @var \Magento\Cms\Api\Data\PageInterfaceFactory $pageDataFactory - * @var \Magento\Cms\Api\Data\PageSearchResultsInterfaceFactory $pageSearchResultFactory - * @var \Magento\Cms\Model\ResourceModel\Page\CollectionFactory $collectionFactory - */ - - $this->dataHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) - ->getMockForAbstractClass(); - - $this->repository = new PageRepository( - $this->pageResource, - $pageFactory, - $pageDataFactory, - $collectionFactory, - $pageSearchResultFactory, - $this->dataHelper, - $this->dataObjectProcessor, - $this->storeManager, - $this->collectionProcessor - ); - } - - /** - * @test - */ - public function testSave() - { - $this->pageResource->expects($this->once()) - ->method('save') - ->with($this->page) - ->willReturnSelf(); - $this->assertEquals($this->page, $this->repository->save($this->page)); - } - - /** - * @test - */ - public function testDeleteById() - { - $pageId = '123'; - - $this->page->expects($this->once()) - ->method('getId') - ->willReturn(true); - $this->page->expects($this->once()) - ->method('load') - ->with($pageId) - ->willReturnSelf(); - $this->pageResource->expects($this->once()) - ->method('delete') - ->with($this->page) - ->willReturnSelf(); - - $this->assertTrue($this->repository->deleteById($pageId)); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\CouldNotSaveException - */ - public function testSaveException() - { - $this->pageResource->expects($this->once()) - ->method('save') - ->with($this->page) - ->willThrowException(new \Exception()); - $this->repository->save($this->page); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\CouldNotDeleteException - */ - public function testDeleteException() - { - $this->pageResource->expects($this->once()) - ->method('delete') - ->with($this->page) - ->willThrowException(new \Exception()); - $this->repository->delete($this->page); - } - - /** - * @test - * - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - */ - public function testGetByIdException() - { - $pageId = '123'; - - $this->page->expects($this->once()) - ->method('getId') - ->willReturn(false); - $this->page->expects($this->once()) - ->method('load') - ->with($pageId) - ->willReturnSelf(); - $this->repository->getById($pageId); - } - - /** - * @test - */ - public function testGetList() - { - $total = 10; - - /** @var \Magento\Framework\Api\SearchCriteriaInterface $criteria */ - $criteria = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteriaInterface::class)->getMock(); - - $this->collection->addItem($this->page); - $this->collection->expects($this->once()) - ->method('getSize') - ->willReturn($total); - - $this->collectionProcessor->expects($this->once()) - ->method('process') - ->with($criteria, $this->collection) - ->willReturnSelf(); - - $this->pageSearchResult->expects($this->once()) - ->method('setSearchCriteria') - ->with($criteria) - ->willReturnSelf(); - $this->pageSearchResult->expects($this->once()) - ->method('setTotalCount') - ->with($total) - ->willReturnSelf(); - $this->pageSearchResult->expects($this->once()) - ->method('setItems') - ->with([$this->page]) - ->willReturnSelf(); - $this->assertEquals($this->pageSearchResult, $this->repository->getList($criteria)); - } -} diff --git a/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php b/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php new file mode 100644 index 0000000000000..3e8d811623616 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/Plugin/ProductTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\Plugin; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Cms\Model\Page; +use Magento\Cms\Model\Plugin\Product; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Product plugin test + */ +class ProductTest extends TestCase +{ + /** + * @var Product + */ + private $plugin; + + /** + * @var MockObject|CatalogProduct + */ + private $product; + + /** + * @var MockObject|Page + */ + private $page; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->product = $this->getMockBuilder(CatalogProduct::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId', 'getOrigData', 'getData', 'getCategoryIds']) + ->getMock(); + + $this->page = $this->getMockBuilder(Page::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'load']) + ->getMock(); + + $this->plugin = $objectManager->getObject( + Product::class, + [ + 'page' => $this->page + ] + ); + } + + public function testAfterGetIdentities() + { + $baseIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + ]; + $id = 12345; + $pageId = 1; + $expectedIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + Page::CACHE_TAG . '_' . $pageId, + ]; + + $this->product->method('getEntityId') + ->willReturn($id); + $this->product->method('getOrigData') + ->with('status') + ->willReturn(2); + $this->product->method('getData') + ->with('status') + ->willReturn(1); + $this->page->method('getId') + ->willReturn(1); + $this->page->method('load') + ->willReturnSelf(); + + $identities = $this->plugin->afterGetIdentities($this->product, $baseIdentities); + + $this->assertEquals($expectedIdentities, $identities); + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Block/CollectionTest.php b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Block/CollectionTest.php index b9b0d6f772c62..26b5d74ffb961 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Block/CollectionTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Block/CollectionTest.php @@ -119,6 +119,9 @@ public function testAfterLoad($item, $storesData) $this->assertEquals($expectedResult[$item->getId()], $item->getStoreId()); } + /** + * @return array + */ public function getItemsDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/CollectionTest.php b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/CollectionTest.php index dd31650cb3a3a..6d45e7bf6ab1d 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/CollectionTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/CollectionTest.php @@ -119,6 +119,9 @@ public function testAfterLoad($item, $storesData) $this->assertEquals($expectedResult[$item->getId()], $item->getStoreId()); } + /** + * @return array + */ public function getItemsDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php index 009a740305a82..6bd0ffd05d137 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php @@ -27,6 +27,9 @@ class FilterTest extends \PHPUnit\Framework\TestCase */ protected $filter; + /** + * @inheritdoc + */ protected function setUp() { $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) @@ -46,6 +49,8 @@ protected function setUp() } /** + * Test processing media directives. + * * @covers \Magento\Cms\Model\Template\Filter::mediaDirective */ public function testMediaDirective() @@ -62,4 +67,27 @@ public function testMediaDirective() ->willReturn($baseMediaDir); $this->assertEquals($expectedResult, $this->filter->mediaDirective($construction)); } + + /** + * Test using media directive with relative path to image. + * + * @covers \Magento\Cms\Model\Template\Filter::mediaDirective + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Image path must be absolute + * + * @return void + */ + public function testMediaDirectiveRelativePath() + { + $baseMediaDir = 'pub/media'; + $construction = [ + '{{media url="wysiwyg/images/../image.jpg"}}', + 'media', + ' url="wysiwyg/images/../image.jpg"' + ]; + $this->storeMock->expects($this->any()) + ->method('getBaseMediaDir') + ->willReturn($baseMediaDir); + $this->filter->mediaDirective($construction); + } } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php index d633c5a21fe32..7a5a6fe7b0c8c 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php @@ -187,6 +187,9 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticDefaultUrl')); } + /** + * @return array + */ public function getConfigDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index a2178489e1298..68c2a164b9047 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -18,7 +18,7 @@ class StorageTest extends \PHPUnit\Framework\TestCase /** * Directory paths samples */ - const STORAGE_ROOT_DIR = '/storage/root/dir'; + const STORAGE_ROOT_DIR = '/storage/root/dir/'; const INVALID_DIRECTORY_OVER_ROOT = '/storage/some/another/dir'; @@ -107,6 +107,13 @@ class StorageTest extends \PHPUnit\Framework\TestCase */ protected $objectManagerHelper; + private $allowedImageExtensions = [ + 'jpg' => 'image/jpg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/png' + ]; + /** * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -114,20 +121,13 @@ class StorageTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); - $this->driverMock = $this->getMockForAbstractClass( - \Magento\Framework\Filesystem\DriverInterface::class, - [], - '', - false, - false, - true, - ['getRealPath'] - ); - $this->driverMock->expects($this->any())->method('getRealPath')->will($this->returnArgument(0)); + $this->driverMock = $this->getMockBuilder(\Magento\Framework\Filesystem\DriverInterface::class) + ->setMethods(['getRealPathSafety']) + ->getMockForAbstractClass(); $this->directoryMock = $this->createPartialMock( \Magento\Framework\Filesystem\Directory\Write::class, - ['delete', 'getDriver', 'create'] + ['delete', 'getDriver', 'create', 'getRelativePath', 'isExist', 'isFile'] ); $this->directoryMock->expects( $this->any() @@ -151,7 +151,7 @@ protected function setUp() $this->adapterFactoryMock = $this->createMock(\Magento\Framework\Image\AdapterFactory::class); $this->imageHelperMock = $this->createPartialMock( \Magento\Cms\Helper\Wysiwyg\Images::class, - ['getStorageRoot'] + ['getStorageRoot', 'getCurrentPath'] ); $this->imageHelperMock->expects( $this->any() @@ -182,12 +182,21 @@ protected function setUp() $this->uploaderFactoryMock = $this->getMockBuilder(\Magento\MediaStorage\Model\File\UploaderFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->sessionMock = $this->createMock(\Magento\Backend\Model\Session::class); + $this->sessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->setMethods( + ['getCurrentPath', 'getName', 'getSessionId', 'getCookieLifetime', 'getCookiePath', 'getCookieDomain'] + ) + ->disableOriginalConstructor() + ->getMock(); $this->backendUrlMock = $this->createMock(\Magento\Backend\Model\Url::class); $this->coreFileStorageMock = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) ->disableOriginalConstructor() ->getMock(); + $allowedExtensions = [ + 'allowed' => $this->allowedImageExtensions, + 'image_allowed' => $this->allowedImageExtensions + ]; $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -210,7 +219,8 @@ protected function setUp() 'dirs' => [ 'exclude' => [], 'include' => [] - ] + ], + 'extensions' => $allowedExtensions ] ); } @@ -240,6 +250,7 @@ public function testDeleteDirectoryOverRoot() \Magento\Framework\Exception\LocalizedException::class, sprintf('Directory %s is not under storage root path.', self::INVALID_DIRECTORY_OVER_ROOT) ); + $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->will($this->returnArgument(0)); $this->imagesStorage->deleteDirectory(self::INVALID_DIRECTORY_OVER_ROOT); } @@ -252,6 +263,7 @@ public function testDeleteRootDirectory() \Magento\Framework\Exception\LocalizedException::class, sprintf('We can\'t delete root directory %s right now.', self::STORAGE_ROOT_DIR) ); + $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->will($this->returnArgument(0)); $this->imagesStorage->deleteDirectory(self::STORAGE_ROOT_DIR); } @@ -402,6 +414,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); @@ -415,4 +431,77 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e $this->imagesStorage->getDirsCollection($path); } + + public function testUploadFile() + { + $path = 'target/path'; + $targetPath = self::STORAGE_ROOT_DIR . $path; + $fileName = 'image.gif'; + $realPath = $targetPath . '/' . $fileName; + $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path; + $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; + $type = 'image'; + $result = [ + 'result' + ]; + $uploader = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Uploader::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'setAllowedExtensions', + 'setAllowRenameFiles', + 'setFilesDispersion', + 'checkMimeType', + 'save', + 'getUploadedFileName' + ] + ) + ->getMock(); + $this->uploaderFactoryMock->expects($this->atLeastOnce())->method('create')->with(['fileId' => 'image']) + ->willReturn($uploader); + $uploader->expects($this->atLeastOnce())->method('setAllowedExtensions') + ->with(array_keys($this->allowedImageExtensions))->willReturnSelf(); + $uploader->expects($this->atLeastOnce())->method('setAllowRenameFiles')->with(true)->willReturnSelf(); + $uploader->expects($this->atLeastOnce())->method('setFilesDispersion')->with(false) + ->willReturnSelf(); + $uploader->expects($this->atLeastOnce())->method('checkMimeType') + ->with(array_values($this->allowedImageExtensions))->willReturnSelf(); + $uploader->expects($this->atLeastOnce())->method('save')->with($targetPath)->willReturn($result); + $uploader->expects($this->atLeastOnce())->method('getUploadedFileName')->willReturn($fileName); + + $this->directoryMock->expects($this->atLeastOnce())->method('getRelativePath')->willReturnMap( + [ + [$realPath, $realPath], + [$thumbnailTargetPath, $thumbnailTargetPath], + [$thumbnailDestination, $thumbnailDestination] + ] + ); + $this->directoryMock->expects($this->atLeastOnce())->method('isFile') + ->willReturnMap( + [ + [$realPath, true], + [$thumbnailDestination, true] + ] + ); + $this->directoryMock->expects($this->atLeastOnce())->method('isExist') + ->willReturnMap( + [ + [$realPath, true], + [$thumbnailTargetPath, true] + ] + ); + + $image = $this->getMockBuilder(\Magento\Catalog\Model\Product\Image::class) + ->disableOriginalConstructor() + ->setMethods(['open', 'keepAspectRatio', 'resize', 'save']) + ->getMock(); + $image->expects($this->atLeastOnce())->method('open')->with($realPath); + $image->expects($this->atLeastOnce())->method('keepAspectRatio')->with(true); + $image->expects($this->atLeastOnce())->method('resize')->with(100, 50); + $image->expects($this->atLeastOnce())->method('save')->with($thumbnailDestination); + + $this->adapterFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($image); + + $this->assertEquals($result, $this->imagesStorage->uploadFile($targetPath, $type)); + } } diff --git a/app/code/Magento/Cms/Test/Unit/Observer/NoCookiesObserverTest.php b/app/code/Magento/Cms/Test/Unit/Observer/NoCookiesObserverTest.php index 8c09d42ec556e..cbb13c6f254eb 100644 --- a/app/code/Magento/Cms/Test/Unit/Observer/NoCookiesObserverTest.php +++ b/app/code/Magento/Cms/Test/Unit/Observer/NoCookiesObserverTest.php @@ -139,6 +139,9 @@ public function testNoCookies($pageUrl) $this->assertEquals($this->noCookiesObserver, $this->noCookiesObserver->execute($this->observerMock)); } + /** + * @return array + */ public function noCookiesDataProvider() { return [ diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php index 3dcf6c4a3fce0..4ffe4a6ad8774 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php @@ -14,7 +14,7 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * BlockActionsTest contains unit tests for \Magento\Cms\Ui\Component\Listing\Column\BlockActions class + * BlockActionsTest contains unit tests for \Magento\Cms\Ui\Component\Listing\Column\BlockActions class. */ class BlockActionsTest extends \PHPUnit\Framework\TestCase { @@ -33,6 +33,9 @@ class BlockActionsTest extends \PHPUnit\Framework\TestCase */ private $urlBuilder; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); @@ -42,7 +45,7 @@ protected function setUp() $processor = $this->getMockBuilder(Processor::class) ->disableOriginalConstructor() ->getMock(); - $context->expects(static::never()) + $context->expects($this->never()) ->method('getProcessor') ->willReturn($processor); @@ -50,19 +53,25 @@ protected function setUp() $this->escaper = $this->getMockBuilder(Escaper::class) ->disableOriginalConstructor() - ->setMethods(['escapeHtml']) + ->setMethods(['escapeHtmlAttr']) ->getMock(); - $this->blockActions = $objectManager->getObject(BlockActions::class, [ - 'context' => $context, - 'urlBuilder' => $this->urlBuilder - ]); + $this->blockActions = $objectManager->getObject( + BlockActions::class, + [ + 'context' => $context, + 'urlBuilder' => $this->urlBuilder + ] + ); $objectManager->setBackwardCompatibleProperty($this->blockActions, 'escaper', $this->escaper); } /** + * Unit test for prepareDataSource method. + * * @covers \Magento\Cms\Ui\Component\Listing\Column\BlockActions::prepareDataSource + * @return void */ public function testPrepareDataSource() { @@ -73,10 +82,10 @@ public function testPrepareDataSource() 'items' => [ [ 'block_id' => $blockId, - 'title' => $title - ] - ] - ] + 'title' => $title, + ], + ], + ], ]; $name = 'item_name'; $expectedItems = [ @@ -87,39 +96,42 @@ public function testPrepareDataSource() 'edit' => [ 'href' => 'test/url/edit', 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => 'test/url/delete', 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), ], - ] + 'post' => true, + '__disableTmpl' => true, + ], ], - ] + ], ]; - $this->escaper->expects(static::once()) - ->method('escapeHtml') + $this->escaper->expects($this->once()) + ->method('escapeHtmlAttr') ->with($title) ->willReturn($title); - $this->urlBuilder->expects(static::exactly(2)) + $this->urlBuilder->expects($this->exactly(2)) ->method('getUrl') ->willReturnMap( [ [ BlockActions::URL_PATH_EDIT, [ - 'block_id' => $blockId + 'block_id' => $blockId, ], 'test/url/edit', ], [ BlockActions::URL_PATH_DELETE, [ - 'block_id' => $blockId + 'block_id' => $blockId, ], 'test/url/delete', ], @@ -129,6 +141,6 @@ public function testPrepareDataSource() $this->blockActions->setData('name', $name); $actual = $this->blockActions->prepareDataSource($items); - static::assertEquals($expectedItems, $actual['data']['items']); + $this->assertEquals($expectedItems, $actual['data']['items']); } } diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/Cms/OptionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/Cms/OptionsTest.php index e681464349d57..ffc4c8042bd83 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/Cms/OptionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/Cms/OptionsTest.php @@ -71,7 +71,7 @@ public function testToOptionArray() $expectedOptions = [ [ 'label' => __('All Store Views'), - 'value' => '0' + 'value' => '0', ], [ 'label' => 'Main Website', @@ -81,11 +81,14 @@ public function testToOptionArray() 'value' => [ [ 'label' => ' Default Store View', - 'value' => '1' + 'value' => '1', + '__disableTmpl' => true, ] - ] + ], + '__disableTmpl' => true, ] - ] + ], + '__disableTmpl' => true, ] ]; @@ -104,14 +107,6 @@ public function testToOptionArray() $this->storeMock->expects($this->atLeastOnce())->method('getName')->willReturn('Default Store View'); $this->storeMock->expects($this->atLeastOnce())->method('getId')->willReturn('1'); - $this->escaperMock->expects($this->atLeastOnce())->method('escapeHtml')->willReturnMap( - [ - ['Default Store View', null, 'Default Store View'], - ['Main Website Store', null, 'Main Website Store'], - ['Main Website', null, 'Main Website'] - ] - ); - $this->assertEquals($expectedOptions, $this->options->toOptionArray()); } } diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index b0cc1bf061a48..2b6c106dfc7c1 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -8,6 +8,9 @@ use Magento\Cms\Ui\Component\Listing\Column\PageActions; use Magento\Framework\Escaper; +/** + * Test for Magento\Cms\Ui\Component\Listing\Column\PageActions class. + */ class PageActionsTest extends \PHPUnit\Framework\TestCase { public function testPrepareItemsByPageId() @@ -62,15 +65,19 @@ public function testPrepareItemsByPageId() 'edit' => [ 'href' => 'test/url/edit', 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => 'test/url/delete', 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), + '__disableTmpl' => true, ], - ] + 'post' => true, + '__disableTmpl' => true, + ], ], ] ]; @@ -79,7 +86,6 @@ public function testPrepareItemsByPageId() ->method('escapeHtml') ->with($title) ->willReturn($title); - // Configure mocks and object data $urlBuilderMock->expects($this->any()) ->method('getUrl') @@ -101,7 +107,6 @@ public function testPrepareItemsByPageId() ], ] ); - $model->setName($name); $items = $model->prepareDataSource($items); // Run test diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php index 54e0e17ab7ad6..ec9cb86c6c9dc 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php @@ -118,11 +118,12 @@ public function testPrepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] - ] - ] - ] - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME, + ], + ], + ], + ], ]; $this->assertEquals( diff --git a/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php b/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php new file mode 100644 index 0000000000000..406b40fbc1647 --- /dev/null +++ b/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Ui\Component; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; + +/** + * Provides extension point to add additional filters to search criteria. + */ +interface AddFilterInterface +{ + /** + * Adds custom filter to search criteria builder based on received filter. + * + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Filter $filter + * @return void + */ + public function addFilter(SearchCriteriaBuilder $searchCriteriaBuilder, Filter $filter); +} diff --git a/app/code/Magento/Cms/Ui/Component/DataProvider.php b/app/code/Magento/Cms/Ui/Component/DataProvider.php index 3298d66b0b877..a0f68f8dde05a 100644 --- a/app/code/Magento/Cms/Ui/Component/DataProvider.php +++ b/app/code/Magento/Cms/Ui/Component/DataProvider.php @@ -5,6 +5,7 @@ */ namespace Magento\Cms\Ui\Component; +use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\App\ObjectManager; @@ -12,6 +13,9 @@ use Magento\Framework\AuthorizationInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\Reporting; +/** + * DataProvider for cms ui. + */ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider { /** @@ -19,6 +23,11 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi */ private $authorization; + /** + * @var AddFilterInterface[] + */ + private $additionalFilterPool; + /** * @param string $name * @param string $primaryFieldName @@ -29,6 +38,8 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi * @param FilterBuilder $filterBuilder * @param array $meta * @param array $data + * @param array $additionalFilterPool + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -39,7 +50,8 @@ public function __construct( RequestInterface $request, FilterBuilder $filterBuilder, array $meta = [], - array $data = [] + array $data = [], + array $additionalFilterPool = [] ) { parent::__construct( $name, @@ -54,9 +66,12 @@ public function __construct( ); $this->meta = array_replace_recursive($meta, $this->prepareMetadata()); + $this->additionalFilterPool = $additionalFilterPool; } /** + * Get authorization info. + * * @deprecated 101.0.7 * @return AuthorizationInterface|mixed */ @@ -85,14 +100,27 @@ public function prepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] - ] - ] - ] - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME, + ], + ], + ], + ], ]; } return $metadata; } + + /** + * @inheritdoc + */ + public function addFilter(Filter $filter) + { + if (!empty($this->additionalFilterPool[$filter->getField()])) { + $this->additionalFilterPool[$filter->getField()]->addFilter($this->searchCriteriaBuilder, $filter); + } else { + parent::addFilter($filter); + } + } } diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php index 60b9f34d29ae6..65940c5d7b4f9 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -13,7 +13,7 @@ use Magento\Framework\Escaper; /** - * Class BlockActions + * Class to build edit and delete link for each item. */ class BlockActions extends Column { @@ -35,8 +35,6 @@ class BlockActions extends Column private $escaper; /** - * Constructor - * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param UrlInterface $urlBuilder @@ -55,40 +53,40 @@ public function __construct( } /** - * Prepare Data Source - * - * @param array $dataSource - * @return array + * @inheritDoc */ public function prepareDataSource(array $dataSource) { if (isset($dataSource['data']['items'])) { foreach ($dataSource['data']['items'] as & $item) { if (isset($item['block_id'])) { - $title = $this->getEscaper()->escapeHtml($item['title']); + $title = $this->getEscaper()->escapeHtmlAttr($item['title']); $item[$this->getData('name')] = [ 'edit' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_EDIT, [ - 'block_id' => $item['block_id'] + 'block_id' => $item['block_id'], ] ), - 'label' => __('Edit') + 'label' => __('Edit'), + '__disableTmpl' => true, ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, [ - 'block_id' => $item['block_id'] + 'block_id' => $item['block_id'], ] ), 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] - ] + 'message' => __('Are you sure you want to delete a %1 record?', $title), + ], + 'post' => true, + '__disableTmpl' => true, + ], ]; } } @@ -99,6 +97,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index ea6882e21c85f..bf5f8d6714ad7 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -80,7 +80,8 @@ public function prepareDataSource(array $dataSource) if (isset($item['page_id'])) { $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl($this->editUrl, ['page_id' => $item['page_id']]), - 'label' => __('Edit') + 'label' => __('Edit'), + '__disableTmpl' => true, ]; $title = $this->getEscaper()->escapeHtml($item['title']); $item[$name]['delete'] = [ @@ -88,8 +89,11 @@ public function prepareDataSource(array $dataSource) 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + 'message' => __('Are you sure you want to delete a %1 record?', $title), + '__disableTmpl' => true, + ], + 'post' => true, + '__disableTmpl' => true, ]; } if (isset($item['identifier'])) { @@ -99,7 +103,8 @@ public function prepareDataSource(array $dataSource) isset($item['_first_store_id']) ? $item['_first_store_id'] : null, isset($item['store_code']) ? $item['store_code'] : null ), - 'label' => __('View') + 'label' => __('View'), + '__disableTmpl' => true, ]; } } @@ -110,6 +115,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php b/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php new file mode 100644 index 0000000000000..9b0c69a4f10c4 --- /dev/null +++ b/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Ui\Component\Page; + +use Magento\Cms\Ui\Component\AddFilterInterface; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; + +/** + * Adds fulltext filter for CMS Page title attribute. + */ +class FulltextFilter implements AddFilterInterface +{ + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @param FilterBuilder $filterBuilder + */ + public function __construct(FilterBuilder $filterBuilder) + { + $this->filterBuilder = $filterBuilder; + } + + /** + * @inheritdoc + */ + public function addFilter(SearchCriteriaBuilder $searchCriteriaBuilder, Filter $filter) + { + $titleFilter = $this->filterBuilder->setField('title') + ->setValue(sprintf('%%%s%%', $filter->getValue())) + ->setConditionType('like') + ->create(); + $searchCriteriaBuilder->addFilter($titleFilter); + } +} diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index d3ccb07c8e8d3..e12ed0971b2c9 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-cms", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-theme": "100.2.*", "magento/module-widget": "101.0.*", @@ -18,7 +18,7 @@ "magento/module-cms-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "102.0.2", + "version": "102.0.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Cms/etc/acl.xml b/app/code/Magento/Cms/etc/acl.xml index b13b58b101f90..3df31923d1eb6 100644 --- a/app/code/Magento/Cms/etc/acl.xml +++ b/app/code/Magento/Cms/etc/acl.xml @@ -12,7 +12,9 @@ <resource id="Magento_Backend::content"> <resource id="Magento_Backend::content_elements"> <resource id="Magento_Cms::page" title="Pages" translate="title" sortOrder="10"> - <resource id="Magento_Cms::save" title="Save Page" translate="title" sortOrder="10" /> + <resource id="Magento_Cms::save" title="Save Page" translate="title" sortOrder="10"> + <resource id="Magento_Cms::save_design" title="Edit Page Design" translate="title" /> + </resource> <resource id="Magento_Cms::page_delete" title="Delete Page" translate="title" sortOrder="20" /> </resource> <resource id="Magento_Cms::block" title="Blocks" translate="title" sortOrder="30" /> diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 8309e3b5b6150..c682a05f6a9f7 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -7,13 +7,14 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Cms\Api\Data\PageSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\PageSearchResults" /> <preference for="Magento\Cms\Api\Data\BlockSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\BlockSearchResults" /> <preference for="Magento\Cms\Api\Data\PageInterface" type="Magento\Cms\Model\Page" /> <preference for="Magento\Cms\Api\Data\BlockInterface" type="Magento\Cms\Model\Block" /> <preference for="Magento\Cms\Api\BlockRepositoryInterface" type="Magento\Cms\Model\BlockRepository" /> - <preference for="Magento\Cms\Api\PageRepositoryInterface" type="Magento\Cms\Model\PageRepository" /> + <preference for="Magento\Cms\Api\PageRepositoryInterface" type="Magento\Cms\Model\PageRepository\ValidationComposite" /> + <preference for="Magento\Cms\Api\GetUtilityPageIdentifiersInterface" type="Magento\Cms\Model\GetUtilityPageIdentifiers" /> <type name="Magento\Cms\Model\Wysiwyg\Config"> <arguments> <argument name="windowSize" xsi:type="array"> @@ -30,29 +31,61 @@ </argument> <argument name="extensions" xsi:type="array"> <item name="allowed" xsi:type="array"> - <item name="jpg" xsi:type="number">1</item> - <item name="jpeg" xsi:type="number">1</item> - <item name="png" xsi:type="number">1</item> - <item name="gif" xsi:type="number">1</item> + <item name="jpg" xsi:type="string">image/jpg</item> + <item name="jpeg" xsi:type="string">image/jpeg</item> + <item name="png" xsi:type="string">image/png</item> + <item name="gif" xsi:type="string">image/gif</item> </item> <item name="image_allowed" xsi:type="array"> - <item name="jpg" xsi:type="number">1</item> - <item name="jpeg" xsi:type="number">1</item> - <item name="png" xsi:type="number">1</item> - <item name="gif" xsi:type="number">1</item> + <item name="jpg" xsi:type="string">image/jpg</item> + <item name="jpeg" xsi:type="string">image/jpeg</item> + <item name="png" xsi:type="string">image/png</item> + <item name="gif" xsi:type="string">image/gif</item> </item> <item name="media_allowed" xsi:type="array"> - <item name="flv" xsi:type="number">1</item> - <item name="swf" xsi:type="number">1</item> - <item name="avi" xsi:type="number">1</item> - <item name="mov" xsi:type="number">1</item> - <item name="rm" xsi:type="number">1</item> - <item name="wmv" xsi:type="number">1</item> + <item name="flv" xsi:type="string">video/x-flv</item> + <item name="avi" xsi:type="string">video/x-msvideo</item> + <item name="mov" xsi:type="string">video/x-sgi-movie</item> + <item name="rm" xsi:type="string">application/vnd.rn-realmedia</item> + <item name="wmv" xsi:type="string">video/x-ms-wmv</item> </item> </argument> <argument name="dirs" xsi:type="array"> - <item name="exclude" xsi:type="string"/> - <item name="include" xsi:type="string"/> + <item name="exclude" xsi:type="array"> + <item name="captcha" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+captcha[/\\]*$</item> + </item> + <item name="catalog/product" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$</item> + </item> + <item name="customer" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+customer[/\\]*$</item> + </item> + <item name="downloadable" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+downloadable[/\\]*$</item> + </item> + <item name="import" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+import[/\\]*$</item> + </item> + <item name="theme" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme[/\\]*$</item> + </item> + <item name="theme_customization" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme_customization[/\\]*$</item> + </item> + <item name="tmp" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">pub[/\\]+media[/\\]+tmp[/\\]*$</item> + </item> + </item> + <item name="include" xsi:type="array"/> </argument> </arguments> </type> @@ -188,4 +221,31 @@ <argument name="collectionProcessor" xsi:type="object">Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor</argument> </arguments> </type> + + <type name="Magento\Cms\Ui\Component\DataProvider"> + <arguments> + <argument name="additionalFilterPool" xsi:type="array"> + <item name="fulltext" xsi:type="object">Magento\Cms\Ui\Component\Page\FulltextFilter</item> + </argument> + </arguments> + </type> + + <type name="Magento\Cms\Model\PageRepository\ValidationComposite"> + <arguments> + <argument name="repository" xsi:type="object">Magento\Cms\Model\PageRepository</argument> + <argument name="validators" xsi:type="array"> + <item name="layout_update" xsi:type="object">Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator</item> + </argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="cms" type="Magento\Cms\Model\Plugin\Product" sortOrder="100"/> + </type> + <preference for="Magento\Cms\Model\Page\CustomLayoutManagerInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager" /> + <type name="Magento\Cms\Model\Page\CustomLayout\CustomLayoutManager"> + <arguments> + <argument name="themeFactory" xsi:type="object">Magento\Framework\View\Design\Theme\FlyweightFactory\Proxy</argument> + </arguments> + </type> + <preference for="Magento\Cms\Model\Page\CustomLayoutRepositoryInterface" type="Magento\Cms\Model\Page\CustomLayout\CustomLayoutRepository" /> </config> diff --git a/app/code/Magento/Cms/etc/events.xml b/app/code/Magento/Cms/etc/events.xml index d6b9ad4ee0248..1ad847e215ccc 100644 --- a/app/code/Magento/Cms/etc/events.xml +++ b/app/code/Magento/Cms/etc/events.xml @@ -36,4 +36,7 @@ <event name="magento_cms_api_data_pageinterface_load_after"> <observer name="legacy_model_cms_page_after_load" instance="Magento\Framework\EntityManager\Observer\AfterEntityLoad" /> </event> + <event name="cms_page_prepare_save"> + <observer name="validate_cms_page" instance="Magento\Cms\Observer\PageValidatorObserver" /> + </event> </config> diff --git a/app/code/Magento/Cms/etc/webapi_rest/di.xml b/app/code/Magento/Cms/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..686305f2ed300 --- /dev/null +++ b/app/code/Magento/Cms/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Cms\Api\PageRepositoryInterface"> + <plugin name="aclCheck" type="Magento\Cms\Observer\PageAclPlugin" sortOrder="100" /> + </type> +</config> diff --git a/app/code/Magento/Cms/etc/webapi_soap/di.xml b/app/code/Magento/Cms/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..686305f2ed300 --- /dev/null +++ b/app/code/Magento/Cms/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Cms\Api\PageRepositoryInterface"> + <plugin name="aclCheck" type="Magento\Cms\Observer\PageAclPlugin" sortOrder="100" /> + </type> +</config> diff --git a/app/code/Magento/Cms/i18n/en_US.csv b/app/code/Magento/Cms/i18n/en_US.csv index e4989777593f8..7e697592bb9d7 100644 --- a/app/code/Magento/Cms/i18n/en_US.csv +++ b/app/code/Magento/Cms/i18n/en_US.csv @@ -152,3 +152,4 @@ Enable,Enable "Custom design to","Custom design to" "Custom Theme","Custom Theme" "Custom Layout","Custom Layout" +"Edit Page Design","Edit Page Design" diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml index 1bc8828ef6c8e..5bf66ef302f04 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml @@ -9,7 +9,11 @@ <container name="root"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content" name="wysiwyg_images.content" template="Magento_Cms::browser/content.phtml"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree" name="wysiwyg_images.tree" template="Magento_Cms::browser/tree.phtml"/> - <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"/> + <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"> + <arguments> + <argument name="image_upload_config_data" xsi:type="object">Magento\Backend\Block\DataProviders\UploadConfig</argument> + </arguments> + </block> </block> </container> </layout> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml index af71e0839243d..daa4209f6e30b 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content */ ?> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml index a4c570f9d65a1..63f647d9b58ee 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml @@ -4,28 +4,26 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Files */ $_width = $block->getImagesWidth(); $_height = $block->getImagesHeight(); ?> -<?php if ($block->getFilesCount() > 0): ?> - <?php foreach ($block->getFiles() as $file): ?> +<?php if ($block->getFilesCount() > 0) : ?> + <?php foreach ($block->getFiles() as $file) : ?> <div data-row="file" class="filecnt" id="<?= $block->escapeHtmlAttr($block->getFileId($file)) ?>"> - <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;width:<?= $block->escapeHtmlAttr($_width) ?>px;"> - <?php if ($block->getFileThumbUrl($file)):?> + <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;"> + <?php if ($block->getFileThumbUrl($file)) : ?> <img src="<?= $block->escapeHtmlAttr($block->getFileThumbUrl($file)) ?>" alt="<?= $block->escapeHtmlAttr($block->getFileName($file)) ?>"/> <?php endif; ?> </p> - <?php if ($block->getFileWidth($file)): ?> + <?php if ($block->getFileWidth($file)) : ?> <small><?= $block->escapeHtml($block->getFileWidth($file)) ?>x<?= $block->escapeHtml($block->getFileHeight($file)) ?> <?= $block->escapeHtml(__('px.')) ?></small><br/> <?php endif; ?> <small><?= $block->escapeHtml($block->getFileShortName($file)) ?></small> </div> <?php endforeach; ?> -<?php else: ?> +<?php else : ?> <div class="empty"><?= $block->escapeHtml(__('No files found')) ?></div> <?php endif; ?> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index 3b3b6dd4a6ca8..5b58b199c8eb6 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader */ + +$resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() + ? "{action: 'resize', maxWidth: " + . $block->getImageUploadMaxWidth() + . ", maxHeight: " + . $block->getImageUploadMaxHeight() + . "}" + : "{action: 'resize'}"; ?> <div id="<?= $block->getHtmlId() ?>" class="uploader"> @@ -99,11 +105,9 @@ require([ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/, maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 - }, { - action: 'resize', - maxWidth: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , - maxHeight: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> - }, { + }, + <?= /* @noEscape */ $resizeConfig ?>, + { action: 'save' }] }); diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml index 4f0ba000c1d12..9603bb4f1a412 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ ?> <div class="tree-panel" > @@ -16,6 +14,6 @@ <a onclick="jQuery('[data-role=tree]').jstree('open_all');"><?= $block->escapeHtml(__('Expand All')) ?></a> </div> </div> - <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getTreeWidgetOptions())) ?>'> + <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getTreeWidgetOptions())) ?>'> </div> </div> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml b/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml index c773750e5d6c6..54fe4c71fb98a 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/page/edit/form/renderer/content.phtml @@ -3,14 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_element = $block->getElement() ?> -<?php if (!$_element->getNoDisplay()): ?> +<?php if (!$_element->getNoDisplay()) : ?> <div class="cms-manage-content-actions"> - <?= trim($_element->getElementHtml()) ?> + <?= /* @noEscape */ trim($_element->getElementHtml()) ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 9f886f6f1345e..793fc7d26cb4a 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml index 5441481f6cea2..c9da67607b3b3 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_form.xml @@ -247,6 +247,7 @@ </field> <field name="layout_update_xml" formElement="textarea"> <argument name="data" xsi:type="array"> + <item name="disabled" xsi:type="boolean">true</item> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">page</item> </item> @@ -257,6 +258,26 @@ <dataScope>layout_update_xml</dataScope> </settings> </field> + <field name="custom_layout_update_select" formElement="select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">page</item> + </item> + </argument> + <settings> + <dataType>text</dataType> + <label translate="true">Custom Layout Update</label> + <tooltip> + <description translate="true"> + List of layout files with an update handle "selectable" + matching *PageIdentifier*_*UpdateID*. + <br/> + See Magento documentation for more information + </description> + </tooltip> + <dataScope>layout_update_selected</dataScope> + </settings> + </field> </fieldset> <fieldset name="custom_design_update" sortOrder="60"> <settings> @@ -287,6 +308,7 @@ <settings> <validation> <rule name="validate-date" xsi:type="boolean">true</rule> + <rule name="validate-date-range" xsi:type="string">custom_theme_from</rule> </validation> <dataType>text</dataType> <label translate="true">To</label> diff --git a/app/code/Magento/Cms/view/frontend/templates/content.phtml b/app/code/Magento/Cms/view/frontend/templates/content.phtml index d3baf14444762..5e8df2b682dc2 100644 --- a/app/code/Magento/Cms/view/frontend/templates/content.phtml +++ b/app/code/Magento/Cms/view/frontend/templates/content.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= /* @noEscape */ $pageData->getPageContent() ?> diff --git a/app/code/Magento/Cms/view/frontend/templates/meta.phtml b/app/code/Magento/Cms/view/frontend/templates/meta.phtml index 3c03fa7d9ae67..40820ba37e7c6 100644 --- a/app/code/Magento/Cms/view/frontend/templates/meta.phtml +++ b/app/code/Magento/Cms/view/frontend/templates/meta.phtml @@ -3,13 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($pageData->getPageMetaKeywords()): ?> +<?php if ($pageData->getPageMetaKeywords()) : ?> <meta name="keywords" content="<?= /* @noEscape */ $pageData->getPageMetaKeywords() ?>"/> <?php endif; ?> -<?php if ($pageData->getPageMetaDescription()): ?> +<?php if ($pageData->getPageMetaDescription()) : ?> <meta name="description" content="<?= /* @noEscape */ $pageData->getPageMetaDescription() ?>"/> <?php endif; ?> diff --git a/app/code/Magento/CmsUrlRewrite/Test/Mftf/LICENSE.txt b/app/code/Magento/CmsUrlRewrite/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/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 <insert your license name here>" 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/app/code/Magento/CmsUrlRewrite/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CmsUrlRewrite/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/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 <insert your license name here>" 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/app/code/Magento/CmsUrlRewrite/Test/Mftf/README.md b/app/code/Magento/CmsUrlRewrite/Test/Mftf/README.md new file mode 100644 index 0000000000000..4b377286964b1 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Cms Url Rewrite Functional Tests + +The Functional Test Module for **Magento Cms Url Rewrite** module. diff --git a/app/code/Magento/CmsUrlRewrite/composer.json b/app/code/Magento/CmsUrlRewrite/composer.json index dbb116e73151b..04198c761fb17 100644 --- a/app/code/Magento/CmsUrlRewrite/composer.json +++ b/app/code/Magento/CmsUrlRewrite/composer.json @@ -2,14 +2,14 @@ "name": "magento/module-cms-url-rewrite", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-cms": "102.0.*", "magento/module-url-rewrite": "101.0.*", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/di.xml similarity index 100% rename from app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml rename to app/code/Magento/CmsUrlRewrite/etc/di.xml diff --git a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php index b33c944c73477..7f2f771a8d0a6 100644 --- a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\App\Config\Source; use Magento\Framework\App\Config\ConfigSourceInterface; @@ -88,12 +89,12 @@ private function loadConfig() } } - foreach ($config as $scope => &$item) { + foreach ($config as $scope => $item) { if ($scope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { - $item = $this->converter->convert($item); + $config[$scope] = $this->converter->convert($item); } else { - foreach ($item as &$scopeItems) { - $scopeItems = $this->converter->convert($scopeItems); + foreach ($item as $scopeCode => $scopeItems) { + $config[$scope][$scopeCode] = $this->converter->convert($scopeItems); } } } diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 4f6d9c14346f0..c07872a630830 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -5,58 +5,80 @@ */ namespace Magento\Config\App\Config\Type; +use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Config\ConfigTypeInterface; +use Magento\Framework\App\Config\Spi\PostProcessorInterface; +use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\App\ObjectManager; use Magento\Config\App\Config\Type\System\Reader; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\Serialize\Serializer\Sensitive as SensitiveSerializer; +use Magento\Framework\Serialize\Serializer\SensitiveFactory as SensitiveSerializerFactory; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\Config\Processor\Fallback; +use Magento\Store\Model\ScopeInterface as StoreScope; /** * System configuration type + * * @api * @since 100.1.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class System implements ConfigTypeInterface { + /** + * Config cache tag. + */ const CACHE_TAG = 'config_scopes'; - const CONFIG_TYPE = 'system'; - /** - * @var \Magento\Framework\App\Config\ConfigSourceInterface + * System config type. */ - private $source; + const CONFIG_TYPE = 'system'; /** - * @var array + * @var string */ - private $data = []; + private static $lockName = 'SYSTEM_CONFIG'; /** - * @var \Magento\Framework\App\Config\Spi\PostProcessorInterface + * Timeout between retrieves to load the configuration from the cache. + * + * Value of the variable in microseconds. + * + * @var int */ - private $postProcessor; + private static $delayTimeout = 50000; /** - * @var \Magento\Framework\App\Config\Spi\PreProcessorInterface + * Lifetime of the lock for write in cache. + * + * Value of the variable in seconds. + * + * @var int */ - private $preProcessor; + private static $lockTimeout = 8; /** - * @var \Magento\Framework\Cache\FrontendInterface + * @var array */ - private $cache; + private $data = []; /** - * @var int + * @var PostProcessorInterface */ - private $cachingNestedLevel; + private $postProcessor; /** - * @var \Magento\Store\Model\Config\Processor\Fallback + * @var FrontendInterface */ - private $fallback; + private $cache; /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var SensitiveSerializer */ private $serializer; @@ -79,39 +101,54 @@ class System implements ConfigTypeInterface * * @var array */ - private $availableDataScopes = null; + private $availableDataScopes; /** - * @param \Magento\Framework\App\Config\ConfigSourceInterface $source - * @param \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor - * @param \Magento\Store\Model\Config\Processor\Fallback $fallback - * @param \Magento\Framework\Cache\FrontendInterface $cache - * @param \Magento\Framework\Serialize\SerializerInterface $serializer - * @param \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor + * @var LockManagerInterface + */ + private $locker; + + /** + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback + * @param FrontendInterface $cache + * @param SerializerInterface $serializer + * @param PreProcessorInterface $preProcessor * @param int $cachingNestedLevel * @param string $configType * @param Reader $reader + * @param SensitiveSerializerFactory|null $sensitiveFactory + * @param LockManagerInterface|null $locker + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - \Magento\Framework\App\Config\ConfigSourceInterface $source, - \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor, - \Magento\Store\Model\Config\Processor\Fallback $fallback, - \Magento\Framework\Cache\FrontendInterface $cache, - \Magento\Framework\Serialize\SerializerInterface $serializer, - \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor, + ConfigSourceInterface $source, + PostProcessorInterface $postProcessor, + Fallback $fallback, + FrontendInterface $cache, + SerializerInterface $serializer, + PreProcessorInterface $preProcessor, $cachingNestedLevel = 1, $configType = self::CONFIG_TYPE, - Reader $reader = null + Reader $reader = null, + SensitiveSerializerFactory $sensitiveFactory = null, + LockManagerInterface $locker = null ) { - $this->source = $source; $this->postProcessor = $postProcessor; - $this->preProcessor = $preProcessor; $this->cache = $cache; - $this->cachingNestedLevel = $cachingNestedLevel; - $this->fallback = $fallback; - $this->serializer = $serializer; $this->configType = $configType; - $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); + $this->reader = $reader ?: ObjectManager::getInstance() + ->get(Reader::class); + $sensitiveFactory = $sensitiveFactory ?? ObjectManager::getInstance() + ->get(SensitiveSerializerFactory::class); + //Using sensitive serializer because any kind of information may + //be stored in configs. + $this->serializer = $sensitiveFactory->create( + ['serializer' => $serializer] + ); + $this->locker = $locker ?: ObjectManager::getInstance()->get(LockManagerInterface::class); } /** @@ -136,32 +173,98 @@ public function get($path = '') { if ($path === '') { $this->data = array_replace_recursive($this->loadAllData(), $this->data); + return $this->data; } + + return $this->getWithParts($path); + } + + /** + * Proceed with parts extraction from path. + * + * @param string $path + * @return array|int|string|boolean + */ + private function getWithParts($path) + { $pathParts = explode('/', $path); - if (count($pathParts) === 1 && $pathParts[0] !== 'default') { + + if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$pathParts[0]])) { - $data = $this->readData(); + $data = $this->loadAllData(); $this->data = array_replace_recursive($data, $this->data); } + return $this->data[$pathParts[0]]; } + $scopeType = array_shift($pathParts); - if ($scopeType === 'default') { + + if ($scopeType === ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$scopeType])) { $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); } + return $this->getDataByPathParts($this->data[$scopeType], $pathParts); } + $scopeId = array_shift($pathParts); + if (!isset($this->data[$scopeType][$scopeId])) { - $this->data = array_replace_recursive($this->loadScopeData($scopeType, $scopeId), $this->data); + $scopeData = $this->loadScopeData($scopeType, $scopeId); + + if (!isset($this->data[$scopeType][$scopeId])) { + $this->data = array_replace_recursive($scopeData, $this->data); + } } + return isset($this->data[$scopeType][$scopeId]) ? $this->getDataByPathParts($this->data[$scopeType][$scopeId], $pathParts) : null; } + /** + * Make lock on data load. + * + * @param callable $dataLoader + * @param bool $flush + * @return array + */ + private function lockedLoadData(callable $dataLoader, bool $flush = false): array + { + $cachedData = $dataLoader(); //optimistic read + + while ($cachedData === false && $this->locker->isLocked(self::$lockName)) { + usleep(self::$delayTimeout); + $cachedData = $dataLoader(); + } + + while ($cachedData === false) { + try { + if ($this->locker->lock(self::$lockName, self::$lockTimeout)) { + if (!$flush) { + $data = $this->readData(); + $this->cacheData($data); + $cachedData = $data; + } else { + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $cachedData = []; + } + } + } finally { + $this->locker->unlock(self::$lockName); + } + + if ($cachedData === false) { + usleep(self::$delayTimeout); + $cachedData = $dataLoader(); + } + } + + return $cachedData; + } + /** * Load configuration data for all scopes * @@ -169,13 +272,13 @@ public function get($path = '') */ private function loadAllData() { - $cachedData = $this->cache->load($this->configType); - if ($cachedData === false) { - $data = $this->readData(); - } else { - $data = $this->serializer->unserialize($cachedData); - } - return $data; + return $this->lockedLoadData(function () { + $cachedData = $this->cache->load($this->configType); + if ($cachedData === false) { + return $cachedData; + } + return $this->serializer->unserialize($cachedData); + }); } /** @@ -186,14 +289,13 @@ private function loadAllData() */ private function loadDefaultScopeData($scopeType) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType); - if ($cachedData === false) { - $data = $this->readData(); - $this->cacheData($data); - } else { - $data = [$scopeType => $this->serializer->unserialize($cachedData)]; - } - return $data; + return $this->lockedLoadData(function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + if ($cachedData === false) { + return $cachedData; + } + return [$scopeType => $this->serializer->unserialize($cachedData)]; + }); } /** @@ -205,23 +307,22 @@ private function loadDefaultScopeData($scopeType) */ private function loadScopeData($scopeType, $scopeId) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); - if ($cachedData === false) { - if ($this->availableDataScopes === null) { - $cachedScopeData = $this->cache->load($this->configType . '_scopes'); - if ($cachedScopeData !== false) { - $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData); + return $this->lockedLoadData(function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + if ($cachedData === false) { + if ($this->availableDataScopes === null) { + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); + if ($cachedScopeData !== false) { + $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData); + } } + if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { + return [$scopeType => [$scopeId => []]]; + } + return false; } - if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { - return [$scopeType => [$scopeId => []]]; - } - $data = $this->readData(); - $this->cacheData($data); - } else { - $data = [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]]; - } - return $data; + return [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]]; + }); } /** @@ -244,8 +345,8 @@ private function cacheData(array $data) [self::CACHE_TAG] ); $scopes = []; - foreach (['websites', 'stores'] as $curScopeType) { - foreach ($data[$curScopeType] as $curScopeId => $curScopeData) { + foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) { + foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { $scopes[$curScopeType][$curScopeId] = 1; $this->cache->save( $this->serializer->serialize($curScopeData), @@ -256,7 +357,7 @@ private function cacheData(array $data) } $this->cache->save( $this->serializer->serialize($scopes), - $this->configType . "_scopes", + $this->configType . '_scopes', [self::CACHE_TAG] ); } @@ -279,6 +380,7 @@ private function getDataByPathParts($data, $pathParts) return null; } } + return $data; } @@ -310,6 +412,11 @@ private function readData(): array public function clean() { $this->data = []; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $this->lockedLoadData( + function () { + return false; + }, + true + ); } } diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index c17df229cf549..63a2b811f93ec 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -143,13 +143,15 @@ public function __construct( \Magento\Config\Model\Config\Structure $configStructure, \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory, \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory, - array $data = [] + array $data = [], + SettingChecker $settingChecker = null ) { parent::__construct($context, $registry, $formFactory, $data); $this->_configFactory = $configFactory; $this->_configStructure = $configStructure; $this->_fieldsetFactory = $fieldsetFactory; $this->_fieldFactory = $fieldFactory; + $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); $this->_scopeLabels = [ self::SCOPE_DEFAULT => __('[GLOBAL]'), @@ -158,18 +160,6 @@ public function __construct( ]; } - /** - * @deprecated 100.1.2 - * @return SettingChecker - */ - private function getSettingChecker() - { - if ($this->settingChecker === null) { - $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class); - } - return $this->settingChecker; - } - /** * Initialize objects required to render config form * @@ -366,9 +356,8 @@ protected function _initElement( $sharedClass = $this->_getSharedCssClass($field); $requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix); + $isReadOnly = $this->isReadOnly($field, $path); - $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) - ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); $formField = $fieldset->addField( $elementId, $field->getType(), @@ -417,7 +406,7 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie { $data = $this->getAppConfigDataValue($path); - $placeholderValue = $this->getSettingChecker()->getPlaceholderValue( + $placeholderValue = $this->settingChecker->getPlaceholderValue( $path, $this->getScope(), $this->getStringScopeCode() @@ -434,6 +423,10 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie $backendModel = $field->getBackendModel(); // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface if (!$backendModel instanceof ProcessorInterface) { + if (array_key_exists($path, $this->_configData)) { + $data = $this->_configData[$path]; + } + $backendModel->setPath($path) ->setValue($data) ->setWebsite($this->getWebsiteCode()) @@ -709,7 +702,7 @@ protected function _getAdditionalElementTypes() } /** - * Temporary moved those $this->getRequest()->getParam('blabla') from the code accross this block + * Temporary moved those $this->getRequest()->getParam('blabla') from the code across this block * to getBlala() methods to be later set from controller with setters */ @@ -718,6 +711,7 @@ protected function _getAdditionalElementTypes() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSectionCode() { @@ -729,6 +723,7 @@ public function getSectionCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getWebsiteCode() { @@ -740,6 +735,7 @@ public function getWebsiteCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreCode() { @@ -797,6 +793,26 @@ private function getAppConfig() return $this->appConfig; } + /** + * Check Path is Readonly + * + * @param \Magento\Config\Model\Config\Structure\Element\Field $field + * @param string $path + * @return boolean + */ + private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path) + { + $isReadOnly = $this->settingChecker->isReadOnly( + $path, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + if (!$isReadOnly) { + $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) + ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); + } + return $isReadOnly; + } + /** * Retrieve deployment config data value by path * diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field.php b/app/code/Magento/Config/Block/System/Config/Form/Field.php index 37b9b1d656ba7..39cc34f092c0f 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field.php @@ -4,10 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Block\System\Config\Form; +use Magento\Backend\Block\Template; +use Magento\Framework\Data\Form\Element\Renderer\RendererInterface; + /** * Render field html element in Stores Configuration * @@ -16,7 +17,7 @@ * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 */ -class Field extends \Magento\Backend\Block\Template implements \Magento\Framework\Data\Form\Element\Renderer\RendererInterface +class Field extends Template implements RendererInterface { /** * Retrieve element HTML markup diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php index 63dbb2b80e334..acf830f363ce6 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php @@ -40,7 +40,7 @@ public function __construct( protected function _getElementHtml(AbstractElement $element) { return $this->dateTimeFormatter->formatObject( - $this->_localeDate->date(intval($element->getValue())), + $this->_localeDate->date((int) $element->getValue()), $this->_localeDate->getDateTimeFormat(\IntlDateFormatter::MEDIUM) ); } diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php index a5cdd7b84f6dc..066254623e814 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Block\System\Config\Form\Field\FieldArray; /** @@ -86,7 +84,9 @@ public function addColumn($name, $params) 'class' => $this->_getParam($params, 'class'), 'renderer' => false, ]; - if (!empty($params['renderer']) && $params['renderer'] instanceof \Magento\Framework\View\Element\AbstractBlock) { + if (!empty($params['renderer']) + && $params['renderer'] instanceof \Magento\Framework\View\Element\AbstractBlock + ) { $this->_columns[$name]['renderer'] = $params['renderer']; } } @@ -227,11 +227,9 @@ public function renderCellTemplate($columnName) $column['size'] . '"' : '') . ' class="' . - (isset( - $column['class'] - ) ? $column['class'] : 'input-text') . '"' . (isset( - $column['style'] - ) ? ' style="' . $column['style'] . '"' : '') . '/>'; + ($column['class'] ?? 'input-text') . '"' . + (isset($column['style']) ? ' style="' . $column['style'] . '"' : '') . + '/>'; } /** diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php index 7f21bf4b92bf4..40ff76ee0e885 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php @@ -44,6 +44,6 @@ protected function _getElementHtml(AbstractElement $element) $format = $this->_localeDate->getDateTimeFormat( \IntlDateFormatter::MEDIUM ); - return $this->dateTimeFormatter->formatObject($this->_localeDate->date(intval($element->getValue())), $format); + return $this->dateTimeFormatter->formatObject($this->_localeDate->date((int)$element->getValue()), $format); } } diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php index bfcb3d249aacd..b62584537e2b3 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * System configuration shipping methods allow all countries select * diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php index e005747ea5ed5..92cd0b0a14e93 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php @@ -27,7 +27,14 @@ class ConfigSetProcessorFactory * lock - save and lock configuration */ const TYPE_DEFAULT = 'default'; + + /** + * @deprecated + * @see TYPE_LOCK_ENV or TYPE_LOCK_CONFIG + */ const TYPE_LOCK = 'lock'; + const TYPE_LOCK_ENV = 'lock-env'; + const TYPE_LOCK_CONFIG = 'lock-config'; /**#@-*/ /**#@-*/ diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index 2f5c10037ef06..a3c7c7d079cbb 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -7,17 +7,19 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSetCommand; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Config\Model\PreparedValueFactory; -use Magento\Framework\App\Config\Value; /** * Processes default flow of config:set command. + * * This processor saves the value of configuration into database. * - * {@inheritdoc} + * @inheritdoc * @api * @since 100.2.0 */ @@ -44,26 +46,36 @@ class DefaultProcessor implements ConfigSetProcessorInterface */ private $preparedValueFactory; + /** + * @var ConfigFactory + */ + private $configFactory; + /** * @param PreparedValueFactory $preparedValueFactory The factory for prepared value * @param DeploymentConfig $deploymentConfig The deployment configuration reader * @param ConfigPathResolver $configPathResolver The resolver for configuration paths according to source type + * @param ConfigFactory|null $configFactory */ public function __construct( PreparedValueFactory $preparedValueFactory, DeploymentConfig $deploymentConfig, - ConfigPathResolver $configPathResolver + ConfigPathResolver $configPathResolver, + ConfigFactory $configFactory = null ) { $this->preparedValueFactory = $preparedValueFactory; $this->deploymentConfig = $deploymentConfig; $this->configPathResolver = $configPathResolver; + + $this->configFactory = $configFactory ?? ObjectManager::getInstance()->get(ConfigFactory::class); } /** * Processes database flow of config:set command. + * * Requires installed application. * - * {@inheritdoc} + * @inheritdoc * @since 100.2.0 */ public function process($path, $value, $scope, $scopeCode) @@ -72,18 +84,20 @@ public function process($path, $value, $scope, $scopeCode) throw new CouldNotSaveException( __( 'The value you set has already been locked. To change the value, use the --%1 option.', - ConfigSetCommand::OPTION_LOCK + ConfigSetCommand::OPTION_LOCK_ENV ) ); } try { - /** @var Value $backendModel */ - $backendModel = $this->preparedValueFactory->create($path, $value, $scope, $scopeCode); - if ($backendModel instanceof Value) { - $resourceModel = $backendModel->getResource(); - $resourceModel->save($backendModel); - } + $config = $this->configFactory->create([ + 'data' => [ + 'scope' => $scope, + 'scope_code' => $scopeCode, + ], + ]); + $config->setDataByPath($path, $value); + $config->save(); } catch (\Exception $exception) { throw new CouldNotSaveException(__('%1', $exception->getMessage()), $exception); } diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php index 0bd28f0f78d96..6fe2adde3c41e 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/LockProcessor.php @@ -16,7 +16,8 @@ /** * Processes file lock flow of config:set command. - * This processor saves the value of configuration and lock it for editing in Admin interface. + * This processor saves the value of configuration into app/etc/env.php + * and locks it for editing in Admin interface. * * {@inheritdoc} */ @@ -49,23 +50,30 @@ class LockProcessor implements ConfigSetProcessorInterface * @var ConfigPathResolver */ private $configPathResolver; + /** + * @var string + */ + private $target; /** * @param PreparedValueFactory $preparedValueFactory The factory for prepared value * @param DeploymentConfig\Writer $writer The deployment configuration writer * @param ArrayManager $arrayManager An array manager for different manipulations with arrays * @param ConfigPathResolver $configPathResolver The resolver for configuration paths according to source type + * @param string $target */ public function __construct( PreparedValueFactory $preparedValueFactory, DeploymentConfig\Writer $writer, ArrayManager $arrayManager, - ConfigPathResolver $configPathResolver + ConfigPathResolver $configPathResolver, + $target = ConfigFilePool::APP_ENV ) { $this->preparedValueFactory = $preparedValueFactory; $this->deploymentConfigWriter = $writer; $this->arrayManager = $arrayManager; $this->configPathResolver = $configPathResolver; + $this->target = $target; } /** @@ -97,7 +105,7 @@ public function process($path, $value, $scope, $scopeCode) * we'll write value just after all validations are triggered. */ $this->deploymentConfigWriter->saveConfig( - [ConfigFilePool::APP_ENV => $this->arrayManager->set($configPath, [], $value)], + [$this->target => $this->arrayManager->set($configPath, [], $value)], false ); } diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php index 06a01c6686bfd..b9b73ae5887b3 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Config\Model\Config\PathValidator; +use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Exception\CouldNotSaveException; @@ -98,12 +99,35 @@ public function __construct( * @param boolean $lock The lock flag * @return string Processor response message * @throws ValidatorException If some validation is wrong - * @throws CouldNotSaveException If cannot save config value - * @throws ConfigurationMismatchException If processor can not be instantiated * @since 100.2.0 + * @deprecated + * @see processWithLockTarget() */ public function process($path, $value, $scope, $scopeCode, $lock) { + return $this->processWithLockTarget($path, $value, $scope, $scopeCode, $lock); + } + + /** + * Processes config:set command with the option to set a target file. + * + * @param string $path The configuration path in format section/group/field_name + * @param string $value The configuration value + * @param string $scope The configuration scope (default, website, or store) + * @param string $scopeCode The scope code + * @param boolean $lock The lock flag + * @param string $lockTarget + * @return string Processor response message + * @throws ValidatorException If some validation is wrong + */ + public function processWithLockTarget( + $path, + $value, + $scope, + $scopeCode, + $lock, + $lockTarget = ConfigFilePool::APP_ENV + ) { try { $this->scopeValidator->isValid($scope, $scopeCode); $this->pathValidator->validate($path); @@ -111,14 +135,23 @@ public function process($path, $value, $scope, $scopeCode, $lock) throw new ValidatorException(__($exception->getMessage()), $exception); } - $processor = $lock - ? $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_LOCK) - : $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_DEFAULT); - $message = $lock - ? 'Value was saved and locked.' - : 'Value was saved.'; + $processor = + $lock + ? ( $lockTarget == ConfigFilePool::APP_ENV + ? $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_LOCK_ENV) + : $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_LOCK_CONFIG) + ) + : $this->configSetProcessorFactory->create(ConfigSetProcessorFactory::TYPE_DEFAULT); + + $message = + $lock + ? ( $lockTarget == ConfigFilePool::APP_ENV + ? 'Value was saved in app/etc/env.php and locked.' + : 'Value was saved in app/etc/config.php and locked.' + ) + : 'Value was saved.'; - // The processing flow depends on --lock option. + // The processing flow depends on --lock and --share options. $processor->process($path, $value, $scope, $scopeCode); $this->hash->regenerate(System::CONFIG_TYPE); diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php index 1df1b3c4bed14..999d8e41af5bc 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\Console\Command; use Magento\Config\App\Config\Type\System; @@ -10,6 +11,7 @@ use Magento\Deploy\Model\DeploymentConfig\ChangeDetector; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -34,6 +36,8 @@ class ConfigSetCommand extends Command const OPTION_SCOPE = 'scope'; const OPTION_SCOPE_CODE = 'scope-code'; const OPTION_LOCK = 'lock'; + const OPTION_LOCK_ENV = 'lock-env'; + const OPTION_LOCK_CONFIG = 'lock-config'; /**#@-*/ /**#@-*/ @@ -108,11 +112,24 @@ protected function configure() InputArgument::OPTIONAL, 'Scope code (required only if scope is not \'default\')' ), + new InputOption( + static::OPTION_LOCK_ENV, + 'e', + InputOption::VALUE_NONE, + 'Lock value which prevents modification in the Admin (will be saved in app/etc/env.php)' + ), + new InputOption( + static::OPTION_LOCK_CONFIG, + 'c', + InputOption::VALUE_NONE, + 'Lock and share value with other installations, prevents modification in the Admin ' + . '(will be saved in app/etc/config.php)' + ), new InputOption( static::OPTION_LOCK, 'l', InputOption::VALUE_NONE, - 'Lock value which prevents modification in the Admin' + 'Deprecated, use the --' . static::OPTION_LOCK_ENV . ' option instead.' ), ]); @@ -122,8 +139,10 @@ protected function configure() /** * Creates and run appropriate processor, depending on input options. * - * {@inheritdoc} + * @param InputInterface $input + * @param OutputInterface $output * @since 100.2.0 + * @return int|null */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -146,12 +165,23 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $message = $this->emulatedAreaProcessor->process(function () use ($input) { - return $this->processorFacadeFactory->create()->process( + + $lock = $input->getOption(static::OPTION_LOCK_ENV) + || $input->getOption(static::OPTION_LOCK_CONFIG) + || $input->getOption(static::OPTION_LOCK); + + $lockTargetPath = ConfigFilePool::APP_ENV; + if ($input->getOption(static::OPTION_LOCK_CONFIG)) { + $lockTargetPath = ConfigFilePool::APP_CONFIG; + } + + return $this->processorFacadeFactory->create()->processWithLockTarget( $input->getArgument(static::ARG_PATH), $input->getArgument(static::ARG_VALUE), $input->getOption(static::OPTION_SCOPE), $input->getOption(static::OPTION_SCOPE_CODE), - $input->getOption(static::OPTION_LOCK) + $lock, + $lockTargetPath ); }); diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index 582f87508089f..aeb57010e4969 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -97,7 +97,7 @@ public function process($scope, $scopeCode, $value, $path) $field = $configStructure->getElementByConfigPath($path); /** @var Value $backendModel */ - $backendModel = $field && $field->hasBackendModel() + $backendModel = $field instanceof Field && $field->hasBackendModel() ? $field->getBackendModel() : $this->configValueFactory->create(); diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php index 7f7d461ea090d..6331c4c60d6d2 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php @@ -6,6 +6,7 @@ namespace Magento\Config\Controller\Adminhtml\System\Config; use Magento\Config\Controller\Adminhtml\System\AbstractConfig; +use Magento\Framework\Exception\NotFoundException; /** * System Configuration Save Controller @@ -54,6 +55,68 @@ public function __construct( $this->string = $string; } + /** + * @inheritdoc + */ + protected function _isAllowed() + { + return parent::_isAllowed() && $this->isSectionAllowed(); + } + + /** + * Checks if user has access to section. + * + * @return bool + */ + private function isSectionAllowed(): bool + { + $sectionId = $this->_request->getParam('section'); + $isAllowed = $this->_configStructure->getElement($sectionId)->isAllowed(); + if (!$isAllowed) { + $groups = $this->getRequest()->getPost('groups'); + $fieldPath = $this->getFirstFieldPath($groups, $sectionId); + + $fieldPaths = $this->_configStructure->getFieldPaths(); + $fieldPath = $fieldPaths[$fieldPath][0] ?? $sectionId; + $explodedConfigPath = explode('/', $fieldPath); + $configSectionId = $explodedConfigPath[0] ?? $sectionId; + + $isAllowed = $this->_configStructure->getElement($configSectionId)->isAllowed(); + } + + return $isAllowed; + } + + /** + * Return field path as string. + * + * @param array $elements + * @param string $fieldPath + * @return string + */ + private function getFirstFieldPath(array $elements, string $fieldPath): string + { + $groupData = []; + foreach ($elements as $elementName => $element) { + if (!empty($element)) { + $fieldPath .= '/' . $elementName; + + if (!empty($element['fields'])) { + $groupData = $element['fields']; + } elseif (!empty($element['groups'])) { + $groupData = $element['groups']; + } + + if (!empty($groupData)) { + $fieldPath = $this->getFirstFieldPath($groupData, $fieldPath); + } + break; + } + } + + return $fieldPath; + } + /** * Get groups for save * @@ -140,9 +203,14 @@ protected function _saveAdvanced() * Save configuration * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { // custom save logic $this->_saveSection(); @@ -156,6 +224,8 @@ public function execute() 'store' => $store, 'groups' => $this->_getGroupsForSave(), ]; + $configData = $this->filterNodes($configData); + /** @var \Magento\Config\Model\Config $configModel */ $configModel = $this->_configFactory->create(['data' => $configData]); $configModel->save(); @@ -184,4 +254,85 @@ public function execute() ] ); } + + /** + * Filter paths that are not defined. + * + * @param string $prefix Path prefix + * @param array $groups Groups data. + * @param string[] $systemXmlConfig Defined paths. + * @return array Filtered groups. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function filterPaths(string $prefix, array $groups, array $systemXmlConfig): array + { + $flippedXmlConfig = array_flip($systemXmlConfig); + $filtered = []; + foreach ($groups as $groupName => $childPaths) { + //When group accepts arbitrary fields and clones them we allow it + $group = $this->_configStructure->getElement($prefix . '/' . $groupName); + if (array_key_exists('clone_fields', $group->getData()) && $group->getData()['clone_fields']) { + $filtered[$groupName] = $childPaths; + continue; + } + + $filtered[$groupName] = ['fields' => [], 'groups' => []]; + //Processing fields + if (array_key_exists('fields', $childPaths)) { + foreach ($childPaths['fields'] as $field => $fieldData) { + //Constructing config path for the $field + $path = $prefix . '/' . $groupName . '/' . $field; + $element = $this->_configStructure->getElement($path); + if ($element + && ($elementData = $element->getData()) + && array_key_exists('config_path', $elementData) + ) { + $path = $elementData['config_path']; + } + //Checking whether it exists in system.xml + if (array_key_exists($path, $flippedXmlConfig)) { + $filtered[$groupName]['fields'][$field] = $fieldData; + } + } + } + //Recursively filtering this group's groups. + if (array_key_exists('groups', $childPaths) && $childPaths['groups']) { + $filteredGroups = $this->filterPaths( + $prefix . '/' . $groupName, + $childPaths['groups'], + $systemXmlConfig + ); + if ($filteredGroups) { + $filtered[$groupName]['groups'] = $filteredGroups; + } + } + + $filtered[$groupName] = array_filter($filtered[$groupName]); + } + + return array_filter($filtered); + } + + /** + * Filters nodes by checking whether they exist in system.xml. + * + * @param array $configData + * @return array + */ + private function filterNodes(array $configData): array + { + if (!empty($configData['groups'])) { + $systemXmlPathsFromKeys = array_keys($this->_configStructure->getFieldPaths()); + $systemXmlPathsFromValues = array_reduce( + array_values($this->_configStructure->getFieldPaths()), + 'array_merge', + [] + ); + //Full list of paths defined in system.xml + $systemXmlConfig = array_merge($systemXmlPathsFromKeys, $systemXmlPathsFromValues); + $configData['groups'] = $this->filterPaths($configData['section'], $configData['groups'], $systemXmlConfig); + } + + return $configData; + } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index bc1515aadb0ca..e562b637fac23 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -5,14 +5,36 @@ */ namespace Magento\Config\Model; +use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker; +use Magento\Config\Model\Config\Structure\Element\Group; +use Magento\Config\Model\Config\Structure\Element\Field; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\ScopeTypeNormalizer; + /** * Backend config model + * * Used to save configuration * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * @method string getSection() + * @method void setSection(string $section) + * @method string getWebsite() + * @method void setWebsite(string $website) + * @method string getStore() + * @method void setStore(string $store) + * @method string getScope() + * @method void setScope(string $scope) + * @method int getScopeId() + * @method void setScopeId(int $scopeId) + * @method string getScopeCode() + * @method void setScopeCode(string $scopeCode) */ class Config extends \Magento\Framework\DataObject { @@ -77,6 +99,21 @@ class Config extends \Magento\Framework\DataObject */ protected $_storeManager; + /** + * @var Config\Reader\Source\Deployed\SettingChecker + */ + private $settingChecker; + + /** + * @var ScopeResolverPool + */ + private $scopeResolverPool; + + /** + * @var ScopeTypeNormalizer + */ + private $scopeTypeNormalizer; + /** * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -85,7 +122,11 @@ class Config extends \Magento\Framework\DataObject * @param \Magento\Config\Model\Config\Loader $configLoader * @param \Magento\Framework\App\Config\ValueFactory $configValueFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker * @param array $data + * @param ScopeResolverPool|null $scopeResolverPool + * @param ScopeTypeNormalizer|null $scopeTypeNormalizer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Config\ReinitableConfigInterface $config, @@ -95,7 +136,10 @@ public function __construct( \Magento\Config\Model\Config\Loader $configLoader, \Magento\Framework\App\Config\ValueFactory $configValueFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, - array $data = [] + SettingChecker $settingChecker = null, + array $data = [], + ScopeResolverPool $scopeResolverPool = null, + ScopeTypeNormalizer $scopeTypeNormalizer = null ) { parent::__construct($data); $this->_eventManager = $eventManager; @@ -105,10 +149,17 @@ public function __construct( $this->_configLoader = $configLoader; $this->_configValueFactory = $configValueFactory; $this->_storeManager = $storeManager; + $this->settingChecker = $settingChecker + ?? ObjectManager::getInstance()->get(SettingChecker::class); + $this->scopeResolverPool = $scopeResolverPool + ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); + $this->scopeTypeNormalizer = $scopeTypeNormalizer + ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); } /** * Save config section + * * Require set: section, website, store and groups * * @throws \Exception @@ -126,11 +177,12 @@ public function save() $oldConfig = $this->_getConfig(true); + /** @var \Magento\Framework\DB\Transaction $deleteTransaction */ $deleteTransaction = $this->_transactionFactory->create(); - /* @var $deleteTransaction \Magento\Framework\DB\Transaction */ + /** @var \Magento\Framework\DB\Transaction $saveTransaction */ $saveTransaction = $this->_transactionFactory->create(); - /* @var $saveTransaction \Magento\Framework\DB\Transaction */ + $changedPaths = []; // Extends for old config data $extraOldGroups = []; @@ -145,6 +197,9 @@ public function save() $saveTransaction, $deleteTransaction ); + + $groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups); + $changedPaths = \array_merge($changedPaths, $groupChangedPaths); } try { @@ -157,7 +212,11 @@ public function save() // website and store codes can be used in event implementation, so set them as well $this->_eventManager->dispatch( "admin_system_config_changed_section_{$this->getSection()}", - ['website' => $this->getWebsite(), 'store' => $this->getStore()] + [ + 'website' => $this->getWebsite(), + 'store' => $this->getStore(), + 'changed_paths' => $changedPaths, + ] ); } catch (\Exception $e) { // re-init configuration @@ -168,6 +227,145 @@ public function save() return $this; } + /** + * Map field name if they were cloned + * + * @param Group $group + * @param string $fieldId + * @return string + */ + private function getOriginalFieldId(Group $group, string $fieldId): string + { + if ($group->shouldCloneFields()) { + $cloneModel = $group->getCloneModel(); + + /** @var \Magento\Config\Model\Config\Structure\Element\Field $field */ + foreach ($group->getChildren() as $field) { + foreach ($cloneModel->getPrefixes() as $prefix) { + if ($prefix['field'] . $field->getId() === $fieldId) { + $fieldId = $field->getId(); + break(2); + } + } + } + } + + return $fieldId; + } + + /** + * Get field object + * + * @param string $sectionId + * @param string $groupId + * @param string $fieldId + * @return Field + */ + private function getField(string $sectionId, string $groupId, string $fieldId): Field + { + /** @var \Magento\Config\Model\Config\Structure\Element\Group $group */ + $group = $this->_configStructure->getElement($sectionId . '/' . $groupId); + $fieldPath = $group->getPath() . '/' . $this->getOriginalFieldId($group, $fieldId); + $field = $this->_configStructure->getElement($fieldPath); + + return $field; + } + + /** + * Get field path + * + * @param Field $field + * @param string $fieldId Need for support of clone_field feature + * @param array &$oldConfig Need for compatibility with _processGroup() + * @param array &$extraOldGroups Need for compatibility with _processGroup() + * @return string + */ + private function getFieldPath(Field $field, string $fieldId, array &$oldConfig, array &$extraOldGroups): string + { + $path = $field->getGroupPath() . '/' . $fieldId; + + /** + * Look for custom defined field path + */ + $configPath = $field->getConfigPath(); + if ($configPath && strrpos($configPath, '/') > 0) { + // Extend old data with specified section group + $configGroupPath = substr($configPath, 0, strrpos($configPath, '/')); + if (!isset($extraOldGroups[$configGroupPath])) { + $oldConfig = $this->extendConfig($configGroupPath, true, $oldConfig); + $extraOldGroups[$configGroupPath] = true; + } + $path = $configPath; + } + + return $path; + } + + /** + * Check is config value changed + * + * @param array $oldConfig + * @param string $path + * @param array $fieldData + * @return bool + */ + private function isValueChanged(array $oldConfig, string $path, array $fieldData): bool + { + if (isset($oldConfig[$path]['value'])) { + $result = !isset($fieldData['value']) || $oldConfig[$path]['value'] !== $fieldData['value']; + } else { + $result = empty($fieldData['inherit']); + } + + return $result; + } + + /** + * Get changed paths + * + * @param string $sectionId + * @param string $groupId + * @param array $groupData + * @param array &$oldConfig + * @param array &$extraOldGroups + * @return array + */ + private function getChangedPaths( + string $sectionId, + string $groupId, + array $groupData, + array &$oldConfig, + array &$extraOldGroups + ): array { + $changedPaths = []; + + if (isset($groupData['fields'])) { + foreach ($groupData['fields'] as $fieldId => $fieldData) { + $field = $this->getField($sectionId, $groupId, $fieldId); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); + if ($this->isValueChanged($oldConfig, $path, $fieldData)) { + $changedPaths[] = $path; + } + } + } + + if (isset($groupData['groups'])) { + $subSectionId = $sectionId . '/' . $groupId; + foreach ($groupData['groups'] as $subGroupId => $subGroupData) { + $subGroupChangedPaths = $this->getChangedPaths( + $subSectionId, + $subGroupId, + $subGroupData, + $oldConfig, + $extraOldGroups + ); + $changedPaths = \array_merge($changedPaths, $subGroupChangedPaths); + } + } + + return $changedPaths; + } + /** * Process group data * @@ -182,7 +380,6 @@ public function save() * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _processGroup( $groupId, @@ -195,92 +392,60 @@ protected function _processGroup( \Magento\Framework\DB\Transaction $deleteTransaction ) { $groupPath = $sectionPath . '/' . $groupId; - $scope = $this->getScope(); - $scopeId = $this->getScopeId(); - $scopeCode = $this->getScopeCode(); - /** - * - * Map field names if they were cloned - */ - /** @var $group \Magento\Config\Model\Config\Structure\Element\Group */ - $group = $this->_configStructure->getElement($groupPath); - // set value for group field entry by fieldname - // use extra memory - $fieldsetData = []; if (isset($groupData['fields'])) { - if ($group->shouldCloneFields()) { - $cloneModel = $group->getCloneModel(); - $mappedFields = []; - - /** @var $field \Magento\Config\Model\Config\Structure\Element\Field */ - foreach ($group->getChildren() as $field) { - foreach ($cloneModel->getPrefixes() as $prefix) { - $mappedFields[$prefix['field'] . $field->getId()] = $field->getId(); - } - } - } + /** @var \Magento\Config\Model\Config\Structure\Element\Group $group */ + $group = $this->_configStructure->getElement($groupPath); + + // set value for group field entry by fieldname + // use extra memory + $fieldsetData = []; foreach ($groupData['fields'] as $fieldId => $fieldData) { - $fieldsetData[$fieldId] = is_array( - $fieldData - ) && isset( - $fieldData['value'] - ) ? $fieldData['value'] : null; + $fieldsetData[$fieldId] = $fieldData['value'] ?? null; } foreach ($groupData['fields'] as $fieldId => $fieldData) { - $originalFieldId = $fieldId; - if ($group->shouldCloneFields() && isset($mappedFields[$fieldId])) { - $originalFieldId = $mappedFields[$fieldId]; + $isReadOnly = $this->settingChecker->isReadOnly( + $groupPath . '/' . $fieldId, + $this->getScope(), + $this->getScopeCode() + ); + + if ($isReadOnly) { + continue; } - /** @var $field \Magento\Config\Model\Config\Structure\Element\Field */ - $field = $this->_configStructure->getElement($groupPath . '/' . $originalFieldId); + $field = $this->getField($sectionPath, $groupId, $fieldId); /** @var \Magento\Framework\App\Config\ValueInterface $backendModel */ - $backendModel = $field->hasBackendModel() ? $field - ->getBackendModel() : $this - ->_configValueFactory - ->create(); + $backendModel = $field->hasBackendModel() + ? $field->getBackendModel() + : $this->_configValueFactory->create(); + if (!isset($fieldData['value'])) { + $fieldData['value'] = null; + } + + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { + $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); + } + $data = [ 'field' => $fieldId, 'groups' => $groups, 'group_id' => $group->getId(), - 'scope' => $scope, - 'scope_id' => $scopeId, - 'scope_code' => $scopeCode, + 'scope' => $this->getScope(), + 'scope_id' => $this->getScopeId(), + 'scope_code' => $this->getScopeCode(), 'field_config' => $field->getData(), - 'fieldset_data' => $fieldsetData + 'fieldset_data' => $fieldsetData, ]; $backendModel->addData($data); - $this->_checkSingleStoreMode($field, $backendModel); - if (false == isset($fieldData['value'])) { - $fieldData['value'] = null; - } - - $path = $field->getGroupPath() . '/' . $fieldId; - /** - * Look for custom defined field path - */ - if ($field && $field->getConfigPath()) { - $configPath = $field->getConfigPath(); - if (!empty($configPath) && strrpos($configPath, '/') > 0) { - // Extend old data with specified section group - $configGroupPath = substr($configPath, 0, strrpos($configPath, '/')); - if (!isset($extraOldGroups[$configGroupPath])) { - $oldConfig = $this->extendConfig($configGroupPath, true, $oldConfig); - $extraOldGroups[$configGroupPath] = true; - } - $path = $configPath; - } - } - - $inherit = !empty($fieldData['inherit']); - + $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); $backendModel->setPath($path)->setValue($fieldData['value']); + $inherit = !empty($fieldData['inherit']); if (isset($oldConfig[$path])) { $backendModel->setConfigId($oldConfig[$path]['config_id']); @@ -360,30 +525,37 @@ public function setDataByPath($path, $value) if ($path === '') { throw new \UnexpectedValueException('Path must not be empty'); } + $pathParts = explode('/', $path); $keyDepth = count($pathParts); - if ($keyDepth !== 3) { + if ($keyDepth < 3) { throw new \UnexpectedValueException( - "Allowed depth of configuration is 3 (<section>/<group>/<field>). Your configuration depth is " - . $keyDepth . " for path '$path'" + 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth ); } + + $section = array_shift($pathParts); + $this->setData('section', $section); + $data = [ - 'section' => $pathParts[0], - 'groups' => [ - $pathParts[1] => [ - 'fields' => [ - $pathParts[2] => ['value' => $value], - ], - ], + 'fields' => [ + array_pop($pathParts) => ['value' => $value], ], ]; - $this->addData($data); + while ($pathParts) { + $data = [ + 'groups' => [ + array_pop($pathParts) => $data, + ], + ]; + } + $groups = array_replace_recursive((array) $this->getData('groups'), $data['groups']); + $this->setData('groups', $groups); } /** - * Get scope name and scopeId - * @todo refactor to scope resolver + * Set scope data + * * @return void */ private function initScope() @@ -391,31 +563,66 @@ private function initScope() if ($this->getSection() === null) { $this->setSection(''); } + + $scope = $this->retrieveScope(); + $this->setScope($this->scopeTypeNormalizer->normalize($scope->getScopeType())); + $this->setScopeCode($scope->getCode()); + $this->setScopeId($scope->getId()); + if ($this->getWebsite() === null) { - $this->setWebsite(''); + $this->setWebsite(StoreScopeInterface::SCOPE_WEBSITES === $this->getScope() ? $scope->getId() : ''); } if ($this->getStore() === null) { - $this->setStore(''); + $this->setStore(StoreScopeInterface::SCOPE_STORES === $this->getScope() ? $scope->getId() : ''); } + } - if ($this->getStore()) { - $scope = 'stores'; - $store = $this->_storeManager->getStore($this->getStore()); - $scopeId = (int)$store->getId(); - $scopeCode = $store->getCode(); - } elseif ($this->getWebsite()) { - $scope = 'websites'; - $website = $this->_storeManager->getWebsite($this->getWebsite()); - $scopeId = (int)$website->getId(); - $scopeCode = $website->getCode(); + /** + * Retrieve scope from initial data + * + * @return ScopeInterface + */ + private function retrieveScope(): ScopeInterface + { + $scopeType = $this->getScope(); + if (!$scopeType) { + switch (true) { + case $this->getStore(): + $scopeType = StoreScopeInterface::SCOPE_STORES; + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite(): + $scopeType = StoreScopeInterface::SCOPE_WEBSITES; + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeType = ScopeInterface::SCOPE_DEFAULT; + $scopeIdentifier = null; + break; + } } else { - $scope = 'default'; - $scopeId = 0; - $scopeCode = ''; + switch (true) { + case $this->getScopeId() !== null: + $scopeIdentifier = $this->getScopeId(); + break; + case $this->getScopeCode() !== null: + $scopeIdentifier = $this->getScopeCode(); + break; + case $this->getStore() !== null: + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite() !== null: + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeIdentifier = null; + break; + } } - $this->setScope($scope); - $this->setScopeId($scopeId); - $this->setScopeCode($scopeCode); + $scope = $this->scopeResolverPool->get($scopeType) + ->getScope($scopeIdentifier); + + return $scope; } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php index 9a483de6a695b..d12569eebe5b2 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php @@ -56,8 +56,9 @@ public function beforeSave() { $value = $this->getValue(); if ($value == 1) { - $customUrl = $this->getData('groups/url/fields/custom/value'); - if (empty($customUrl)) { + $customUrlField = $this->getData('groups/url/fields/custom/value'); + $customUrlConfig = $this->_config->getValue('admin/url/custom'); + if (empty($customUrlField) && empty($customUrlConfig)) { throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.')); } } diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php index b86b86ad3bb8c..25303093ace5d 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php @@ -14,6 +14,8 @@ namespace Magento\Config\Model\Config\Backend\Currency; /** + * Base currency class + * * @api * @since 100.0.2 */ @@ -26,18 +28,19 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value */ protected function _getAllowedCurrencies() { - if (!$this->isFormData() || $this->getData('groups/options/fields/allow/inherit')) { - return explode( + $allowValue = $this->getData('groups/options/fields/allow/value'); + $allowedCurrencies = $allowValue === null || $this->getData('groups/options/fields/allow/inherit') + ? explode( ',', (string)$this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, $this->getScope(), $this->getScopeId() ) - ); - } + ) + : (array) $allowValue; - return (array)$this->getData('groups/options/fields/allow/value'); + return $allowedCurrencies; } /** @@ -71,7 +74,7 @@ protected function _getCurrencyBase() $this->getScopeId() ); } - return strval($value); + return (string)$value; } /** @@ -88,7 +91,7 @@ protected function _getCurrencyDefault() $this->getScopeId() ); } - return strval($value); + return (string)$value; } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php index 3f80e01802b8d..e0e1525fd04cc 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php @@ -59,14 +59,14 @@ public function afterSave() $frequencyMonthly = \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY; $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int)$time[1], # Minute + (int)$time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week ]; - $cronExprString = join(' ', $cronExprArray); + $cronExprString = implode(' ', $cronExprArray); try { /** @var $configValue \Magento\Framework\App\Config\ValueInterface */ diff --git a/app/code/Magento/Config/Model/Config/Backend/Email/Sender.php b/app/code/Magento/Config/Model/Config/Backend/Email/Sender.php index 7350302606503..d9ed5e17f79c9 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Email/Sender.php +++ b/app/code/Magento/Config/Model/Config/Backend/Email/Sender.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * System config email sender field backend model */ @@ -33,7 +31,9 @@ public function beforeSave() } if (strlen($value) > 255) { - throw new \Magento\Framework\Exception\LocalizedException(__('Maximum sender name length is 255. Please correct your settings.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Maximum sender name length is 255. Please correct your settings.') + ); } return $this; } diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php index fa5a12af51fcc..8d9403bef2125 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php +++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php @@ -1,16 +1,14 @@ <?php /** - * Encrypted config field backend model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Model\Config\Backend; /** + * Encrypted config field backend model + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php index 3c36baf6f31f4..163124eae1204 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php +++ b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php @@ -73,13 +73,13 @@ public function afterSave() if ($enabled) { $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int)$time[1], # Minute + (int)$time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week ]; - $cronExprString = join(' ', $cronExprArray); + $cronExprString = implode(' ', $cronExprArray); } else { $cronExprString = ''; } diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php index 3d5713357c39c..4d5da764db470 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php +++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php @@ -52,7 +52,18 @@ protected function _afterLoad() { $value = $this->getValue(); if (!is_array($value)) { - $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + try { + $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + } catch (\Exception $e) { + $this->_logger->critical( + sprintf( + 'Failed to unserialize %s config value. The error is: %s', + $this->getPath(), + $e->getMessage() + ) + ); + $this->setValue(false); + } } } diff --git a/app/code/Magento/Config/Model/Config/Importer.php b/app/code/Magento/Config/Model/Config/Importer.php index 70ffdaec829b2..a54af2ead5048 100644 --- a/app/code/Magento/Config/Model/Config/Importer.php +++ b/app/code/Magento/Config/Model/Config/Importer.php @@ -124,13 +124,15 @@ public function import(array $data) $this->scopeConfig->clean(); } - $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () use ($changedData, $data) { + $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () use ($changedData) { $this->scope->setCurrentScope(Area::AREA_ADMINHTML); // Invoke saving of new values. $this->saveProcessor->process($changedData); - $this->flagManager->saveFlag(static::FLAG_CODE, $data); }); + + $this->scope->setCurrentScope($currentScope); + $this->flagManager->saveFlag(static::FLAG_CODE, $data); } catch (\Exception $e) { throw new InvalidTransitionException(__('%1', $e->getMessage()), $e); } finally { diff --git a/app/code/Magento/Config/Model/Config/Source/Dev/Dbautoup.php b/app/code/Magento/Config/Model/Config/Source/Dev/Dbautoup.php index e1977c74d77fb..2ec9ea0e37326 100644 --- a/app/code/Magento/Config/Model/Config/Source/Dev/Dbautoup.php +++ b/app/code/Magento/Config/Model/Config/Source/Dev/Dbautoup.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Model\Config\Source\Dev; /** @@ -20,9 +18,18 @@ class Dbautoup implements \Magento\Framework\Option\ArrayInterface public function toOptionArray() { return [ - ['value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_ALWAYS, 'label' => __('Always (during development)')], - ['value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_ONCE, 'label' => __('Only Once (version upgrade)')], - ['value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_NEVER, 'label' => __('Never (production)')] + [ + 'value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_ALWAYS, + 'label' => __('Always (during development)'), + ], + [ + 'value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_ONCE, + 'label' => __('Only Once (version upgrade)'), + ], + [ + 'value' => \Magento\Framework\App\ResourceConnection::AUTO_UPDATE_NEVER, + 'label' => __('Never (production)'), + ], ]; } } diff --git a/app/code/Magento/Config/Model/Config/Source/Email/Template.php b/app/code/Magento/Config/Model/Config/Source/Email/Template.php index 04222733418d3..c6b28cd7c46a9 100644 --- a/app/code/Magento/Config/Model/Config/Source/Email/Template.php +++ b/app/code/Magento/Config/Model/Config/Source/Email/Template.php @@ -62,6 +62,12 @@ public function toOptionArray() $templateLabel = $this->_emailConfig->getTemplateLabel($templateId); $templateLabel = __('%1 (Default)', $templateLabel); array_unshift($options, ['value' => $templateId, 'label' => $templateLabel]); + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); return $options; } } diff --git a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php index b3474674cf76d..5beff0d043ade 100644 --- a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php +++ b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Locale currency source - */ namespace Magento\Config\Model\Config\Source\Locale; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\ListsInterface; + /** + * Locale currency source. + * * @api * @since 100.0.2 */ @@ -21,27 +24,70 @@ class Currency implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @var \Magento\Framework\Locale\ListsInterface + * @var ListsInterface */ protected $_localeLists; /** - * @param \Magento\Framework\Locale\ListsInterface $localeLists + * @var ScopeConfigInterface */ - public function __construct(\Magento\Framework\Locale\ListsInterface $localeLists) - { + private $config; + + /** + * @var array + */ + private $installedCurrencies; + + /** + * @param ListsInterface $localeLists + * @param ScopeConfigInterface $config + */ + public function __construct( + ListsInterface $localeLists, + ScopeConfigInterface $config = null + ) { $this->_localeLists = $localeLists; + $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * @return array + * @inheritdoc */ public function toOptionArray() { if (!$this->_options) { $this->_options = $this->_localeLists->getOptionCurrencies(); } - $options = $this->_options; + + $selected = array_flip($this->getInstalledCurrencies()); + + $options = array_filter( + $this->_options, + function ($option) use ($selected) { + return isset($selected[$option['value']]); + } + ); + return $options; } + + /** + * Retrieve Installed Currencies. + * + * @return array + */ + private function getInstalledCurrencies() + { + if (!$this->installedCurrencies) { + $this->installedCurrencies = explode( + ',', + $this->config->getValue( + 'system/currency/installed', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ); + } + + return $this->installedCurrencies; + } } diff --git a/app/code/Magento/Config/Model/Config/Source/Nooptreq.php b/app/code/Magento/Config/Model/Config/Source/Nooptreq.php index 03fe5ca2abccc..1c9eb801dfec7 100644 --- a/app/code/Magento/Config/Model/Config/Source/Nooptreq.php +++ b/app/code/Magento/Config/Model/Config/Source/Nooptreq.php @@ -11,15 +11,19 @@ */ class Nooptreq implements \Magento\Framework\Option\ArrayInterface { + const VALUE_NO = ''; + const VALUE_OPTIONAL = 'opt'; + const VALUE_REQUIRED = 'req'; + /** * @return array */ public function toOptionArray() { return [ - ['value' => '', 'label' => __('No')], - ['value' => 'opt', 'label' => __('Optional')], - ['value' => 'req', 'label' => __('Required')] + ['value' => self::VALUE_NO, 'label' => __('No')], + ['value' => self::VALUE_OPTIONAL, 'label' => __('Optional')], + ['value' => self::VALUE_REQUIRED, 'label' => __('Required')] ]; } } diff --git a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php index 115a372e6150a..c60d634210339 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php +++ b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php @@ -11,7 +11,8 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api - * @since 100.2.0 + * @deprecated class location was changed + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction */ class ConcealInProductionConfigList implements ElementVisibilityInterface { @@ -54,7 +55,7 @@ public function __construct(State $state, array $configs = []) /** * @inheritdoc - * @since 100.2.0 + * @deprecated */ public function isHidden($path) { @@ -66,7 +67,7 @@ public function isHidden($path) /** * @inheritdoc - * @since 100.2.0 + * @deprecated */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php index d277e24857659..8003132d2b822 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Element/Group.php +++ b/app/code/Magento/Config/Model/Config/Structure/Element/Group.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Model\Config\Structure\Element; /** diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php new file mode 100755 index 0000000000000..d5ded9292864a --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\ElementVisibility; + +use Magento\Config\Model\Config\Structure\ElementVisibilityInterface; +use Magento\Framework\App\State; + +/** + * Defines status of visibility of form elements on Stores > Settings > Configuration page + * in Admin Panel in Production mode. + * @api + */ +class ConcealInProduction implements ElementVisibilityInterface +{ + /** + * The list of form element paths with concrete visibility status. + * + * E.g. + * + * ```php + * [ + * 'general/locale/code' => ElementVisibilityInterface::DISABLED, + * 'general/country' => ElementVisibilityInterface::HIDDEN, + * ]; + * ``` + * + * It means that: + * - field Locale (in group Locale Options in section General) will be disabled + * - group Country Options (in section General) will be hidden + * + * @var array + */ + private $configs = []; + + /** + * The object that has information about the state of the system. + * + * @var State + */ + private $state; + + /** + * + * The list of form element paths which ignore visibility status. + * + * E.g. + * + * ```php + * [ + * 'general/country/default' => '', + * ]; + * ``` + * + * It means that: + * - field 'default' in group Country Options (in section General) will be showed, even if all group(section) + * will be hidden. + * + * @var array + */ + private $exemptions = []; + + /** + * @param State $state The object that has information about the state of the system + * @param array $configs The list of form element paths with concrete visibility status. + * @param array $exemptions The list of form element paths which ignore visibility status. + */ + public function __construct(State $state, array $configs = [], array $exemptions = []) + { + $this->state = $state; + $this->configs = $configs; + $this->exemptions = $exemptions; + } + + /** + * @inheritdoc + * @since 100.2.0 + */ + public function isHidden($path) + { + $path = $this->normalizePath($path); + if ($this->state->getMode() === State::MODE_PRODUCTION + && preg_match('/(?<group>(?<section>.*?)\/.*?)\/.*?/', $path, $match)) { + $group = $match['group']; + $section = $match['section']; + $exemptions = array_keys($this->exemptions); + $checkedItems = []; + foreach ([$path, $group, $section] as $itemPath) { + $checkedItems[] = $itemPath; + if (!empty($this->configs[$itemPath])) { + return $this->configs[$itemPath] === static::HIDDEN + && empty(array_intersect($checkedItems, $exemptions)); + } + } + } + + return false; + } + + /** + * @inheritdoc + * @since 100.2.0 + */ + public function isDisabled($path) + { + $path = $this->normalizePath($path); + if ($this->state->getMode() === State::MODE_PRODUCTION) { + while (true) { + if (!empty($this->configs[$path])) { + return $this->configs[$path] === static::DISABLED; + } + + $position = strripos($path, '/'); + if ($position === false) { + break; + } + $path = substr($path, 0, $position); + } + } + + return false; + } + + /** + * Returns normalized path. + * + * @param string $path The path to be normalized + * @return string The normalized path + */ + private function normalizePath($path) + { + return trim($path, '/'); + } +} diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php new file mode 100755 index 0000000000000..29148a244dcc6 --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\ElementVisibility; + +use Magento\Config\Model\Config\Structure\ElementVisibilityInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\ConfigOptionsListConstants as Constants; + +/** + * Defines status of visibility of form elements on Stores > Settings > Configuration page + * when Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION is enabled + * otherwise rule from Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction is used + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * + * @api + */ +class ConcealInProductionWithoutScdOnDemand implements ElementVisibilityInterface +{ + /** + * @var ConcealInProduction Element visibility rules in the Production mode + */ + private $concealInProduction; + + /** + * @var DeploymentConfig The application deployment configuration + */ + private $deploymentConfig; + + /** + * @param ConcealInProductionFactory $concealInProductionFactory + * @param DeploymentConfig $deploymentConfig Deployment configuration reader + * @param array $configs The list of form element paths with concrete visibility status. + * @param array $exemptions The list of form element paths which ignore visibility status. + */ + public function __construct( + ConcealInProductionFactory $concealInProductionFactory, + DeploymentConfig $deploymentConfig, + array $configs = [], + array $exemptions = [] + ) { + $this->concealInProduction = $concealInProductionFactory + ->create(['configs' => $configs, 'exemptions' => $exemptions]); + $this->deploymentConfig = $deploymentConfig; + } + + /** + * @inheritdoc + */ + public function isHidden($path): bool + { + if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { + return $this->concealInProduction->isHidden($path); + } + return false; + } + + /** + * @inheritdoc + */ + public function isDisabled($path): bool + { + if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { + return $this->concealInProduction->isDisabled($path); + } + return false; + } +} diff --git a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php index f6f3a0be187a3..e615678550108 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php +++ b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php @@ -55,17 +55,13 @@ protected function _cmp($elementA, $elementB) { $sortIndexA = 0; if ($this->_hasValue('sortOrder', $elementA)) { - $sortIndexA = floatval($elementA['sortOrder']); + $sortIndexA = (float)$elementA['sortOrder']; } $sortIndexB = 0; if ($this->_hasValue('sortOrder', $elementB)) { - $sortIndexB = floatval($elementB['sortOrder']); + $sortIndexB = (float)$elementB['sortOrder']; } - if ($sortIndexA == $sortIndexB) { - return 0; - } - - return $sortIndexA < $sortIndexB ? -1 : 1; + return $sortIndexA <=> $sortIndexB; } } diff --git a/app/code/Magento/Config/Model/Config/Structure/Reader.php b/app/code/Magento/Config/Model/Config/Structure/Reader.php index 5916649588bcb..c83c2e1ae1320 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Reader.php +++ b/app/code/Magento/Config/Model/Config/Structure/Reader.php @@ -124,6 +124,7 @@ protected function _readFiles($fileList) * Processing nodes of the document before merging * * @param string $content + * @throws \Magento\Framework\Config\Dom\ValidationException * @return string */ protected function processingDocument($content) @@ -131,7 +132,12 @@ protected function processingDocument($content) $object = new DataObject(); $document = new \DOMDocument(); - $document->loadXML($content); + try { + $document->loadXML($content); + } catch (\Exception $e) { + throw new \Magento\Framework\Config\Dom\ValidationException($e->getMessage()); + } + $this->compiler->compile($document->documentElement, $object, $object); return $document->saveXML(); diff --git a/app/code/Magento/Config/Model/ResourceModel/Config.php b/app/code/Magento/Config/Model/ResourceModel/Config.php index d8ea2410ce860..594a9df719daa 100644 --- a/app/code/Magento/Config/Model/ResourceModel/Config.php +++ b/app/code/Magento/Config/Model/ResourceModel/Config.php @@ -5,6 +5,8 @@ */ namespace Magento\Config\Model\ResourceModel; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** * Core Resource Resource Model * @@ -34,7 +36,7 @@ protected function _construct() * @param int $scopeId * @return $this */ - public function saveConfig($path, $value, $scope, $scopeId) + public function saveConfig($path, $value, $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeId = 0) { $connection = $this->getConnection(); $select = $connection->select()->from( @@ -70,7 +72,7 @@ public function saveConfig($path, $value, $scope, $scopeId) * @param int $scopeId * @return $this */ - public function deleteConfig($path, $scope, $scopeId) + public function deleteConfig($path, $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeId = 0) { $connection = $this->getConnection(); $connection->delete( diff --git a/app/code/Magento/Config/Setup/ConfigOptionsList.php b/app/code/Magento/Config/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..45e3987d282f1 --- /dev/null +++ b/app/code/Magento/Config/Setup/ConfigOptionsList.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigDataFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; + +/** + * Deployment configuration options required for the Config module. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + /** + * Input key for the option. + */ + const INPUT_KEY_DEBUG_LOGGING = 'enable-debug-logging'; + + /** + * Path to the value in the deployment config. + */ + const CONFIG_PATH_DEBUG_LOGGING = 'dev/debug/debug_logging'; + + /** + * @var ConfigDataFactory + */ + private $configDataFactory; + + /** + * @param ConfigDataFactory $configDataFactory + */ + public function __construct(ConfigDataFactory $configDataFactory) + { + $this->configDataFactory = $configDataFactory; + } + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_DEBUG_LOGGING, + SelectConfigOption::FRONTEND_WIZARD_RADIO, + [true, false, 1, 0], + self::CONFIG_PATH_DEBUG_LOGGING, + 'Enable debug logging' + ) + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $config = []; + if (isset($options[self::INPUT_KEY_DEBUG_LOGGING])) { + $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV); + if ($options[self::INPUT_KEY_DEBUG_LOGGING] === 'true' + || $options[self::INPUT_KEY_DEBUG_LOGGING] === '1') { + $value = 1; + } else { + $value = 0; + } + $configData->set(self::CONFIG_PATH_DEBUG_LOGGING, $value); + $config[] = $configData; + } + + return $config; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + return []; + } +} diff --git a/app/code/Magento/Config/Setup/InstallSchema.php b/app/code/Magento/Config/Setup/InstallSchema.php index 2fd267f0dacc7..e832e63e64a9e 100644 --- a/app/code/Magento/Config/Setup/InstallSchema.php +++ b/app/code/Magento/Config/Setup/InstallSchema.php @@ -59,6 +59,12 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con '64k', [], 'Config Value' + )->addColumn( + 'updated_at', + \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE], + 'Updated At' )->addIndex( $setup->getIdxName( 'core_config_data', diff --git a/app/code/Magento/Config/Setup/UpgradeSchema.php b/app/code/Magento/Config/Setup/UpgradeSchema.php new file mode 100644 index 0000000000000..db162ea1fb0bf --- /dev/null +++ b/app/code/Magento/Config/Setup/UpgradeSchema.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Config\Setup; + +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; + +/** + * Upgrade the AsynchronousOperations module DB scheme + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * {@inheritdoc} + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + if (version_compare($context->getVersion(), '2.1.0', '<')) { + $this->addUpdatedAtField($setup); + } + $setup->endSetup(); + } + + /** + * Add updated at column + * + * @param SchemaSetupInterface $setup + * @return $this + */ + protected function addUpdatedAtField(SchemaSetupInterface $setup) + { + if (!($setup->getConnection()->tableColumnExists( + $setup->getTable('core_config_data'), + 'updated_at' + ))) { + $setup->getConnection()->addColumn( + $setup->getTable('core_config_data'), + 'updated_at', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + 'length' => null, + 'nullable' => false, + 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE, + 'comment' => 'Updated At' + ] + ); + } + + return $this; + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml new file mode 100644 index 0000000000000..bd23292d3ee6a --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveConfigActionGroup"> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="clickSaveConfigBtn"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminWebsiteCountryOptionsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminWebsiteCountryOptionsActionGroup.xml new file mode 100644 index 0000000000000..d80736529173f --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminWebsiteCountryOptionsActionGroup.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AllowOnlyOneCountryActionGroup"> + <arguments> + <argument name="country" type="string"/> + </arguments> + <conditionalClick selector="{{AdminConfigurationGeneralCountryOptionsSection.countryOptions}}" dependentSelector="{{AdminConfigurationGeneralCountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="openCountryOptionsTab"/> + <waitForElementVisible selector="{{AdminConfigurationGeneralCountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible"/> + <uncheckOption selector="{{AdminConfigurationGeneralCountryOptionsSection.generalCountryDefaultInherit}}" stepKey="uncheckDefaultCountryInheritCheckbox"/> + <selectOption selector="{{AdminConfigurationGeneralCountryOptionsSection.generalCountryDefault}}" userInput="{{country}}" stepKey="chooseDefaultCountry"/> + <uncheckOption selector="{{AdminConfigurationGeneralCountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckAllowInheritCheckbox"/> + <selectOption selector="{{AdminConfigurationGeneralCountryOptionsSection.allowedCountries}}" userInput="{{country}}" stepKey="chooseAllowedCountries"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AllowAllCountriesExceptOneActionGroup" extends="AllowOnlyOneCountryActionGroup"> + <remove keyForRemoval="uncheckDefaultCountryInheritCheckbox"/> + <remove keyForRemoval="chooseDefaultCountry"/> + <remove keyForRemoval="chooseAllowedCountries"/> + <unselectOption selector="{{AdminConfigurationGeneralCountryOptionsSection.allowedCountries}}" userInput="{{country}}" after="uncheckAllowInheritCheckbox" stepKey="unselectCountry"/> + </actionGroup> + <actionGroup name="SetWebsiteCountryOptionsToDefaultActionGroup"> + <conditionalClick selector="{{AdminConfigurationGeneralCountryOptionsSection.countryOptions}}" dependentSelector="{{AdminConfigurationGeneralCountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="openCountryOptionsTab"/> + <waitForElementVisible selector="{{AdminConfigurationGeneralCountryOptionsSection.topDestinations}}" stepKey="waitCheckboxToBeVisible"/> + <checkOption selector="{{AdminConfigurationGeneralCountryOptionsSection.generalCountryAllowInherit}}" stepKey="setAllowInheritToDefault"/> + <checkOption selector="{{AdminConfigurationGeneralCountryOptionsSection.generalCountryDefaultInherit}}" stepKey="setDefaultCountryInheritToDefault"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml new file mode 100644 index 0000000000000..f563cdd3e1288 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetTaxClassForShipping"> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForAdminSalesTaxClassPageLoad"/> + <conditionalClick selector="{{AdminSalesTaxConfigSection.taxClassesTab}}" dependentSelector="{{AdminSalesTaxConfigSection.checkIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{AdminSalesTaxConfigSection.shippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <uncheckOption selector="{{AdminSalesTaxConfigSection.enableTaxClassForShipping}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{AdminSalesTaxConfigSection.shippingTaxClass}}" userInput="Taxable Goods" stepKey="setShippingTaxClass"/> + <click selector="{{AdminSalesTaxConfigSection.taxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + </actionGroup> + <actionGroup name="ResetTaxClassForShipping"> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxConfigPagetoReset"/> + <waitForPageLoad stepKey="waitForAdminSalesTaxClassPageLoad2"/> + <conditionalClick selector="{{AdminSalesTaxConfigSection.taxClassesTab}}" dependentSelector="{{AdminSalesTaxConfigSection.checkIfTaxClassesTabExpand}}" visible="true" stepKey="openTaxClassTab"/> + <waitForElementVisible selector="{{AdminSalesTaxConfigSection.shippingTaxClass}}" stepKey="seeShippingTaxClass2"/> + <selectOption selector="{{AdminSalesTaxConfigSection.shippingTaxClass}}" userInput="None" stepKey="resetShippingTaxClass"/> + <checkOption selector="{{AdminSalesTaxConfigSection.enableTaxClassForShipping}}" stepKey="useSystemValue"/> + <click selector="{{AdminSalesTaxConfigSection.taxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml new file mode 100644 index 0000000000000..5d8fe5489df05 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWebUrlOptionsActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EnableWebUrlOptions"> + <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{WebSection.UrlOptionsTab}}" dependentSelector="{{WebSection.CheckIfUrlOptionsTabExpand}}" visible="true" stepKey="expandUrlSectionTab"/> + <waitForElementVisible selector="{{UrlOptionsSection.addStoreCodeToUrl}}" stepKey="seeAddStoreCodeToUrl"/> + <uncheckOption selector="{{UrlOptionsSection.systemValueForStoreCode}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{UrlOptionsSection.addStoreCodeToUrl}}" userInput="Yes" stepKey="enableStoreCode"/> + <click selector="{{WebSection.UrlOptionsTab}}" stepKey="collapseUrlOptions"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + </actionGroup> + <actionGroup name="ResetWebUrlOptions"> + <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPagetoReset"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <conditionalClick selector="{{WebSection.UrlOptionsTab}}" dependentSelector="{{WebSection.CheckIfUrlOptionsTabExpand}}" visible="true" stepKey="closeUrlSectionTab"/> + <waitForElementVisible selector="{{UrlOptionsSection.addStoreCodeToUrl}}" stepKey="seeAddStoreCodeToUrl2"/> + <!--<uncheckOption selector="{{UrlOptionsSection.systemValueForStoreCode}}" stepKey="uncheckUseSystemValue"/>--> + <selectOption selector="{{UrlOptionsSection.addStoreCodeToUrl}}" userInput="No" stepKey="enableStoreCode"/> + <checkOption selector="{{UrlOptionsSection.systemValueForStoreCode}}" stepKey="checkUseSystemValue"/> + <click selector="{{WebSection.UrlOptionsTab}}" stepKey="collapseUrlOptions"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/Data/DeveloperOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/DeveloperOptionsData.xml new file mode 100644 index 0000000000000..bd4bd69d84669 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/DeveloperOptionsData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableTranslateInlineForStorefront"> + <data key="path">dev/translate_inline/active</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + + <entity name="DisableTranslateInlineForStorefront"> + <!-- Default value --> + <data key="path">dev/translate_inline/active</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml new file mode 100644 index 0000000000000..e998730d11ae7 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SetLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">LocaleOptionsFrance</requiredEntity> + </entity> + <entity name="LocaleOptionsFrance" type="code"> + <data key="value">fr_FR</data> + </entity> + + <entity name="DefaultLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">LocaleOptionsUSA</requiredEntity> + </entity> + <entity name="LocaleOptionsUSA" type="code"> + <data key="value">en_US</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml new file mode 100644 index 0000000000000..5010e383ba55c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">DefaultConfigWebUrlOptions</requiredEntity> + </entity> + <entity name="DefaultConfigWebUrlOptions" type="url_use_store_value"> + <data key="value">0</data> + </entity> + + <entity name="EnableWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">WebUrlOptionsYes</requiredEntity> + </entity> + <entity name="WebUrlOptionsYes" type="url_use_store_value"> + <data key="value">1</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/LICENSE.txt b/app/code/Magento/Config/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Config/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 <insert your license name here>" 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/app/code/Magento/Config/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Config/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Config/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 <insert your license name here>" 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/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml new file mode 100644 index 0000000000000..6398d51cda916 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="GeneralLocaleOptionsConfig" dataType="locale_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="locale_options_config"> + <object key="locale" dataType="locale_options_config"> + <object key="fields" dataType="locale_options_config"> + <object key="code" dataType="code"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml new file mode 100644 index 0000000000000..58809f8b41e28 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="WebUrlOptionsConfig" dataType="web_url_use_store" type="create" auth="adminFormKey" url="/admin/system_config/save/section/web/" + method="POST" successRegex="/messages-message-success/" returnRegex=""> + <object key="groups" dataType="web_url_use_store"> + <object key="url" dataType="web_url_use_store"> + <object key="fields" dataType="web_url_use_store"> + <object key="use_store" dataType="url_use_store_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml new file mode 100644 index 0000000000000..6b3c6e1a40871 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchConfigurationPage" url="admin/system_config/edit/section/catalog/" area="admin" module="Magento_Config"> + <section name="AdminCatalogSearchEngineConfigurationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml new file mode 100644 index 0000000000000..90b4e6d0bdaa9 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminConfigPage" url="admin/system_config/" area="admin" module="Magento_Config"> + <section name="AdminConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml new file mode 100644 index 0000000000000..35903fe33d06c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigurationGeneralSectionPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminConfigurationGeneralSectionPage" url="admin/system_config/edit/section/general/{{group_anchor}}" parameterized="true" area="admin" module="Magento_Config"> + <!--Will be extended in other modules--> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml new file mode 100644 index 0000000000000..1a99ff6533dbb --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSalesConfigPage" url="admin/system_config/edit/section/sales/{{var1}}" area="admin" parameterized="true" module="Magento_Config"> + <section name="AdminSalesConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminSalesTaxClassPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesTaxClassPage.xml new file mode 100644 index 0000000000000..e790d89888526 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminSalesTaxClassPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSalesTaxClassPage" url="admin/system_config/edit/section/tax/" area="admin" module="Magento_Config"> + <section name="AdminSalesTaxConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/README.md b/app/code/Magento/Config/Test/Mftf/README.md new file mode 100644 index 0000000000000..060168a5fa643 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Config Functional Tests + +The Functional Test Module for **Magento Config** module. diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.xml new file mode 100644 index 0000000000000..2cde11434b26d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogSearchEngineConfigurationSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchEngineConfigurationSection"> + <element name="searchEngineOptimization" type="button" selector="#catalog_seo-head"/> + <element name="openedEngineOptimization" type="button" selector="#catalog_seo-head.open"/> + <element name="systemValueUseCategoriesPath" type="checkbox" selector="#catalog_seo_product_use_categories_inherit"/> + <element name="selectUseCategoriesPatForProductUrls" type="select" selector="#catalog_seo_product_use_categories"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml new file mode 100644 index 0000000000000..4099b55157709 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminConfigSection"> + <element name="saveButton" type="button" selector="#save" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml new file mode 100644 index 0000000000000..62b5cdcf9ceec --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSalesConfigSection"> + <element name="enableMAPUseSystemValue" type="checkbox" selector="#sales_msrp_enabled_inherit"/> + <element name="enableMAPSelect" type="select" selector="#sales_msrp_enabled"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesTaxConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesTaxConfigSection.xml new file mode 100644 index 0000000000000..0db4a8a910c27 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesTaxConfigSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminSalesTaxConfigSection"> + <element name="taxClassesTab" type="button" selector="#tax_classes-head"/> + <element name="checkIfTaxClassesTabExpand" type="button" selector="#tax_classes-head:not(.open)"/> + <element name="shippingTaxClass" type="select" selector="#tax_classes_shipping_tax_class"/> + <element name="enableTaxClassForShipping" type="checkbox" selector="#tax_classes_shipping_tax_class_inherit"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml new file mode 100644 index 0000000000000..409a9222eebec --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="ContentManagementSection"> + <element name="WYSIWYGOptions" type="button" selector="#cms_wysiwyg-head"/> + <element name="CheckIfTabExpand" type="button" selector="#cms_wysiwyg-head:not(.open)"/> + <element name="EnableSystemValue" type="button" selector="#cms_wysiwyg_enabled_inherit"/> + <element name="EnableWYSIWYG" type="button" selector="#cms_wysiwyg_enabled"/> + <element name="SwitcherSystemValue" type="button" selector="#cms_wysiwyg_editor_inherit"/> + <element name="Switcher" type="button" selector="#cms_wysiwyg_editor" /> + <element name="Save" type="button" selector="#save" timeout="30"/> + </section> + <section name="WebSection"> + <element name="DefaultLayoutsTab" type="button" selector="#web_default_layouts-head"/> + <element name="CheckIfTabExpand" type="button" selector="#web_default_layouts-head:not(.open)"/> + <element name="UrlOptionsTab" type="button" selector="#web_url-head"/> + <element name="CheckIfUrlOptionsTabExpand" type="button" selector="#web_url-head:not(.open)"/> + </section> + <section name="DefaultLayoutsSection"> + <element name="productLayout" type="select" selector="#web_default_layouts_default_product_layout"/> + <element name="categoryLayout" type="select" selector="#web_default_layouts_default_category_layout"/> + <element name="pageLayout" type="select" selector="#web_default_layouts_default_cms_layout"/> + </section> + <section name="UrlOptionsSection"> + <element name="addStoreCodeToUrl" type="select" selector="#web_url_use_store"/> + <element name="systemValueForStoreCode" type="checkbox" selector="#web_url_use_store_inherit"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml new file mode 100644 index 0000000000000..fd47524de8620 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/StoreConfigSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StoreConfigSection"> + <element name="CheckIfTabExpand" type="button" selector="#general_store_information-head:not(.open)"/> + <element name="StoreInformation" type="button" selector="#general_store_information-head"/> + <element name="City" type="input" selector="#general_store_information_city"/> + <element name="Save" type="button" selector="#save"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php index 40aa110382ede..9ca4e6138babe 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php @@ -11,6 +11,8 @@ use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Serialize\Serializer\Sensitive; +use Magento\Framework\Serialize\Serializer\SensitiveFactory; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; use Magento\Config\App\Config\Type\System\Reader; @@ -74,22 +76,34 @@ public function setUp() ->getMockForAbstractClass(); $this->preProcessor = $this->getMockBuilder(PreProcessorInterface::class) ->getMockForAbstractClass(); - $this->serializer = $this->getMockBuilder(SerializerInterface::class) + $this->serializer = $this->getMockBuilder(Sensitive::class) + ->disableOriginalConstructor() ->getMock(); $this->reader = $this->getMockBuilder(Reader::class) ->disableOriginalConstructor() ->getMock(); + $sensitiveFactory = $this->getMockBuilder(SensitiveFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $sensitiveFactory->expects($this->any()) + ->method('create') + ->willReturn($this->serializer); + /** @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject $serializerMock */ + $serializerMock = $this->getMockBuilder(SerializerInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->configType = new System( $this->source, $this->postProcessor, $this->fallback, $this->cache, - $this->serializer, + $serializerMock, $this->preProcessor, 1, 'system', - $this->reader + $this->reader, + $sensitiveFactory ); } @@ -133,49 +147,4 @@ public function testGetCachedWithLoadAllData() ->willReturn($data); $this->assertEquals($data, $this->configType->get('')); } - - public function testGetNotCached() - { - $path = 'stores/default/dev/unsecure/url'; - $url = 'http://magento.test/'; - - $dataToCache = [ - 'unsecure' => [ - 'url' => $url - ] - ]; - $data = [ - 'default' => [], - 'websites' => [], - 'stores' => [ - 'default' => [ - 'dev' => [ - 'unsecure' => [ - 'url' => $url - ] - ] - ] - ] - ]; - $this->cache->expects($this->any()) - ->method('load') - ->willReturnOnConsecutiveCalls(false, false); - - $this->serializer->expects($this->atLeastOnce()) - ->method('serialize') - ->willReturn(serialize($dataToCache)); - $this->cache->expects($this->atLeastOnce()) - ->method('save') - ->willReturnSelf(); - $this->reader->expects($this->once()) - ->method('read') - ->willReturn($data); - $this->postProcessor->expects($this->once()) - ->method('process') - ->with($data) - ->willReturn($data); - - $this->assertEquals($url, $this->configType->get($path)); - $this->assertEquals($url, $this->configType->get($path)); - } } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/DwstreeTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/DwstreeTest.php index d3750022d93de..1cb393b212199 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/DwstreeTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/DwstreeTest.php @@ -121,6 +121,9 @@ public function testInitTabs($section, $website, $store) ); } + /** + * @return array + */ public function initTabsDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php index de18d45d26864..011bcfee64af5 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php @@ -11,6 +11,11 @@ class FileTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Config\Block\System\Config\Form\Field\File */ @@ -24,6 +29,8 @@ class FileTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->testData = [ 'before_element_html' => 'test_before_element_html', @@ -40,7 +47,10 @@ protected function setUp() $this->file = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\File::class, - ['data' => $this->testData] + [ + 'escaper' => $this->escaperMock, + 'data' => $this->testData + ] ); $formMock = new \Magento\Framework\DataObject(); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index d0648ab008234..f64c68413f922 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -4,15 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** - * Tests for \Magento\Framework\Data\Form\Field\Image + * Test for \Magento\Framework\Data\Form\Field\Image. */ namespace Magento\Config\Test\Unit\Block\System\Config\Form\Field; class ImageTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Framework\Url|\PHPUnit_Framework_MockObject_MockObject */ @@ -31,10 +34,13 @@ class ImageTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->urlBuilderMock = $this->createMock(\Magento\Framework\Url::class); $this->image = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\Image::class, [ + 'escaper' => $this->escaperMock, 'urlBuilder' => $this->urlBuilderMock, ] ); @@ -54,6 +60,8 @@ protected function setUp() } /** + * Get element with value and check data. + * * @covers \Magento\Config\Block\System\Config\Form\Field\Image::_getUrl */ public function testGetElementHtmlWithValue() @@ -74,7 +82,7 @@ public function testGetElementHtmlWithValue() 'showInWebsite' => '1', 'showInStore' => '1', 'label' => null, - 'backend_model' => \Magento\BackendModelConfig\Backend\Image::class, + 'backend_model' => \Magento\Config\Model\Config\Backend\Image::class, 'upload_dir' => [ 'config' => 'system/filesystem/media', 'scope_info' => '1', @@ -87,13 +95,24 @@ public function testGetElementHtmlWithValue() ], '_elementType' => 'field', 'path' => 'catalog/placeholder', - ]); + ] + ); $expectedHtmlId = $this->testData['html_id_prefix'] . $this->testData['html_id'] . $this->testData['html_id_suffix']; + $this->escaperMock->expects($this->once()) + ->method('escapeUrl') + ->with($url . $this->testData['path'] . '/' . $this->testData['value']) + ->willReturn($url . $this->testData['path'] . '/' . $this->testData['value']); + $this->escaperMock->expects($this->exactly(3)) + ->method('escapeHtmlAttr') + ->with($this->testData['value']) + ->willReturn($this->testData['value']); + $this->escaperMock->expects($this->atLeastOnce())->method('escapeHtml')->willReturn($expectedHtmlId); $html = $this->image->getElementHtml(); + $this->assertContains('class="input-file"', $html); $this->assertContains('<input', $html); $this->assertContains('type="file"', $html); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/RegexceptionsTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/RegexceptionsTest.php index 4f53f1072e035..0b4d5f7ef15f7 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/RegexceptionsTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/RegexceptionsTest.php @@ -128,7 +128,8 @@ public function testRenderCellTemplateWrongColumnName() $this->object->addColumn($wrongColumnName, $this->cellParameters); - $this->expectException('\Exception', 'Wrong column name specified.'); + $this->expectException('\Exception'); + $this->expectExceptionMessage('Wrong column name specified.'); $this->object->renderCellTemplate($columnName); } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index be3b8e2ead0c1..3799136aea9c0 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -7,6 +7,11 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific */ @@ -20,8 +25,11 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->_object = $testHelper->getObject( - \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class + \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class, + ['escaper' => $this->escaperMock] ); $this->_object->setData('html_id', 'spec_element'); $this->_formMock = $this->createPartialMock( @@ -85,6 +93,9 @@ public function testGetHtmlWhenValueIsEmpty($value) $this->assertNotEmpty($this->_object->getHtml()); } + /** + * @return array + */ public function getHtmlWhenValueIsEmptyDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php index 9d76363213d0b..bb109bcb25f06 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php @@ -224,6 +224,9 @@ public function testRender($expanded, $nested, $extra) } } + /** + * @return array + */ public function renderDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php index 72e8386bddaf6..771f5e0ecc31d 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Test\Unit\Block\System\Config; use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker; @@ -100,9 +98,14 @@ protected function setUp() $this->_urlModelMock = $this->createMock(\Magento\Backend\Model\Url::class); $configFactoryMock = $this->createMock(\Magento\Config\Model\Config\Factory::class); $this->_formFactoryMock = $this->createPartialMock(\Magento\Framework\Data\FormFactory::class, ['create']); - $this->_fieldsetFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Fieldset\Factory::class); + $this->_fieldsetFactoryMock = $this->createMock( + \Magento\Config\Block\System\Config\Form\Fieldset\Factory::class + ); $this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class); $this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $settingCheckerMock = $this->getMockBuilder(SettingChecker::class) + ->disableOriginalConstructor() + ->getMock(); $this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); @@ -124,7 +127,10 @@ protected function setUp() $this->returnValue(['section1/group1/field1' => 'some_value']) ); - $this->_formMock = $this->createPartialMock(\Magento\Framework\Data\Form::class, ['setParent', 'setBaseUrl', 'addFieldset']); + $this->_formMock = $this->createPartialMock( + \Magento\Framework\Data\Form::class, + ['setParent', 'setBaseUrl', 'addFieldset'] + ); $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) ->getMockForAbstractClass(); @@ -150,6 +156,7 @@ protected function setUp() 'fieldsetFactory' => $this->_fieldsetFactoryMock, 'fieldFactory' => $this->_fieldFactoryMock, 'context' => $context, + 'settingChecker' => $settingCheckerMock, ]; $objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data); @@ -224,6 +231,9 @@ public function testInitForm($sectionIsVisible) $this->assertEquals($this->_formMock, $object->getForm()); } + /** + * @return array + */ public function initFormDataProvider() { return [ @@ -337,6 +347,9 @@ public function testInitGroup($shouldCloneFields, $prefixes, $callNum) $object->initForm(); } + /** + * @return array + */ public function initGroupDataProvider() { return [ @@ -523,7 +536,7 @@ public function testInitFields( $elementVisibilityMock = $this->getMockBuilder(ElementVisibilityInterface::class) ->getMockForAbstractClass(); - $elementVisibilityMock->expects($this->once()) + $elementVisibilityMock->expects($this->any()) ->method('isDisabled') ->willReturn($isDisabled); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ConfigSetProcessorFactoryTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ConfigSetProcessorFactoryTest.php index 1fa0310ca62eb..a8f40106eb564 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ConfigSetProcessorFactoryTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ConfigSetProcessorFactoryTest.php @@ -40,7 +40,7 @@ protected function setUp() $this->model = new ConfigSetProcessorFactory( $this->objectManagerMock, [ - ConfigSetProcessorFactory::TYPE_LOCK => LockProcessor::class, + ConfigSetProcessorFactory::TYPE_LOCK_ENV => LockProcessor::class, ConfigSetProcessorFactory::TYPE_DEFAULT => DefaultProcessor::class, 'wrongType' => \stdClass::class, ] @@ -58,7 +58,7 @@ public function testCreate() $this->assertInstanceOf( ConfigSetProcessorInterface::class, - $this->model->create(ConfigSetProcessorFactory::TYPE_LOCK) + $this->model->create(ConfigSetProcessorFactory::TYPE_LOCK_ENV) ); } diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php index 066b0fbe84b50..b5665aa8ae7d0 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php @@ -7,13 +7,14 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSet\DefaultProcessor; +use Magento\Config\Model\Config; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Store\Model\ScopeInterface; use Magento\Config\Model\PreparedValueFactory; use Magento\Framework\App\Config\Value; -use Magento\Framework\App\Config\ValueInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use PHPUnit_Framework_MockObject_MockObject as Mock; @@ -55,17 +56,18 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase */ private $resourceModelMock; + /** + * @var ConfigFactory|Mock + */ + private $configFactory; + /** * @inheritdoc */ protected function setUp() { - $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->configPathResolverMock = $this->getMockBuilder(ConfigPathResolver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->configPathResolverMock = $this->createMock(ConfigPathResolver::class); $this->resourceModelMock = $this->getMockBuilder(AbstractDb::class) ->disableOriginalConstructor() ->setMethods(['save']) @@ -74,14 +76,14 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getResource']) ->getMock(); - $this->preparedValueFactoryMock = $this->getMockBuilder(PreparedValueFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $this->preparedValueFactoryMock = $this->createMock(PreparedValueFactory::class); + $this->configFactory = $this->createMock(ConfigFactory::class); $this->model = new DefaultProcessor( $this->preparedValueFactoryMock, $this->deploymentConfigMock, - $this->configPathResolverMock + $this->configPathResolverMock, + $this->configFactory ); } @@ -97,16 +99,16 @@ protected function setUp() public function testProcess($path, $value, $scope, $scopeCode) { $this->configMockForProcessTest($path, $scope, $scopeCode); - - $this->preparedValueFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->valueMock); - $this->valueMock->expects($this->once()) - ->method('getResource') - ->willReturn($this->resourceModelMock); - $this->resourceModelMock->expects($this->once()) + $scopeData = ['scope' => $scope, 'scope_code' => $scopeCode]; + + $config = $this->createMock(Config::class); + $this->configFactory->method('create') + ->with(['data' => $scopeData]) + ->willReturn($config); + $config->method('setDataByPath') + ->with($path, $value); + $config->expects($this->once()) ->method('save') - ->with($this->valueMock) ->willReturnSelf(); $this->model->process($path, $value, $scope, $scopeCode); @@ -124,28 +126,6 @@ public function processDataProvider() ]; } - public function testProcessWithWrongValueInstance() - { - $path = 'test/test/test'; - $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT; - $scopeCode = null; - $value = 'value'; - $valueInterfaceMock = $this->getMockBuilder(ValueInterface::class) - ->getMockForAbstractClass(); - - $this->configMockForProcessTest($path, $scope, $scopeCode); - - $this->preparedValueFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($valueInterfaceMock); - $this->valueMock->expects($this->never()) - ->method('getResource'); - $this->resourceModelMock->expects($this->never()) - ->method('save'); - - $this->model->process($path, $value, $scope, $scopeCode); - } - /** * @param string $path * @param string $scope @@ -166,7 +146,9 @@ private function configMockForProcessTest($path, $scope, $scopeCode) /** * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage The value you set has already been locked. To change the value, use the --lock option. + * @codingStandardsIgnoreStart + * @expectedExceptionMessage The value you set has already been locked. To change the value, use the --lock-env option. + * @codingStandardsIgnoreEnd */ public function testProcessLockedValue() { @@ -183,6 +165,9 @@ public function testProcessLockedValue() ->method('resolve') ->willReturn('system/default/test/test/test'); + $this->configFactory->expects($this->never()) + ->method('create'); + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); } } diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockConfigProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockConfigProcessorTest.php new file mode 100644 index 0000000000000..c727184efb4fc --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockConfigProcessorTest.php @@ -0,0 +1,220 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Config\Test\Unit\Console\Command\ConfigSet; + +use Magento\Config\Console\Command\ConfigSet\LockProcessor; +use Magento\Config\Model\PreparedValueFactory; +use Magento\Framework\App\Config\ConfigPathResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Store\Model\ScopeInterface; +use PHPUnit_Framework_MockObject_MockObject as Mock; + +/** + * Test for ShareProcessor. + * + * @see ShareProcessor + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LockConfigProcessorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var LockProcessor + */ + private $model; + + /** + * @var PreparedValueFactory|Mock + */ + private $preparedValueFactory; + + /** + * @var DeploymentConfig\Writer|Mock + */ + private $deploymentConfigWriterMock; + + /** + * @var ArrayManager|Mock + */ + private $arrayManagerMock; + + /** + * @var ConfigPathResolver|Mock + */ + private $configPathResolver; + + /** + * @var Value|Mock + */ + private $valueMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->preparedValueFactory = $this->getMockBuilder(PreparedValueFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->deploymentConfigWriterMock = $this->getMockBuilder(DeploymentConfig\Writer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configPathResolver = $this->getMockBuilder(ConfigPathResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->valueMock = $this->getMockBuilder(Value::class) + ->setMethods(['validateBeforeSave', 'beforeSave', 'setValue', 'getValue', 'afterSave']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new LockProcessor( + $this->preparedValueFactory, + $this->deploymentConfigWriterMock, + $this->arrayManagerMock, + $this->configPathResolver, + ConfigFilePool::APP_CONFIG + ); + } + + /** + * Tests process of share flow. + * + * @param string $path + * @param string $value + * @param string $scope + * @param string|null $scopeCode + * @dataProvider processDataProvider + */ + public function testProcess($path, $value, $scope, $scopeCode) + { + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->with($path, $value, $scope, $scopeCode) + ->willReturn($this->valueMock); + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->arrayManagerMock->expects($this->once()) + ->method('set') + ->with('system/default/test/test/test', [], $value) + ->willReturn([ + 'system' => [ + 'default' => [ + 'test' => [ + 'test' => [ + 'test' => $value + ] + ] + ] + ] + ]); + $this->valueMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + $this->deploymentConfigWriterMock->expects($this->once()) + ->method('saveConfig') + ->with( + [ + ConfigFilePool::APP_CONFIG => [ + 'system' => [ + 'default' => [ + 'test' => [ + 'test' => [ + 'test' => $value + ] + ] + ] + ] + ] + ], + false + ); + $this->valueMock->expects($this->once()) + ->method('validateBeforeSave'); + $this->valueMock->expects($this->once()) + ->method('beforeSave'); + $this->valueMock->expects($this->once()) + ->method('afterSave'); + + $this->model->process($path, $value, $scope, $scopeCode); + } + + /** + * @return array + */ + public function processDataProvider() + { + return [ + ['test/test/test', 'value', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null], + ['test/test/test', 'value', ScopeInterface::SCOPE_WEBSITE, 'base'], + ['test/test/test', 'value', ScopeInterface::SCOPE_STORE, 'test'], + ]; + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Filesystem is not writable. + */ + public function testProcessNotReadableFs() + { + $path = 'test/test/test'; + $value = 'value'; + + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->willReturn($this->valueMock); + $this->valueMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->arrayManagerMock->expects($this->once()) + ->method('set') + ->with('system/default/test/test/test', [], $value) + ->willReturn(null); + $this->deploymentConfigWriterMock->expects($this->once()) + ->method('saveConfig') + ->willThrowException(new FileSystemException(__('Filesystem is not writable.'))); + + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Invalid values + */ + public function testCustomException() + { + $path = 'test/test/test'; + $value = 'value'; + + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->willReturn($this->valueMock); + $this->arrayManagerMock->expects($this->never()) + ->method('set'); + $this->valueMock->expects($this->once()) + ->method('getValue'); + $this->valueMock->expects($this->once()) + ->method('afterSave') + ->willThrowException(new \Exception('Invalid values')); + $this->deploymentConfigWriterMock->expects($this->never()) + ->method('saveConfig'); + + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + } +} diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockEnvProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockEnvProcessorTest.php new file mode 100644 index 0000000000000..4e0248f886028 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockEnvProcessorTest.php @@ -0,0 +1,220 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Config\Test\Unit\Console\Command\ConfigSet; + +use Magento\Config\Console\Command\ConfigSet\LockProcessor; +use Magento\Config\Model\PreparedValueFactory; +use Magento\Framework\App\Config\ConfigPathResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Store\Model\ScopeInterface; +use PHPUnit_Framework_MockObject_MockObject as Mock; + +/** + * Test for LockProcessor. + * + * @see LockProcessor + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LockEnvProcessorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var LockProcessor + */ + private $model; + + /** + * @var PreparedValueFactory|Mock + */ + private $preparedValueFactory; + + /** + * @var DeploymentConfig\Writer|Mock + */ + private $deploymentConfigWriterMock; + + /** + * @var ArrayManager|Mock + */ + private $arrayManagerMock; + + /** + * @var ConfigPathResolver|Mock + */ + private $configPathResolver; + + /** + * @var Value|Mock + */ + private $valueMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->preparedValueFactory = $this->getMockBuilder(PreparedValueFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->deploymentConfigWriterMock = $this->getMockBuilder(DeploymentConfig\Writer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->configPathResolver = $this->getMockBuilder(ConfigPathResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->valueMock = $this->getMockBuilder(Value::class) + ->setMethods(['validateBeforeSave', 'beforeSave', 'setValue', 'getValue', 'afterSave']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new LockProcessor( + $this->preparedValueFactory, + $this->deploymentConfigWriterMock, + $this->arrayManagerMock, + $this->configPathResolver, + ConfigFilePool::APP_ENV + ); + } + + /** + * Tests process of lock flow. + * + * @param string $path + * @param string $value + * @param string $scope + * @param string|null $scopeCode + * @dataProvider processDataProvider + */ + public function testProcess($path, $value, $scope, $scopeCode) + { + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->with($path, $value, $scope, $scopeCode) + ->willReturn($this->valueMock); + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->arrayManagerMock->expects($this->once()) + ->method('set') + ->with('system/default/test/test/test', [], $value) + ->willReturn([ + 'system' => [ + 'default' => [ + 'test' => [ + 'test' => [ + 'test' => $value + ] + ] + ] + ] + ]); + $this->valueMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + $this->deploymentConfigWriterMock->expects($this->once()) + ->method('saveConfig') + ->with( + [ + ConfigFilePool::APP_ENV => [ + 'system' => [ + 'default' => [ + 'test' => [ + 'test' => [ + 'test' => $value + ] + ] + ] + ] + ] + ], + false + ); + $this->valueMock->expects($this->once()) + ->method('validateBeforeSave'); + $this->valueMock->expects($this->once()) + ->method('beforeSave'); + $this->valueMock->expects($this->once()) + ->method('afterSave'); + + $this->model->process($path, $value, $scope, $scopeCode); + } + + /** + * @return array + */ + public function processDataProvider() + { + return [ + ['test/test/test', 'value', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null], + ['test/test/test', 'value', ScopeInterface::SCOPE_WEBSITE, 'base'], + ['test/test/test', 'value', ScopeInterface::SCOPE_STORE, 'test'], + ]; + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Filesystem is not writable. + */ + public function testProcessNotReadableFs() + { + $path = 'test/test/test'; + $value = 'value'; + + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->willReturn($this->valueMock); + $this->valueMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->arrayManagerMock->expects($this->once()) + ->method('set') + ->with('system/default/test/test/test', [], $value) + ->willReturn(null); + $this->deploymentConfigWriterMock->expects($this->once()) + ->method('saveConfig') + ->willThrowException(new FileSystemException(__('Filesystem is not writable.'))); + + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Invalid values + */ + public function testCustomException() + { + $path = 'test/test/test'; + $value = 'value'; + + $this->configPathResolver->expects($this->once()) + ->method('resolve') + ->willReturn('system/default/test/test/test'); + $this->preparedValueFactory->expects($this->once()) + ->method('create') + ->willReturn($this->valueMock); + $this->arrayManagerMock->expects($this->never()) + ->method('set'); + $this->valueMock->expects($this->once()) + ->method('getValue'); + $this->valueMock->expects($this->once()) + ->method('afterSave') + ->willThrowException(new \Exception('Invalid values')); + $this->deploymentConfigWriterMock->expects($this->never()) + ->method('saveConfig'); + + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + } +} diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php deleted file mode 100644 index 4535e9ad888c2..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/LockProcessorTest.php +++ /dev/null @@ -1,219 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Config\Test\Unit\Console\Command\ConfigSet; - -use Magento\Config\Console\Command\ConfigSet\LockProcessor; -use Magento\Config\Model\PreparedValueFactory; -use Magento\Framework\App\Config\ConfigPathResolver; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\Config\Value; -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Stdlib\ArrayManager; -use Magento\Store\Model\ScopeInterface; -use PHPUnit_Framework_MockObject_MockObject as Mock; - -/** - * Test for LockProcessor. - * - * @see LockProcessor - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class LockProcessorTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var LockProcessor - */ - private $model; - - /** - * @var PreparedValueFactory|Mock - */ - private $preparedValueFactory; - - /** - * @var DeploymentConfig\Writer|Mock - */ - private $deploymentConfigWriterMock; - - /** - * @var ArrayManager|Mock - */ - private $arrayManagerMock; - - /** - * @var ConfigPathResolver|Mock - */ - private $configPathResolver; - - /** - * @var Value|Mock - */ - private $valueMock; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->preparedValueFactory = $this->getMockBuilder(PreparedValueFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->deploymentConfigWriterMock = $this->getMockBuilder(DeploymentConfig\Writer::class) - ->disableOriginalConstructor() - ->getMock(); - $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->configPathResolver = $this->getMockBuilder(ConfigPathResolver::class) - ->disableOriginalConstructor() - ->getMock(); - $this->valueMock = $this->getMockBuilder(Value::class) - ->setMethods(['validateBeforeSave', 'beforeSave', 'setValue', 'getValue', 'afterSave']) - ->disableOriginalConstructor() - ->getMock(); - - $this->model = new LockProcessor( - $this->preparedValueFactory, - $this->deploymentConfigWriterMock, - $this->arrayManagerMock, - $this->configPathResolver - ); - } - - /** - * Tests process of lock flow. - * - * @param string $path - * @param string $value - * @param string $scope - * @param string|null $scopeCode - * @dataProvider processDataProvider - */ - public function testProcess($path, $value, $scope, $scopeCode) - { - $this->preparedValueFactory->expects($this->once()) - ->method('create') - ->with($path, $value, $scope, $scopeCode) - ->willReturn($this->valueMock); - $this->configPathResolver->expects($this->once()) - ->method('resolve') - ->willReturn('system/default/test/test/test'); - $this->arrayManagerMock->expects($this->once()) - ->method('set') - ->with('system/default/test/test/test', [], $value) - ->willReturn([ - 'system' => [ - 'default' => [ - 'test' => [ - 'test' => [ - 'test' => $value - ] - ] - ] - ] - ]); - $this->valueMock->expects($this->once()) - ->method('getValue') - ->willReturn($value); - $this->deploymentConfigWriterMock->expects($this->once()) - ->method('saveConfig') - ->with( - [ - ConfigFilePool::APP_ENV => [ - 'system' => [ - 'default' => [ - 'test' => [ - 'test' => [ - 'test' => $value - ] - ] - ] - ] - ] - ], - false - ); - $this->valueMock->expects($this->once()) - ->method('validateBeforeSave'); - $this->valueMock->expects($this->once()) - ->method('beforeSave'); - $this->valueMock->expects($this->once()) - ->method('afterSave'); - - $this->model->process($path, $value, $scope, $scopeCode); - } - - /** - * @return array - */ - public function processDataProvider() - { - return [ - ['test/test/test', 'value', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null], - ['test/test/test', 'value', ScopeInterface::SCOPE_WEBSITE, 'base'], - ['test/test/test', 'value', ScopeInterface::SCOPE_STORE, 'test'], - ]; - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Filesystem is not writable. - */ - public function testProcessNotReadableFs() - { - $path = 'test/test/test'; - $value = 'value'; - - $this->preparedValueFactory->expects($this->once()) - ->method('create') - ->willReturn($this->valueMock); - $this->valueMock->expects($this->once()) - ->method('getValue') - ->willReturn($value); - $this->configPathResolver->expects($this->once()) - ->method('resolve') - ->willReturn('system/default/test/test/test'); - $this->arrayManagerMock->expects($this->once()) - ->method('set') - ->with('system/default/test/test/test', [], $value) - ->willReturn(null); - $this->deploymentConfigWriterMock->expects($this->once()) - ->method('saveConfig') - ->willThrowException(new FileSystemException(__('Filesystem is not writable.'))); - - $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage Invalid values - */ - public function testCustomException() - { - $path = 'test/test/test'; - $value = 'value'; - - $this->configPathResolver->expects($this->once()) - ->method('resolve') - ->willReturn('system/default/test/test/test'); - $this->preparedValueFactory->expects($this->once()) - ->method('create') - ->willReturn($this->valueMock); - $this->arrayManagerMock->expects($this->never()) - ->method('set'); - $this->valueMock->expects($this->once()) - ->method('getValue'); - $this->valueMock->expects($this->once()) - ->method('afterSave') - ->willThrowException(new \Exception('Invalid values')); - $this->deploymentConfigWriterMock->expects($this->never()) - ->method('saveConfig'); - - $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); - } -} diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ProcessorFacadeTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ProcessorFacadeTest.php index 4e65ab3f4cc21..ac4dda2a98517 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ProcessorFacadeTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/ProcessorFacadeTest.php @@ -11,6 +11,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Config\Model\Config\PathValidator; +use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Exception\CouldNotSaveException; @@ -122,7 +123,13 @@ public function testProcess() $this->assertSame( 'Value was saved.', - $this->model->process('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, false) + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + false + ) ); } @@ -132,12 +139,19 @@ public function testProcess() */ public function testProcessWithValidatorException(LocalizedException $exception) { - $this->expectException(ValidatorException::class, 'Some error'); + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Some error'); $this->scopeValidatorMock->expects($this->once()) ->method('isValid') ->willThrowException($exception); - $this->model->process('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, false); + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + false + ); } /** @@ -172,7 +186,13 @@ public function testProcessWithConfigurationMismatchException() $this->configMock->expects($this->never()) ->method('clean'); - $this->model->process('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, false); + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + false + ); } /** @@ -198,17 +218,50 @@ public function testProcessWithCouldNotSaveException() $this->configMock->expects($this->never()) ->method('clean'); - $this->model->process('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, false); + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + false + ); + } + + public function testExecuteLockEnv() + { + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->willReturn(true); + $this->configSetProcessorFactoryMock->expects($this->once()) + ->method('create') + ->with(ConfigSetProcessorFactory::TYPE_LOCK_ENV) + ->willReturn($this->processorMock); + $this->processorMock->expects($this->once()) + ->method('process') + ->with('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); + $this->configMock->expects($this->once()) + ->method('clean'); + + $this->assertSame( + 'Value was saved in app/etc/env.php and locked.', + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + true + ) + ); } - public function testExecuteLock() + public function testExecuteLockConfig() { $this->scopeValidatorMock->expects($this->once()) ->method('isValid') ->willReturn(true); $this->configSetProcessorFactoryMock->expects($this->once()) ->method('create') - ->with(ConfigSetProcessorFactory::TYPE_LOCK) + ->with(ConfigSetProcessorFactory::TYPE_LOCK_CONFIG) ->willReturn($this->processorMock); $this->processorMock->expects($this->once()) ->method('process') @@ -217,8 +270,15 @@ public function testExecuteLock() ->method('clean'); $this->assertSame( - 'Value was saved and locked.', - $this->model->process('test/test/test', 'test', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, true) + 'Value was saved in app/etc/config.php and locked.', + $this->model->processWithLockTarget( + 'test/test/test', + 'test', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + true, + ConfigFilePool::APP_CONFIG + ) ); } } diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php index 39f9c47361352..4f7327486d64a 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php @@ -94,7 +94,7 @@ public function testExecute() ->method('create') ->willReturn($this->processorFacadeMock); $this->processorFacadeMock->expects($this->once()) - ->method('process') + ->method('processWithLockTarget') ->willReturn('Some message'); $this->emulatedAreProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php deleted file mode 100644 index 069a1c20b2966..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php +++ /dev/null @@ -1,292 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Config\Test\Unit\Controller\Adminhtml\System\Config; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Config\Controller\Adminhtml\System\Config\Save - */ - protected $_controller; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_requestMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_configFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_eventManagerMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_authMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sectionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_cacheMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_responseMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sectionCheckerMock; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirect; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp() - { - $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); - - $configStructureMock = $this->createMock(\Magento\Config\Model\Config\Structure::class); - $this->_configFactoryMock = $this->createMock(\Magento\Config\Model\Config\Factory::class); - $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - - $helperMock = $this->createMock(\Magento\Backend\Helper\Data::class); - - $this->messageManagerMock = $this->createPartialMock( - \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addException'] - ); - - $this->_authMock = $this->createPartialMock(\Magento\Backend\Model\Auth::class, ['getUser']); - - $this->_sectionMock = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Section::class); - - $this->_cacheMock = $this->createMock(\Magento\Framework\App\Cache\Type\Layout::class); - - $configStructureMock->expects($this->any())->method('getElement')->willReturn($this->_sectionMock); - $configStructureMock->expects($this->any())->method('getSectionList')->willReturn( - [ - 'some_key_0' => '0', - 'some_key_1' => '1' - ] - ); - - $helperMock->expects($this->any())->method('getUrl')->willReturnArgument(0); - - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultRedirect->expects($this->atLeastOnce()) - ->method('setPath') - ->with('adminhtml/system_config/edit') - ->willReturnSelf(); - $resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $resultRedirectFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($this->resultRedirect); - - $arguments = [ - 'request' => $this->_requestMock, - 'response' => $this->_responseMock, - 'helper' => $helperMock, - 'eventManager' => $this->_eventManagerMock, - 'auth' => $this->_authMock, - 'messageManager' => $this->messageManagerMock, - 'resultRedirectFactory' => $resultRedirectFactory - ]; - - $this->_sectionCheckerMock = $this->createMock( - \Magento\Config\Controller\Adminhtml\System\ConfigSectionChecker::class - ); - - $context = $helper->getObject(\Magento\Backend\App\Action\Context::class, $arguments); - $this->_controller = $this->getMockBuilder(\Magento\Config\Controller\Adminhtml\System\Config\Save::class) - ->setMethods(['deniedAction']) - ->setConstructorArgs( - [ - $context, - $configStructureMock, - $this->_sectionCheckerMock, - $this->_configFactoryMock, - $this->_cacheMock, - new \Magento\Framework\Stdlib\StringUtils(), - ] - ) - ->getMock(); - } - - public function testIndexActionWithAllowedSection() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - $this->messageManagerMock->expects($this->once())->method('addSuccess')->with('You saved the configuration.'); - - $groups = ['some_key' => 'some_value']; - $requestParamMap = [ - ['section', null, 'test_section'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $requestPostMap = [['groups', null, $groups], ['config_state', null, 'test_config_state']]; - - $this->_requestMock->expects($this->any())->method('getPost')->will($this->returnValueMap($requestPostMap)); - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $backendConfigMock->expects($this->once())->method('save'); - - $params = [ - 'section' => 'test_section', - 'website' => 'test_website', - 'store' => 'test_store', - 'groups' => $groups, - ]; - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->with( - ['data' => $params] - )->will( - $this->returnValue($backendConfigMock) - ); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionSaveState() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(false)); - $inputData = [ - 'some_key' => 'some_value', - 'some_key_0' => '0', - 'some_key_1' => 'some_value_1', - ]; - $extraData = [ - 'some_key_0' => '0', - 'some_key_1' => '1', - ]; - - $userMock = $this->createMock(\Magento\User\Model\User::class); - $userMock->expects($this->once())->method('saveExtra')->with(['configState' => $extraData]); - $this->_authMock->expects($this->once())->method('getUser')->will($this->returnValue($userMock)); - $this->_requestMock->expects( - $this->any() - )->method( - 'getPost' - )->with( - 'config_state' - )->will( - $this->returnValue($inputData) - ); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionGetGroupForSave() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - - $fixturePath = __DIR__ . '/_files/'; - $groups = require_once $fixturePath . 'groups_array.php'; - $requestParamMap = [ - ['section', null, 'test_section'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $requestPostMap = [['groups', null, $groups], ['config_state', null, 'test_config_state']]; - - $files = require_once $fixturePath . 'files_array.php'; - - $this->_requestMock->expects($this->any())->method('getPost')->will($this->returnValueMap($requestPostMap)); - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - $this->_requestMock->expects( - $this->once() - )->method( - 'getFiles' - )->with( - 'groups' - )->will( - $this->returnValue($files) - ); - - $groupToSave = require_once $fixturePath . 'expected_array.php'; - - $params = [ - 'section' => 'test_section', - 'website' => 'test_website', - 'store' => 'test_store', - 'groups' => $groupToSave, - ]; - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->with( - ['data' => $params] - )->will( - $this->returnValue($backendConfigMock) - ); - $backendConfigMock->expects($this->once())->method('save'); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionSaveAdvanced() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - - $requestParamMap = [ - ['section', null, 'advanced'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->will( - $this->returnValue($backendConfigMock) - ); - $backendConfigMock->expects($this->once())->method('save'); - - $this->_cacheMock->expects($this->once())->method('clean')->with(\Zend_Cache::CLEANING_MODE_ALL); - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } -} diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php deleted file mode 100644 index a74fd9ef1eedf..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'some.key' => 'some.val', - 'group.1' => [ - 'fields' => [ - 'f1.1' => ['value' => 'f1.1.val'], - 'f1.2' => ['value' => 'f1.2.val'], - 'g1.1' => ['value' => 'g1.1.val'], - ], - ], - 'group.2' => [ - 'fields' => ['f2.1' => ['value' => 'f2.1.val'], 'f2.2' => ['value' => 'f2.2.val']], - 'groups' => [ - 'group.2.1' => [ - 'fields' => [ - 'f2.1.1' => ['value' => 'f2.1.1.val'], - 'f2.1.2' => ['value' => 'f2.1.2.val'], - ], - 'groups' => [ - 'group.2.1.1' => [ - 'fields' => [ - 'f2.1.1.1' => ['value' => 'f2.1.1.1.val'], - 'f2.1.1.2' => ['value' => 'f2.1.1.2.val'], - ], - ], - ], - ], - ], - ] -]; diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php deleted file mode 100644 index 3bc0f7a466733..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'group.1' => [ - 'fields' => ['f1.1' => ['value' => 'f1.1.val'], 'f1.2' => ['value' => 'f1.2.val']], - ], - 'group.2' => [ - 'fields' => [ - 'f2.1' => ['value' => 'f2.1.val'], - 'f2.2' => ['value' => 'f2.2.val'], - 'f2.3' => ['value' => ''], - ], - 'groups' => [ - 'group.2.1' => [ - 'fields' => [ - 'f2.1.1' => ['value' => 'f2.1.1.val'], - 'f2.1.2' => ['value' => 'f2.1.2.val'], - 'f2.1.3' => ['value' => ''], - ], - 'groups' => [ - 'group.2.1.1' => [ - 'fields' => [ - 'f2.1.1.1' => ['value' => 'f2.1.1.1.val'], - 'f2.1.1.2' => ['value' => 'f2.1.1.2.val'], - 'f2.1.1.3' => ['value' => ''], - ], - ], - ], - ], - ], - ], - 'group.3' => 'some.data', -]; diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php deleted file mode 100644 index dde65986e8a3e..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return ['some.key' => 'some.val', 'group.1' => ['fields' => ['g1.1' => ['value' => 'g1.1.val']]]]; diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/AddressTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/AddressTest.php index bacbda537fb1d..e6b774db041c3 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/AddressTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/AddressTest.php @@ -40,6 +40,9 @@ public function testBeforeSave($value, $expectedValue) $this->assertEquals($expectedValue, $this->model->getValue()); } + /** + * @return array + */ public function beforeSaveDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/SenderTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/SenderTest.php index 8e559ff8284ed..e38c247c3861a 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/SenderTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/Email/SenderTest.php @@ -41,6 +41,9 @@ public function testBeforeSave($value, $expectedValue) $this->assertEquals($expectedValue, $this->model->getValue()); } + /** + * @return array + */ public function beforeSaveDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php index 493fdf9505c4c..048df95f98649 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Model\Context; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; class SerializedTest extends \PHPUnit\Framework\TestCase { @@ -18,14 +19,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase /** @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $loggerMock; + protected function setUp() { $objectManager = new ObjectManager($this); $this->serializerMock = $this->createMock(Json::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); $contextMock = $this->createMock(Context::class); $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $contextMock->method('getEventDispatcher') ->willReturn($eventManagerMock); + $contextMock->method('getLogger') + ->willReturn($this->loggerMock); $this->serializedConfig = $objectManager->getObject( Serialized::class, [ @@ -52,6 +59,9 @@ public function testAfterLoad($expected, $value, $numCalls, $unserializedValue = $this->assertEquals($expected, $this->serializedConfig->getValue()); } + /** + * @return array + */ public function afterLoadDataProvider() { return [ @@ -69,6 +79,20 @@ public function afterLoadDataProvider() ]; } + public function testAfterLoadWithException() + { + $value = '{"key":'; + $expected = false; + $this->serializedConfig->setValue($value); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('critical'); + $this->serializedConfig->afterLoad(); + $this->assertEquals($expected, $this->serializedConfig->getValue()); + } + /** * @param string $expected * @param int|double|string|array|boolean|null $value @@ -87,6 +111,9 @@ public function testBeforeSave($expected, $value, $numCalls, $serializedValue = $this->assertEquals($expected, $this->serializedConfig->getValue()); } + /** + * @return array + */ public function beforeSaveDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/ImporterTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/ImporterTest.php index 0fdf4532462ac..4d8eec0aa76ba 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/ImporterTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/ImporterTest.php @@ -156,6 +156,9 @@ public function testImport() $this->scopeMock->expects($this->at(2)) ->method('setCurrentScope') ->with('oldScope'); + $this->scopeMock->expects($this->at(3)) + ->method('setCurrentScope') + ->with('oldScope'); $this->flagManagerMock->expects($this->once()) ->method('saveFlag') ->with(Importer::FLAG_CODE, $data); diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php index e869fe8556bf7..a5878a04e3e60 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Source/Email/TemplateTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Test\Unit\Model\Config\Source\Email; class TemplateTest extends \PHPUnit\Framework\TestCase @@ -34,7 +32,9 @@ protected function setUp() { $this->_coreRegistry = $this->createMock(\Magento\Framework\Registry::class); $this->_emailConfig = $this->createMock(\Magento\Email\Model\Template\Config::class); - $this->_templatesFactory = $this->createMock(\Magento\Email\Model\ResourceModel\Template\CollectionFactory::class); + $this->_templatesFactory = $this->createMock( + \Magento\Email\Model\ResourceModel\Template\CollectionFactory::class + ); $this->_model = new \Magento\Config\Model\Config\Source\Email\Template( $this->_coreRegistry, $this->_templatesFactory, @@ -76,9 +76,21 @@ public function testToOptionArray() $this->returnValue('Template New') ); $expectedResult = [ - ['value' => 'template_new', 'label' => 'Template New (Default)'], - ['value' => 'template_one', 'label' => 'Template One'], - ['value' => 'template_two', 'label' => 'Template Two'], + [ + 'value' => 'template_new', + 'label' => 'Template New (Default)', + '__disableTmpl' => true + ], + [ + 'value' => 'template_one', + 'label' => 'Template One', + '__disableTmpl' => true + ], + [ + 'value' => 'template_two', + 'label' => 'Template Two', + '__disableTmpl' => true + ], ]; $this->_model->setPath('template/new'); $this->assertEquals($expectedResult, $this->_model->toOptionArray()); diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/AbstractElementTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/AbstractElementTest.php index 51432366bb441..e602e0407feff 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/AbstractElementTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/AbstractElementTest.php @@ -141,6 +141,9 @@ public function testIsVisibleReturnsTrueForProperScopes($settings, $scope) $this->assertTrue($this->_model->isVisible()); } + /** + * @return array + */ public function isVisibleReturnsTrueForProperScopesDataProvider() { return [ @@ -170,6 +173,9 @@ public function testIsVisibleReturnsFalseForNonProperScopes($settings, $scope) $this->assertFalse($this->_model->isVisible()); } + /** + * @return array + */ public function isVisibleReturnsFalseForNonProperScopesDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php index 5cad923264e00..ba74b93d9ad76 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php @@ -8,6 +8,11 @@ use Magento\Config\Model\Config\Structure\ConcealInProductionConfigList; use Magento\Framework\App\State; +/** + * @deprecated Original class has changed the location + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * @see \Magento\Config\Test\Unit\Model\Config\Structure\ElementVisibility\ConcealInProductionTest + */ class ConcealInProductionConfigListTest extends \PHPUnit\Framework\TestCase { /** @@ -43,6 +48,8 @@ protected function setUp() * @param string $mageMode * @param bool $expectedResult * @dataProvider disabledDataProvider + * + * @deprecated */ public function testIsDisabled($path, $mageMode, $expectedResult) { @@ -54,6 +61,8 @@ public function testIsDisabled($path, $mageMode, $expectedResult) /** * @return array + * + * @deprecated */ public function disabledDataProvider() { @@ -78,6 +87,8 @@ public function disabledDataProvider() * @param string $mageMode * @param bool $expectedResult * @dataProvider hiddenDataProvider + * + * @deprecated */ public function testIsHidden($path, $mageMode, $expectedResult) { @@ -89,6 +100,8 @@ public function testIsHidden($path, $mageMode, $expectedResult) /** * @return array + * + * @deprecated */ public function hiddenDataProvider() { diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php index 30c567fb490e6..750a829eef7ec 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/FieldTest.php @@ -88,6 +88,9 @@ public function testIsNegative($data, $isNegative) $this->assertEquals($isNegative, $this->_getFieldObject($data, $isNegative)->isNegative()); } + /** + * @return array + */ public function dataProvider() { return [ @@ -110,6 +113,9 @@ public function testIsValueSatisfy($data, $isNegative, $value, $expected) $this->assertEquals($expected, $this->_getFieldObject($data, $isNegative)->isValueSatisfy($value)); } + /** + * @return array + */ public function isValueSatisfyDataProvider() { return [ @@ -135,6 +141,9 @@ public function testGetValues($data, $isNegative, $expected) $this->assertEquals($expected, $this->_getFieldObject($data, $isNegative)->getValues()); } + /** + * @return array + */ public function getValuesDataProvider() { $complexDataValues = [self::COMPLEX_VALUE1, self::COMPLEX_VALUE2, self::COMPLEX_VALUE3]; diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/MapperTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/MapperTest.php index 1c758bfcbefaa..c6cd03cf8f35b 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/MapperTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/Dependency/MapperTest.php @@ -98,7 +98,8 @@ public function testGetDependenciesWhenDependentIsInvisible($isValueSatisfy) { $expected = []; $rowData = array_values($this->_testData); - for ($i = 0; $i < count($this->_testData); ++$i) { + $count = count($this->_testData); + for ($i = 0; $i < $count; ++$i) { $data = $rowData[$i]; $dependentPath = 'some path ' . $i; $field = $this->_getField( @@ -149,6 +150,9 @@ public function testGetDependenciesWhenDependentIsInvisible($isValueSatisfy) $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function getDependenciesDataProvider() { return [[true], [false]]; @@ -158,7 +162,8 @@ public function testGetDependenciesIsVisible() { $expected = []; $rowData = array_values($this->_testData); - for ($i = 0; $i < count($this->_testData); ++$i) { + $count = count($this->_testData); + for ($i = 0; $i < $count; ++$i) { $data = $rowData[$i]; $field = $this->_getField( true, diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/FieldTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/FieldTest.php index b45cea0f4b7e7..4e06c96362b50 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/FieldTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/FieldTest.php @@ -1,18 +1,16 @@ <?php /** - * \Magento\Config\Model\Config\Structure\Element\Field - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Config\Test\Unit\Model\Config\Structure\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** + * Test for \Magento\Config\Model\Config\Structure\Element\Field. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FieldTest extends \PHPUnit\Framework\TestCase @@ -57,7 +55,9 @@ protected function setUp() $this->_sourceFactoryMock = $this->createMock(\Magento\Config\Model\Config\SourceFactory::class); $this->_commentFactoryMock = $this->createMock(\Magento\Config\Model\Config\CommentFactory::class); $this->_blockFactoryMock = $this->createMock(\Magento\Framework\View\Element\BlockFactory::class); - $this->_depMapperMock = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Dependency\Mapper::class); + $this->_depMapperMock = $this->createMock( + \Magento\Config\Model\Config\Structure\Element\Dependency\Mapper::class + ); $this->_model = $objectManager->getObject( \Magento\Config\Model\Config\Structure\Element\Field::class, @@ -290,7 +290,8 @@ public function testGetOptionsWithConstantValOptions() $option = [ [ 'label' => 'test', - 'value' => "{{\Magento\Config\Test\Unit\Model\Config\Structure\Element\FieldTest::FIELD_TEST_CONSTANT}}", + 'value' => + "{{\Magento\Config\Test\Unit\Model\Config\Structure\Element\FieldTest::FIELD_TEST_CONSTANT}}", ], ]; $expected = [ @@ -336,7 +337,10 @@ public function testGetOptionsUsesProvidedMethodOfSourceModel() ['source_model' => 'Source_Model_Name::retrieveElements', 'path' => 'path', 'type' => 'multiselect'], 'scope' ); - $sourceModelMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['setPath', 'retrieveElements']); + $sourceModelMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['setPath', 'retrieveElements'] + ); $this->_sourceFactoryMock->expects( $this->once() )->method( @@ -358,7 +362,10 @@ public function testGetOptionsParsesResultOfProvidedMethodOfSourceModelIfTypeIsN ['source_model' => 'Source_Model_Name::retrieveElements', 'path' => 'path', 'type' => 'select'], 'scope' ); - $sourceModelMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['setPath', 'retrieveElements']); + $sourceModelMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['setPath', 'retrieveElements'] + ); $this->_sourceFactoryMock->expects( $this->once() )->method( diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/IteratorTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/IteratorTest.php index 1a0f3d03b060c..dcb7a90e55290 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/IteratorTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Element/IteratorTest.php @@ -68,6 +68,9 @@ public function testIsLast($elementId, $result) $this->assertEquals($result, $this->_model->isLast($elementMock)); } + /** + * @return array + */ public function isLastDataProvider() { return [[1, false], [2, false], [3, true]]; diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php new file mode 100644 index 0000000000000..873d447d9868c --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Test\Unit\Model\Config\Structure\ElementVisibility; + +use Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction; +use Magento\Config\Model\Config\Structure\ElementVisibilityInterface; +use Magento\Framework\App\State; + +class ConcealInProductionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var State|\PHPUnit_Framework_MockObject_MockObject + */ + private $stateMock; + + /** + * @var ConcealInProduction + */ + private $model; + + protected function setUp() + { + $this->stateMock = $this->getMockBuilder(State::class) + ->disableOriginalConstructor() + ->getMock(); + + $configs = [ + 'section1/group1/field1' => ElementVisibilityInterface::DISABLED, + 'section1/group1' => ElementVisibilityInterface::HIDDEN, + 'section1' => ElementVisibilityInterface::DISABLED, + 'section1/group2' => 'no', + 'section2/group1' => ElementVisibilityInterface::DISABLED, + 'section2/group2' => ElementVisibilityInterface::HIDDEN, + 'section3' => ElementVisibilityInterface::HIDDEN, + 'section3/group1/field1' => 'no', + ]; + $exemptions = [ + 'section1/group1/field3' => '', + 'section1/group2/field1' => '', + 'section2/group2/field1' => '', + 'section3/group2' => '', + ]; + + $this->model = new ConcealInProduction($this->stateMock, $configs, $exemptions); + } + + /** + * @param string $path + * @param string $mageMode + * @param bool $isDisabled + * @param bool $isHidden + * @dataProvider disabledDataProvider + * @return void + */ + public function testCheckVisibility(string $path, string $mageMode, bool $isHidden, bool $isDisabled) + { + $this->stateMock->expects($this->any()) + ->method('getMode') + ->willReturn($mageMode); + + $this->assertSame($isHidden, $this->model->isHidden($path)); + $this->assertSame($isDisabled, $this->model->isDisabled($path)); + } + + /** + * @return array + */ + public function disabledDataProvider(): array + { + return [ + //visibility of field 'section1/group1/field1' should be applied + ['section1/group1/field1', State::MODE_PRODUCTION, false, true], + ['section1/group1/field1', State::MODE_DEFAULT, false, false], + ['section1/group1/field1', State::MODE_DEVELOPER, false, false], + //visibility of group 'section1/group1' should be applied + ['section1/group1/field2', State::MODE_PRODUCTION, true, false], + ['section1/group1/field2', State::MODE_DEFAULT, false, false], + ['section1/group1/field2', State::MODE_DEVELOPER, false, false], + //exemption should be applied for section1/group2/field1 + ['section1/group2/field1', State::MODE_PRODUCTION, false, false], + ['section1/group2/field1', State::MODE_DEFAULT, false, false], + ['section1/group2/field1', State::MODE_DEVELOPER, false, false], + //as 'section1/group2' has neither Disable nor Hidden rule, this field should be visible + ['section1/group2/field2', State::MODE_PRODUCTION, false, false], + //exemption should be applied for section1/group1/field3 + ['section1/group1/field3', State::MODE_PRODUCTION, false, false], + //visibility of group 'section2/group1' should be applied + ['section2/group1/field1', State::MODE_PRODUCTION, false, true], + //exemption should be applied for section2/group2/field1 + ['section2/group2/field1', State::MODE_PRODUCTION, false, false], + //any rule should not be applied + ['section2/group3/field1', State::MODE_PRODUCTION, false, false], + //any rule should not be applied + ['section3/group1/field1', State::MODE_PRODUCTION, false, false], + //visibility of section 'section3' should be applied + ['section3/group1/field2', State::MODE_PRODUCTION, true, false], + //exception from 'section3/group2' should be applied + ['section3/group2/field1', State::MODE_PRODUCTION, false, false], + + ]; + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php new file mode 100644 index 0000000000000..ae213c19a5337 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Test\Unit\Model\Config\Structure\ElementVisibility; + +use Magento\Framework\App\DeploymentConfig; +use \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction; +use \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionFactory; +use Magento\Framework\Config\ConfigOptionsListConstants as Constants; +use Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand; +use Magento\Config\Model\Config\Structure\ElementVisibilityInterface; + +class ConcealInProductionWithoutScdOnDemandTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ConcealInProduction|\PHPUnit_Framework_MockObject_MockObject + */ + private $concealInProductionMock; + + /** + * @var ConcealInProductionWithoutScdOnDemand + */ + private $model; + + /** + * @var DeploymentConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $deploymentConfigMock; + + protected function setUp() + { + $concealInProductionFactoryMock = $this->createMock(ConcealInProductionFactory::class); + + $this->concealInProductionMock = $this->createMock(ConcealInProduction::class); + + $this->deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + + $configs = [ + 'section1/group1/field1' => ElementVisibilityInterface::DISABLED, + 'section1/group1' => ElementVisibilityInterface::HIDDEN, + 'section1' => ElementVisibilityInterface::DISABLED, + 'section1/group2' => 'no', + 'section2/group1' => ElementVisibilityInterface::DISABLED, + 'section2/group2' => ElementVisibilityInterface::HIDDEN, + 'section3' => ElementVisibilityInterface::HIDDEN, + 'section3/group1/field1' => 'no', + ]; + $exemptions = [ + 'section1/group1/field3' => '', + 'section1/group2/field1' => '', + 'section2/group2/field1' => '', + 'section3/group2' => '', + ]; + + $concealInProductionFactoryMock->expects($this->any()) + ->method('create') + ->with(['configs' => $configs, 'exemptions' => $exemptions]) + ->willReturn($this->concealInProductionMock); + + $this->model = new ConcealInProductionWithoutScdOnDemand( + $concealInProductionFactoryMock, + $this->deploymentConfigMock, + $configs, + $exemptions + ); + } + + /** + * @return void + */ + public function testIsHiddenScdOnDemandEnabled() + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(true); + $this->concealInProductionMock->expects($this->never()) + ->method('isHidden'); + + $this->assertFalse($this->model->isHidden($path)); + } + + /** + * @return void + */ + public function testIsDisabledScdOnDemandEnabled() + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(true); + $this->concealInProductionMock->expects($this->never()) + ->method('isDisabled'); + + $this->assertFalse($this->model->isDisabled($path)); + } + + /** + * @param bool $isHidden + * + * @dataProvider visibilityDataProvider + * @return void + */ + public function testIsHiddenScdOnDemandDisabled(bool $isHidden) + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(false); + $this->concealInProductionMock->expects($this->once()) + ->method('isHidden') + ->with($path) + ->willReturn($isHidden); + + $this->assertSame($isHidden, $this->model->isHidden($path)); + } + + /** + * @param bool $isDisabled + * + * @dataProvider visibilityDataProvider + * @return void + */ + public function testIsDisabledScdOnDemandDisabled(bool $isDisabled) + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(false); + $this->concealInProductionMock->expects($this->once()) + ->method('isDisabled') + ->with($path) + ->willReturn($isDisabled); + + $this->assertSame($isDisabled, $this->model->isDisabled($path)); + } + + /** + * @return array + */ + public function visibilityDataProvider(): array + { + return [ + [true], + [false], + ]; + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibilityCompositeTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibilityCompositeTest.php index a3bdb6f008f1d..b779c29c93155 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibilityCompositeTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibilityCompositeTest.php @@ -44,7 +44,7 @@ protected function setUp() public function testException() { $visibility = [ - 'stdClass' => new \StdClass() + 'stdClass' => new \stdClass() ]; new ElementVisibilityComposite($visibility); diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/ExtendsTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/ExtendsTest.php index 95e8246c6a3d3..7762c8993b24b 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/ExtendsTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/ExtendsTest.php @@ -44,6 +44,9 @@ public function testMapWithBadPath() $this->_sut->map($sourceData); } + /** + * @return array + */ public function mapDataProvider() { return [ @@ -55,6 +58,9 @@ public function mapDataProvider() ]; } + /** + * @return array + */ protected function _emptySectionsNodeData() { $data = ['config' => ['system' => ['sections' => 'some_non_array']]]; @@ -62,6 +68,9 @@ protected function _emptySectionsNodeData() return [$data, $data]; } + /** + * @return array + */ protected function _extendFromASiblingData() { $source = $result = [ @@ -81,6 +90,9 @@ protected function _extendFromASiblingData() return [$source, $result]; } + /** + * @return array + */ protected function _extendFromNodeOnHigherLevelData() { $source = $result = [ @@ -114,6 +126,9 @@ protected function _extendFromNodeOnHigherLevelData() return [$source, $result]; } + /** + * @return array + */ protected function _extendWithMerge() { $source = $result = [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/Helper/RelativePathConverterTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/Helper/RelativePathConverterTest.php index c671a5326c4de..dd95574ffa62d 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/Helper/RelativePathConverterTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/Mapper/Helper/RelativePathConverterTest.php @@ -24,7 +24,8 @@ public function testConvertWithInvalidRelativePath() $exceptionMessage = sprintf('Invalid relative path %s in %s node', $relativePath, $nodePath); - $this->expectException('InvalidArgumentException', $exceptionMessage); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); $this->_sut->convert($nodePath, $relativePath); } @@ -35,7 +36,8 @@ public function testConvertWithInvalidRelativePath() */ public function testConvertWithInvalidArguments($nodePath, $relativePath) { - $this->expectException('InvalidArgumentException', 'Invalid arguments'); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Invalid arguments'); $this->_sut->convert($nodePath, $relativePath); } @@ -50,11 +52,17 @@ public function testConvert($nodePath, $relativePath, $result) $this->assertEquals($result, $this->_sut->convert($nodePath, $relativePath)); } + /** + * @return array + */ public function convertWithInvalidArgumentsDataProvider() { return [['', ''], ['some/node', ''], ['', 'some/node']]; } + /** + * @return array + */ public function convertDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php index e67ea6ec0fba1..6c059f4b69b70 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php @@ -221,6 +221,9 @@ private function getElementReturnsEmptyElementIfNotExistingElementIsRequested( return $elementMock; } + /** + * @return array + */ public function emptyElementDataProvider() { return [ @@ -389,6 +392,9 @@ public function testGetFieldPathsByAttribute($attributeName, $attributeValue, $p $this->assertEquals($paths, $this->_model->getFieldPathsByAttribute($attributeName, $attributeValue)); } + /** + * @return array + */ public function getFieldPathsByAttributeDataProvider() { return [ diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index 2832e8e54e5f6..a731be96af963 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -5,129 +5,183 @@ */ namespace Magento\Config\Test\Unit\Model; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Config\Model\Config\Structure\Reader; +use Magento\Framework\DB\TransactionFactory; +use Magento\Config\Model\Config\Loader; +use Magento\Framework\App\Config\ValueFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Config\Model\Config\Structure; +use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\App\ScopeInterface; +use Magento\Store\Model\ScopeTypeNormalizer; +use Magento\Framework\DB\Transaction; +use Magento\Framework\App\Config\Value; +use Magento\Store\Model\Website; +use Magento\Config\Model\Config\Structure\Element\Group; +use Magento\Config\Model\Config\Structure\Element\Field; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Config\Model\Config + * @var Config */ - protected $_model; + private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|MockObject */ - protected $_eventManagerMock; + private $eventManagerMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Reader|MockObject */ - protected $_structureReaderMock; + private $structureReaderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var TransactionFactory|MockObject */ - protected $_transFactoryMock; + private $transFactoryMock; /** - * @var \Magento\Framework\App\Config\ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ReinitableConfigInterface|MockObject */ - protected $_appConfigMock; + private $appConfigMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Loader|MockObject */ - protected $_applicationMock; + private $configLoaderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ValueFactory|MockObject */ - protected $_configLoaderMock; + private $dataFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ - protected $_dataFactoryMock; + private $storeManager; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var Structure|MockObject */ - protected $_storeManager; + private $configStructure; /** - * @var \Magento\Config\Model\Config\Structure + * @var SettingChecker|MockObject */ - protected $_configStructure; + private $settingsChecker; + + /** + * @var ScopeResolverPool|MockObject + */ + private $scopeResolverPool; + + /** + * @var ScopeResolverInterface|MockObject + */ + private $scopeResolver; + + /** + * @var ScopeInterface|MockObject + */ + private $scope; + + /** + * @var ScopeTypeNormalizer|MockObject + */ + private $scopeTypeNormalizer; protected function setUp() { - $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_structureReaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Structure\Reader::class, + $this->eventManagerMock = $this->createMock(ManagerInterface::class); + $this->structureReaderMock = $this->createPartialMock( + Reader::class, ['getConfiguration'] ); - $this->_configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); + $this->configStructure = $this->createMock(Structure::class); - $this->_structureReaderMock->expects( - $this->any() - )->method( - 'getConfiguration' - )->will( - $this->returnValue($this->_configStructure) - ); + $this->structureReaderMock->method('getConfiguration') + ->willReturn($this->configStructure); - $this->_transFactoryMock = $this->createPartialMock( - \Magento\Framework\DB\TransactionFactory::class, - ['create'] + $this->transFactoryMock = $this->createPartialMock( + TransactionFactory::class, + ['create', 'addObject'] ); - $this->_appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $this->_configLoaderMock = $this->createPartialMock( - \Magento\Config\Model\Config\Loader::class, + $this->appConfigMock = $this->createMock(ReinitableConfigInterface::class); + $this->configLoaderMock = $this->createPartialMock( + Loader::class, ['getConfigByPath'] ); - $this->_dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); - - $this->_storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); - - $this->_model = new \Magento\Config\Model\Config( - $this->_appConfigMock, - $this->_eventManagerMock, - $this->_configStructure, - $this->_transFactoryMock, - $this->_configLoaderMock, - $this->_dataFactoryMock, - $this->_storeManager + $this->dataFactoryMock = $this->createMock(ValueFactory::class); + + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + $this->settingsChecker = $this->createMock(SettingChecker::class); + + $this->scopeResolverPool = $this->createMock(ScopeResolverPool::class); + $this->scopeResolver = $this->createMock(ScopeResolverInterface::class); + $this->scopeResolverPool->method('get') + ->willReturn($this->scopeResolver); + $this->scope = $this->createMock(ScopeInterface::class); + $this->scopeResolver->method('getScope') + ->willReturn($this->scope); + + $this->scopeTypeNormalizer = $this->createMock(ScopeTypeNormalizer::class); + + $this->model = new Config( + $this->appConfigMock, + $this->eventManagerMock, + $this->configStructure, + $this->transFactoryMock, + $this->configLoaderMock, + $this->dataFactoryMock, + $this->storeManager, + $this->settingsChecker, + [], + $this->scopeResolverPool, + $this->scopeTypeNormalizer ); } public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed() { - $this->_configLoaderMock->expects($this->never())->method('getConfigByPath'); - $this->_model->save(); + $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); + $this->model->save(); } public function testSaveEmptiesNonSetArguments() { - $this->_structureReaderMock->expects($this->never())->method('getConfiguration'); - $this->assertNull($this->_model->getSection()); - $this->assertNull($this->_model->getWebsite()); - $this->assertNull($this->_model->getStore()); - $this->_model->save(); - $this->assertSame('', $this->_model->getSection()); - $this->assertSame('', $this->_model->getWebsite()); - $this->assertSame('', $this->_model->getStore()); + $this->structureReaderMock->expects($this->never())->method('getConfiguration'); + $this->assertNull($this->model->getSection()); + $this->assertNull($this->model->getWebsite()); + $this->assertNull($this->model->getStore()); + $this->model->save(); + $this->assertSame('', $this->model->getSection()); + $this->assertSame('', $this->model->getWebsite()); + $this->assertSame('', $this->model->getStore()); } public function testSaveToCheckAdminSystemConfigChangedSectionEvent() { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); + $transactionMock = $this->createMock(Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -136,7 +190,7 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('website') ); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -145,121 +199,216 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('store') ); - $this->_model->setGroups(['1' => ['data']]); - $this->_model->save(); + $this->model->setGroups(['1' => ['data']]); + $this->model->save(); } - public function testSaveToCheckScopeDataSet() + public function testDoNotSaveReadOnlyFields() { - $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - - $this->_eventManagerMock->expects( - $this->at(0) - )->method( - 'dispatch' - )->with( - $this->equalTo('admin_system_config_changed_section_'), - $this->arrayHasKey('website') - ); - - $this->_eventManagerMock->expects( - $this->at(0) - )->method( - 'dispatch' - )->with( - $this->equalTo('admin_system_config_changed_section_'), - $this->arrayHasKey('store') - ); + $transactionMock = $this->createMock(Transaction::class); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); + + $this->settingsChecker->method('isReadOnly') + ->willReturn(true); + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); + + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + $this->model->setSection('section'); + + $group = $this->createMock(Group::class); + $group->method('getPath') + ->willReturn('section/1'); + + $field = $this->createMock(Field::class); + $field->method('getGroupPath') + ->willReturn('section/1'); + $field->method('getId') + ->willReturn('key'); + + $this->configStructure->expects($this->at(0)) + ->method('getElement') + ->with('section/1') + ->willReturn($group); + $this->configStructure->expects($this->at(1)) + ->method('getElement') + ->with('section/1') + ->willReturn($group); + $this->configStructure->expects($this->at(2)) + ->method('getElement') + ->with('section/1/key') + ->willReturn($field); - $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); - - $field = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Field::class); - - $this->_configStructure->expects( - $this->at(0) - )->method( - 'getElement' - )->with( - '/1' - )->will( - $this->returnValue($group) - ); - - $this->_configStructure->expects( - $this->at(1) - )->method( - 'getElement' - )->with( - '/1/key' - )->will( - $this->returnValue($field) + $backendModel = $this->createPartialMock( + Value::class, + ['addData'] ); + $this->dataFactoryMock->method('create') + ->willReturn($backendModel); - $website = $this->createMock(\Magento\Store\Model\Website::class); - $website->expects($this->any())->method('getCode')->will($this->returnValue('website_code')); - $this->_storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($website)); - $this->_storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); - $this->_storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); + $this->transFactoryMock->expects($this->never()) + ->method('addObject'); + $backendModel->expects($this->never()) + ->method('addData'); - $this->_model->setWebsite('website'); + $this->model->save(); + } - $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + public function testSaveToCheckScopeDataSet() + { + $transactionMock = $this->createMock(Transaction::class); + $this->transFactoryMock->method('create') + ->willReturn($transactionMock); + + $this->configLoaderMock->method('getConfigByPath') + ->willReturn([]); + + $this->eventManagerMock->expects($this->at(0)) + ->method('dispatch') + ->with( + $this->equalTo('admin_system_config_changed_section_section'), + $this->arrayHasKey('website') + ); + $this->eventManagerMock->expects($this->at(0)) + ->method('dispatch') + ->with( + $this->equalTo('admin_system_config_changed_section_section'), + $this->arrayHasKey('store') + ); + + $group = $this->createMock(Group::class); + $group->method('getPath')->willReturn('section/1'); + + $field = $this->createMock(Field::class); + $field->method('getGroupPath')->willReturn('section/1'); + $field->method('getId')->willReturn('key'); + + $this->configStructure->expects($this->at(0)) + ->method('getElement') + ->with('section/1') + ->willReturn($group); + $this->configStructure->expects($this->at(1)) + ->method('getElement') + ->with('section/1') + ->willReturn($group); + $this->configStructure->expects($this->at(2)) + ->method('getElement') + ->with('section/1/key') + ->willReturn($field); + $this->configStructure->expects($this->at(3)) + ->method('getElement') + ->with('section/1') + ->willReturn($group); + $this->configStructure->expects($this->at(4)) + ->method('getElement') + ->with('section/1/key') + ->willReturn($field); + + $this->scopeResolver->method('getScope') + ->with('1') + ->willReturn($this->scope); + $this->scope->expects($this->atLeastOnce()) + ->method('getScopeType') + ->willReturn('website'); + $this->scope->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $this->scope->expects($this->atLeastOnce()) + ->method('getCode') + ->willReturn('website_code'); + $this->scopeTypeNormalizer->expects($this->atLeastOnce()) + ->method('normalize') + ->with('website') + ->willReturn('websites'); + $website = $this->createMock(Website::class); + $this->storeManager->method('getWebsites')->willReturn([$website]); + $this->storeManager->method('isSingleStoreMode')->willReturn(true); + + $this->model->setWebsite('1'); + $this->model->setSection('section'); + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); $backendModel = $this->createPartialMock( - \Magento\Framework\App\Config\Value::class, + Value::class, ['setPath', 'addData', '__sleep', '__wakeup'] ); - $backendModel->expects( - $this->once() - )->method( - 'addData' - )->with( - [ + $backendModel->method('addData') + ->with([ 'field' => 'key', 'groups' => [1 => ['fields' => ['key' => ['data']]]], 'group_id' => null, 'scope' => 'websites', - 'scope_id' => 0, + 'scope_id' => 1, 'scope_code' => 'website_code', 'field_config' => null, 'fieldset_data' => ['key' => null], - ] - ); - $backendModel->expects( - $this->once() - )->method( - 'setPath' - )->with( - '/key' - )->will( - $this->returnValue($backendModel) - ); + ]); + $backendModel->expects($this->once()) + ->method('setPath') + ->with('section/1/key') + ->willReturn($backendModel); - $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + $this->dataFactoryMock->method('create') + ->willReturn($backendModel); - $this->_model->save(); + $this->model->save(); } - public function testSetDataByPath() + /** + * @param string $path + * @param string $value + * @param string $section + * @param array $groups + * @return void + * @dataProvider setDataByPathDataProvider + */ + public function testSetDataByPath(string $path, string $value, string $section, array $groups) { - $value = 'value'; - $path = '<section>/<group>/<field>'; - $this->_model->setDataByPath($path, $value); - $expected = [ - 'section' => '<section>', - 'groups' => [ - '<group>' => [ - 'fields' => [ - '<field>' => ['value' => $value], + $this->model->setDataByPath($path, $value); + $this->assertEquals($section, $this->model->getData('section')); + $this->assertEquals($groups, $this->model->getData('groups')); + } + + /** + * @return array + */ + public function setDataByPathDataProvider(): array + { + return [ + 'depth 3' => [ + 'a/b/c', + 'value1', + 'a', + [ + 'b' => [ + 'fields' => [ + 'c' => ['value' => 'value1'], + ], + ], + ], + ], + 'depth 5' => [ + 'a/b/c/d/e', + 'value1', + 'a', + [ + 'b' => [ + 'groups' => [ + 'c' => [ + 'groups' => [ + 'd' => [ + 'fields' => [ + 'e' => ['value' => 'value1'], + ], + ], + ], + ], + ], ], ], ], ]; - $this->assertSame($expected, $this->_model->getData()); } /** @@ -268,33 +417,32 @@ public function testSetDataByPath() */ public function testSetDataByPathEmpty() { - $this->_model->setDataByPath('', 'value'); + $this->model->setDataByPath('', 'value'); } /** * @param string $path - * @param string $expectedException - * + * @return void * @dataProvider setDataByPathWrongDepthDataProvider */ - public function testSetDataByPathWrongDepth($path, $expectedException) + public function testSetDataByPathWrongDepth(string $path) { - $expectedException = 'Allowed depth of configuration is 3 (<section>/<group>/<field>). ' . $expectedException; - $this->expectException('\UnexpectedValueException', $expectedException); + $currentDepth = count(explode('/', $path)); + $expectedException = 'Minimal depth of configuration is 3. Your configuration depth is ' . $currentDepth; + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage($expectedException); $value = 'value'; - $this->_model->setDataByPath($path, $value); + $this->model->setDataByPath($path, $value); } /** * @return array */ - public function setDataByPathWrongDepthDataProvider() + public function setDataByPathWrongDepthDataProvider(): array { return [ - 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"], - 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"], - 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path" - . " 'section/group/field/sub-field'", ], + 'depth 2' => ['section/group'], + 'depth 1' => ['section'], ]; } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Placeholder/EnvironmentTest.php b/app/code/Magento/Config/Test/Unit/Model/Placeholder/EnvironmentTest.php index 8217ff09c0541..e4c01e794fb0f 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Placeholder/EnvironmentTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Placeholder/EnvironmentTest.php @@ -52,6 +52,9 @@ public function testGenerate($path, $scope, $scopeId, $expected) ); } + /** + * @return array + */ public function getGenerateDataProvider() { return [ diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 0160339d50209..9a50bfced8f61 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-config", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-store": "100.2.*", "magento/module-cron": "100.2.*", @@ -13,7 +13,7 @@ "magento/module-deploy": "100.2.*" }, "type": "magento2-module", - "version": "101.0.2", + "version": "101.0.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml index c21c06c7f3e1f..5e54f177776ba 100644 --- a/app/code/Magento/Config/etc/adminhtml/di.xml +++ b/app/code/Magento/Config/etc/adminhtml/di.xml @@ -15,6 +15,8 @@ <arguments> <argument name="visibility" xsi:type="array"> <item name="productionVisibility" xsi:type="object">Magento\Config\Model\Config\Structure\ConcealInProductionConfigList</item> + <item name="concealInProduction" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction</item> + <item name="concealInProductionWithoutScdOnDemand" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index bcddd8ceaf27a..87a0e666d2d7b 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -77,6 +77,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Lock\Backend\Cache"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> + </arguments> + </type> <type name="Magento\Config\App\Config\Type\System"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregatedProxy</argument> @@ -85,6 +90,7 @@ <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> </arguments> </type> <type name="Magento\Config\App\Config\Type\System\Reader"> @@ -296,10 +302,21 @@ <arguments> <argument name="processors" xsi:type="array"> <item name="default" xsi:type="string">Magento\Config\Console\Command\ConfigSet\DefaultProcessor</item> - <item name="lock" xsi:type="string">Magento\Config\Console\Command\ConfigSet\LockProcessor</item> + <item name="lock-env" xsi:type="string">Magento\Config\Console\Command\ConfigSet\VirtualLockEnvProcessor</item> + <item name="lock-config" xsi:type="string">Magento\Config\Console\Command\ConfigSet\VirtualLockConfigProcessor</item> </argument> </arguments> </type> + <virtualType name="Magento\Config\Console\Command\ConfigSet\VirtualLockEnvProcessor" type="Magento\Config\Console\Command\ConfigSet\LockProcessor"> + <arguments> + <argument name="target" xsi:type="string">app_env</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Config\Console\Command\ConfigSet\VirtualLockConfigProcessor" type="Magento\Config\Console\Command\ConfigSet\LockProcessor"> + <arguments> + <argument name="target" xsi:type="string">app_config</argument> + </arguments> + </virtualType> <type name="Magento\Deploy\Model\DeploymentConfig\ImporterPool"> <arguments> <argument name="importers" xsi:type="array"> diff --git a/app/code/Magento/Config/etc/module.xml b/app/code/Magento/Config/etc/module.xml index b64cbe2b72623..b7df33554af90 100644 --- a/app/code/Magento/Config/etc/module.xml +++ b/app/code/Magento/Config/etc/module.xml @@ -6,5 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Config" setup_version="2.0.0"/> + <module name="Magento_Config" setup_version="2.1.0"> + <sequence> + <module name="Magento_Store"/> + </sequence> + </module> </config> diff --git a/app/code/Magento/Config/etc/system_file.xsd b/app/code/Magento/Config/etc/system_file.xsd index 5a2b915262a9a..f1688b2e35371 100644 --- a/app/code/Magento/Config/etc/system_file.xsd +++ b/app/code/Magento/Config/etc/system_file.xsd @@ -474,7 +474,7 @@ </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[A-Za-z0-9\\:]+" /> + <xs:pattern value="[A-Za-z0-9_\\:]+" /> <xs:minLength value="5" /> </xs:restriction> </xs:simpleType> diff --git a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml index e8082dae94d98..49a75d36fd8a5 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @deprecated * @var $block \Magento\Backend\Block\Page\System\Config\Robots\Reset * @var $jsonHelper \Magento\Framework\Json\Helper\Data */ -$jsonHelper = $this->helper('Magento\Framework\Json\Helper\Data'); +$jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); ?> <script> @@ -19,8 +17,8 @@ $jsonHelper = $this->helper('Magento\Framework\Json\Helper\Data'); 'jquery' ], function ($) { window.resetRobotsToDefault = function(){ - $('#design_search_engine_robots_custom_instructions').val(<?php - /* @escapeNotVerified */ echo $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) + $('#design_search_engine_robots_custom_instructions').val(<?= + /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) ?>); } }); diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml index 9bdca2bdfd280..d11f60134f7e4 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> -<?php /** * @methods * getTitle() - string @@ -16,7 +12,7 @@ * getForm() - html */ ?> -<form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> +<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> <div class="accordion"> <?= $block->getChildHtml('form') ?> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml index 93af2cfa653f8..b9cb02927a78f 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php @@ -13,30 +10,30 @@ $_htmlId = $block->getHtmlId() ? $block->getHtmlId() : '_' . uniqid(); $_colspan = $block->isAddAfter() ? 2 : 1; ?> -<div class="design_theme_ua_regexp" id="grid<?= /* @escapeNotVerified */ $_htmlId ?>"> +<div class="design_theme_ua_regexp" id="grid<?= $block->escapeHtmlAttr($_htmlId) ?>"> <div class="admin__control-table-wrapper"> - <table class="admin__control-table" id="<?= /* @escapeNotVerified */ $block->getElement()->getId() ?>"> + <table class="admin__control-table" id="<?= $block->escapeHtmlAttr($block->getElement()->getId()) ?>"> <thead> <tr> - <?php foreach ($block->getColumns() as $columnName => $column): ?> - <th><?= /* @escapeNotVerified */ $column['label'] ?></th> - <?php endforeach;?> - <th class="col-actions" colspan="<?= /* @escapeNotVerified */ $_colspan ?>">Action</th> + <?php foreach ($block->getColumns() as $columnName => $column) : ?> + <th><?= $block->escapeHtml($column['label']) ?></th> + <?php endforeach; ?> + <th class="col-actions" colspan="<?= (int)$_colspan ?>"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> <tfoot> <tr> <td colspan="<?= count($block->getColumns())+$_colspan ?>" class="col-actions-add"> - <button id="addToEndBtn<?= /* @escapeNotVerified */ $_htmlId ?>" class="action-add" title="<?= /* @escapeNotVerified */ __('Add') ?>" type="button"> - <span><?= /* @escapeNotVerified */ $block->getAddButtonLabel() ?></span> + <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" class="action-add" title="<?= $block->escapeHtmlAttr(__('Add')) ?>" type="button"> + <span><?= $block->escapeHtml($block->getAddButtonLabel()) ?></span> </button> </td> </tr> </tfoot> - <tbody id="addRow<?= /* @escapeNotVerified */ $_htmlId ?>"></tbody> + <tbody id="addRow<?= $block->escapeHtmlAttr($_htmlId) ?>"></tbody> </table> </div> - <input type="hidden" name="<?= /* @escapeNotVerified */ $block->getElement()->getName() ?>[__empty]" value="" /> + <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>[__empty]" value="" /> <script> require([ @@ -44,23 +41,28 @@ $_colspan = $block->isAddAfter() ? 2 : 1; 'prototype' ], function (mageTemplate) { // create row creator - window.arrayRow<?= /* @escapeNotVerified */ $_htmlId ?> = { + window.arrayRow<?= $block->escapeJs($_htmlId) ?> = { // define row prototypeJS template template: mageTemplate( '<tr id="<%- _id %>">' - <?php foreach ($block->getColumns() as $columnName => $column): ?> - + '<td>' - + '<?= /* @escapeNotVerified */ $block->renderCellTemplate($columnName) ?>' - + '<\/td>' - <?php endforeach; ?> - - <?php if ($block->isAddAfter()): ?> - + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span><?= /* @escapeNotVerified */ __('Add after') ?><\/span><\/button><\/td>' - <?php endif; ?> + <?php foreach ($block->getColumns() as $columnName => $column) : ?> + + '<td>' + + '<?= $block->escapeJs($block->renderCellTemplate($columnName)) ?>' + + '<\/td>' + <?php endforeach; ?> - + '<td class="col-actions"><button onclick="arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>.del(\'<%- _id %>\')" class="action-delete" type="button"><span><?= /* @escapeNotVerified */ __('Delete') ?><\/span><\/button><\/td>' - +'<\/tr>' + <?php if ($block->isAddAfter()) : ?> + + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span>' + + '<?= $block->escapeJs($block->escapeHtml(__('Add after'))) ?>' + + '<\/span><\/button><\/td>' + <?php endif; ?> + + + '<td class="col-actions"><button ' + + 'onclick="arrayRow<?= $block->escapeJs($_htmlId) ?>.del(\'<%- _id %>\')" ' + + 'class="action-delete" type="button">' + + '<span><?= $block->escapeJs($block->escapeHtml(__('Delete'))) ?><\/span><\/button><\/td>' + + '<\/tr>' ), add: function(rowData, insertAfterId) { @@ -73,56 +75,61 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } else { var d = new Date(); templateValues = { - <?php foreach ($block->getColumns() as $columnName => $column): ?> - <?= /* @escapeNotVerified */ $columnName ?>: '', - 'option_extra_attrs': {}, - <?php endforeach; ?> + <?php foreach ($block->getColumns() as $columnName => $column) : ?> + <?= $block->escapeJs($columnName) ?>: '', + 'option_extra_attrs': {}, + <?php endforeach; ?> _id: '_' + d.getTime() + '_' + d.getMilliseconds() }; } // Insert new row after specified row or at the bottom if (insertAfterId) { - Element.insert($(insertAfterId), {after: this.template(templateValues)}); - } else { - Element.insert($('addRow<?= /* @escapeNotVerified */ $_htmlId ?>'), {bottom: this.template(templateValues)}); - } + Element.insert($(insertAfterId), {after: this.template(templateValues)}); + } else { + Element.insert($('addRow<?= $block->escapeJs($_htmlId) ?>'), {bottom: this.template(templateValues)}); + } - // Fill controls with data - if (rowData) { - var rowInputElementNames = Object.keys(rowData.column_values); - for (var i = 0; i < rowInputElementNames.length; i++) { - if ($(rowInputElementNames[i])) { - $(rowInputElementNames[i]).setValue(rowData.column_values[rowInputElementNames[i]]); + // Fill controls with data + if (rowData) { + var rowInputElementNames = Object.keys(rowData.column_values); + for (var i = 0; i < rowInputElementNames.length; i++) { + if ($(rowInputElementNames[i])) { + $(rowInputElementNames[i]).setValue(rowData.column_values[rowInputElementNames[i]]); + } } } - } - // Add event for {addAfterBtn} button - <?php if ($block->isAddAfter()): ?> - Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); + // Add event for {addAfterBtn} button + <?php if ($block->isAddAfter()) : ?> + Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); <?php endif; ?> - }, + }, - del: function(rowId) { - $(rowId).remove(); - } + del: function(rowId) { + $(rowId).remove(); + } } // bind add action to "Add" button in last row - Event.observe('addToEndBtn<?= /* @escapeNotVerified */ $_htmlId ?>', 'click', arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>.add.bind(arrayRow<?= /* @escapeNotVerified */ $_htmlId ?>, false, false)); + Event.observe('addToEndBtn<?= $block->escapeJs($_htmlId) ?>', + 'click', + arrayRow<?= $block->escapeJs($_htmlId) ?>.add.bind( + arrayRow<?= $block->escapeJs($_htmlId) ?>, false, false + ) + ); // add existing rows <?php foreach ($block->getArrayRows() as $_rowId => $_row) { - /* @escapeNotVerified */ echo "arrayRow{$_htmlId}.add(" . $_row->toJson() . ");\n"; + echo /* @noEscape */ "arrayRow{$block->escapeJs($_htmlId)}.add(" . /* @noEscape */ $_row->toJson() . ");\n"; } ?> // Toggle the grid availability, if element is disabled (depending on scope) - <?php if ($block->getElement()->getDisabled()):?> - toggleValueElements({checked: true}, $('grid<?= /* @escapeNotVerified */ $_htmlId ?>').parentNode); - <?php endif;?> + <?php if ($block->getElement()->getDisabled()) : ?> + toggleValueElements({checked: true}, $('grid<?= $block->escapeJs($_htmlId) ?>').parentNode); + <?php endif; ?> }); </script> </div> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml index b703641acadb8..297687786833d 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <script> require([ @@ -71,7 +68,7 @@ originModel.prototype = { { this.reload = false; this.loader = new varienLoader(true); - this.regionsUrl = "<?= /* @escapeNotVerified */ $block->getUrl('directory/json/countryRegion') ?>"; + this.regionsUrl = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('directory/json/countryRegion'))) ?>"; this.bindCountryRegionRelation(); }, @@ -146,21 +143,32 @@ originModel.prototype = { var value = this.regionElement.value; var disabled = this.regionElement.disabled; if (data.length) { - var html = '<select name="'+this.regionElement.name+'" id="'+this.regionElement.id+'" class="required-entry select" title="'+this.regionElement.title+'"'+(disabled?" disabled":"")+'>'; + var select = document.createElement('select'); + select.setAttribute('name', this.regionElement.name); + select.setAttribute('title', this.regionElement.title); + select.setAttribute('id', this.regionElement.id); + select.setAttribute('class', 'required-entry select'); + if (disabled) { + select.setAttribute('disabled', ''); + } for (var i in data) { if (data[i].label) { - html+= '<option value="'+data[i].value+'"'; - if (this.regionElement.value && (this.regionElement.value == data[i].value || this.regionElement.value == data[i].label)) { - html+= ' selected'; + var option = document.createElement('option'); + option.setAttribute('value', data[i].value); + option.innerText = data[i].label; + if (this.regionElement.value && + (this.regionElement.value == data[i].value || this.regionElement.value == data[i].label) + ) { + option.setAttribute('selected', ''); } - html+='>'+data[i].label+'<\/option>'; + select.add(option); } } - html+= '<\/select>'; var parentNode = this.regionElement.parentNode; var regionElementId = this.regionElement.id; - parentNode.innerHTML = html; + parentNode.innerHTML = select.outerHTML; + this.regionElement = $(regionElementId); } else if (this.reload) { this.clearRegionField(disabled); @@ -168,10 +176,18 @@ originModel.prototype = { } }, clearRegionField: function(disabled) { - var html = '<input type="text" name="' + this.regionElement.name + '" id="' + this.regionElement.id + '" class="input-text" title="' + this.regionElement.title + '"' + (disabled ? " disabled" : "") + '>'; + var text = document.createElement('input'); + text.setAttribute('type', 'text'); + text.setAttribute('name', this.regionElement.name); + text.setAttribute('title', this.regionElement.title); + text.setAttribute('id', this.regionElement.id); + text.setAttribute('class', 'input-text'); + if (disabled) { + text.setAttribute('disabled', ''); + } var parentNode = this.regionElement.parentNode; var regionElementId = this.regionElement.id; - parentNode.innerHTML = html; + parentNode.innerHTML = text.outerHTML; this.regionElement = $(regionElementId); } } diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml index 6677b80076478..78ab4e5acffc3 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml @@ -3,34 +3,31 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Backend\Block\Template */ ?> <div class="field field-store-switcher"> - <label class="label" for="store_switcher"><?= /* @escapeNotVerified */ __('Current Configuration Scope:') ?></label> + <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Current Configuration Scope:')) ?></label> <div class="control"> <select id="store_switcher" class="system-config-store-switcher" onchange="location.href=this.options[this.selectedIndex].getAttribute('url')"> - <?php foreach ($block->getStoreSelectOptions() as $_value => $_option): ?> - <?php if (isset($_option['is_group'])): ?> - <?php if ($_option['is_close']): ?> + <?php foreach ($block->getStoreSelectOptions() as $_value => $_option) : ?> + <?php if (isset($_option['is_group'])) : ?> + <?php if ($_option['is_close']) : ?> </optgroup> - <?php else: ?> - <optgroup label="<?= $block->escapeHtml($_option['label']) ?>" style="<?= /* @escapeNotVerified */ $_option['style'] ?>"> + <?php else : ?> + <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>" style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> <?php endif; ?> <?php continue ?> <?php endif; ?> - <option value="<?= $block->escapeHtml($_value) ?>" url="<?= /* @escapeNotVerified */ $_option['url'] ?>" <?= $_option['selected'] ? 'selected="selected"' : '' ?> style="<?= /* @escapeNotVerified */ $_option['style'] ?>"> - <?= $block->escapeHtml($_option['label']) ?> - </option> + <option value="<?= $block->escapeHtmlAttr($_value) ?>" url="<?= $block->escapeUrl($_option['url']) ?>" <?= $_option['selected'] ? 'selected="selected"' : '' ?> style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> + <?= $block->escapeHtml($_option['label']) ?> + </option> <?php endforeach ?> </select> </div> <?= $block->getHintHtml() ?> - <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> + <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> <div class="actions"> - <a href="<?= /* @escapeNotVerified */ $block->getUrl('*/system_store') ?>"><?= /* @escapeNotVerified */ __('Stores') ?></a> + <a href="<?= $block->escapeUrl($block->getUrl('*/system_store')) ?>"><?= $block->escapeHtml(__('Stores')) ?></a> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml index d7000f28b5ef3..dd56ba584bd80 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/tabs.phtml @@ -4,52 +4,46 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Config\Block\System\Config\Tabs */ ?> -<?php if ($block->getTabs()): ?> - <div id="<?= /* @escapeNotVerified */ $block->getId() ?>" class="config-nav"> +<?php if ($block->getTabs()) : ?> + <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>" class="config-nav"> <?php /** @var $_tab \Magento\Config\Model\Config\Structure\Element\Tab */ - foreach ($block->getTabs() as $_tab): - ?> - - <?php - $activeCollapsible = false; - foreach ($_tab->getChildren() as $_section) { - if ($block->isSectionActive($_section)) { - $activeCollapsible = true; - } + foreach ($block->getTabs() as $_tab) : + $activeCollapsible = false; + foreach ($_tab->getChildren() as $_section) { + if ($block->isSectionActive($_section)) { + $activeCollapsible = true; } - ?> + } ?> <div class="config-nav-block admin__page-nav _collapsed - <?php if ($_tab->getClass()): ?> - <?= /* @escapeNotVerified */ $_tab->getClass() ?> + <?php if ($_tab->getClass()) : ?> + <?= $block->escapeHtmlAttr($_tab->getClass()) ?> <?php endif ?>" - data-mage-init='{"collapsible":{"active": "<?= /* @escapeNotVerified */ $activeCollapsible ?>", - "openedState": "_show", - "closedState": "_hide", - "collapsible": true, - "animate": 200}}'> + data-mage-init='{"collapsible":{"active": "<?= $block->escapeHtmlAttr($activeCollapsible) ?>", + "openedState": "_show", + "closedState": "_hide", + "collapsible": true, + "animate": 200}}'> <div class="admin__page-nav-title title _collapsible" data-role="title"> - <strong><?= /* @escapeNotVerified */ $_tab->getLabel() ?></strong> + <strong><?= $block->escapeHtml($_tab->getLabel()) ?></strong> </div> <ul class="admin__page-nav-items items" data-role="content"> <?php $_iterator = 1; ?> <?php /** @var $_section \Magento\Config\Model\Config\Structure\Element\Section */ - foreach ($_tab->getChildren() as $_section): ?> + foreach ($_tab->getChildren() as $_section) : ?> <li class="admin__page-nav-item item - <?= /* @escapeNotVerified */ $_section->getClass() ?> - <?php if ($block->isSectionActive($_section)): ?> _active<?php endif ?> + <?= $block->escapeHtml($_section->getClass()) ?> + <?php if ($block->isSectionActive($_section)) : ?> _active<?php endif ?> <?= $_tab->getChildren()->isLast($_section) ? ' _last' : '' ?>"> - <a href="<?= /* @escapeNotVerified */ $block->getSectionUrl($_section) ?>" + <a href="<?= $block->escapeUrl($block->getSectionUrl($_section)) ?>" class="admin__page-nav-link item-nav"> - <span><?= /* @escapeNotVerified */ $_section->getLabel() ?></span> + <span><?= $block->escapeHtml($_section->getLabel()) ?></span> </a> </li> <?php $_iterator++; ?> @@ -57,8 +51,6 @@ </ul> </div> - <?php - endforeach; - ?> + <?php endforeach; ?> </div> <?php endif; ?> diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php index 58462b873d8b1..7146108f61fe1 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableImportExport\Model\Export; -use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; use Magento\ImportExport\Model\Import; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +/** + * Customizes output during export + */ class RowCustomizer implements RowCustomizerInterface { /** @@ -36,6 +43,19 @@ class RowCustomizer implements RowCustomizerInterface self::CONFIGURABLE_VARIATIONS_LABELS_COLUMN ]; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + /** * Prepare configurable data for export * @@ -49,6 +69,9 @@ public function prepareData($collection, $productIds) $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); + // set global scope during export + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + while ($product = $productCollection->fetchItem()) { $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); $this->configurableData[$product->getId()] = []; diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index 718a7dba73eb2..1b439c4cb5dac 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -1,21 +1,18 @@ <?php /** - * Import entity configurable product type model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ConfigurableImportExport\Model\Import\Product\Type; use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; /** - * Importing configurable products + * Import entity configurable product type model * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -34,16 +31,24 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ const ERROR_DUPLICATED_VARIATIONS = 'duplicatedVariations'; + const ERROR_UNIDENTIFIABLE_VARIATION = 'unidentifiableVariation'; + /** * Validation failure message template definitions * * @var array */ protected $_messageTemplates = [ - self::ERROR_ATTRIBUTE_CODE_IS_NOT_SUPER => 'Attribute with code "%s" is not super', - self::ERROR_INVALID_OPTION_VALUE => 'Invalid option value for attribute "%s"', - self::ERROR_INVALID_WEBSITE => 'Invalid website code for super attribute', - self::ERROR_DUPLICATED_VARIATIONS => 'SKU %s contains duplicated variations', + self::ERROR_ATTRIBUTE_CODE_IS_NOT_SUPER => + 'Attribute with code "%s" is not super', + self::ERROR_INVALID_OPTION_VALUE => + 'Invalid option value for attribute "%s"', + self::ERROR_INVALID_WEBSITE => + 'Invalid website code for super attribute', + self::ERROR_DUPLICATED_VARIATIONS => + 'SKU %s contains duplicated variations', + self::ERROR_UNIDENTIFIABLE_VARIATION => + 'Configurable variation "%s" is unidentifiable', ]; /** @@ -249,9 +254,8 @@ protected function _getSuperAttributeId($productId, $attributeId) { if (isset($this->_productSuperAttrs["{$productId}_{$attributeId}"])) { return $this->_productSuperAttrs["{$productId}_{$attributeId}"]; - } else { - return null; } + return null; } /** @@ -453,7 +457,8 @@ protected function _processSuperData() ]; $subEntityId = $this->connection->fetchOne( $this->connection->select()->from( - ['cpe' => $this->_resource->getTableName('catalog_product_entity')], ['entity_id'] + ['cpe' => $this->_resource->getTableName('catalog_product_entity')], + ['entity_id'] )->where($metadata->getLinkField() . ' = ?', $assocId) ); $this->_superAttributesData['relation'][] = [ @@ -471,14 +476,22 @@ protected function _processSuperData() * @param array $rowData * * @return array + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _parseVariations($rowData) { $additionalRows = []; - if (!isset($rowData['configurable_variations'])) { + if (empty($rowData['configurable_variations'])) { return $additionalRows; + } elseif (!empty($rowData['store_view_code'])) { + throw new LocalizedException( + __( + 'Product with assigned super attributes should not have specified "%1" value', + 'store_view_code' + ) + ); } $variations = explode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $rowData['configurable_variations']); foreach ($variations as $variation) { @@ -489,8 +502,10 @@ protected function _parseVariations($rowData) foreach ($fieldAndValuePairsText as $nameAndValue) { $nameAndValue = explode(ImportProduct::PAIR_NAME_VALUE_SEPARATOR, $nameAndValue); if (!empty($nameAndValue)) { - $value = isset($nameAndValue[1]) ? trim($nameAndValue[1]) : ''; - $fieldName = trim($nameAndValue[0]); + $value = isset($nameAndValue[1]) ? + trim($nameAndValue[1]) : ''; + //Ignoring field names' case. + $fieldName = strtolower(trim($nameAndValue[0])); if ($fieldName) { $fieldAndValuePairs[$fieldName] = $value; } @@ -511,8 +526,19 @@ protected function _parseVariations($rowData) $additionalRow = []; $position += 1; } + } else { + $errorCode = self::ERROR_UNIDENTIFIABLE_VARIATION; + throw new LocalizedException( + __( + sprintf( + $this->_messageTemplates[$errorCode], + $variation + ) + ) + ); } } + return $additionalRows; } @@ -608,7 +634,7 @@ protected function _insertData() } /** - * Get new supper attribute id. + * Get new super attribute id. * * @return int */ @@ -823,7 +849,14 @@ protected function configurableInBunch($bunch) public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) { $error = false; - $dataWithExtraVirtualRows = $this->_parseVariations($rowData); + try { + $dataWithExtraVirtualRows = $this->_parseVariations($rowData); + } catch (LocalizedException $exception) { + $this->_entityModel->addRowError($exception->getMessage(), $rowNum); + + return false; + } + $skus = []; $rowData['price'] = isset($rowData['price']) && $rowData['price'] ? $rowData['price'] : '0.00'; if (!empty($dataWithExtraVirtualRows)) { @@ -835,12 +868,19 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) if (isset($option['_super_products_sku'])) { if (in_array($option['_super_products_sku'], $skus)) { $error = true; - $this->_entityModel->addRowError(sprintf($this->_messageTemplates[self::ERROR_DUPLICATED_VARIATIONS], $option['_super_products_sku']), $rowNum); + $this->_entityModel->addRowError( + sprintf( + $this->_messageTemplates[self::ERROR_DUPLICATED_VARIATIONS], + $option['_super_products_sku'] + ), + $rowNum + ); } $skus[] = $option['_super_products_sku']; } $error |= !parent::isRowValid($option, $rowNum, $isNewProduct); } + return !$error; } diff --git a/app/code/Magento/ConfigurableImportExport/Test/Mftf/LICENSE.txt b/app/code/Magento/ConfigurableImportExport/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ConfigurableImportExport/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 <insert your license name here>" 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/app/code/Magento/ConfigurableImportExport/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ConfigurableImportExport/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ConfigurableImportExport/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 <insert your license name here>" 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/app/code/Magento/ConfigurableImportExport/Test/Mftf/README.md b/app/code/Magento/ConfigurableImportExport/Test/Mftf/README.md new file mode 100644 index 0000000000000..e496ea6011c7f --- /dev/null +++ b/app/code/Magento/ConfigurableImportExport/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Configurable Import Export Functional Tests + +The Functional Test Module for **Magento Configurable Import Export** module. diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php index f6912fe8b6d6c..1ff78ed89709a 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php @@ -392,6 +392,9 @@ protected function _getBunch() ]; } + /** + * @return array + */ protected function _getSuperAttributes() { return [ @@ -560,15 +563,56 @@ public function testIsRowValid() '_type' => 'configurable', '_product_websites' => 'website_1', ]; + //Checking that variations' field names are case-insensitive with this + //product. + $caseInsensitiveSKU = 'configurableskuI22CaseInsensitive'; + $caseInsensitiveProduct = [ + 'sku' => $caseInsensitiveSKU, + 'store_view_code' => null, + 'attribute_set_code' => 'Default', + 'product_type' => 'configurable', + 'name' => 'Configurable Product 21', + 'product_websites' => 'website_1', + 'configurable_variation_labels' => 'testattr2=Select Color, testattr3=Select Size', + 'configurable_variations' => 'SKU=testconf2-attr2val1-testattr3v1,' + . 'testattr2=attr2val1,' + . 'testattr3=testattr3v1,' + . 'display=1|sku=testconf2-attr2val1-testattr3v2,' + . 'testattr2=attr2val1,' + . 'testattr3=testattr3v2,' + . 'display=0', + '_store' => null, + '_attribute_set' => 'Default', + '_type' => 'configurable', + '_product_websites' => 'website_1', + ]; $bunch[] = $badProduct; + $bunch[] = $caseInsensitiveProduct; // Set _attributes to avoid error in Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType. $this->setPropertyValue($this->configurable, '_attributes', [ $badProduct[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [], ]); + //Avoiding errors about attributes not being super + $this->setPropertyValue( + $this->configurable, + '_superAttributes', + [ + 'testattr2' => ['options' => ['attr2val1' => 1]], + 'testattr3' => [ + 'options' => [ + 'testattr3v2' => 1, + 'testattr3v1' => 1, + ], + ], + ] + ); foreach ($bunch as $rowData) { $result = $this->configurable->isRowValid($rowData, 0, !isset($this->_oldSku[$rowData['sku']])); $this->assertNotNull($result); + if ($rowData['sku'] === $caseInsensitiveSKU) { + $this->assertTrue($result); + } } } diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index 7fddf1081cddf..9fa93aadb87b8 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -2,16 +2,17 @@ "name": "magento/module-configurable-import-export", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-catalog-import-export": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-import-export": "100.2.*", "magento/module-configurable-product": "100.2.*", - "magento/framework": "101.0.*" + "magento/framework": "101.0.*", + "magento/module-store": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Created.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Created.php index 9ebd5f3ee3705..92c40f71f2db9 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Created.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Created.php @@ -16,7 +16,7 @@ class Created extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'catalog/product/attribute/new/created.phtml'; + protected $_template = 'Magento_ConfigurableProduct::catalog/product/attribute/new/created.phtml'; /** * Core registry diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php index 8848fc78dad6d..7f90d96e8a23e 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php @@ -8,22 +8,12 @@ use Magento\Ui\Component\Control\Container; use Magento\Catalog\Block\Adminhtml\Product\Edit\Button\Generic; use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; -use Magento\Catalog\Model\Product\Type; /** * Class Save */ class Save extends Generic { - /** - * @var array - */ - private static $availableProductTypes = [ - ConfigurableType::TYPE_CODE, - Type::TYPE_SIMPLE, - Type::TYPE_VIRTUAL - ]; - /** * {@inheritdoc} */ @@ -165,6 +155,6 @@ protected function getSaveAction() */ protected function isConfigurableProduct() { - return in_array($this->getProduct()->getTypeId(), self::$availableProductTypes); + return !$this->getProduct()->isComposite() || $this->getProduct()->getTypeId() === ConfigurableType::TYPE_CODE; } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config.php index c02a922c71b5c..1c5d01da574cf 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config.php @@ -18,7 +18,7 @@ class Config extends Widget implements TabInterface /** * @var string */ - protected $_template = 'catalog/product/edit/super/config.phtml'; + protected $_template = 'Magento_ConfigurableProduct::catalog/product/edit/super/config.phtml'; /** * Core registry diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php index a994532d5a69e..0233bfd2445ac 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php @@ -98,6 +98,8 @@ public function __construct( } /** + * Return currency symbol. + * * @return string */ public function getCurrencySymbol() @@ -184,6 +186,7 @@ public function getEditProductUrl($id) * Retrieve attributes data * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function getAttributes() { @@ -274,6 +277,8 @@ public function getImageUploadUrl() } /** + * Return product qty. + * * @param Product $product * @return float */ @@ -283,6 +288,8 @@ public function getProductStockQty(Product $product) } /** + * Return variation wizard. + * * @param array $initData * @return string */ @@ -298,6 +305,8 @@ public function getVariationWizard($initData) } /** + * Return product configuration matrix. + * * @return array|null */ public function getProductMatrix() @@ -309,6 +318,8 @@ public function getProductMatrix() } /** + * Return product attributes. + * * @return array|null */ public function getProductAttributes() @@ -316,10 +327,13 @@ public function getProductAttributes() if ($this->productAttributes === null) { $this->prepareVariations(); } + return $this->productAttributes; } /** + * Prepare product variations. + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return void * TODO: move to class @@ -344,36 +358,13 @@ protected function prepareVariations() $price = $product->getPrice(); $variationOptions = []; foreach ($usedProductAttributes as $attribute) { - if (!isset($attributes[$attribute->getAttributeId()])) { - $attributes[$attribute->getAttributeId()] = [ - 'code' => $attribute->getAttributeCode(), - 'label' => $attribute->getStoreLabel(), - 'id' => $attribute->getAttributeId(), - 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], - 'chosen' => [], - ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { - $attributes[$attribute->getAttributeId()]['options'][] = [ - 'attribute_code' => $attribute->getAttributeCode(), - 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), - ]; - } - } - } - $optionId = $variation[$attribute->getId()]['value']; - $variationOption = [ - 'attribute_code' => $attribute->getAttributeCode(), - 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $optionId, - 'label' => $variation[$attribute->getId()]['label'], - 'value' => $optionId, - ]; - $variationOptions[] = $variationOption; - $attributes[$attribute->getAttributeId()]['chosen'][] = $variationOption; + list($attributes, $variationOptions) = $this->prepareAttributes( + $attributes, + $attribute, + $configurableAttributes, + $variation, + $variationOptions + ); } $productMatrix[] = [ @@ -387,7 +378,8 @@ protected function prepareVariations() 'price' => $price, 'options' => $variationOptions, 'weight' => $product->getWeight(), - 'status' => $product->getStatus() + 'status' => $product->getStatus(), + '__disableTmpl' => true, ]; } } @@ -395,4 +387,58 @@ protected function prepareVariations() $this->productMatrix = $productMatrix; $this->productAttributes = array_values($attributes); } + + /** + * Prepare attributes. + * + * @param array $attributes + * @param object $attribute + * @param array $configurableAttributes + * @param array $variation + * @param array $variationOptions + * @return array + */ + private function prepareAttributes( + array $attributes, + $attribute, + array $configurableAttributes, + array $variation, + array $variationOptions + ): array { + if (!isset($attributes[$attribute->getAttributeId()])) { + $attributes[$attribute->getAttributeId()] = [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'id' => $attribute->getAttributeId(), + 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], + 'chosen' => [], + '__disableTmpl' => true + ]; + foreach ($attribute->getOptions() as $option) { + if (!empty($option->getValue())) { + $attributes[$attribute->getAttributeId()]['options'][] = [ + 'attribute_code' => $attribute->getAttributeCode(), + 'attribute_label' => $attribute->getStoreLabel(0), + 'id' => $option->getValue(), + 'label' => $option->getLabel(), + 'value' => $option->getValue(), + '__disableTmpl' => true, + ]; + } + } + } + $optionId = $variation[$attribute->getId()]['value']; + $variationOption = [ + 'attribute_code' => $attribute->getAttributeCode(), + 'attribute_label' => $attribute->getStoreLabel(0), + 'id' => $optionId, + 'label' => $variation[$attribute->getId()]['label'], + 'value' => $optionId, + '__disableTmpl' => true, + ]; + $variationOptions[] = $variationOption; + $attributes[$attribute->getAttributeId()]['chosen'][] = $variationOption; + + return [$attributes, $variationOptions]; + } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php index d1e4db41cd209..b128458665d73 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -19,6 +19,8 @@ class Configurable extends Renderer implements IdentityInterface { /** * Path in config to the setting which defines if parent or child product should be used to generate a thumbnail. + * @deprecated moved to model because of class refactoring + * @see \Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver::CONFIG_THUMBNAIL_SOURCE */ const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; @@ -55,28 +57,6 @@ public function getOptionList() return $this->_productConfig->getOptions($this->getItem()); } - /** - * {@inheritdoc} - */ - public function getProductForThumbnail() - { - /** - * Show parent product thumbnail if it must be always shown according to the related setting in system config - * or if child thumbnail is not available - */ - if ($this->_scopeConfig->getValue( - self::CONFIG_THUMBNAIL_SOURCE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) == ThumbnailSource::OPTION_USE_PARENT_IMAGE || - !($this->getChildProduct()->getThumbnail() && $this->getChildProduct()->getThumbnail() != 'no_selection') - ) { - $product = $this->getProduct(); - } else { - $product = $this->getChildProduct(); - } - return $product; - } - /** * Return identifiers for produced content * @@ -90,4 +70,12 @@ public function getIdentities() } return $identities; } + + /** + * @inheritdoc + */ + public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) + { + return parent::getProductPriceHtml($this->getChildProduct()); + } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index b5d02f64e6eb5..42e3231c8990e 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -7,6 +7,7 @@ */ namespace Magento\ConfigurableProduct\Block\Product\View\Type; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; use Magento\Customer\Model\Session; @@ -15,6 +16,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Configurable product view type. + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -74,6 +77,11 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView */ private $customerSession; + /** + * @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices + */ + private $variationPrices; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils @@ -86,6 +94,7 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView * @param array $data * @param Format|null $localeFormat * @param Session|null $customerSession + * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +108,8 @@ public function __construct( ConfigurableAttributeData $configurableAttributeData, array $data = [], Format $localeFormat = null, - Session $customerSession = null + Session $customerSession = null, + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null ) { $this->priceCurrency = $priceCurrency; $this->helper = $helper; @@ -109,6 +119,9 @@ public function __construct( $this->configurableAttributeData = $configurableAttributeData; $this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(Format::class); $this->customerSession = $customerSession ?: ObjectManager::getInstance()->get(Session::class); + $this->variationPrices = $variationPrices ?: ObjectManager::getInstance()->get( + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class + ); parent::__construct( $context, @@ -126,7 +139,7 @@ public function __construct( public function getCacheKeyInfo() { $parentData = parent::getCacheKeyInfo(); - $parentData[] = $this->priceCurrency->getCurrencySymbol(); + $parentData[] = $this->priceCurrency->getCurrency()->getCode(); $parentData[] = $this->customerSession->getCustomerGroupId(); return $parentData; } @@ -171,13 +184,15 @@ public function getAllowProducts() $products = []; $skipSaleableCheck = $this->catalogProduct->getSkipSaleableCheck(); $allProducts = $this->getProduct()->getTypeInstance()->getUsedProducts($this->getProduct(), null); + /** @var $product \Magento\Catalog\Model\Product */ foreach ($allProducts as $product) { - if ($product->isSaleable() || $skipSaleableCheck) { + if ($skipSaleableCheck || ((int) $product->getStatus()) === Status::STATUS_ENABLED) { $products[] = $product; } } $this->setAllowProducts($products); } + return $this->getData('allow_products'); } @@ -211,9 +226,6 @@ public function getJsonConfig() $store = $this->getCurrentStore(); $currentProduct = $this->getProduct(); - $regularPrice = $currentProduct->getPriceInfo()->getPrice('regular_price'); - $finalPrice = $currentProduct->getPriceInfo()->getPrice('final_price'); - $options = $this->helper->getOptions($currentProduct, $this->getAllowProducts()); $attributesData = $this->configurableAttributeData->getAttributesData($currentProduct, $options); @@ -223,17 +235,7 @@ public function getJsonConfig() 'currencyFormat' => $store->getCurrentCurrency()->getOutputFormat(), 'optionPrices' => $this->getOptionPrices(), 'priceFormat' => $this->localeFormat->getPriceFormat(), - 'prices' => [ - 'oldPrice' => [ - 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()), - ], - 'basePrice' => [ - 'amount' => $this->localeFormat->getNumber($finalPrice->getAmount()->getBaseAmount()), - ], - 'finalPrice' => [ - 'amount' => $this->localeFormat->getNumber($finalPrice->getAmount()->getValue()), - ], - ], + 'prices' => $this->variationPrices->getFormattedPrices($this->getProduct()->getPriceInfo()), 'productId' => $currentProduct->getId(), 'chooseText' => __('Choose an Option...'), 'images' => $this->getOptionImages(), @@ -279,6 +281,8 @@ protected function getOptionImages() } /** + * Collect price options. + * * @return array */ protected function getOptionPrices() @@ -317,6 +321,11 @@ protected function getOptionPrices() ), ], 'tierPrices' => $tierPrices, + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $product->getMsrp() + ), + ], ]; } return $prices; diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index 6f5f106a8bb24..45057a3591044 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -50,7 +50,11 @@ public function __construct( */ public function execute() { - $this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions())); + $result = []; + if ($this->getRequest()->isPost()) { + $result = $this->saveAttributeOptions(); + } + $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); } /** diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php index 5cd8b6a7d0b95..b5940e36aa792 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php @@ -158,7 +158,7 @@ protected function getVariationMatrix() $configurableMatrix = json_decode($configurableMatrix, true); foreach ($configurableMatrix as $item) { - if ($item['newProduct']) { + if (isset($item['newProduct']) && $item['newProduct']) { $result[$item['variationKey']] = $this->mapData($item); if (isset($item['qty'])) { diff --git a/app/code/Magento/ConfigurableProduct/CustomerData/ConfigurableItem.php b/app/code/Magento/ConfigurableProduct/CustomerData/ConfigurableItem.php index 0a4fc20578ed9..e8169fd926ae5 100644 --- a/app/code/Magento/ConfigurableProduct/CustomerData/ConfigurableItem.php +++ b/app/code/Magento/ConfigurableProduct/CustomerData/ConfigurableItem.php @@ -11,6 +11,9 @@ /** * Configurable item + * + * @deprecated moved to model because of class refactoring + * @see \Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver */ class ConfigurableItem extends DefaultItem { @@ -26,6 +29,7 @@ class ConfigurableItem extends DefaultItem * @param \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool * @param \Magento\Checkout\Helper\Data $checkoutHelper * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Framework\Escaper|null $escaper */ public function __construct( \Magento\Catalog\Helper\Image $imageHelper, @@ -33,14 +37,16 @@ public function __construct( \Magento\Framework\UrlInterface $urlBuilder, \Magento\Catalog\Helper\Product\ConfigurationPool $configurationPool, \Magento\Checkout\Helper\Data $checkoutHelper, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Framework\Escaper $escaper = null ) { parent::__construct( $imageHelper, $msrpHelper, $urlBuilder, $configurationPool, - $checkoutHelper + $checkoutHelper, + $escaper ); $this->_scopeConfig = $scopeConfig; } @@ -60,7 +66,11 @@ protected function getProductForThumbnail() ); $product = $config == ThumbnailSource::OPTION_USE_PARENT_IMAGE - || (!$this->getChildProduct()->getThumbnail() || $this->getChildProduct()->getThumbnail() == 'no_selection') + || ( + !$this->getChildProduct() || + !$this->getChildProduct()->getThumbnail() || + $this->getChildProduct()->getThumbnail() == 'no_selection' + ) ? $this->getProduct() : $this->getChildProduct(); diff --git a/app/code/Magento/ConfigurableProduct/Helper/Data.php b/app/code/Magento/ConfigurableProduct/Helper/Data.php index 1de82eaad3196..385228c1f2b1f 100644 --- a/app/code/Magento/ConfigurableProduct/Helper/Data.php +++ b/app/code/Magento/ConfigurableProduct/Helper/Data.php @@ -10,6 +10,7 @@ /** * Class Data + * * Helper class for getting options * @api * @since 100.0.2 @@ -85,8 +86,9 @@ public function getOptions($currentProduct, $allowedProducts) $productAttribute = $attribute->getProductAttribute(); $productAttributeId = $productAttribute->getId(); $attributeValue = $product->getData($productAttribute->getAttributeCode()); - - $options[$productAttributeId][$attributeValue][] = $productId; + if ($product->isSalable()) { + $options[$productAttributeId][$attributeValue][] = $productId; + } $options['index'][$productId][$productAttributeId] = $attributeValue; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php new file mode 100644 index 0000000000000..38204790a910f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Inventory; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; + +/** + * Process parent stock item + */ +class ParentItemProcessor +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param Configurable $configurableType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct( + Configurable $configurableType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration + ) { + $this->configurableType = $configurableType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Process parent products + * + * @param Product $product + * @return void + */ + public function process(Product $product) + { + $parentIds = $this->configurableType->getParentIdsByChild($product->getId()); + foreach ($parentIds as $productId) { + $this->processStockForParent((int)$productId); + } + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + private function processStockForParent(int $productId) + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + + $childrenIds = $this->configurableType->getChildrenIds($productId); + $criteria->setProductsFilter($childrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $childrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $childrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { + $parentStockItem->setIsInStock($childrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent( + StockItemInterface $parentStockItem, + bool $childrenIsInStock + ): bool { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 01981b5dae9db..7e11fd02793ea 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,6 +9,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Configurable product link management. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementInterface { /** @@ -67,7 +71,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getChildren($sku) { @@ -106,11 +110,15 @@ public function getChildren($sku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function addChild($sku, $childSku) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $child = $this->productRepository->get($childSku); $childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]); @@ -123,15 +131,17 @@ public function addChild($sku, $childSku) throw new StateException(__('Parent product does not have configurable product options')); } - $attributeIds = []; + $attributeData = []; foreach ($configurableProductOptions as $configurableProductOption) { $attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode(); if (!$child->getData($attributeCode)) { throw new StateException(__('Child product does not have attribute value %1', $attributeCode)); } - $attributeIds[] = $configurableProductOption->getAttributeId(); + $attributeData[$configurableProductOption->getAttributeId()] = [ + 'position' => $configurableProductOption->getPosition() + ]; } - $configurableOptionData = $this->getConfigurableAttributesData($attributeIds); + $configurableOptionData = $this->getConfigurableAttributesData($attributeData); /** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */ $optionFactory = $this->getOptionsFactory(); @@ -144,7 +154,11 @@ public function addChild($sku, $childSku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function removeChild($sku, $childSku) { @@ -191,16 +205,16 @@ private function getOptionsFactory() /** * Get Configurable Attribute Data * - * @param int[] $attributeIds + * @param int[] $attributeData * @return array */ - private function getConfigurableAttributesData($attributeIds) + private function getConfigurableAttributesData($attributeData) { $configurableAttributesData = []; $attributeValues = []; $attributes = $this->attributeFactory->create() ->getCollection() - ->addFieldToFilter('attribute_id', $attributeIds) + ->addFieldToFilter('attribute_id', array_keys($attributeData)) ->getItems(); foreach ($attributes as $attribute) { foreach ($attribute->getOptions() as $option) { @@ -217,6 +231,7 @@ private function getConfigurableAttributesData($attributeIds) 'attribute_id' => $attribute->getId(), 'code' => $attribute->getAttributeCode(), 'label' => $attribute->getStoreLabel(), + 'position' => $attributeData[$attribute->getId()]['position'], 'values' => $attributeValues, ]; } diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php new file mode 100644 index 0000000000000..68c574194817c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; + +/** + * Extender of product identities for child of configurable products + */ +class ProductIdentitiesExtender +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param Configurable $configurableType + * @param ProductRepositoryInterface $productRepository + */ + public function __construct(Configurable $configurableType, ProductRepositoryInterface $productRepository) + { + $this->configurableType = $configurableType; + $this->productRepository = $productRepository; + } + + /** + * Add parent identities to product identities + * + * @param Product $subject + * @param array $identities + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetIdentities(Product $subject, array $identities): array + { + $identities = (array) $identities; + + foreach ($this->configurableType->getParentIdsByChild($subject->getId()) as $parentId) { + $parentProduct = $this->productRepository->getById($parentId); + $identities = array_merge($identities, (array) $parentProduct->getIdentities()); + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php deleted file mode 100644 index ac42e320f3ad9..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\ConfigurableProduct\Model\Product\Cache\Tag; - -use Magento\Framework\App\Cache\Tag\StrategyInterface; - -/** - * Add parent invalidation tags - */ -class Configurable implements StrategyInterface -{ - /** - * Configurable product type resource - * - * @var \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable - */ - private $catalogProductTypeConfigurable; - - /** - * @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable - */ - public function __construct( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable - ) { - $this->catalogProductTypeConfigurable = $catalogProductTypeConfigurable; - } - - /** - * {@inheritdoc} - */ - public function getTags($object) - { - if (!is_object($object)) { - throw new \InvalidArgumentException('Provided argument is not an object'); - } - - if (!($object instanceof \Magento\Catalog\Model\Product)) { - throw new \InvalidArgumentException('Provided argument must be a product'); - } - - $result = $object->getIdentities(); - - foreach ($this->catalogProductTypeConfigurable->getParentIdsByChild($object->getId()) as $parentId) { - $result[] = \Magento\Catalog\Model\Product::CACHE_TAG . '_' . $parentId; - } - return $result; - } -} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php new file mode 100644 index 0000000000000..ef757902097e4 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Product\Configuration\Item; + +use Magento\Catalog\Model\Config\Source\Product\Thumbnail; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Catalog\Model\Product; +use Magento\Store\Model\ScopeInterface; + +/** + * Resolves the product from a configured item. + */ +class ItemProductResolver implements ItemResolverInterface +{ + /** + * Path in config to the setting which defines if parent or child product should be used to generate a thumbnail. + */ + const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get the final product from a configured item by product type and selection. + * + * @param ItemInterface $item + * @return ProductInterface + */ + public function getFinalProduct(ItemInterface $item): ProductInterface + { + /** + * Show parent product thumbnail if it must be always shown according to the related setting in system config + * or if child thumbnail is not available. + */ + $finalProduct = $item->getProduct(); + $childProduct = $this->getChildProduct($item); + + if ($childProduct !== null && $this->isUseChildProduct($childProduct)) { + $finalProduct = $childProduct; + } + + return $finalProduct; + } + + /** + * Get item configurable child product. + * + * @param ItemInterface $item + * @return Product|null + */ + private function getChildProduct(ItemInterface $item) + { + $option = $item->getOptionByCode('simple_product'); + + return $option ? $option->getProduct() : null; + } + + /** + * Is need to use child product + * + * @param Product $childProduct + * @return bool + */ + private function isUseChildProduct(Product $childProduct): bool + { + $configValue = $this->scopeConfig->getValue( + self::CONFIG_THUMBNAIL_SOURCE, + ScopeInterface::SCOPE_STORE + ); + $childThumb = $childProduct->getData('thumbnail'); + return $configValue !== Thumbnail::OPTION_USE_PARENT_IMAGE + && $childThumb !== null + && $childThumb !== 'no_selection'; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php index d42d4ccafdd01..fb0735018b686 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php @@ -10,6 +10,8 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable as ResourceModelConfigurable; use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\ConfigurableProduct\Api\Data\OptionInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; /** * Class SaveHandler @@ -76,7 +78,7 @@ public function execute($entity, $arguments = []) } /** - * Save attributes for configurable product + * Save only newly created attributes for configurable product * * @param ProductInterface $product * @param array $attributes @@ -85,26 +87,57 @@ public function execute($entity, $arguments = []) private function saveConfigurableProductAttributes(ProductInterface $product, array $attributes) { $ids = []; + $existingAttributeIds = []; + foreach ($this->optionRepository->getList($product->getSku()) as $option) { + $existingAttributeIds[$option->getAttributeId()] = $option; + } /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $attribute */ foreach ($attributes as $attribute) { - $attribute->setId(null); - $ids[] = $this->optionRepository->save($product->getSku(), $attribute); + if (!in_array($attribute->getAttributeId(), array_keys($existingAttributeIds)) + || $this->isOptionChanged($existingAttributeIds[$attribute->getAttributeId()], $attribute) + ) { + $attribute->setId(null); + $ids[] = $this->optionRepository->save($product->getSku(), $attribute); + } } - return $ids; } /** - * Remove product attributes + * Remove product attributes which no longer used * * @param ProductInterface $product * @return void */ private function deleteConfigurableProductAttributes(ProductInterface $product) { - $list = $this->optionRepository->getList($product->getSku()); - foreach ($list as $item) { - $this->optionRepository->deleteById($product->getSku(), $item->getId()); + $newAttributeIds = []; + foreach ($product->getExtensionAttributes()->getConfigurableProductOptions() as $option) { + $newAttributeIds[$option->getAttributeId()] = $option; + } + foreach ($this->optionRepository->getList($product->getSku()) as $option) { + if (!in_array($option->getAttributeId(), array_keys($newAttributeIds)) + || $this->isOptionChanged($option, $newAttributeIds[$option->getAttributeId()]) + ) { + $this->optionRepository->deleteById($product->getSku(), $option->getId()); + } + } + } + + /** + * Check if existing option is changed + * + * @param OptionInterface $option + * @param Attribute $attribute + * @return bool + */ + private function isOptionChanged(OptionInterface $option, Attribute $attribute) + { + if ($option->getLabel() == $attribute->getLabel() + && $option->getPosition() == $attribute->getPosition() + ) { + return false; } + return true; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index e6345af40f37a..113ef8fea1418 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -3,17 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Model\Product\Type; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; +use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State as AppState; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; /** * Configurable product type implementation @@ -23,6 +25,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -192,6 +195,11 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $salableProcessor; + /** + * @var AppState + */ + private $appState; + /** * @codingStandardsIgnoreStart/End * @@ -215,6 +223,7 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param ProductInterfaceFactory $productFactory * @param SalableProcessor $salableProcessor + * @param AppState|null $appState * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -239,7 +248,8 @@ public function __construct( \Magento\Customer\Model\Session $customerSession = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, ProductInterfaceFactory $productFactory = null, - SalableProcessor $salableProcessor = null + SalableProcessor $salableProcessor = null, + AppState $appState = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -254,6 +264,7 @@ public function __construct( $this->productFactory = $productFactory ?: ObjectManager::getInstance() ->get(ProductInterfaceFactory::class); $this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class); + $this->appState = $appState ?? ObjectManager::getInstance()->get(AppState::class); parent::__construct( $catalogProductOption, $eavConfig, @@ -266,7 +277,6 @@ public function __construct( $productRepository, $serializer ); - } /** @@ -453,6 +463,10 @@ public function getConfigurableAttributes($product) ['group' => 'CONFIGURABLE', 'method' => __METHOD__] ); if (!$product->hasData($this->_configurableAttributes)) { + // for new product do not load configurable attributes + if (!$product->getId()) { + return []; + } $configurableAttributes = $this->getConfigurableAttributeCollection($product); $this->extensionAttributesJoinProcessor->process($configurableAttributes); $configurableAttributes->orderByPosition()->load(); @@ -682,7 +696,7 @@ private function saveConfigurableOptions(ProductInterface $product) ->setProductId($product->getData($metadata->getLinkField())) ->save(); } - /** @var $configurableAttributesCollection \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection */ + /** @var $configurableAttributesCollection \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection */ $configurableAttributesCollection = $this->_attributeCollectionFactory->create(); $configurableAttributesCollection->setProductFilter($product); $configurableAttributesCollection->addFieldToFilter( @@ -926,6 +940,8 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p return $result; } } + } elseif (is_string($result)) { + return __($result)->render(); } } @@ -1239,6 +1255,7 @@ public function getUsedProducts($product, $requiredAttributeIds = null) $product->getData($metadata->getLinkField()), $product->getStoreId(), $this->getCustomerSession()->getCustomerGroupId(), + $this->appState->getAreaCode(), $requiredAttributeIds ]; $cacheKey = $this->getUsedProductsCacheKey($keyParts); @@ -1276,6 +1293,8 @@ public function getSalableUsedProducts(\Magento\Catalog\Model\Product $product, * Load collection on sub-products for specified configurable product * * Load collection of sub-products, apply result to specified configurable product and store result to cache + * Please note $salableOnly parameter is used for backwards compatibility because of deprecated method + * getSalableUsedProducts * Number of loaded sub-products depends on $salableOnly parameter * $salableOnly = true - result array contains only salable sub-products * $salableOnly = false - result array contains all sub-products @@ -1292,7 +1311,7 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach if (!$product->hasData($dataFieldName)) { $usedProducts = $this->readUsedProductsCacheData($cacheKey); if ($usedProducts === null) { - $collection = $this->getConfiguredUsedProductCollection($product); + $collection = $this->getConfiguredUsedProductCollection($product, false); if ($salableOnly) { $collection = $this->salableProcessor->process($collection); } @@ -1377,7 +1396,7 @@ function ($item) { */ private function getUsedProductsCacheKey($keyParts) { - return md5(implode('_', $keyParts)); + return sha1(implode('_', $keyParts)); } /** @@ -1386,26 +1405,55 @@ private function getUsedProductsCacheKey($keyParts) * Retrieve related products collection with additional configuration * * @param \Magento\Catalog\Model\Product $product + * @param bool $skipStockFilter * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection */ - private function getConfiguredUsedProductCollection(\Magento\Catalog\Model\Product $product) - { + private function getConfiguredUsedProductCollection( + \Magento\Catalog\Model\Product $product, + $skipStockFilter = true + ) { $collection = $this->getUsedProductCollection($product); + + if ($skipStockFilter) { + $collection->setFlag('has_stock_status_filter', true); + } + $collection - ->setFlag('has_stock_status_filter', true) - ->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes()) + ->addAttributeToSelect($this->getAttributesForCollection($product)) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); - $requiredAttributes = ['name', 'price', 'weight', 'image', 'thumbnail', 'status', 'media_gallery']; - foreach ($requiredAttributes as $attributeCode) { - $collection->addAttributeToSelect($attributeCode); - } - foreach ($this->getUsedProductAttributes($product) as $usedProductAttribute) { - $collection->addAttributeToSelect($usedProductAttribute->getAttributeCode()); - } $collection->addMediaGalleryData(); $collection->addTierPriceData(); + return $collection; } + + /** + * @return array + */ + private function getAttributesForCollection(\Magento\Catalog\Model\Product $product) + { + $productAttributes = $this->getCatalogConfig()->getProductAttributes(); + + $requiredAttributes = [ + 'name', + 'price', + 'weight', + 'image', + 'thumbnail', + 'status', + 'visibility', + 'media_gallery' + ]; + + $usedAttributes = array_map( + function($attr) { + return $attr->getAttributeCode(); + }, + $this->getUsedProductAttributes($product) + ); + + return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes)); + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php index a6bec4ac6274f..c4cbed58f1587 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php @@ -18,7 +18,7 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel implements \Magento\ConfigurableProduct\Api\Data\OptionInterface { - /**#@+ + /** * Constants for field names */ const KEY_ATTRIBUTE_ID = 'attribute_id'; @@ -27,9 +27,10 @@ class Attribute extends \Magento\Framework\Model\AbstractExtensibleModel impleme const KEY_IS_USE_DEFAULT = 'is_use_default'; const KEY_VALUES = 'values'; const KEY_PRODUCT_ID = 'product_id'; - /**#@-*/ - /**#@-*/ + /** + * @var MetadataPool|\Magento\Framework\EntityManager\MetadataPool + */ private $metadataPool; /** diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bee334596e990..f2bf3116af9e4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php new file mode 100644 index 0000000000000..a60730b06fad2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations; + +/** + * Configurable product variation prices. + */ +class Prices +{ + /** + * @var \Magento\Framework\Locale\Format + */ + private $localeFormat; + + /** + * Prices constructor. + * @param \Magento\Framework\Locale\Format $localeFormat + */ + public function __construct(\Magento\Framework\Locale\Format $localeFormat) + { + $this->localeFormat = $localeFormat; + } + + /** + * Get product prices for configurable variations + * + * @param \Magento\Framework\Pricing\PriceInfo\Base $priceInfo + * @return array + */ + public function getFormattedPrices(\Magento\Framework\Pricing\PriceInfo\Base $priceInfo) + { + $regularPrice = $priceInfo->getPrice('regular_price'); + $finalPrice = $priceInfo->getPrice('final_price'); + + return [ + 'oldPrice' => [ + 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()), + ], + 'basePrice' => [ + 'amount' => $this->localeFormat->getNumber($finalPrice->getAmount()->getBaseAmount()), + ], + 'finalPrice' => [ + 'amount' => $this->localeFormat->getNumber($finalPrice->getAmount()->getValue()), + ], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index a462a5ffd9edd..66f7711c25913 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -233,10 +233,6 @@ public function duplicateImagesForVariations($productsData) foreach ($simpleProductData['media_gallery']['images'] as $imageId => $image) { $image['variation_id'] = $variationId; - if (isset($imagesForCopy[$imageId][0])) { - // skip duplicate image for first product - unset($imagesForCopy[$imageId][0]); - } $imagesForCopy[$imageId][] = $image; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php index 4450bbd75e574..85493b81bc6d4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php @@ -69,7 +69,7 @@ public function convertToBuyRequest(CartItemInterface $cartItem) if (is_array($options)) { $requestData = []; foreach ($options as $option) { - $requestData['super_attribute'][$option->getOptionId()] = $option->getOptionValue(); + $requestData['super_attribute'][$option->getOptionId()] = (string) $option->getOptionValue(); } return $this->objectFactory->create($requestData); } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php index 958d802682d52..ef5005d7bf5e0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php @@ -91,6 +91,12 @@ public function getSelect(AbstractAttribute $superAttribute, int $productId, Sco ] ), [] + )->joinLeft( + ['attribute_option' => $this->attributeResource->getTable('eav_attribute_option')], + 'attribute_option.option_id = entity_value.value', + [] + )->order( + 'attribute_option.sort_order ASC' )->where( 'super_attribute.product_id = ?', $productId diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index a30ec81528dd3..b7bbf7aa1871c 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -1,197 +1,313 @@ <?php /** - * Configurable Products Price Indexer Resource model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; -use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\Store\Api\StoreResolverInterface; -use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; +use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\BaseFinalPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Model\Configuration; /** + * Configurable Products Price Indexer Resource model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice +class Configurable implements DimensionalIndexerInterface { /** - * @var StoreResolverInterface + * @var BaseFinalPrice */ - private $storeResolver; + private $baseFinalPrice; /** - * Class constructor - * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Framework\Module\Manager $moduleManager - * @param string|null $connectionName - * @param StoreResolverInterface|null $storeResolver + * @var IndexTableStructureFactory */ - public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Framework\Module\Manager $moduleManager, - $connectionName = null, - StoreResolverInterface $storeResolver = null - ) { - parent::__construct($context, $tableStrategy, $eavConfig, $eventManager, $moduleManager, $connectionName); - $this->storeResolver = $storeResolver ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - StoreResolverInterface::class - ); - } + private $indexTableStructureFactory; /** - * @param null|int|array $entityIds - * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable + * @var TableMaintainer */ - protected function reindex($entityIds = null) - { - if ($this->hasEntity() || !empty($entityIds)) { - $this->prepareFinalPriceDataForType($entityIds, $this->getTypeId()); - $this->_applyCustomOption(); - $this->_applyConfigurableOption(); - $this->_movePriceDataToIndexTable($entityIds); - } - return $this; - } + private $tableMaintainer; /** - * Retrieve table name for custom option temporary aggregation data - * - * @return string + * @var MetadataPool */ - protected function _getConfigurableOptionAggregateTable() - { - return $this->tableStrategy->getTableName('catalog_product_index_price_cfg_opt_agr'); - } + private $metadataPool; /** - * Retrieve table name for custom option prices data - * - * @return string + * @var \Magento\Framework\App\ResourceConnection */ - protected function _getConfigurableOptionPriceTable() - { - return $this->tableStrategy->getTableName('catalog_product_index_price_cfg_opt'); + private $resource; + + /** + * @var bool + */ + private $fullReindexAction; + + /** + * @var string + */ + private $connectionName; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + private $connection; + + /** + * @var BasePriceModifier + */ + private $basePriceModifier; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param BaseFinalPrice $baseFinalPrice + * @param IndexTableStructureFactory $indexTableStructureFactory + * @param TableMaintainer $tableMaintainer + * @param MetadataPool $metadataPool + * @param \Magento\Framework\App\ResourceConnection $resource + * @param BasePriceModifier $basePriceModifier + * @param bool $fullReindexAction + * @param string $connectionName + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + BaseFinalPrice $baseFinalPrice, + IndexTableStructureFactory $indexTableStructureFactory, + TableMaintainer $tableMaintainer, + MetadataPool $metadataPool, + \Magento\Framework\App\ResourceConnection $resource, + BasePriceModifier $basePriceModifier, + $fullReindexAction = false, + $connectionName = 'indexer', + ScopeConfigInterface $scopeConfig = null + ) { + $this->baseFinalPrice = $baseFinalPrice; + $this->indexTableStructureFactory = $indexTableStructureFactory; + $this->tableMaintainer = $tableMaintainer; + $this->connectionName = $connectionName; + $this->metadataPool = $metadataPool; + $this->resource = $resource; + $this->fullReindexAction = $fullReindexAction; + $this->basePriceModifier = $basePriceModifier; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * Prepare table structure for custom option temporary aggregation data + * @inheritdoc * - * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable + * @throws \Exception */ - protected function _prepareConfigurableOptionAggregateTable() + public function executeByDimensions(array $dimensions, \Traversable $entityIds) { - $this->getConnection()->delete($this->_getConfigurableOptionAggregateTable()); - return $this; + $this->tableMaintainer->createMainTmpTable($dimensions); + + $temporaryPriceTable = $this->indexTableStructureFactory->create([ + 'tableName' => $this->tableMaintainer->getMainTmpTable($dimensions), + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'price', + 'finalPriceField' => 'final_price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', + ]); + $select = $this->baseFinalPrice->getQuery( + $dimensions, + \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE, + iterator_to_array($entityIds) + ); + $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); + $this->tableMaintainer->getConnection()->query($query); + + $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); + $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); } /** - * Prepare table structure for custom option prices data + * Apply configurable option + * + * @param IndexTableStructure $temporaryPriceTable + * @param array $dimensions + * @param array $entityIds * - * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable + * @return $this + * @throws \Exception */ - protected function _prepareConfigurableOptionPriceTable() - { - $this->getConnection()->delete($this->_getConfigurableOptionPriceTable()); + private function applyConfigurableOption( + IndexTableStructure $temporaryPriceTable, + array $dimensions, + array $entityIds + ) { + $temporaryOptionsTableName = 'catalog_product_index_price_cfg_opt_temp'; + $this->getConnection()->createTemporaryTableLike( + $temporaryOptionsTableName, + $this->getTable('catalog_product_index_price_cfg_opt_tmp'), + true + ); + + $this->fillTemporaryOptionsTable($temporaryOptionsTableName, $dimensions, $entityIds); + $this->updateTemporaryTable($temporaryPriceTable->getTableName(), $temporaryOptionsTableName); + + $this->getConnection()->delete($temporaryOptionsTableName); + return $this; } /** - * Calculate minimal and maximal prices for configurable product options - * and apply it to final price + * Put data into catalog product price indexer config option temp table + * + * @param string $temporaryOptionsTableName + * @param array $dimensions + * @param array $entityIds * - * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return void + * @throws \Exception */ - protected function _applyConfigurableOption() + private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, array $dimensions, array $entityIds) { - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - $connection = $this->getConnection(); - $coaTable = $this->_getConfigurableOptionAggregateTable(); - $copTable = $this->_getConfigurableOptionPriceTable(); + $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $linkField = $metadata->getLinkField(); - $this->_prepareConfigurableOptionAggregateTable(); - $this->_prepareConfigurableOptionPriceTable(); - - $subSelect = $this->getSelect(); - $subSelect->join( + $select = $this->getConnection()->select()->from( + ['i' => $this->getMainTable($dimensions)], + [] + )->join( ['l' => $this->getTable('catalog_product_super_link')], - 'l.product_id = e.entity_id', + 'l.product_id = i.entity_id', [] )->join( ['le' => $this->getTable('catalog_product_entity')], 'le.' . $linkField . ' = l.parent_id', - ['parent_id' => 'entity_id'] - )->join( - ['i' => $this->_getDefaultFinalPriceTable()], - 'le.entity_id = i.entity_id', [] ); - $select = $connection->select(); - $select - ->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '') - ->columns([ - 'sub.parent_id', - 'sub.entity_id', - 'sub.customer_group_id', - 'sub.website_id', - 'sub.price', - 'sub.tier_price' - ]); - - $query = $select->insertFromSelect($coaTable); - $connection->query($query); - - $select = $connection->select()->from( - [$coaTable], + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->isConfigShowOutOfStock()) { + $select->join( + ['si' => $this->getTable('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + } + + $select->columns( [ - 'parent_id', + 'le.entity_id', 'customer_group_id', 'website_id', - 'MIN(price)', - 'MAX(price)', + 'MIN(final_price)', + 'MAX(final_price)', 'MIN(tier_price)', ] )->group( - ['parent_id', 'customer_group_id', 'website_id'] + ['le.entity_id', 'customer_group_id', 'website_id'] ); + if ($entityIds !== null) { + $select->where('le.entity_id IN (?)', $entityIds); + } + $query = $select->insertFromSelect($temporaryOptionsTableName); + $this->getConnection()->query($query); + } - $query = $select->insertFromSelect($copTable); - $connection->query($query); - - $table = ['i' => $this->_getDefaultFinalPriceTable()]; - $select = $connection->select()->join( - ['io' => $copTable], + /** + * Update data in the catalog product price indexer temp table + * + * @param string $temporaryPriceTableName + * @param string $temporaryOptionsTableName + * + * @return void + */ + private function updateTemporaryTable(string $temporaryPriceTableName, string $temporaryOptionsTableName) + { + $table = ['i' => $temporaryPriceTableName]; + $selectForCrossUpdate = $this->getConnection()->select()->join( + ['io' => $temporaryOptionsTableName], 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . ' AND i.website_id = io.website_id', [] ); - $select->columns( + // adds price of custom option, that was applied in DefaultPrice::_applyCustomOption + $selectForCrossUpdate->columns( [ - 'min_price' => new \Zend_Db_Expr('i.min_price - i.orig_price + io.min_price'), - 'max_price' => new \Zend_Db_Expr('i.max_price - i.orig_price + io.max_price'), + 'min_price' => new \Zend_Db_Expr('i.min_price - i.price + io.min_price'), + 'max_price' => new \Zend_Db_Expr('i.max_price - i.price + io.max_price'), 'tier_price' => 'io.tier_price', ] ); - $query = $select->crossUpdateFromSelect($table); - $connection->query($query); + $query = $selectForCrossUpdate->crossUpdateFromSelect($table); + $this->getConnection()->query($query); + } + + /** + * Get main table + * + * @param array $dimensions + * @return string + */ + private function getMainTable($dimensions) + { + if ($this->fullReindexAction) { + return $this->tableMaintainer->getMainReplicaTable($dimensions); + } + return $this->tableMaintainer->getMainTable($dimensions); + } + + /** + * Get connection + * + * @return \Magento\Framework\DB\Adapter\AdapterInterface + * @throws \DomainException + */ + private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface + { + if ($this->connection === null) { + $this->connection = $this->resource->getConnection($this->connectionName); + } - $connection->delete($coaTable); - $connection->delete($copTable); + return $this->connection; + } - return $this; + /** + * Get table + * + * @param string $tableName + * @return string + */ + private function getTable($tableName) + { + return $this->resource->getTableName($tableName, $this->connectionName); + } + + /** + * Is flag Show Out Of Stock setted + * + * @return bool + */ + private function isConfigShowOutOfStock(): bool + { + return $this->scopeConfig->isSetFlag( + Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + ScopeInterface::SCOPE_STORE + ); } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php new file mode 100644 index 0000000000000..de616a43d92ae --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; + +/** + * Used in Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProvider + * to provide queries to select configurable product option with lowest price + * + * @see app/code/Magento/ConfigurableProduct/etc/di.xml + */ +class LinkedProductSelectBuilderComposite implements LinkedProductSelectBuilderInterface +{ + /** + * @var LinkedProductSelectBuilderInterface[] + */ + private $linkedProductSelectBuilder; + + /** + * @param LinkedProductSelectBuilderInterface[] $linkedProductSelectBuilder + */ + public function __construct($linkedProductSelectBuilder) + { + $this->linkedProductSelectBuilder = $linkedProductSelectBuilder; + } + + /** + * {@inheritdoc} + */ + public function build($productId) + { + $selects = []; + foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { + $selects = array_merge($selects, $productSelectBuilder->build($productId)); + } + + return $selects; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php index 3c9689a1c4eb9..3611d95f0c6ac 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -17,6 +17,7 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionProvider; use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; class Configurable extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -109,27 +110,34 @@ public function saveProducts($mainProduct, array $productIds) } $productId = $mainProduct->getData($this->optionProvider->getProductEntityLinkField()); + $select = $this->getConnection()->select()->from( + ['t' => $this->getMainTable()], + ['product_id'] + )->where( + 't.parent_id = ?', + $productId + ); - $data = []; - foreach ($productIds as $id) { - $data[] = ['product_id' => (int) $id, 'parent_id' => (int) $productId]; - } + $existingProductIds = $this->getConnection()->fetchCol($select); + $insertProductIds = array_diff($productIds, $existingProductIds); + $deleteProductIds = array_diff($existingProductIds, $productIds); - if (!empty($data)) { - $this->getConnection()->insertOnDuplicate( + if (!empty($insertProductIds)) { + $insertData = []; + foreach ($insertProductIds as $id) { + $insertData[] = ['product_id' => (int) $id, 'parent_id' => (int) $productId]; + } + $this->getConnection()->insertMultiple( $this->getMainTable(), - $data, - ['product_id', 'parent_id'] + $insertData ); } - $where = ['parent_id = ?' => $productId]; - if (!empty($productIds)) { - $where['product_id NOT IN(?)'] = $productIds; + if (!empty($deleteProductIds)) { + $where = ['parent_id = ?' => $productId, 'product_id IN (?)' => $deleteProductIds]; + $this->getConnection()->delete($this->getMainTable(), $where); } - $this->getConnection()->delete($this->getMainTable(), $where); - // configurable product relations should be added to relation table $this->catalogProductRelation->processRelations($productId, $productIds); @@ -165,10 +173,13 @@ public function getChildrenIds($parentId, $required = true) $parentId ); - $childrenIds = [0 => []]; - foreach ($this->getConnection()->fetchAll($select) as $row) { - $childrenIds[0][$row['product_id']] = $row['product_id']; - } + $childrenIds = [ + 0 => array_column( + $this->getConnection()->fetchAll($select), + 'product_id', + 'product_id' + ) + ]; return $childrenIds; } @@ -181,7 +192,6 @@ public function getChildrenIds($parentId, $required = true) */ public function getParentIdsByChild($childId) { - $parentIds = []; $select = $this->getConnection() ->select() ->from(['l' => $this->getMainTable()], []) @@ -190,10 +200,7 @@ public function getParentIdsByChild($childId) 'e.' . $this->optionProvider->getProductEntityLinkField() . ' = l.parent_id', ['e.entity_id'] )->where('l.product_id IN(?)', $childId); - - foreach ($this->getConnection()->fetchAll($select) as $row) { - $parentIds[] = $row['entity_id']; - } + $parentIds = $this->getConnection()->fetchCol($select); return $parentIds; } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php index 7ea83099f2589..e93c44893bf58 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute.php @@ -8,8 +8,8 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -85,22 +85,30 @@ public function saveLabel($attribute) 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, ]; $valueId = $connection->fetchOne($select, $bind); + if ($valueId) { - $storeId = (int)$attribute->getStoreId() ?: $this->_storeManager->getStore()->getId(); + $connection->insertOnDuplicate( + $this->_labelTable, + [ + 'product_super_attribute_id' => (int)$attribute->getId(), + 'store_id' => (int)$attribute->getStoreId() ?: $this->_storeManager->getStore()->getId(), + 'use_default' => (int)$attribute->getUseDefault(), + 'value' => $attribute->getLabel(), + ], + ['value', 'use_default'] + ); } else { // if attribute label not exists, always store on default store (0) - $storeId = Store::DEFAULT_STORE_ID; + $connection->insert( + $this->_labelTable, + [ + 'product_super_attribute_id' => (int)$attribute->getId(), + 'store_id' => Store::DEFAULT_STORE_ID, + 'use_default' => (int)$attribute->getUseDefault(), + 'value' => $attribute->getLabel(), + ] + ); } - $connection->insertOnDuplicate( - $this->_labelTable, - [ - 'product_super_attribute_id' => (int)$attribute->getId(), - 'use_default' => (int)$attribute->getUseDefault(), - 'store_id' => $storeId, - 'value' => $attribute->getLabel(), - ], - ['value', 'use_default'] - ); return $this; } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php index efddb278df36c..8c80a56a649e7 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php @@ -5,7 +5,7 @@ */ namespace Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer; -use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; /** * A plugin for a salable resolver. @@ -13,17 +13,16 @@ class SalableResolver { /** - * @var LowestPriceOptionsProviderInterface + * @var TypeConfigurable */ - private $lowestPriceOptionsProvider; + private $typeConfigurable; /** - * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider + * @param TypeConfigurable $typeConfigurable */ - public function __construct( - LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider - ) { - $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider; + public function __construct(TypeConfigurable $typeConfigurable) + { + $this->typeConfigurable = $typeConfigurable; } /** @@ -33,9 +32,7 @@ public function __construct( * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver $subject * @param bool $result * @param \Magento\Framework\Pricing\SaleableInterface $salableItem - * * @return bool - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterIsSalable( @@ -43,10 +40,8 @@ public function afterIsSalable( $result, \Magento\Framework\Pricing\SaleableInterface $salableItem ) { - if ($salableItem->getTypeId() == 'configurable' && $result) { - if (!$this->lowestPriceOptionsProvider->getProducts($salableItem)) { - $result = false; - } + if ($salableItem->getTypeId() === TypeConfigurable::TYPE_CODE && $result) { + $result = $this->typeConfigurable->isSalable($salableItem); } return $result; diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImages.php b/app/code/Magento/ConfigurableProduct/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImages.php new file mode 100644 index 0000000000000..dab71d81930c4 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImages.php @@ -0,0 +1,129 @@ +<?php +/** + * Product initialization helper + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Controller\Adminhtml\Product\Initialization\Helper; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Framework\Filesystem; +use Magento\Catalog\Model\Product\Media\Config as MediaConfig; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\App\RequestInterface; + +/** + * Class cleaning configuration tmp images + */ +class CleanConfigurationTmpImages +{ + /** + * @var Database + */ + private $fileStorageDb; + + /** + * @var MediaConfig + */ + private $mediaConfig; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + * @param Database $fileStorageDb + * @param MediaConfig $mediaConfig + * @param Filesystem $filesystem + */ + public function __construct( + RequestInterface $request, + Database $fileStorageDb, + MediaConfig $mediaConfig, + Filesystem $filesystem + ) { + $this->request = $request; + $this->fileStorageDb = $fileStorageDb; + $this->mediaConfig = $mediaConfig; + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Clean Tmp configurable images + * + * @param Helper $subject + * @param Product $configurableProduct + * + * @return Product + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterInitialize(Helper $subject, Product $configurableProduct): Product + { + // Clean tmp + $configurations = $this->getConfigurations(); + foreach ($configurations as $simpleProductData) { + if (!isset($simpleProductData['media_gallery']['images'])) { + continue; + } + foreach ($simpleProductData['media_gallery']['images'] as $image) { + $file = $this->getFilenameFromTmp($image['file']); + if ($this->fileStorageDb->checkDbUsage()) { + $filename = $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaShortUrl($file)); + $this->fileStorageDb->deleteFile($filename); + } else { + $filename = $this->mediaConfig->getTmpMediaPath($file); + $this->mediaDirectory->delete($filename); + } + } + } + + return $configurableProduct; + } + + /** + * Trim .tmp ending from filename + * + * @param string $file + * + * @return string + */ + private function getFilenameFromTmp(string $file): string + { + return strrpos($file, '.tmp') == strlen($file) - 4 ? substr($file, 0, strlen($file) - 4) : $file; + } + + /** + * Get configurations from request + * + * @return array + */ + private function getConfigurations(): array + { + $result = []; + $configurableMatrix = $this->request->getParam('configurable-matrix-serialized', "[]"); + if (!empty($configurableMatrix)) { + $configurableMatrix = json_decode($configurableMatrix, true); + foreach ($configurableMatrix as $item) { + if (empty($item['was_changed']) && empty($item['newProduct'])) { + continue; + } + $result[] = $item; + } + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php index 0afde6144bf88..4c4bd01950300 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Attribute/InStockOptionSelectBuilder.php @@ -5,6 +5,7 @@ */ namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\Status; use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface; use Magento\Framework\DB\Select; @@ -20,13 +21,21 @@ class InStockOptionSelectBuilder * @var Status */ private $stockStatusResource; - + /** + * @var StockConfigurationInterface + */ + private $stockConfig; + /** * @param Status $stockStatusResource + * @param StockConfigurationInterface $stockConfig */ - public function __construct(Status $stockStatusResource) - { + public function __construct( + Status $stockStatusResource, + StockConfigurationInterface $stockConfig + ) { $this->stockStatusResource = $stockStatusResource; + $this->stockConfig = $stockConfig; } /** @@ -40,14 +49,16 @@ public function __construct(Status $stockStatusResource) */ public function afterGetSelect(OptionSelectBuilderInterface $subject, Select $select) { - $select->joinInner( - ['stock' => $this->stockStatusResource->getMainTable()], - 'stock.product_id = entity.entity_id', - [] - )->where( - 'stock.stock_status = ?', - \Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK - ); + if (!$this->stockConfig->isShowOutOfStock()) { + $select->joinInner( + ['stock' => $this->stockStatusResource->getMainTable()], + 'stock.product_id = entity.entity_id', + [] + )->where( + 'stock.stock_status = ?', + \Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK + ); + } return $select; } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php new file mode 100644 index 0000000000000..34e04c107d0a8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Class Product + * + * @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition + */ +class Product +{ + /** + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + */ + public function beforeValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + $product = $this->getProductToValidate($subject, $model); + if ($model->getProduct() !== $product) { + // We need to replace product only for validation and keep original product for all other cases. + $clone = clone $model; + $clone->setProduct($product); + $model = $clone; + } + + return [$model]; + } + + /** + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + * + * @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product + */ + private function getProductToValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $model->getProduct(); + + $attrCode = $subject->getAttribute(); + + /* Check for attributes which are not available for configurable products */ + if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) { + /** @var \Magento\Catalog\Model\AbstractModel $childProduct */ + $childProduct = current($model->getChildren())->getProduct(); + if ($childProduct->hasData($attrCode)) { + $product = $childProduct; + } + } + + return $product; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php new file mode 100644 index 0000000000000..8bdde2aeb0cff --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; + +/** + * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product + */ +class CommonTaxCollector +{ + /** + * Apply Tax Class ID from child item for configurable product + * + * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject + * @param QuoteDetailsItemInterface $result + * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory + * @param AbstractItem $item + * @return QuoteDetailsItemInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMapItem( + \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, + QuoteDetailsItemInterface $result, + QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, + AbstractItem $item + ) : QuoteDetailsItemInterface { + if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { + $childItem = $item->getChildren()[0]; + $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index 3d42217de5f91..5581fcc07b861 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php @@ -64,7 +64,7 @@ public function resolvePrice(\Magento\Framework\Pricing\SaleableInterface $produ foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) { $productPrice = $this->priceResolver->resolvePrice($subProduct); - $price = $price ? min($price, $productPrice) : $productPrice; + $price = isset($price) ? min($price, $productPrice) : $productPrice; } return (float)$price; diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php index 66bc3db7ee89d..781bbde66360f 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; use Magento\Framework\App\ResourceConnection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** * Retrieve list of products where each product contains lower price than others at least for one possible price type @@ -31,7 +33,12 @@ class LowestPriceOptionsProvider implements LowestPriceOptionsProviderInterface private $collectionFactory; /** - * Key is product id. Value is array of prepared linked products + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Key is product id and store id. Value is array of prepared linked products * * @var array */ @@ -41,15 +48,19 @@ class LowestPriceOptionsProvider implements LowestPriceOptionsProviderInterface * @param ResourceConnection $resourceConnection * @param LinkedProductSelectBuilderInterface $linkedProductSelectBuilder * @param CollectionFactory $collectionFactory + * @param StoreManagerInterface $storeManager */ public function __construct( ResourceConnection $resourceConnection, LinkedProductSelectBuilderInterface $linkedProductSelectBuilder, - CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + StoreManagerInterface $storeManager = null ) { $this->resource = $resourceConnection; $this->linkedProductSelectBuilder = $linkedProductSelectBuilder; $this->collectionFactory = $collectionFactory; + $this->storeManager = $storeManager + ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -57,18 +68,19 @@ public function __construct( */ public function getProducts(ProductInterface $product) { - if (!isset($this->linkedProductMap[$product->getId()])) { + $key = $this->storeManager->getStore()->getId() . '-' . $product->getId(); + if (!isset($this->linkedProductMap[$key])) { $productIds = $this->resource->getConnection()->fetchCol( '(' . implode(') UNION (', $this->linkedProductSelectBuilder->build($product->getId())) . ')' ); - $this->linkedProductMap[$product->getId()] = $this->collectionFactory->create() + $this->linkedProductMap[$key] = $this->collectionFactory->create() ->addAttributeToSelect( ['price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id'] ) ->addIdFilter($productIds) ->getItems(); } - return $this->linkedProductMap[$product->getId()]; + return $this->linkedProductMap[$key]; } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php index 611523a60b06d..816de36b16f96 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Pricing\Render; +use Magento\Catalog\Pricing\Price\TierPrice; + /** * Responsible for displaying tier price box on configurable product page. * @@ -17,9 +19,28 @@ class TierPriceBox extends FinalPriceBox */ public function toHtml() { - // Hide tier price block in case of MSRP. - if (!$this->isMsrpPriceApplicable()) { + // Hide tier price block in case of MSRP or in case when no options with tier price. + if (!$this->isMsrpPriceApplicable() && $this->isTierPriceApplicable()) { return parent::toHtml(); } } + + /** + * Check if at least one of simple products has tier price. + * + * @return bool + */ + private function isTierPriceApplicable(): bool + { + $product = $this->getSaleableItem(); + foreach ($product->getTypeInstance()->getUsedProducts($product) as $simpleProduct) { + if ($simpleProduct->isSalable() + && !empty($simpleProduct->getPriceInfo()->getPrice(TierPrice::PRICE_CODE)->getTierPriceList()) + ) { + return true; + } + } + + return false; + } } diff --git a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php index 7bc56569dea44..57cc287aa24aa 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php +++ b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php @@ -44,6 +44,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); $attributes = [ 'country_of_manufacture', + 'manufacturer', 'minimal_price', 'msrp', 'msrp_display_actual_price_type', @@ -56,18 +57,24 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface 'color' ]; foreach ($attributes as $attributeCode) { - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to') - ); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { - $relatedProductTypes[] = Configurable::TYPE_CODE; - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - $attributeCode, - 'apply_to', - implode(',', $relatedProductTypes) + if ($attribute = $eavSetup->getAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to' + )) { + $relatedProductTypes = explode( + ',', + $attribute ); + if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to', + implode(',', $relatedProductTypes) + ); + } } } } diff --git a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php index 326af02fe39bb..ca100cbf85bfc 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php +++ b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php @@ -5,12 +5,13 @@ */ namespace Magento\ConfigurableProduct\Setup; +use Magento\Catalog\Model\Product; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; -use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\UpgradeDataInterface; /** * Upgrade Data script @@ -41,25 +42,100 @@ public function __construct(EavSetupFactory $eavSetupFactory) public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); + /** @var EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); + if (version_compare($context->getVersion(), '2.2.0') < 0) { - /** @var EavSetup $eavSetup */ - $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'tier_price', 'apply_to') - ); - $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes); - if ($key !== false) { - unset($relatedProductTypes[$key]); - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - 'tier_price', - 'apply_to', - implode(',', $relatedProductTypes) - ); + $relatedProductTypes = $this->getRelatedProductTypes('tier_price', $eavSetup); + if (!empty($relatedProductTypes)) { + $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes); + if ($key !== false) { + unset($relatedProductTypes[$key]); + $this->updateRelatedProductTypes('tier_price', $relatedProductTypes, $eavSetup); + } } } + if (version_compare($context->getVersion(), '2.2.1') < 0) { + $relatedProductTypes = $this->getRelatedProductTypes('manufacturer', $eavSetup); + if (!empty($relatedProductTypes) && !in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $this->updateRelatedProductTypes('manufacturer', $relatedProductTypes, $eavSetup); + } + } + + if (version_compare($context->getVersion(), '2.2.2', '<')) { + $this->upgradeQuoteItemPrice($setup); + } + $setup->endSetup(); } + + /** + * Get related product types for attribute. + * + * @param string $attributeId + * @param EavSetup $eavSetup + * @return array + */ + private function getRelatedProductTypes(string $attributeId, EavSetup $eavSetup) + { + if ($attribute = $eavSetup->getAttribute( + Product::ENTITY, + $attributeId, + 'apply_to' + )) { + return explode( + ',', + $attribute + ); + } + return []; + } + + /** + * Update related product types for attribute. + * + * @param string $attributeId + * @param array $relatedProductTypes + * @param EavSetup $eavSetup + * @return void + */ + private function updateRelatedProductTypes(string $attributeId, array $relatedProductTypes, EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + Product::ENTITY, + $attributeId, + 'apply_to', + implode(',', $relatedProductTypes) + ); + } + + /** + * Update 'price' value for quote items without price of configurable products subproducts. + * + * @param ModuleDataSetupInterface $setup + */ + private function upgradeQuoteItemPrice(ModuleDataSetupInterface $setup) + { + $connectionName = 'checkout'; + $connection = $setup->getConnection($connectionName); + $quoteItemTable = $setup->getTable('quote_item', $connectionName); + + $select = $connection->select(); + $select->joinLeft( + ['qi2' => $quoteItemTable], + 'qi1.parent_item_id = qi2.item_id', + ['price'] + )->where( + 'qi1.price = 0' + . ' AND qi1.parent_item_id IS NOT NULL' + . ' AND qi2.product_type = "' . Configurable::TYPE_CODE . '"' + ); + $updateQuoteItem = $connection->updateFromSelect( + $select, + ['qi1' => $quoteItemTable] + ); + $connection->query($updateQuoteItem); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..58ebd207eeddf --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- + + Create a configurable product with three options for color: red, white, and blue + + Expected start state = logged in as an admin + End state = on the product edit page in the admin + + --> + <actionGroup name="createConfigurableProduct"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + + <!-- fill in basic configurable product values --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + + <!-- create configurations for colors the product is available in --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillDefaultLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForFilters"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{colorProductAttribute.default_label}}" selector="{{AdminCreateProductConfigurationsPanelSection.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.createNewValue}}" stepKey="clickOnCreateNewValue1"/> + <fillField userInput="{{colorProductAttribute1.name}}" selector="{{AdminCreateProductConfigurationsPanelSection.attributeName}}" stepKey="fillFieldForNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.saveAttribute}}" stepKey="clickOnSaveNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.createNewValue}}" stepKey="clickOnCreateNewValue2"/> + <fillField userInput="{{colorProductAttribute2.name}}" selector="{{AdminCreateProductConfigurationsPanelSection.attributeName}}" stepKey="fillFieldForNewAttribute2"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.saveAttribute}}" stepKey="clickOnSaveNewAttribute2"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.createNewValue}}" stepKey="clickOnCreateNewValue3"/> + <fillField userInput="{{colorProductAttribute3.name}}" selector="{{AdminCreateProductConfigurationsPanelSection.attributeName}}" stepKey="fillFieldForNewAttribute3"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.saveAttribute}}" stepKey="clickOnSaveNewAttribute3"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.applyUniquePricesByAttributeToEachSku}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanelSection.selectAttribute}}" userInput="{{colorProductAttribute.default_label}}" stepKey="selectAttributes"/> + <fillField selector="{{AdminCreateProductConfigurationsPanelSection.attribute('0')}}" userInput="{{colorProductAttribute1.price}}" stepKey="fillAttributePrice1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanelSection.attribute('1')}}" userInput="{{colorProductAttribute2.price}}" stepKey="fillAttributePrice2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanelSection.attribute('2')}}" userInput="{{colorProductAttribute3.price}}" stepKey="fillAttributePrice3"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanelSection.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetSection.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + <seeInTitle userInput="{{product.name}}" stepKey="seeProductNameInTitle"/> + </actionGroup> + + <!--Click in Next Step and see Title--> + <actionGroup name="AdminConfigurableWizardMoveToNextStepActionGroup"> + <arguments> + <argument name="title" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanelSection.next}}" stepKey="clickNextButton"/> + <waitForPageLoad stepKey="waitForNextStepLoaded"/> + <see userInput="{{title}}" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + </actionGroup> + + <!-- Add unique image to configurable product option--> + <actionGroup name="AddUniqueImageToConfigurableProductOptionActionGroup"> + <arguments> + <argument name="image" defaultValue="ProductImage"/> + <argument name="frontend_label" type="string"/> + <argument name="label" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanelSection.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> + <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanelSection.selectImagesButton}}" stepKey="selectOption"/> + <attachFile selector="{{AdminCreateProductConfigurationsPanelSection.uploadImagesButton(label)}}" userInput="{{image.file}}" stepKey="uploadFile"/> + <waitForElementNotVisible selector="{{AdminProductImagesSection.uploadProgressBar}}" stepKey="waitForUpload"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanelSection.imageFile(image.fileName)}}" stepKey="waitForThumbnail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..abbef02adc520 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateApiConfigurableProductActionGroup"> + <arguments> + <argument name="productName" defaultValue="{{ApiConfigurableProductWithOutCategory.name}}" type="string"/> + </arguments> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"> + <field key="name">{{productName}}</field> + </createData> + + <!-- Create attribute with 2 options to be used in children products --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addAttributeToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </actionGroup> + + <actionGroup name="AdminCreateConfigurableProductChildQty1ActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <createData entity="ApiSimpleSingleQty" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + </actionGroup> + + <!-- Create the configurable product, children are not visible individually --> + <actionGroup name="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwoHidden" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + </actionGroup> + + <!--Create the configurable product with three child products--> + <actionGroup name="AdminCreateApiConfigurableProductWithThreeChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <remove keyForRemoval="createConfigProductOption"/> + <remove keyForRemoval="createConfigChildProduct1"/> + <remove keyForRemoval="createConfigChildProduct2"/> + <remove keyForRemoval="createConfigProductAddChild1"/> + <remove keyForRemoval="createConfigProductAddChild2"/> + + <createData entity="ProductAttributeOption3" after="createConfigProductAttributeOption2" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="3" after="getConfigAttributeOption2" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <createData entity="ApiSimpleOne" stepKey="createChildProduct1"> + <field key="price">50</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleOne" stepKey="createChildProduct2"> + <field key="price">60</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleOne" stepKey="createChildProduct3"> + <field key="price">70</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOptions"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct3"/> + </createData> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..6c47a24315c9a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateConfigurableProductActionGroup"> + <arguments> + <argument name="product"/> + <argument name="category" type="string"/> + <argument name="attributeSet"/> + <argument name="configurableAttributeCode" defaultValue="color" type="string"/> + <argument name="configurationsPrice" defaultValue="0" type="string"/> + <argument name="configurationsQty" defaultValue="0" type="string"/> + </arguments> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category}}]" stepKey="fillCategory"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + + <!--Apply Attribute Set for product--> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSet.label}}" stepKey="searchForAttrSet"/> + <waitForAjaxLoad stepKey="waitForLoad"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResultByName(attributeSet.label)}}" stepKey="selectAttrSetProd"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveEditedProductForProduct"/> + + <!--Click "Create Configurations" button in configurations field--> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPanel"/> + + <!--Select attribute "Color"--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickFiltersExpand"/> + <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('attribute_code')}}" userInput="{{configurableAttributeCode}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickAttributeColorCheckbox"/> + + <!--Click the "Next" button--> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextButton"/> + + <!--Select All--> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAllSecond"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButtonSecond"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToEachSkuSecond"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="{{configurationsPrice}}" stepKey="enterAttributePriceSecond"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSkuSecond"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="{{configurationsQty}}" stepKey="enterAttributeQuantitySecond"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStepSecond"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateSecondProducts"/> + + <!-- Save the product --> + <click selector="{{AdminProductFormActionSection.saveArrow}}" stepKey="openSaveDropDown"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSave"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickConfirm"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="assertSuccess"/> + </actionGroup> + + <actionGroup name="AdminGenerateProductConfigurations"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="qty" type="string"/> + <argument name="price" type="string"/> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.attributeCodeFilterInput}}" userInput="{{attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanelSection.selectAllByAttribute(attributeCode)}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanelSection.selectAllByAttribute(attributeCode)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="waitForNextPageOpened1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="{{price}}" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="{{qty}}" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + </actionGroup> + + <actionGroup name="AdminCreateConfigurableProductTwoAttributesWithOptionsActionGroup" extends="AdminCreateConfigurableProductActionGroup"> + <arguments> + <argument name="attributeOption" type="string"/> + <argument name="attributeOption1" type="string"/> + <argument name="configurableAttributeCode1" type="string"/> + <argument name="attribute1Option" type="string"/> + <argument name="attribute1Option1" type="string"/> + <argument name="attribute1Option2" type="string"/> + </arguments> + + <remove keyForRemoval="startEditAttrSet"/> + <remove keyForRemoval="searchForAttrSet"/> + <remove keyForRemoval="waitForLoad"/> + <remove keyForRemoval="selectAttrSetProd"/> + <remove keyForRemoval="saveEditedProductForProduct"/> + <remove keyForRemoval="clickClearFilters"/> + <remove keyForRemoval="clickFiltersExpand"/> + <remove keyForRemoval="fillFilter"/> + <remove keyForRemoval="clickSearch"/> + <remove keyForRemoval="clickAttributeColorCheckbox"/> + <remove keyForRemoval="clickOnSelectAllSecond"/> + + <fillField userInput="{{configurationsPrice}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{configurationsQty}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + + <!--Select attributes --> + <click selector="{{AdminCreateProductConfigurationsPanel.checkboxByName(configurableAttributeCode)}}" after="openConfigurationPanel" stepKey="selectAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.checkboxByName(configurableAttributeCode1)}}" after="selectAttribute" stepKey="selectAttribute1"/> + + <!--Select options--> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attributeOption)}}" after="clickNextButton" stepKey="selectAttributeOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attributeOption1)}}" after="selectAttributeOption" stepKey="selectAttributeOption1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option)}}" after="selectAttributeOption1" stepKey="selectAttribute1Option"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option1)}}" after="selectAttribute1Option" stepKey="selectAttribute1Option1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option2)}}" after="selectAttribute1Option1" stepKey="selectAttribute1Option2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductWithExcludingOptionsActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductWithExcludingOptionsActionGroup.xml new file mode 100644 index 0000000000000..c4485ab5f5c9b --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductWithExcludingOptionsActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateConfigurableProductExcludeOptionActionGroup" extends="AdminCreateConfigurableProductActionGroup"> + <arguments> + <!-- custom multiselect attribute option name to start --> + <argument name="customAttributeOptionNameFrom" type="string"/> + <!-- custom multiselect attribute option name to end --> + <argument name="customAttributeOptionNameTill" type="string"/> + <argument name="excludedOption"/> + </arguments> + + <selectOption userInput="{{customAttributeOptionNameFrom}}" selector="{{AdminNewAttributePanelSection.attributeSelect(ProductAttributeFrontendLabel.label)}}" after="saveEditedProductForProduct" stepKey="selectAttribute"/> + <dragAndDrop selector1="{{AdminNewAttributePanelSection.attributeName(customAttributeOptionNameFrom)}}" selector2="{{AdminNewAttributePanelSection.attributeName(customAttributeOptionNameTill)}}" after="selectAttribute" stepKey="selectOptions"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(excludedOption.name)}}" after="clickOnSelectAllSecond" stepKey="deselectOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontSelectConfigurableAttributeOptionActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontSelectConfigurableAttributeOptionActionGroup.xml new file mode 100644 index 0000000000000..cfd3fb71b682f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontSelectConfigurableAttributeOptionActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectConfigurableAttributeOptionActionGroup"> + <arguments> + <argument name="attributeLabel" type="string"/> + <argument name="optionValue" type="string"/> + <argument name="qty" type="string" defaultValue="1"/> + </arguments> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productOptionSelect(attributeLabel)}}" stepKey="waitForOptionSelectVisible"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect(attributeLabel)}}" userInput="{{optionValue}}" stepKey="selectProductOption"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{qty}}" stepKey="fillProductQuantity"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml new file mode 100644 index 0000000000000..a2f824dd8864e --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="BaseConfigurableProduct" type="product"> + <data key="sku" unique="suffix">configurable</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">configurable</data> + <data key="price">123.00</data> + <data key="weight">2</data> + <data key="urlKey" unique="suffix">configurableurlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ConfigurableProductAddChild" type="ConfigurableProductAddChild"> + <var key="sku" entityKey="sku" entityType="product" /> + <var key="childSku" entityKey="sku" entityType="product2"/> + </entity> + <entity name="ApiConfigurableProduct" type="product"> + <data key="sku" unique="suffix">api-configurable-product</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">API Configurable Product</data> + <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiConfigurableProductWithOutCategory" type="product"> + <data key="sku" unique="suffix">api-configurable-product-with-out-category</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">API Configurable Product</data> + <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml new file mode 100644 index 0000000000000..e98175bc8d40b --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ConfigurableProductTwoOptions" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> + </entity> + <entity name="ConfigurableProductThreeOptions" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml new file mode 100644 index 0000000000000..e1cd70790a6b2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ValueIndex1" type="ValueIndex"> + <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> + </entity> + <entity name="ValueIndex2" type="ValueIndex"> + <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> + </entity> + <entity name="ValueIndex3" type="ValueIndex"> + <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> + </entity> +</entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/LICENSE.txt b/app/code/Magento/ConfigurableProduct/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/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 <insert your license name here>" 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/app/code/Magento/ConfigurableProduct/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ConfigurableProduct/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/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 <insert your license name here>" 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/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml new file mode 100644 index 0000000000000..6a77e97d8f276 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_add_child-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="ConfigurableProductAddChild" dataType="ConfigurableProductAddChild" type="create" auth="adminOauth" url="/V1/configurable-products/{sku}/child" method="POST"> + <contentType>application/json</contentType> + <field key="childSku">string</field> + </operation> +</operations> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml new file mode 100644 index 0000000000000..37e6be683c2fe --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/configurable_product_options-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateConfigurableProductOption" dataType="ConfigurableProductOption" type="create" auth="adminOauth" url="/V1/configurable-products/{sku}/options" method="POST"> + <contentType>application/json</contentType> + <object dataType="ConfigurableProductOption" key="option"> + <field key="attribute_id">integer</field> + <field key="label">string</field> + <array key="values"> + <value>ValueIndex</value> + </array> + </object> + </operation> +</operations> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml new file mode 100644 index 0000000000000..2f1db19a1fd64 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/extension_attribute_configurable_product_options-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateExtensionAttributeConfigProductOption" dataType="ExtensionAttributeConfigProductOption" type="create"> + <contentType>application/json</contentType> + <array key="configurable_product_options"> + <object dataType="ExtensionAttributeConfigProductOption" key="configurable_product_options"> + <array key="0"> + <value>ConfigProductOption</value> + </array> + </object> + </array> + </operation> +</operations> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml new file mode 100644 index 0000000000000..8d955fcc94431 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Metadata/valueIndex-meta.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="ValueIndex" dataType="ValueIndex" type="create"> + <field key="value_index">integer</field> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..15392480108a5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminNewAttributePanel"/> + <section name="AdminProductFormConfigurationsSection"/> + <section name="AdminCreateProductConfigurationsPanelSection"/> + <section name="AdminChooseAffectedAttributeSetSection"/> + </page> +</pages> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/StorefrontCustomerWishlistPage.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/StorefrontCustomerWishlistPage.xml new file mode 100644 index 0000000000000..7b7b5e8b39aa6 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Page/StorefrontCustomerWishlistPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerWishlistPage" url="/wishlist/" area="storefront" module="Magento_Wishlist"> + <section name="StorefrontCustomerWishlistSection" /> + <section name="StorefrontCustomerWishlistProductSection" /> + </page> +</pages> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/README.md b/app/code/Magento/ConfigurableProduct/Test/Mftf/README.md new file mode 100644 index 0000000000000..fb3770d722a63 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Configurable Product Functional Tests + +The Functional Test Module for **Magento Configurable Product** module. diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml new file mode 100644 index 0000000000000..ca3e2e9cee1b1 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminChooseAffectedAttributeSetSection"> + <element name="confirm" type="button" selector="button[data-index='confirm_button']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml new file mode 100644 index 0000000000000..f9e72f302d6f8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCreateProductConfigurationsPanelSection"> + <element name="next" type="button" selector=".steps-wizard-navigation .action-next-step" timeout="30"/> + <element name="generateConfigure" type="button" selector="//div[@class='nav-bar-outer-actions']//*[contains(text(),'Generate Products')]"/> + <element name="createNewAttribute" type="button" selector=".select-attributes-actions button[title='Create New Attribute']" timeout="30"/> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> + <element name="attributeCode" type="input" selector=".admin__control-text[name='attribute_code']"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> + <element name="selectAll" type="button" selector=".action-select-all"/> + <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> + <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> + <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> + <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> + <element name="applyUniquePricesByAttributeToEachSku" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']"/> + <element name="selectAttribute" type="select" selector="#select-each-price" timeout="30"/> + <element name="attribute" type="input" selector="#apply-single-price-input-{{var1}}" parameterized="true"/> + <element name="applySingleQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-single-inventory-radio']" timeout="30"/> + <element name="quantity" type="input" selector="#apply-single-inventory-input"/> + <element name="optionPrice" type="input" selector="//*[text()='{{optionName}}']/../..//input[contains(@id, 'apply-single-price-input')]" parameterized="true"/> + + <!--Image Section--> + <element name="applyUniqueImagesToEachSkus" type="radio" selector="label[for='apply-unique-images-radio']" timeout="30"/> + <element name="selectImagesButton" type="select" selector="#apply-images-attributes" timeout="30"/> + <element name="uploadImagesButton" type="file" selector="//span[text()='{{optionName}}']/../../div[@data-role='gallery']//input[@type='file']" timeout="30" parameterized="true"/> + <element name="imageFile" type="text" selector="div[data-role='gallery'] img[src*='{{url}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml new file mode 100644 index 0000000000000..7a722959c9996 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewAttributePanelSection"> + <element name="container" type="text" selector="#create_new_attribute"/> + <element name="saveAttribute" type="button" selector="#save"/> + <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> + <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> + <element name="inputType" type="select" selector="select[name='frontend_input']"/> + <element name="addOption" type="button" selector="#add_new_option_button"/> + <element name="isDefault" type="radio" selector="[data-role='options-container'] tr:nth-of-type({{row}}) input[name='default[]']" parameterized="true"/> + <element name="optionAdminValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][0]']" parameterized="true"/> + <element name="optionDefaultStoreValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][1]']" parameterized="true"/> + <element name="deleteOption" type="button" selector="#delete_button_option_{{row}}" parameterized="true"/> + <element name="attributeSelect" type="select" selector="product[{{var}}]" parameterized="true"/> + <element name="attributeName" type="select" selector="//option[text()='{{var}}']" parameterized="true"/> + <element name="useInSearch" type="select" selector="#is_searchable"/> + <element name="visibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> + <element name="comparableOnStorefront" type="select" selector="#is_comparable"/> + <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> + <element name="useInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="storefrontPropertiesTab" type="button" selector="#front_fieldset-wrapper"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml new file mode 100644 index 0000000000000..cf50c383075f5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormConfigurationsSection"> + <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> + <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> + <element name="currentVariationsRows" type="button" selector=".data-row"/> + <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> + <element name="currentVariationsSkuCells" type="textarea" selector=".admin__control-fields[data-index='sku_container']"/> + <element name="currentVariationsPriceCells" type="textarea" selector=".admin__control-fields[data-index='price_container']"/> + <element name="currentVariationsQuantityCells" type="textarea" selector=".admin__control-fields[data-index='quantity_container']"/> + <element name="currentVariationsAttributesCells" type="textarea" selector=".admin__control-fields[data-index='attributes']"/> + <element name="currentVariationsStatusCells" type="textarea" selector="._no-header[data-index='status']"/> + <element name="actionsBtn" type="button" selector="(//button[@class='action-select']/span[contains(text(), 'Select')])[{{var1}}]" parameterized="true"/> + <element name="removeProductBtn" type="button" selector="//a[text()='Remove Product']"/> + <element name="disableProductBtn" type="button" selector="//a[text()='Disable Product']"/> + <element name="enableProductBtn" type="button" selector="//a[text()='Enable Product']"/> + <element name="configurableMatrixSku" type="input" selector="input[name='configurable-matrix[{{index}}][sku]']" parameterized="true"/> + <element name="skuValidationMessage" type="text" selector="input[name='configurable-matrix[{{index}}][sku]'] + label" parameterized="true"/> + <element name="stepsWizardTitle" type="text" selector="div.content:not([style='display: none;']) .steps-wizard-title"/> + <element name="attributeEntityByName" type="text" selector="//div[@class='attribute-entity']//div[normalize-space(.)='{{attributeLabel}}']" parameterized="true"/> + <element name="fileUploaderInput" type="file" selector="//input[@type='file' and @class='file-uploader-input']"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml new file mode 100644 index 0000000000000..da738d95d038e --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerWishlistProductSection"> + <element name="productImageByImageName" type="text" selector="//main//li//a//img[contains(@src, '{{var1}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml new file mode 100644 index 0000000000000..c7d270ab306df --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerWishlistSection"> + <element name="successMsg" type="text" selector="div.message-success.success.message"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSidebarSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSidebarSection.xml new file mode 100644 index 0000000000000..1fb2dc07393a7 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontCustomerWishlistSidebarSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerWishlistSidebarSection"> + <element name="productImageByImageName" type="text" selector="//main//ol[@id='wishlist-sidebar']//a//img[contains(@src, '{{var1}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..4f2320666efbc --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="productAttributeTitle" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> + <element name="productAttributeOptions" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> + <element name="stockIndication" type="block" selector=".stock" /> + <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> + <element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/> + <element name="productPriceBox" type="block" selector=".price-box"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml new file mode 100644 index 0000000000000..63209419fbbb5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Update product"/> + <title value="Adding new options with images and prices to Configurable Product"/> + <description value="Test case verifies possibility to add new options for configurable attribute for existing configurable product."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8398"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <actionGroup ref="AdminCreateApiConfigurableProductActionGroup" stepKey="createConfigurableProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open edit product page--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigProductCreateConfigurableProduct.id$$)}}" stepKey="goToProductEditPage"/> + + <!--Open edit configuration wizard--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> + <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToAttributeValuesStep"> + <argument name="title" value="Attribute Values"/> + </actionGroup> + <seeElement selector="{{AdminProductFormConfigurationsSection.attributeEntityByName($$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$)}}" stepKey="seeAttribute"/> + + <!--Create one color option via "Create New Value" link--> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue"/> + <fillField userInput="green" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToBulkStep"> + <argument name="title" value="Bulk Images, Price and Quantity"/> + </actionGroup> + + <!-- Add images to configurable product attribute options --> + <actionGroup ref="AddUniqueImageToConfigurableProductOptionActionGroup" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="ImageUpload"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="AddUniqueImageToConfigurableProductOptionActionGroup" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="ImageUpload1"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="AddUniqueImageToConfigurableProductOptionActionGroup" stepKey="addImageToConfigurableProductOptionThree"> + <argument name="image" value="ImageUpload2"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="green"/> + </actionGroup> + + <!--Add prices to configurable product attribute options--> + <click selector="{{AdminCreateProductConfigurationsPanelSection.applyUniquePricesByAttributeToEachSku}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" + selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" stepKey="selectAttributes"/> + <fillField userInput="10" selector="{{AdminCreateProductConfigurationsPanelSection.optionPrice($$getConfigAttributeOption1CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice"/> + <fillField userInput="20" selector="{{AdminCreateProductConfigurationsPanelSection.optionPrice($$getConfigAttributeOption2CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice1"/> + <fillField userInput="30" selector="{{AdminCreateProductConfigurationsPanelSection.optionPrice('green')}}" stepKey="fillAttributePrice2"/> + + <!-- Add quantity to product attribute options --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToSummaryStep"> + <argument name="title" value="Summary"/> + </actionGroup> + + <!--Click Generate Configure button--> + <click selector="{{AdminCreateProductConfigurationsPanelSection.generateConfigure}}" stepKey="clickGenerateConfigure"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to frontend and check image and price--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertFirstOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload.filename}}"/> + <argument name="price" value="10"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertSecondOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload1.filename}}"/> + <argument name="price" value="20"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertThirdOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="green"/> + <argument name="image" value="{{ImageUpload2.filename}}"/> + <argument name="price" value="30"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml new file mode 100644 index 0000000000000..03f4d8461cebb --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckValidatorConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create a Configurable Product via the Admin"/> + <title value="Check that validator works correctly when creating Configurations for Configurable Products"/> + <description value="Verify validator works correctly for Configurable Products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13719"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productCount" value="2"/> + </actionGroup> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openAdminProductPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetProductsFilter" /> + + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productAttributeWithDropdownTwoOptions"/> + </actionGroup> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributesGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetAttributesFilter" /> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Find the product that we just created using the product grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Create configurations for product we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + + <!--Create new attribute--> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="waitForNewAttributePageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <switchToIFrame selector="{{AdminNewAttributePanelSection.newAttributeIFrame}}" stepKey="enterAttributePanelIFrame"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.defaultLabel}}" time="30" stepKey="waitForIframeLoad"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{productAttributeWithDropdownTwoOptions.attribute_code}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AdminNewAttributePanelSection.inputType}}" userInput="{{colorProductAttribute.input_type}}" stepKey="selectAttributeInputType"/> + <!--Add option to attribute--> + <click selector="{{AdminNewAttributePanelSection.addOption}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminNewAttributePanelSection.isDefault('1')}}" time="30" stepKey="waitForOptionRow"/> + <fillField selector="{{AdminNewAttributePanelSection.optionAdminValue('0')}}" userInput="ThisIsLongNameNameLengthMoreThanSixtyFourThisIsLongNameNameLength" stepKey="fillAdminLabel"/> + <fillField selector="{{AdminNewAttributePanelSection.optionDefaultStoreValue('0')}}" userInput="{{colorProductAttribute1.name}}" stepKey="fillDefaultLabel1"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanelSection.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + + <!-- Generate products --> + <actionGroup ref="AdminGenerateProductConfigurations" stepKey="generateProducts"> + <argument name="attributeCode" value="{{productAttributeWithDropdownTwoOptions.attribute_code}}"/> + <argument name="qty" value="100"/> + <argument name="price" value="10"/> + </actionGroup> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> + + <!--Close modal window--> + <click selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="clickOnClosePopup"/> + <waitForElementNotVisible selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="waitForDialogClosed"/> + + <!--See that validation message is shown under the fields--> + <scrollTo selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" stepKey="scrollTConfigurationTab"/> + <see userInput="Please enter less or equal than 64 symbols." selector="{{AdminProductFormConfigurationsSection.skuValidationMessage('0')}}" stepKey="seeValidationMessage"/> + + <!--Edit "SKU" with valid quantity--> + <fillField selector="{{AdminProductFormConfigurationsSection.configurableMatrixSku('0')}}" userInput="{{ApiConfigurableProduct.sku}}-thisIsShortName" stepKey="fillValidValue"/> + + <!--Click on "Save"--> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + + <!--Click on "Confirm". Product is saved, success message appears --> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml new file mode 100644 index 0000000000000..af463c9042357 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingProductQtyAfterOrderCancelTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product quantity after order cancel"/> + <title value="Products quantity return after order cancel"/> + <description value="Checking product quantity after the order cancel"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13790"/> + <group value="configurableProduct"/> + </annotations> + <before> + <!--Create category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create configurable product and add it to the category--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Create attribute--> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!--Add the attribute to default attribute set--> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!--Get the option of the attribute--> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!--Create simple product and give it the attribute with option--> + <createData entity="ApiSimpleWithQty100" stepKey="createConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + </createData> + <!--Create configurable product--> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + </createData> + <!--Add simple product to the configurable product--> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct"/> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Clear grid filters--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrderGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <!--Delete entities--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromStorefront"/> + </after> + + <!--Go to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + + <!--Go to the configurable product page on Storefront--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.sku$$)}}" stepKey="goToProductPage"/> + <!--Select option--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption.label$$" stepKey="selectOption"/> + <!--Add product to the Shopping cart--> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createConfigProduct.name$"/> + <argument name="quantity" value="4"/> + </actionGroup> + + <!--Open Shopping cart--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCartFromMinicart"/> + <!--Place order--> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + <argument name="paymentMethod" value="Check / Money order"/> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!--Open order--> + <actionGroup ref="OpenOrderById" stepKey="openOrderById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + + <!--Start create invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!--Create partial invoice--> + <actionGroup ref="CreatePartialInvoice" stepKey="createPartialInvoice"> + <argument name="productSku" value="$createConfigChildProduct.sku$"/> + <argument name="qtyToInvoice" value="1"/> + </actionGroup> + <!--Submit Invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Create Shipment--> + <actionGroup ref="StartCreateShipmentFromOrderPage" stepKey="startCreateShipment"/> + <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> + <actionGroup ref="SubmitShipment" stepKey="submitShipment"/> + + <!--Cancel order--> + <actionGroup ref="CancelProcessingOrder" stepKey="cancelOrder"/> + <!--Check quantities in "Items Ordered" table--> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Invoiced 1" stepKey="seeInvoicedQuantity"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 1" stepKey="seeShippedQuantity"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQuantity"/> + + <!--Go to catalog products page on Admin--> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGrid"> + <argument name="product" value="$$createConfigChildProduct$$"/> + </actionGroup> + + <!--Check quantity of configurable child product--> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml new file mode 100644 index 0000000000000..c754b34139ad5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml @@ -0,0 +1,352 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminConfigurableProductChildrenOutOfStockTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> + <severity value="CRITICAL"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> + <testCaseId value="MAGETWO-79939"/> + <group value="configurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" stepKey="lookForOfStock"/> + + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Edit the quantity of the simple first product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductPageSaved"/> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" stepKey="lookForOutOfStock2"/> + + <!-- Find the second simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <!-- Edit the quantity of the second simple product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <waitForPageLoad stepKey="waitForProductPageSaved2"/> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see userInput="OUT OF STOCK" selector="{{StorefrontProductInfoMainSection.stockIndication}}" stepKey="lookForOutOfStock3"/> + </test> + + <test name="AdminConfigurableProductOutOfStockDeleteChildrenTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are deleted"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are deleted"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-79939"/> + <group value="configurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.stockIndication}}" stepKey="lookForOutOfStock"/> + + <!-- Delete the first simple product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigChildProduct1"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see stepKey="lookForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> + + <!-- Delete the second simple product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigChildProduct2"> + <argument name="product" value="$$createConfigChildProduct2$$"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see userInput="OUT OF STOCK" selector="{{StorefrontProductInfoMainSection.stockIndication}}" stepKey="lookForOutOfStock3"/> + </test> + + <test name="AdminConfigurableProductOutOfStockAndDeleteCombinationTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are a combination of 'Out of Stock' and deleted"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are a combination of 'Out of Stock' and deleted"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-79939"/> + <group value="configurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.stockIndication}}" stepKey="lookForInOfStock"/> + + <!-- Delete the first simple product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigChildProduct1"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.stockIndication}}" stepKey="lookForInOfStock2"/> + + <!-- Find the second simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <!-- Edit the quantity of the second simple product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <waitForPageLoad stepKey="waitForProductPageSaved2"/> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK" stepKey="lookForOutOfStock"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductTest.xml new file mode 100644 index 0000000000000..88b3c6ce9965a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductTest.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateConfigurableProductTest"> + <annotations> + <features value="Product Creation"/> + <stories value="Create a Configurable Product via the Admin"/> + <title value="Create a Configurable Product via the Admin."/> + <description value="You should be able to create a Configurable Product via the Admin."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-26041"/> + <group value="configurable"/> + <group value="product"/> + </annotations> + + <!-- Create Category And Login As Admin --> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Delete Created Products And Category Then Logout --> + <after> + <!-- Delete all created products (including virtual) --> + <actionGroup ref="DeleteAllProductsOnProductsGridPageFilteredByName" stepKey="deleteAllCreatedProducts"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <!-- Delete created product attribute --> + <actionGroup ref="DeleteProductAttribute" stepKey="deleteCreatedProductAttribute"> + <argument name="productAttribute" value="colorProductAttribute"/> + </actionGroup> + <!-- Logout --> + <actionGroup ref="logout" stepKey="logout"/> + <!-- Delete created category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Create configurable product... --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{_defaultCategory.name}}]" stepKey="searchAndSelectCategory"/> + <selectOption userInput="{{_defaultProduct.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + + <!-- Create Product Attribute --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <waitForPageLoad stepKey="waitForPageLoadAfterSwitch"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillDefaultLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + + <!-- Choose Created Attribute --> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{colorProductAttribute.default_label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + + <!-- Create First Value for Attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue1"/> + <fillField userInput="{{colorProductAttribute1.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute1"/> + + <!-- Create Second Value for Attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue2"/> + <fillField userInput="{{colorProductAttribute2.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute2"/> + + <!-- Create Third Value for Attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue3"/> + <fillField userInput="{{colorProductAttribute3.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute3"/> + + <!-- Select All Values and Go On--> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + + <!-- Apply Unique Prices By Attribute To Each SKU --> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesByAttributeToEachSku}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" userInput="{{colorProductAttribute.default_label}}" stepKey="selectAttributes"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attribute1}}" userInput="{{colorProductAttribute1.price}}" stepKey="fillAttributePrice1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attribute2}}" userInput="{{colorProductAttribute2.price}}" stepKey="fillAttributePrice2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attribute3}}" userInput="{{colorProductAttribute3.price}}" stepKey="fillAttributePrice3"/> + + <!-- Apply Unique Quantity to Each SKUs --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + + <!-- Finish Creating Configurations --> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + + <!-- Save Product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + + <!-- Make Sure Product Is Created --> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="seeProductNameInTitle"/> + + <!-- Make Sure Configurations Created Correctly --> + <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="3" stepKey="seeNumberOfRows"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeAttributeName1InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeAttributeName2InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeAttributeName3InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeAttributeSku1InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeAttributeSku2InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeAttributeSku3InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{colorProductAttribute1.price}}" stepKey="seeUniquePrice1InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{colorProductAttribute2.price}}" stepKey="seeUniquePrice2InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{colorProductAttribute3.price}}" stepKey="seeUniquePrice3InField"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsQuantityCells}}" userInput="{{colorProductAttribute.attribute_quantity}}" stepKey="seeQuantityInField"/> + + <!-- Go To StoreFront --> + <amOnPage url="/" stepKey="amOnStorefront"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad3"/> + + <!-- Go To Just Created Category --> + <click userInput="{{_defaultCategory.name}}" stepKey="clickOnCategoryName"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad4"/> + + <!-- Check Product Presence And Go Into The Product View --> + <see userInput="{{_defaultProduct.name}}" stepKey="assertProductPresent"/> + <see userInput="{{colorProductAttribute1.price}}" stepKey="assertProductPricePresent"/> + <click userInput="{{_defaultProduct.name}}" stepKey="clickOnProductName"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + + <!-- Check Page's Title, Product Name, Price, Sku --> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{_defaultProduct.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{colorProductAttribute1.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> + <see userInput="{{_defaultProduct.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + + <!-- Check Options --> + <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="{{colorProductAttribute.default_label}}" stepKey="seeColorAttributeName1"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeInDropDown1"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeInDropDown2"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeInDropDown3"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml new file mode 100644 index 0000000000000..5daf699294155 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View configurable product details on storefront"/> + <title value="Check that 'trie price' block not available for simple product from options without 'trie price'"/> + <description value="Check that 'trie price' block not available for simple product from options without 'trie price'"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13789"/> + <useCaseId value="MAGETWO-96457"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Configurable product--> + <actionGroup ref="AdminCreateApiConfigurableProductActionGroup" stepKey="createConfigurableProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!--Go to storefront product page an check price box css--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1CreateConfigurableProduct.value$$" stepKey="selectOption"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productPriceBox}}" userInput="class" stepKey="grabGrabPriceClass"/> + <assertContains actual="$grabGrabPriceClass" expected="price-box price-final_price" expectedType="string" stepKey="assertEquals"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml new file mode 100644 index 0000000000000..cac379e11e79a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductChildSearchTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View configurable product details in storefront"/> + <title value="Guest customer should be able to search configurable product by attributes of child products"/> + <description value="Guest customer should be able to search configurable product by attributes of child products"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-75997"/> + <group value="configurable"/> + <group value="product"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Create an attribute with two options to be used in the second child product --> + <createData entity="ProductAttributeMultiselectTwoOptions" stepKey="createConfigProductAttributeMultiSelect"/> + <createData entity="ProductAttributeOption3" stepKey="createConfigProductAttributeOption1Multiselect"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + <createData entity="ProductAttributeOption4" stepKey="createConfigProductAttributeOption2Multiselect"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the second attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + + <!-- Get the first option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the first option of the second attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </getData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + + <!-- Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Fill Short Description for first simple product --> + <updateData entity="ApiSimpleProductUpdateDescription" createDataKey="createConfigChildProduct1" stepKey="updateSimpleProduct1Description" /> + + <!-- Fill Short Description for first simple product --> + <updateData entity="ApiSimpleProductUpdateName" createDataKey="createConfigChildProduct1" stepKey="updateSimpleProduct1Name"/> + + <!-- Create an attribute with two options to be used in the first child product (in the UI) --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttributeSelect"/> + <createData entity="ProductAttributeOption5" stepKey="createConfigProductAttributeSelectOption1"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + <createData entity="ProductAttributeOption6" stepKey="createConfigProductAttributeSelectOption2"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet3"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!-- Go to the product page for the first product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductGrid"/> + <waitForPageLoad stepKey="waitForProductGridLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <!-- Edit the attribute for the first simple product --> + <selectOption selector="{{AdminModifyAttributesSection.dropDownAttributeByName($$createConfigProductAttributeSelect.default_frontend_label$$)}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$" stepKey="editSelectAttribute"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + + <!-- Go to the product page for the second product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductGrid2"/> + <waitForPageLoad stepKey="waitForProductGridLoad2"/> + <actionGroup ref="filterProductGridBySku" stepKey="searchForSimpleProduct2"> + <argument name="product" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <!-- Edit the attribute for the first second product --> + <selectOption selector="{{AdminModifyAttributesSection.dropDownAttributeByName($$createConfigProductAttributeMultiSelect.default_frontend_label$$)}}" userInput="$$createConfigProductAttributeOption2Multiselect.option[store_labels][0][label]$$" stepKey="editSelectAttribute2"/> + <scrollToTopOfPage stepKey="scrollToTop2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess2"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigProductAttributeMultiSelect" stepKey="deleteConfigProductAttributeMultiSelect"/> + <deleteData createDataKey="createConfigProductAttributeSelect" stepKey="deleteConfigProductAttributeSelect"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + </after> + + <!-- Quick search the storefront for the first attribute option --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$" stepKey="searchStorefront1"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearch1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productTitleByName('$$createConfigProduct.name$$')}}" stepKey="seeProduct1"/> + + <!-- Quick search the storefront for the second attribute option --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeOption2Multiselect.option[store_labels][0][label]$$" stepKey="searchStorefront2"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearch2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productTitleByName('$$createConfigProduct.name$$')}}" stepKey="seeProduct2"/> + + <!-- Quick search the storefront for the first product description --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="'$$createConfigChildProduct1.custom_attributes[short_description]$$'" stepKey="searchStorefront3"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearch3"/> + <seeElement selector="{{StorefrontCategoryProductSection.productTitleByName('$$createConfigProduct.name$$')}}" stepKey="seeProduct3"/> + + <!-- Quick search the storefront for the first product name --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="'$$createConfigChildProduct1.name$$'" stepKey="searchStorefront4"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearch4"/> + <seeElement selector="{{StorefrontCategoryProductSection.productTitleByName('$$createConfigProduct.name$$')}}" stepKey="seeProduct4"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml new file mode 100644 index 0000000000000..bddd71f565c33 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductWithFileCustomOptionTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Add configurable product to cart"/> + <title value="Correct error message and redirect with invalid file custom option"/> + <description value="Configurable product has file custom option. When adding to cart with an invalid filetype, the correct error message is shown, and options remain selected."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-93318"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteAllProductsOnProductsGridPageFilteredByName" stepKey="deleteAllCreatedProducts"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="DeleteProductAttribute" stepKey="deleteCreatedProductAttribute"> + <argument name="productAttribute" value="colorProductAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create a configurable product via the UI --> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!--Add custom option to configurable product--> + <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"> + <argument name="option" value="ProductOptionFile"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Go to storefront--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageLoad"/> + <click selector="{{StorefrontNavigationSection.topCategory($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.categoryTitle}}" userInput="$$createCategory.name$$" stepKey="seeOnCategoryPage"/> + <!--Add configurable product to cart--> + <moveMouseOver selector="{{StorefrontCategoryProductSection.productTitleByName(BaseConfigurableProduct.name)}}" stepKey="hoverProductInGrid"/> + <click selector="{{StorefrontCategoryProductSection.productAddToCartByName(BaseConfigurableProduct.name)}}" stepKey="tryAddToCartFromCategoryPage"/> + <waitForPageLoad stepKey="waitForRedirectToProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url(BaseConfigurableProduct.urlKey)}}" stepKey="seeOnProductPage"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorProductAttribute2.name}}" stepKey="selectColor"/> + <!--Try invalid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="lorem_ipsum.docx" stepKey="attachInvalidFile"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartInvalidFile"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}" stepKey="waitForErrorMessageInvalidFile"/> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="The file 'lorem_ipsum.docx' for '{{ProductOptionFile.title}}' has an invalid extension." stepKey="seeMessageInvalidFile"/> + <!--Option remains selected--> + <seeOptionIsSelected selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeOptionRemainSelected"/> + <!--Try valid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="attachValidFile"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$11.99" stepKey="seePriceUpdated"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="BaseConfigurableProduct.name"/> + </actionGroup> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{BaseConfigurableProduct.name}} to your shopping cart." stepKey="seeSuccessMessage"/> + + <!--Check item in cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <seeElement selector="{{CheckoutCartProductSection.productLinkByName(BaseConfigurableProduct.name)}}" stepKey="seeProductInCart"/> + <see selector="{{CheckoutCartProductSection.productOptionByNameAndAttribute(BaseConfigurableProduct.name, colorProductAttribute.default_label)}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeSelectedOption"/> + <see selector="{{CheckoutCartProductSection.productOptionByNameAndAttribute(BaseConfigurableProduct.name, ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="seeCorrectOptionFile"/> + <!--Delete cart item--> + <click selector="{{CheckoutCartProductSection.removeItem}}" stepKey="deleteCartItem"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml new file mode 100644 index 0000000000000..72bcc3a91b59d --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSortingByPriceForConfigurableProductWithCatalogRuleAppliedTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View soting by price in storefront"/> + <title value="Sorting by price for Configurable with Catalog Rule applied"/> + <description value="Sort by price should be correct if the apply Catalog Rule to child product of configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76081"/> + <group value="configurable_product"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">5.00</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10.00</field> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="ProductAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">15.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">25.00</field> + </createData> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllCatalogPriceRule" stepKey="deleteCatalogRules"/> + <!--SKU Product Attribute is enabled for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="attributeLabel" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToYes"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!-- Delete the rule --> + <actionGroup ref="RemoveCatalogPriceRule" stepKey="deletePriceRule"> + <argument name="ruleName" value="CatalogRule96PercentDiscount.name" /> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCatalogRuleGridFilters"/> + <!--SKU Product Attribute is disable for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="attributeLabel" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToNo"> + <argument name="useForPromoRule" value="No"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearAttributeGridFilters"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Open category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct"/> + + <!--Create and apply catalog price rule--> + <actionGroup ref="AdminStartCreateNewCatalogRuleActionGroup" stepKey="startCreateNewCatalogRulePage"/> + <actionGroup ref="AdminFillCatalogRuleFormActionGroup" stepKey="fillRuleFields"> + <argument name="catalogRule" value="CatalogRule96PercentDiscount"/> + </actionGroup> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillRuleConditions"> + <argument name="condition" value="{{CatalogRuleProductConditions.productSku}}"/> + <argument name="conditionType" value="is"/> + <argument name="conditionValue" value="$$createConfigChildProduct3.sku$$"/> + </actionGroup> + <actionGroup ref="AdminSaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyRule"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + + <!--Reopen category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage2"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('1')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('2')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('3')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php index 8df6df53cc065..2d73b61245a4b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php @@ -38,7 +38,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->productMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['isReadonly', 'isDuplicable']) + ->setMethods(['isReadonly', 'isDuplicable', 'isComposite']) ->getMockForAbstractClass(); $this->registryMock->expects(static::any()) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Steps/SelectAttributesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Steps/SelectAttributesTest.php index 040329dbb3d87..33b87467950fd 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Steps/SelectAttributesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Steps/SelectAttributesTest.php @@ -114,6 +114,9 @@ public function testGetAddNewAttributeButton($isAllowed, $result) $this->assertEquals($result, $this->selectAttributes->getAddNewAttributeButton()); } + /** + * @return array + */ public function attributesDataProvider() { return [ diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php index e199841cbcdc4..698a7ac9f2693 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Cart/Item/Renderer/ConfigurableTest.php @@ -8,157 +8,79 @@ use Magento\Catalog\Model\Config\Source\Product\Thumbnail as ThumbnailSource; use Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable as Renderer; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ConfigurableTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\View\ConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $_configManager; - - /** @var \Magento\Catalog\Helper\Image|\PHPUnit_Framework_MockObject_MockObject */ - protected $_imageHelper; - - /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $_scopeConfig; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $productConfigMock; + /** + * @var \Magento\Framework\View\ConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configManager; - /** @var Renderer */ - protected $_renderer; + /** + * @var \Magento\Catalog\Helper\Image|\PHPUnit_Framework_MockObject_MockObject + */ + private $imageHelper; - protected function setUp() - { - parent::setUp(); - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_configManager = $this->createMock(\Magento\Framework\View\ConfigInterface::class); - $this->_imageHelper = $this->createPartialMock( - \Magento\Catalog\Helper\Image::class, - ['init', 'resize', '__toString'] - ); - $this->_scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->productConfigMock = $this->createMock(\Magento\Catalog\Helper\Product\Configuration::class); - $this->_renderer = $objectManagerHelper->getObject( - \Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable::class, - [ - 'viewConfig' => $this->_configManager, - 'imageHelper' => $this->_imageHelper, - 'scopeConfig' => $this->_scopeConfig, - 'productConfig' => $this->productConfigMock - ] - ); - } + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; /** - * Child thumbnail is available and config option is not set to use parent thumbnail. + * @var \PHPUnit_Framework_MockObject_MockObject */ - public function testGetProductForThumbnail() - { - $childHasThumbnail = true; - $useParentThumbnail = false; - $products = $this->_initProducts($childHasThumbnail, $useParentThumbnail); - - $productForThumbnail = $this->_renderer->getProductForThumbnail(); - $this->assertSame( - $products['childProduct'], - $productForThumbnail, - 'Child product was expected to be returned.' - ); - } + private $productConfigMock; /** - * Child thumbnail is not available and config option is not set to use parent thumbnail. + * @var \Magento\Backend\Block\Template\Context|\PHPUnit_Framework_MockObject_MockObject */ - public function testGetProductForThumbnailChildThumbnailNotAvailable() - { - $childHasThumbnail = false; - $useParentThumbnail = false; - $products = $this->_initProducts($childHasThumbnail, $useParentThumbnail); - - $productForThumbnail = $this->_renderer->getProductForThumbnail(); - $this->assertSame( - $products['parentProduct'], - $productForThumbnail, - 'Parent product was expected to be returned.' - ); - } + private $contextMock; /** - * Child thumbnail is available and config option is set to use parent thumbnail. + * @var \Magento\Framework\View\Layout|PHPUnit_Framework_MockObject_MockObject */ - public function testGetProductForThumbnailConfigUseParent() - { - $childHasThumbnail = true; - $useParentThumbnail = true; - $products = $this->_initProducts($childHasThumbnail, $useParentThumbnail); - - $productForThumbnail = $this->_renderer->getProductForThumbnail(); - $this->assertSame( - $products['parentProduct'], - $productForThumbnail, - 'Parent product was expected to be returned ' . - 'if "checkout/cart/configurable_product_image option" is set to "parent" in system config.' - ); - } + private $layoutMock; /** - * Initialize parent configurable product and child product. - * - * @param bool $childHasThumbnail - * @param bool $useParentThumbnail - * @return \Magento\Catalog\Model\Product[]|\PHPUnit_Framework_MockObject_MockObject[] + * @var Renderer */ - protected function _initProducts($childHasThumbnail = true, $useParentThumbnail = false) + private $renderer; + + protected function setUp() { - /** Set option which can force usage of parent product thumbnail when configurable product is displayed */ - $thumbnailToBeUsed = $useParentThumbnail - ? ThumbnailSource::OPTION_USE_PARENT_IMAGE - : ThumbnailSource::OPTION_USE_OWN_IMAGE; - $this->_scopeConfig->expects( - $this->any() - )->method( - 'getValue' - )->with( - Renderer::CONFIG_THUMBNAIL_SOURCE - )->will( - $this->returnValue($thumbnailToBeUsed) - ); + parent::setUp(); + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->contextMock = $this->createPartialMock(\Magento\Backend\Block\Template\Context::class, ['getLayout']); + $this->layoutMock = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getBlock']); + $this->contextMock->expects($this->once())->method('getLayout')->willReturn($this->layoutMock); - /** Initialized parent product */ - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $parentProduct */ - $parentProduct = $this->createMock(\Magento\Catalog\Model\Product::class); - - /** Initialize child product */ - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $childProduct */ - $childProduct = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getThumbnail', '__wakeup']); - $childThumbnail = $childHasThumbnail ? 'thumbnail.jpg' : 'no_selection'; - $childProduct->expects($this->any())->method('getThumbnail')->will($this->returnValue($childThumbnail)); - - /** Mock methods which return parent and child products */ - /** @var \Magento\Quote\Model\Quote\Item\Option|\PHPUnit_Framework_MockObject_MockObject $itemOption */ - $itemOption = $this->createMock(\Magento\Quote\Model\Quote\Item\Option::class); - $itemOption->expects($this->any())->method('getProduct')->will($this->returnValue($childProduct)); - /** @var \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject $item */ - $item = $this->createMock(\Magento\Quote\Model\Quote\Item::class); - $item->expects($this->any())->method('getProduct')->will($this->returnValue($parentProduct)); - $item->expects( - $this->any() - )->method( - 'getOptionByCode' - )->with( - 'simple_product' - )->will( - $this->returnValue($itemOption) + $this->configManager = $this->createMock(\Magento\Framework\View\ConfigInterface::class); + $this->imageHelper = $this->createPartialMock( + \Magento\Catalog\Helper\Image::class, + ['init', 'resize', '__toString'] + ); + $this->scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->productConfigMock = $this->createMock(\Magento\Catalog\Helper\Product\Configuration::class); + $this->renderer = $objectManagerHelper->getObject( + \Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable::class, + [ + 'viewConfig' => $this->configManager, + 'imageHelper' => $this->imageHelper, + 'scopeConfig' => $this->scopeConfig, + 'productConfig' => $this->productConfigMock, + 'context' => $this->contextMock, + ] ); - $this->_renderer->setItem($item); - - return ['parentProduct' => $parentProduct, 'childProduct' => $childProduct]; } public function testGetOptionList() { $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class); - $this->_renderer->setItem($itemMock); + $this->renderer->setItem($itemMock); $this->productConfigMock->expects($this->once())->method('getOptions')->with($itemMock); - $this->_renderer->getOptionList(); + $this->renderer->getOptionList(); } public function testGetIdentities() @@ -168,7 +90,45 @@ public function testGetIdentities() $product->expects($this->exactly(2))->method('getIdentities')->will($this->returnValue($productTags)); $item = $this->createMock(\Magento\Quote\Model\Quote\Item::class); $item->expects($this->exactly(2))->method('getProduct')->will($this->returnValue($product)); - $this->_renderer->setItem($item); - $this->assertEquals(array_merge($productTags, $productTags), $this->_renderer->getIdentities()); + $this->renderer->setItem($item); + $this->assertEquals(array_merge($productTags, $productTags), $this->renderer->getIdentities()); + } + + /** + * Product price renderer test. + * + * @return void + */ + public function testGetProductPriceHtml() + { + $priceHtml = 'some price html'; + $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); + + $item = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + $item->expects($this->atLeastOnce())->method('getProduct')->willReturn($productMock); + + $priceRenderMock = $this->getMockBuilder(\Magento\Framework\Pricing\Render::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->layoutMock->expects($this->once()) + ->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderMock); + + $priceRenderMock->expects($this->once()) + ->method('render') + ->with( + \Magento\Catalog\Pricing\Price\ConfiguredPriceInterface::CONFIGURED_PRICE_CODE, + $productMock, + [ + 'include_container' => true, + 'display_minimal_price' => true, + 'zone' => \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + ] + )->willReturn($priceHtml); + + $this->renderer->setItem($item); + $this->assertEquals($priceHtml, $this->renderer->getProductPriceHtml($productMock)); } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 1908d897be6da..20b0905b7707b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -48,6 +48,11 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase */ private $priceCurrency; + /** + * @var \Magento\Directory\Model\Currency|\PHPUnit_Framework_MockObject_MockObject + */ + private $currency; + /** * @var \Magento\ConfigurableProduct\Model\ConfigurableAttributeData|\PHPUnit_Framework_MockObject_MockObject */ @@ -73,6 +78,11 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase */ private $customerSession; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $variationPricesMock; + protected function setUp() { $this->mockContextObject(); @@ -122,6 +132,9 @@ protected function setUp() $this->context->expects($this->once()) ->method('getResolver') ->willReturn($fileResolverMock); + $this->currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) + ->disableOriginalConstructor() + ->getMock(); $this->configurableAttributeData = $this->getMockBuilder( \Magento\ConfigurableProduct\Model\ConfigurableAttributeData::class ) @@ -136,6 +149,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->variationPricesMock = $this->createMock( + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class + ); + $this->block = new \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable( $this->context, $this->arrayUtils, @@ -147,7 +164,8 @@ protected function setUp() $this->configurableAttributeData, [], $this->localeFormat, - $this->customerSession + $this->customerSession, + $this->variationPricesMock ); } @@ -192,10 +210,10 @@ public function cacheKeyProvider() : array 2 => null, 'base_url' => null, 'template' => null, - 3 => '$', + 3 => 'USD', 4 => null, ], - '$', + 'USD', null, ] ]; @@ -223,7 +241,10 @@ public function testGetCacheKeyInfo(array $expected, string $priceCurrency = nul ->method('getStore') ->willReturn($storeMock); $this->priceCurrency->expects($this->once()) - ->method('getCurrencySymbol') + ->method('getCurrency') + ->willReturn($this->currency); + $this->currency->expects($this->once()) + ->method('getCode') ->willReturn($priceCurrency); $this->customerSession->expects($this->once()) ->method('getCustomerGroupId') @@ -249,12 +270,8 @@ public function testGetJsonConfig() 'getAmount', ]) ->getMockForAbstractClass(); - $priceMock->expects($this->any()) - ->method('getAmount') - ->willReturn($amountMock); - + $priceMock->expects($this->any())->method('getAmount')->willReturn($amountMock); $tierPriceMock = $this->getTierPriceMock($amountMock, $priceQty, $percentage); - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); @@ -272,27 +289,16 @@ public function testGetJsonConfig() ['tier_price', $tierPriceMock], ]); - $productMock->expects($this->any()) - ->method('getTypeInstance') - ->willReturn($productTypeMock); - $productMock->expects($this->any()) - ->method('getPriceInfo') - ->willReturn($priceInfoMock); - $productMock->expects($this->any()) - ->method('isSaleable') - ->willReturn(true); - $productMock->expects($this->any()) - ->method('getId') - ->willReturn($productId); + $productMock->expects($this->any())->method('getTypeInstance')->willReturn($productTypeMock); + $productMock->expects($this->any())->method('getPriceInfo')->willReturn($priceInfoMock); + $productMock->expects($this->any())->method('isSaleable')->willReturn(true); + $productMock->expects($this->any())->method('getId')->willReturn($productId); $this->helper->expects($this->any()) ->method('getOptions') ->with($productMock, [$productMock]) ->willReturn([]); - - $this->product->expects($this->any()) - ->method('getSkipSaleableCheck') - ->willReturn(true); + $this->product->expects($this->any())->method('getSkipSaleableCheck')->willReturn(true); $attributesData = [ 'attributes' => [], @@ -304,9 +310,7 @@ public function testGetJsonConfig() ->with($productMock, []) ->willReturn($attributesData); - $this->localeFormat->expects($this->any()) - ->method('getPriceFormat') - ->willReturn([]); + $this->localeFormat->expects($this->atLeastOnce())->method('getPriceFormat')->willReturn([]); $this->localeFormat->expects($this->any()) ->method('getNumber') ->willReturnMap([ @@ -315,30 +319,43 @@ public function testGetJsonConfig() [$percentage, $percentage], ]); + $this->variationPricesMock->expects($this->once()) + ->method('getFormattedPrices') + ->with($priceInfoMock) + ->willReturn( + [ + 'oldPrice' => [ + 'amount' => $amount, + ], + 'basePrice' => [ + 'amount' => $amount, + ], + 'finalPrice' => [ + 'amount' => $amount, + ], + ] + ); + $expectedArray = $this->getExpectedArray($productId, $amount, $priceQty, $percentage); $expectedJson = json_encode($expectedArray); - $this->jsonEncoder->expects($this->once()) - ->method('encode') - ->with($expectedArray) - ->willReturn($expectedJson); + $this->jsonEncoder->expects($this->once())->method('encode')->with($expectedArray)->willReturn($expectedJson); $this->block->setData('product', $productMock); - $result = $this->block->getJsonConfig(); $this->assertEquals($expectedJson, $result); } /** - * Retrieve array with expected parameters for method getJsonConfig() + * Retrieve array with expected parameters for method getJsonConfig(). * - * @param $productId - * @param $amount - * @param $priceQty - * @param $percentage + * @param int $productId + * @param float $amount + * @param int $priceQty + * @param int $percentage * @return array */ - private function getExpectedArray($productId, $amount, $priceQty, $percentage) + private function getExpectedArray(int $productId, float $amount, int $priceQty, int $percentage): array { $expectedArray = [ 'attributes' => [], @@ -362,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage) 'percentage' => $percentage, ], ], + 'msrpPrice' => [ + 'amount' => null, + ], ], ], 'priceFormat' => [], diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php index c89d6e2e9ac45..ae9015a880586 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ConfigurableProduct\Test\Unit\Helper; class DataTest extends \PHPUnit\Framework\TestCase @@ -71,11 +69,13 @@ public function testGetOptions(array $expected, array $data) $this->_imageHelperMock->expects($this->any()) ->method('init') - ->willReturnMap([ - [$data['current_product_mock'], 'product_page_image_large', [], $imageHelper1], - [$data['allowed_products'][0], 'product_page_image_large', [], $imageHelper1], - [$data['allowed_products'][1], 'product_page_image_large', [], $imageHelper2], - ]); + ->willReturnMap( + [ + [$data['current_product_mock'], 'product_page_image_large', [], $imageHelper1], + [$data['allowed_products'][0], 'product_page_image_large', [], $imageHelper1], + [$data['allowed_products'][1], 'product_page_image_large', [], $imageHelper2], + ] + ); } $this->assertEquals( @@ -89,7 +89,10 @@ public function testGetOptions(array $expected, array $data) */ public function getOptionsDataProvider() { - $currentProductMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeInstance', '__wakeup']); + $currentProductMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getTypeInstance', '__wakeup'] + ); $provider = []; $provider[] = [ [], @@ -103,7 +106,10 @@ public function getOptionsDataProvider() $attributes = []; for ($i = 1; $i < $attributesCount; $i++) { $attribute = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getProductAttribute']); - $productAttribute = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getId', 'getAttributeCode']); + $productAttribute = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['getId', 'getAttributeCode'] + ); $productAttribute->expects($this->any()) ->method('getId') ->will($this->returnValue('attribute_id_' . $i)); @@ -124,13 +130,20 @@ public function getOptionsDataProvider() ->will($this->returnValue($typeInstanceMock)); $allowedProducts = []; for ($i = 1; $i <= 2; $i++) { - $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData', 'getImage', 'getId', '__wakeup', 'getMediaGalleryImages']); + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getData', 'getImage', 'getId', '__wakeup', 'getMediaGalleryImages', 'isSalable'] + ); $productMock->expects($this->any()) ->method('getData') ->will($this->returnCallback([$this, 'getDataCallback'])); $productMock->expects($this->any()) ->method('getId') ->will($this->returnValue('product_id_' . $i)); + $productMock + ->expects($this->any()) + ->method('isSalable') + ->will($this->returnValue(true)); if ($i == 2) { $productMock->expects($this->any()) ->method('getImage') @@ -191,11 +204,13 @@ public function testGetGalleryImages() $this->_imageHelperMock->expects($this->exactly(3)) ->method('init') - ->willReturnMap([ - [$productMock, 'product_page_image_small', [], $this->_imageHelperMock], - [$productMock, 'product_page_image_medium_no_frame', [], $this->_imageHelperMock], - [$productMock, 'product_page_image_large_no_frame', [], $this->_imageHelperMock], - ]) + ->willReturnMap( + [ + [$productMock, 'product_page_image_small', [], $this->_imageHelperMock], + [$productMock, 'product_page_image_medium_no_frame', [], $this->_imageHelperMock], + [$productMock, 'product_page_image_large_no_frame', [], $this->_imageHelperMock], + ] + ) ->willReturnSelf(); $this->_imageHelperMock->expects($this->exactly(3)) ->method('setImageFile') @@ -215,7 +230,6 @@ public function testGetGalleryImages() \Magento\Framework\Data\Collection::class, $this->_model->getGalleryImages($productMock) ); - } /** @@ -228,9 +242,9 @@ private function getImagesCollection() ->getMock(); $items = [ - new \Magento\Framework\DataObject([ - 'file' => 'test_file' - ]), + new \Magento\Framework\DataObject( + ['file' => 'test_file'] + ), ]; $collectionMock->expects($this->any()) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Attribute/LockValidatorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Attribute/LockValidatorTest.php index 090c464d49307..3565aba66427c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Attribute/LockValidatorTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Attribute/LockValidatorTest.php @@ -108,6 +108,11 @@ public function testValidateException() $this->validate(true); } + /** + * @param $exception + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function validate($exception) { $attrTable = 'someAttributeTable'; diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php index acbb976318b3b..279880d546424 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php @@ -158,7 +158,7 @@ public function testAddChild() ->getMock(); $optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class) ->disableOriginalConstructor() - ->setMethods(['getProductAttribute', 'getAttributeId']) + ->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId']) ->getMock(); $productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -216,6 +216,7 @@ public function testAddChild() $productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color'); $simple->expects($this->any())->method('getData')->willReturn('color'); $optionMock->expects($this->any())->method('getAttributeId')->willReturn('1'); + $optionMock->expects($this->any())->method('getPosition')->willReturn('0'); $optionsFactoryMock->expects($this->any())->method('create')->willReturn([$optionMock]); $attributeFactoryMock->expects($this->any())->method('create')->willReturn($attributeMock); @@ -223,6 +224,7 @@ public function testAddChild() $attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf(); $attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]); + $attributeMock->expects($this->any())->method('getId')->willReturn(1); $attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]); $extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions'); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/OptionRepositoryTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/OptionRepositoryTest.php index 2d824e52c7244..7991d8fececb4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/OptionRepositoryTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/OptionRepositoryTest.php @@ -356,7 +356,8 @@ public function testGetListNotConfigurableProduct() */ public function testValidateNewOptionData($attributeId, $label, $optionValues, $msg) { - $this->expectException(\Magento\Framework\Exception\InputException::class, $msg); + $this->expectException(\Magento\Framework\Exception\InputException::class); + $this->expectExceptionMessage($msg); $optionValueMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\OptionValueInterface::class) ->setMethods(['getValueIndex', 'getPricingValue', 'getIsPercent']) ->getMockForAbstractClass(); @@ -388,6 +389,9 @@ public function testValidateNewOptionData($attributeId, $label, $optionValues, $ $this->model->validateNewOptionData($optionMock); } + /** + * @return array + */ public function validateOptionDataProvider() { return [ diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php new file mode 100644 index 0000000000000..d29f163ee1129 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin; + +use Magento\ConfigurableProduct\Model\Plugin\ProductIdentitiesExtender; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; + +/** + * Class ProductIdentitiesExtenderTest + */ +class ProductIdentitiesExtenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Configurable + */ + private $configurableTypeMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ProductRepositoryInterface + */ + private $productRepositoryMock; + + /** + * @var ProductIdentitiesExtender + */ + private $plugin; + + protected function setUp() + { + $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->getMock(); + + $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock, $this->productRepositoryMock); + } + + public function testAfterGetIdentities() + { + $productId = 1; + $productIdentity = 'cache_tag_1'; + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $parentProductId = 2; + $parentProductIdentity = 'cache_tag_2'; + $parentProductMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $productMock->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $this->configurableTypeMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with($productId) + ->willReturn([$parentProductId]); + $this->productRepositoryMock->expects($this->once()) + ->method('getById') + ->with($parentProductId) + ->willReturn($parentProductMock); + $parentProductMock->expects($this->once()) + ->method('getIdentities') + ->willReturn([$parentProductIdentity]); + + $productIdentities = $this->plugin->afterGetIdentities($productMock, [$productIdentity]); + $this->assertEquals([$productIdentity, $parentProductIdentity], $productIdentities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php deleted file mode 100644 index 519288a50c858..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Cache\Tag; - -use Magento\ConfigurableProduct\Model\Product\Cache\Tag\Configurable; - -class ConfigurableTest extends \PHPUnit\Framework\TestCase -{ - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|Configurable - */ - private $typeResource; - - /** - * @var Configurable - */ - private $model; - - protected function setUp() - { - $this->typeResource = $this->createMock( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable::class - ); - - $this->model = new Configurable($this->typeResource); - } - - public function testGetWithScalar() - { - $this->expectException(\InvalidArgumentException::class, 'Provided argument is not an object'); - $this->model->getTags('scalar'); - } - - public function testGetTagsWithObject() - { - $this->expectException(\InvalidArgumentException::class, 'Provided argument must be a product'); - $this->model->getTags(new \StdClass()); - } - - public function testGetTagsWithVariation() - { - $product = $this->createMock(\Magento\Catalog\Model\Product::class); - - $identities = ['id1', 'id2']; - - $product->expects($this->once()) - ->method('getIdentities') - ->willReturn($identities); - - $parentId = 4; - $this->typeResource->expects($this->once()) - ->method('getParentIdsByChild') - ->willReturn([$parentId]); - - $expected = array_merge($identities, [\Magento\Catalog\Model\Product::CACHE_TAG . '_' . $parentId]); - - $this->assertEquals($expected, $this->model->getTags($product)); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php new file mode 100644 index 0000000000000..8df6dddc3eb5a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Configuration\Item; + +use Magento\Catalog\Model\Config\Source\Product\Thumbnail; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; +use Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote\Item\Option; +use PHPUnit\Framework\TestCase; + +/** + * ItemProductResolver test + */ +class ItemProductResolverTest extends TestCase +{ + /** + * @var ItemProductResolver + */ + private $model; + + /** + * @var ItemInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $item; + + /** + * @var Product | \PHPUnit_Framework_MockObject_MockObject + */ + private $parentProduct; + + /** + * @var ScopeConfigInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var OptionInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $option; + + /** + * @var Product | \PHPUnit_Framework_MockObject_MockObject + */ + private $childProduct; + + /** + * Set up method + * + * @return void + */ + protected function setUp() + { + parent::setUp(); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->parentProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->parentProduct + ->method('getSku') + ->willReturn('parent_product'); + + $this->childProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->childProduct + ->method('getSku') + ->willReturn('child_product'); + + $this->option = $this->getMockBuilder(Option::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->option + ->method('getProduct') + ->willReturn($this->childProduct); + + $this->item = $this->getMockBuilder(ItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->item + ->expects($this->once()) + ->method('getProduct') + ->willReturn($this->parentProduct); + + $this->model = new ItemProductResolver($this->scopeConfig); + } + + /** + * Test for deleted child product from configurable product + * + * @return void + */ + public function testGetFinalProductChildIsNull() + { + $this->item->method('getOptionByCode') + ->willReturn(null); + + $finalProduct = $this->model->getFinalProduct($this->item); + $this->assertEquals( + $this->parentProduct->getSku(), + $finalProduct->getSku() + ); + } + + /** + * Tests child product from configurable product + * + * @dataProvider provideScopeConfig + * @param string $expectedSku + * @param string $scopeValue + * @param string | null $thumbnail + * @return void + */ + public function testGetFinalProductChild($expectedSku, $scopeValue, $thumbnail) + { + $this->item->method('getOptionByCode') + ->willReturn($this->option); + + $this->childProduct->method('getData') + ->willReturn($thumbnail); + + $this->scopeConfig->method('getValue') + ->willReturn($scopeValue); + + $finalProduct = $this->model->getFinalProduct($this->item); + $this->assertEquals($expectedSku, $finalProduct->getSku()); + } + + /** + * Data provider for scope test + * + * @return array + */ + public function provideScopeConfig(): array + { + return [ + ['child_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'thumbnail'], + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'thumbnail'], + + ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, null], + ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'no_selection'], + + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, null], + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'no_selection'], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php index 6fda5b867ccef..851595422f596 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php @@ -88,6 +88,7 @@ public function testExecuteWithInvalidProductType() public function testExecuteWithEmptyExtensionAttributes() { $sku = 'test'; + $configurableProductLinks = [1, 2, 3]; $product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku']) @@ -105,16 +106,16 @@ public function testExecuteWithEmptyExtensionAttributes() ->disableOriginalConstructor() ->getMockForAbstractClass(); - $product->expects(static::once()) + $product->expects(static::atLeastOnce()) ->method('getExtensionAttributes') ->willReturn($extensionAttributes); - $extensionAttributes->expects(static::exactly(2)) + $extensionAttributes->expects(static::atLeastOnce()) ->method('getConfigurableProductOptions') ->willReturn([]); - $extensionAttributes->expects(static::once()) + $extensionAttributes->expects(static::atLeastOnce()) ->method('getConfigurableProductLinks') - ->willReturn([]); + ->willReturn($configurableProductLinks); $this->optionRepository->expects(static::once()) ->method('getList') @@ -133,7 +134,10 @@ public function testExecuteWithEmptyExtensionAttributes() public function testExecute() { $sku = 'config-1'; - $id = 25; + $idOld = 25; + $idNew = 26; + $attributeIdOld = 11; + $attributeIdNew = 22; $configurableProductLinks = [1, 2, 3]; $product = $this->getMockBuilder(Product::class) @@ -143,7 +147,7 @@ public function testExecute() $product->expects(static::once()) ->method('getTypeId') ->willReturn(ConfigurableModel::TYPE_CODE); - $product->expects(static::exactly(3)) + $product->expects(static::exactly(4)) ->method('getSku') ->willReturn($sku); @@ -156,30 +160,36 @@ public function testExecute() ->method('getExtensionAttributes') ->willReturn($extensionAttributes); - $attribute = $this->getMockBuilder(Attribute::class) + $attributeNew = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['getAttributeId', 'loadByProductAndAttribute', 'setId', 'getId']) ->getMock(); - $this->processSaveOptions($attribute, $sku, $id); - - $option = $this->getMockForAbstractClass(OptionInterface::class); - $option->expects(static::once()) + $attributeNew->expects(static::atLeastOnce()) + ->method('getAttributeId') + ->willReturn($attributeIdNew); + $this->processSaveOptions($attributeNew, $sku, $idNew); + + $optionOld = $this->getMockForAbstractClass(OptionInterface::class); + $optionOld->expects(static::atLeastOnce()) + ->method('getAttributeId') + ->willReturn($attributeIdOld); + $optionOld->expects(static::atLeastOnce()) ->method('getId') - ->willReturn($id); + ->willReturn($idOld); - $list = [$option]; - $this->optionRepository->expects(static::once()) + $list = [$optionOld]; + $this->optionRepository->expects(static::atLeastOnce()) ->method('getList') ->with($sku) ->willReturn($list); $this->optionRepository->expects(static::once()) ->method('deleteById') - ->with($sku, $id); + ->with($sku, $idOld); $configurableAttributes = [ - $attribute + $attributeNew ]; - $extensionAttributes->expects(static::exactly(2)) + $extensionAttributes->expects(static::atLeastOnce()) ->method('getConfigurableProductOptions') ->willReturn($configurableAttributes); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php index 64b9b3776442a..0fc650a4113c6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php @@ -6,22 +6,47 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class PriceTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */ + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var ConfigurablePrice + */ protected $model; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + /** + * @var ManagerInterface|MockObject + */ + private $eventManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->eventManagerMock = $this->createPartialMock( + ManagerInterface::class, + ['dispatch'] + ); $this->model = $this->objectManagerHelper->getObject( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class + ConfigurablePrice::class, + ['eventManager' => $this->eventManagerMock] ); } @@ -29,29 +54,29 @@ public function testGetFinalPrice() { $finalPrice = 10; $qty = 1; - $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) - ->getMock(); - $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class) + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getProduct']) + ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice']) ->getMock(); - $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) + /** @var PriceInfoBase|MockObject $priceInfo */ + $priceInfo = $this->getMockBuilder(PriceInfoBase::class) ->disableOriginalConstructor() ->setMethods(['getPrice']) ->getMock(); - $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + /** @var PriceInterface|MockObject $price */ + $price = $this->getMockBuilder(PriceInterface::class) ->disableOriginalConstructor() ->getMock(); - $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) + /** @var AmountInterface|MockObject $amount */ + $amount = $this->getMockBuilder(AmountInterface::class) ->disableOriginalConstructor() ->getMock(); $configurableProduct->expects($this->any()) ->method('getCustomOption') ->willReturnMap([['simple_product', false], ['option_ids', false]]); - $customOption->expects($this->never())->method('getProduct'); $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); $price->expects($this->once())->method('getAmount')->willReturn($amount); @@ -60,4 +85,60 @@ public function testGetFinalPrice() $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); } + + public function testGetFinalPriceWithSimpleProduct() + { + $finalPrice = 10; + $qty = 1; + $customerGroupId = 1; + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->createPartialMock( + Product::class, + ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId'] + ); + /** @var Option|MockObject $customOption */ + $customOption = $this->createPartialMock( + Option::class, + ['getProduct'] + ); + /** @var Product|MockObject $simpleProduct */ + $simpleProduct = $this->createPartialMock( + Product::class, + ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption'] + ); + + $configurableProduct->method('getCustomOption') + ->willReturnMap([ + ['simple_product', $customOption], + ['option_ids', false] + ]); + $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId); + $configurableProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $customOption->method('getProduct')->willReturn($simpleProduct); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setCustomerGroupId') + ->with($customerGroupId) + ->willReturnSelf(); + $simpleProduct->method('getPrice')->willReturn($finalPrice); + $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice); + $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]); + + $this->assertEquals( + $finalPrice, + $this->model->getFinalPrice($qty, $configurableProduct), + 'The final price calculation is wrong' + ); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php new file mode 100644 index 0000000000000..6d7067666989c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable\Variations; + +use PHPUnit\Framework\TestCase; + +class PricesTest extends TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $localeFormatMock; + + /** + * @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices + */ + private $model; + + protected function setUp() + { + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\Format::class); + $this->model = new \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices( + $this->localeFormatMock + ); + } + + public function testGetFormattedPrices() + { + $expected = [ + 'oldPrice' => [ + 'amount' => 500 + ], + 'basePrice' => [ + 'amount' => 1000 + ], + 'finalPrice' => [ + 'amount' => 500 + ] + ]; + $priceInfoMock = $this->createMock(\Magento\Framework\Pricing\PriceInfo\Base::class); + $priceMock = $this->createMock(\Magento\Framework\Pricing\Price\PriceInterface::class); + $priceInfoMock->expects($this->atLeastOnce())->method('getPrice')->willReturn($priceMock); + + $amountMock = $this->createMock(\Magento\Framework\Pricing\Amount\AmountInterface::class); + $amountMock->expects($this->atLeastOnce())->method('getValue')->willReturn(500); + $amountMock->expects($this->atLeastOnce())->method('getBaseAmount')->willReturn(1000); + $priceMock->expects($this->atLeastOnce())->method('getAmount')->willReturn($amountMock); + + $this->localeFormatMock->expects($this->atLeastOnce()) + ->method('getNumber') + ->withConsecutive([500], [1000], [500]) + ->will($this->onConsecutiveCalls(500, 1000, 500)); + + $this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock)); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 6ffdede34d04c..c351d12fa813d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -8,26 +8,28 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Config; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\EntityManager\EntityMetadata; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Customer\Model\Session; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory + as ProductCollectionFactory; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory; +use Magento\Customer\Model\Session; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; -use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Framework\EntityManager\MetadataPool; /** * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) - * @codingStandardsIgnoreFile */ class ConfigurableTest extends \PHPUnit\Framework\TestCase { @@ -154,8 +156,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productCollectionFactory = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory::class) + $this->productCollectionFactory = $this->getMockBuilder(ProductCollectionFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -197,11 +198,6 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->productFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterfaceFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->salableProcessor = $this->createMock(SalableProcessor::class); $this->model = $this->objectHelper->getObject( @@ -286,8 +282,7 @@ public function testSave() $product->expects($this->atLeastOnce()) ->method('getData') ->willReturnMap($dataMap); - $attribute = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class) + $attribute = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['addData', 'setStoreId', 'setProductId', 'save', '__wakeup', '__sleep']) ->getMock(); @@ -384,7 +379,7 @@ public function testGetUsedProducts() ['_cache_instance_used_product_attributes', null, []] ] ); - + $this->catalogConfig->expects($this->any())->method('getProductAttributes')->willReturn([]); $productCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); $productCollection->expects($this->once())->method('setProductFilter')->willReturnSelf(); $productCollection->expects($this->atLeastOnce())->method('setFlag')->willReturnSelf(); @@ -469,8 +464,7 @@ public function testGetConfigurableAttributesAsArray($productStore) $eavAttribute->expects($this->once())->method('getSource')->willReturn($attributeSource); $eavAttribute->expects($this->atLeastOnce())->method('getStoreLabel')->willReturn('Store Label'); - $attribute = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class) + $attribute = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['getProductAttribute', '__wakeup', '__sleep']) ->getMock(); @@ -513,17 +507,34 @@ public function getConfigurableAttributesAsArrayDataProvider() ]; } + public function testGetConfigurableAttributesNewProduct() + { + $configurableAttributes = '_cache_instance_configurable_attributes'; + + /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->setMethods(['hasData', 'getId']) + ->disableOriginalConstructor() + ->getMock(); + + $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); + $product->expects($this->once())->method('getId')->willReturn(null); + + $this->assertEquals([], $this->model->getConfigurableAttributes($product)); + } + public function testGetConfigurableAttributes() { $configurableAttributes = '_cache_instance_configurable_attributes'; /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['getData', 'hasData', 'setData']) + ->setMethods(['getData', 'hasData', 'setData', 'getId']) ->disableOriginalConstructor() ->getMock(); $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); + $product->expects($this->once())->method('getId')->willReturn(1); $attributeCollection = $this->getMockBuilder(Collection::class) ->setMethods(['setProductFilter', 'orderByPosition', 'load']) @@ -579,8 +590,7 @@ public function testHasOptionsConfigurableAttribute() ->setMethods(['__wakeup', 'getAttributeCode', 'getOptions', 'hasData', 'getData']) ->disableOriginalConstructor() ->getMock(); - $attributeMock = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class) + $attributeMock = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->getMock(); @@ -686,7 +696,7 @@ function ($value) { ->disableOriginalConstructor() ->getMock(); $usedAttributeMock = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class + Attribute::class ) ->setMethods(['getProductAttribute']) ->disableOriginalConstructor() diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/PluginTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/PluginTest.php index 8d54c465f7431..5aca8736db153 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/PluginTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/PluginTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type; /** diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php index 78707332f60c8..bdb3edd125725 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/VariationHandlerTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ConfigurableProduct\Test\Unit\Model\Product; use Magento\Catalog\Model\Product\Type; @@ -65,9 +63,14 @@ protected function setUp() $this->objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->productFactoryMock = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create']); $this->entityFactoryMock = $this->createPartialMock(\Magento\Eav\Model\EntityFactory::class, ['create']); - $this->attributeSetFactory = $this->createPartialMock(\Magento\Eav\Model\Entity\Attribute\SetFactory::class, ['create']); + $this->attributeSetFactory = $this->createPartialMock( + \Magento\Eav\Model\Entity\Attribute\SetFactory::class, + ['create'] + ); $this->stockConfiguration = $this->createMock(\Magento\CatalogInventory\Api\StockConfigurationInterface::class); - $this->configurableProduct = $this->createMock(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::class); + $this->configurableProduct = $this->createMock( + \Magento\ConfigurableProduct\Model\Product\Type\Configurable::class + ); $this->product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getMediaGallery']); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php index 235c16c9b556c..537288618b648 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php @@ -66,7 +66,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->select = $this->getMockBuilder(Select::class) - ->setMethods(['from', 'joinInner', 'joinLeft', 'where', 'columns']) + ->setMethods(['from', 'joinInner', 'joinLeft', 'where', 'columns', 'order']) ->disableOriginalConstructor() ->getMock(); $this->connectionMock->expects($this->atLeastOnce()) @@ -113,9 +113,27 @@ public function testGetSelect() $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); $this->select->expects($this->exactly(1))->method('columns')->willReturnSelf(); $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); - $this->select->expects($this->exactly(3))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(4))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); + $this->attributeResourceMock->expects($this->exactly(9)) + ->method('getTable') + ->will( + $this->returnValueMap( + [ + ['catalog_product_super_attribute', 'catalog_product_super_attribute value'], + ['catalog_product_entity', 'catalog_product_entity value'], + ['catalog_product_super_link', 'catalog_product_super_link value'], + ['eav_attribute', 'eav_attribute value'], + ['catalog_product_entity', 'catalog_product_entity value'], + ['catalog_product_super_attribute_label', 'catalog_product_super_attribute_label value'], + ['eav_attribute_option', 'eav_attribute_option value'], + ['eav_attribute_option_value', 'eav_attribute_option_value value'] + ] + ) + ); + $this->abstractAttributeMock->expects($this->atLeastOnce()) ->method('getAttributeId') ->willReturn('getAttributeId value'); @@ -139,9 +157,26 @@ public function testGetSelectWithBackendModel() $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); $this->select->expects($this->exactly(0))->method('columns')->willReturnSelf(); $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); - $this->select->expects($this->exactly(1))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(2))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); + $this->attributeResourceMock->expects($this->exactly(7)) + ->method('getTable') + ->will( + $this->returnValueMap( + [ + ['catalog_product_super_attribute', 'catalog_product_super_attribute value'], + ['catalog_product_entity', 'catalog_product_entity value'], + ['catalog_product_super_link', 'catalog_product_super_link value'], + ['eav_attribute', 'eav_attribute value'], + ['catalog_product_entity', 'catalog_product_entity value'], + ['catalog_product_super_attribute_label', 'catalog_product_super_attribute_label value'], + ['eav_attribute_option', 'eav_attribute_option value'] + ] + ) + ); + $this->abstractAttributeMock->expects($this->atLeastOnce()) ->method('getAttributeId') ->willReturn('getAttributeId value'); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/Configurable/AttributeTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/Configurable/AttributeTest.php index a6c7f00c2dfbe..e7b033ff84fd0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/Configurable/AttributeTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/Configurable/AttributeTest.php @@ -53,7 +53,7 @@ protected function setUp() ); } - public function testSaveLabel() + public function testSaveNewLabel() { $attributeId = 4354; @@ -70,7 +70,7 @@ public function testSaveLabel() ] )->willReturn(0); - $this->connection->expects($this->once())->method('insertOnDuplicate')->with( + $this->connection->expects($this->once())->method('insert')->with( 'catalog_product_super_attribute_label', [ 'product_super_attribute_id' => $attributeId, @@ -79,12 +79,48 @@ public function testSaveLabel() 'value' => 'test', ] ); - $attributeMode = $this->getMockBuilder(AttributeModel::class)->setMethods( + $attributeMock = $this->getMockBuilder(AttributeModel::class)->setMethods( ['getId', 'getUseDefault', 'getLabel'] )->disableOriginalConstructor()->getMock(); - $attributeMode->expects($this->any())->method('getId')->willReturn($attributeId); - $attributeMode->expects($this->any())->method('getUseDefault')->willReturn(0); - $attributeMode->expects($this->any())->method('getLabel')->willReturn('test'); - $this->assertEquals($this->attribute, $this->attribute->saveLabel($attributeMode)); + $attributeMock->expects($this->atLeastOnce())->method('getId')->willReturn($attributeId); + $attributeMock->expects($this->atLeastOnce())->method('getUseDefault')->willReturn(0); + $attributeMock->expects($this->atLeastOnce())->method('getLabel')->willReturn('test'); + $this->assertEquals($this->attribute, $this->attribute->saveLabel($attributeMock)); + } + + public function testSaveExistingLabel() + { + $attributeId = 4354; + + $select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + $this->connection->expects($this->once())->method('select')->willReturn($select); + $select->expects($this->once())->method('from')->willReturnSelf(); + $select->expects($this->at(1))->method('where')->willReturnSelf(); + $select->expects($this->at(2))->method('where')->willReturnSelf(); + $this->connection->expects($this->once())->method('fetchOne')->with( + $select, + [ + 'product_super_attribute_id' => $attributeId, + 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + ] + )->willReturn(1); + + $this->connection->expects($this->once())->method('insertOnDuplicate')->with( + 'catalog_product_super_attribute_label', + [ + 'product_super_attribute_id' => $attributeId, + 'use_default' => 0, + 'store_id' => 1, + 'value' => 'test', + ] + ); + $attributeMock = $this->getMockBuilder(AttributeModel::class)->setMethods( + ['getId', 'getUseDefault', 'getLabel', 'getStoreId'] + )->disableOriginalConstructor()->getMock(); + $attributeMock->expects($this->atLeastOnce())->method('getId')->willReturn($attributeId); + $attributeMock->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + $attributeMock->expects($this->atLeastOnce())->method('getUseDefault')->willReturn(0); + $attributeMock->expects($this->atLeastOnce())->method('getLabel')->willReturn('test'); + $this->assertEquals($this->attribute, $this->attribute->saveLabel($attributeMock)); } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/ConfigurableTest.php index 5a494d1c7a19b..cda9c300cd1d9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Product/Type/ConfigurableTest.php @@ -142,19 +142,48 @@ public function testSaveProducts() $this->optionProvider->expects($this->once()) ->method('getProductEntityLinkField') ->willReturnSelf(); - $this->connectionMock->expects($this->once()) - ->method('insertOnDuplicate') - ->willReturnSelf(); - $this->resource->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); $this->resource->expects($this->any())->method('getTableName')->willReturn('table name'); - $statement = $this->getMockBuilder(\Zend_Db_Statement::class)->disableOriginalConstructor()->getMock(); - $statement->method('fetchAll')->willReturn([1]); + $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->setMethods(['from', 'where']) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->exactly(1))->method('from')->willReturnSelf(); + $select->expects($this->exactly(1))->method('where')->willReturnSelf(); + + $this->connectionMock->expects($this->atLeastOnce()) + ->method('select') + ->willReturn($select); + + $existingProductIds = [1, 2]; + $this->connectionMock->expects($this->once()) + ->method('fetchCol') + ->with($select) + ->willReturn($existingProductIds); + + $this->connectionMock->expects($this->once()) + ->method('insertMultiple') + ->with( + 'table name', + [ + ['product_id' => 3, 'parent_id' => 3], + ['product_id' => 4, 'parent_id' => 3], + ] + ) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('delete') + ->with( + 'table name', + ['parent_id = ?' => 3, 'product_id IN (?)' => [1]] + ) + ->willReturnSelf(); $this->assertSame( $this->configurable, - $this->configurable->saveProducts($this->product, [1, 2, 3]) + $this->configurable->saveProducts($this->product, [2, 3, 4]) ); } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/SuggestedAttributeListTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/SuggestedAttributeListTest.php index 95c12fe570708..4a51a8b3d191d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/SuggestedAttributeListTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/SuggestedAttributeListTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ConfigurableProduct\Test\Unit\Model; class SuggestedAttributeListTest extends \PHPUnit\Framework\TestCase @@ -42,9 +40,13 @@ class SuggestedAttributeListTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->configurableAttributeHandler = $this->createMock(\Magento\ConfigurableProduct\Model\ConfigurableAttributeHandler::class); + $this->configurableAttributeHandler = $this->createMock( + \Magento\ConfigurableProduct\Model\ConfigurableAttributeHandler::class + ); $this->resourceHelperMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Helper::class); - $this->collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class); + $this->collectionMock = $this->createMock( + \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class + ); $this->resourceHelperMock->expects( $this->once() )->method( @@ -73,7 +75,10 @@ protected function setUp() $this->returnValueMap($valueMap) ); $methods = ['getId', 'getFrontendLabel', 'getAttributeCode', 'getSource']; - $this->attributeMock = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, $methods); + $this->attributeMock = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, + $methods + ); $this->collectionMock->expects( $this->once() )->method( diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php index 3bad81126f510..114fb7168ab33 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Observer/HideUnsupportedAttributeTypesTest.php @@ -59,6 +59,12 @@ private function createTarget(\PHPUnit_Framework_MockObject_MockObject $request, ); } + /** + * @param $popup + * @param string $productTab + * + * @return MockObject + */ private function createRequestMock($popup, $productTab = 'variations') { $request = $this->getMockBuilder(RequestInterface::class) @@ -107,6 +113,9 @@ public function testExecuteWithDefaultTypes(array $supportedTypes, array $origin $this->assertEquals(null, $target->execute($event)); } + /** + * @return array + */ public function executeDataProvider() { return [ @@ -143,11 +152,23 @@ public function executeDataProvider() ]; } + /** + * @param $value + * @param $label + * + * @return array + */ private function createFrontendInputValue($value, $label) { return ['value' => $value, 'label' => $label]; } + /** + * @param array $originalValues + * @param array $expectedValues + * + * @return MockObject + */ private function createForm(array $originalValues = [], array $expectedValues = []) { $form = $this->getMockBuilder(\Magento\Framework\Data\Form::class) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php new file mode 100644 index 0000000000000..71bd6fd83c24d --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Catalog\Model\Product\Pricing\Renderer; + +use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; +use Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer\SalableResolver as SalableResolverPlugin; +use Magento\Framework\Pricing\SaleableInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class SalableResolverTest + */ +class SalableResolverTest extends TestCase +{ + /** + * @var TypeConfigurable|MockObject + */ + private $typeConfigurable; + + /** + * @var SalableResolverPlugin + */ + private $salableResolver; + + protected function setUp() + { + $this->typeConfigurable = $this->createMock(TypeConfigurable::class); + $this->salableResolver = new SalableResolverPlugin($this->typeConfigurable); + } + + /** + * @param SaleableInterface|MockObject $salableItem + * @param bool $isSalable + * @param bool $typeIsSalable + * @param bool $expectedResult + * @return void + * @dataProvider afterIsSalableDataProvider + */ + public function testAfterIsSalable($salableItem, bool $isSalable, bool $typeIsSalable, bool $expectedResult) + { + $salableResolver = $this->createMock(SalableResolver::class); + + $this->typeConfigurable->method('isSalable') + ->willReturn($typeIsSalable); + + $result = $this->salableResolver->afterIsSalable($salableResolver, $isSalable, $salableItem); + $this->assertEquals($expectedResult, $result); + } + + /** + * Data provider for testAfterIsSalable + * + * @return array + */ + public function afterIsSalableDataProvider(): array + { + $simpleSalableItem = $this->createMock(SaleableInterface::class); + $simpleSalableItem->method('getTypeId') + ->willReturn('simple'); + + $configurableSalableItem = $this->createMock(SaleableInterface::class); + $configurableSalableItem->method('getTypeId') + ->willReturn('configurable'); + + return [ + [ + $simpleSalableItem, + true, + false, + true, + ], + [ + $configurableSalableItem, + true, + false, + false, + ], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImagesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImagesTest.php new file mode 100644 index 0000000000000..94c00c6e22d86 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Controller/Adminhtml/Product/Initialization/Helper/CleanConfigurationTmpImagesTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Controller\Adminhtml\Product\Initialization\Helper; + +use Magento\ConfigurableProduct\Plugin\Controller\Adminhtml\Product\Initialization\Helper\CleanConfigurationTmpImages; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\App\RequestInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper as ProductInitializationHelper; +use Magento\Catalog\Model\Product; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\MediaStorage\Helper\File\Storage\Database; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class for testing cleaning configuration tmp images + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CleanConfigurationTmpImagesTest extends TestCase +{ + /** + * @var CleanConfigurationTmpImages + */ + private $cleanConfigurationTmpImages; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Config|MockObject + */ + protected $mediaConfig; + + /** + * @var Write|MockObject + */ + private $mediaDirectory; + + /** + * @var ProductInitializationHelper|MockObject + */ + private $subjectMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $fileStorageDb = $this->createMock(Database::class); + $this->mediaConfig = $this->createMock(Config::class); + $this->mediaDirectory = $this->createMock(Write::class); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->getMockForAbstractClass(); + $this->subjectMock = $this->getMockBuilder(ProductInitializationHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->cleanConfigurationTmpImages = $this->objectManagerHelper->getObject( + CleanConfigurationTmpImages::class, + [ + 'request' => $this->requestMock, + 'fileStorageDb' => $fileStorageDb, + 'mediaConfig' => $this->mediaConfig, + 'mediaDirectory' => $this->mediaDirectory + ] + ); + } + + /** + * Prepare configurable matrix + * + * @return array + */ + private function getConfigurableMatrix() + { + return [ + [ + 'newProduct' => false, + 'id' => 'product2', + 'sku' => 'simple2_sku', + 'name' => 'simple2_name', + 'price' => '3.33', + 'configurable_attribute' => 'simple2_configurable_attribute', + 'was_changed' => true, + 'media_gallery' => [ + 'images' => [ + [ + 'file' => 'a/b/test_image.png.tmp', + ], + ], + ], + ], + ]; + } + + /** + * Test after initialize + * + * @return void + */ + public function testAfterInitialize() + { + $configurableMatrix = $this->getConfigurableMatrix(); + $this->requestMock->method('getParam') + ->willReturnMap( + [ + ['store', 0, 0], + ['configurable-matrix-serialized', "[]", json_encode($configurableMatrix)] + ] + ); + + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */ + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mediaConfig->expects($this->once())->method('getTmpMediaPath'); + $this->mediaDirectory->expects($this->once())->method('delete'); + + $this->assertSame( + $productMock, + $this->cleanConfigurationTmpImages->afterInitialize($this->subjectMock, $productMock) + ); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php new file mode 100644 index 0000000000000..54b7b71dd5ed8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php @@ -0,0 +1,226 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\Backend\Helper\Data; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product as ValidatorPlugin; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\Locale\Format; +use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Rule\Model\Condition\Context; +use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariable) + */ +class ProductTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var SalesRuleProduct + */ + private $validator; + + /** + * @var \Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product + */ + private $validatorPlugin; + + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->validator = $this->createValidator(); + $this->validatorPlugin = $this->objectManager->getObject(ValidatorPlugin::class); + } + + /** + * @return \Magento\SalesRule\Model\Rule\Condition\Product + */ + private function createValidator(): SalesRuleProduct + { + /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ + $contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Data|\PHPUnit_Framework_MockObject_MockObject $backendHelperMock */ + $backendHelperMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Config|\PHPUnit_Framework_MockObject_MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductFactory|\PHPUnit_Framework_MockObject_MockObject $productFactoryMock */ + $productFactoryMock = $this->getMockBuilder(ProductFactory::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $productRepositoryMock */ + $productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->getMockForAbstractClass(); + $attributeLoaderInterfaceMock = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributesByCode']) + ->getMock(); + $attributeLoaderInterfaceMock + ->expects($this->any()) + ->method('getAttributesByCode') + ->willReturn([]); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */ + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['loadAllAttributes', 'getConnection', 'getTable']) + ->getMock(); + $productMock->expects($this->any()) + ->method('loadAllAttributes') + ->willReturn($attributeLoaderInterfaceMock); + /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ + $collectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject $formatMock */ + $formatMock = new Format( + $this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock() + ); + + return new SalesRuleProduct( + $contextMock, + $backendHelperMock, + $configMock, + $productFactoryMock, + $productRepositoryMock, + $productMock, + $collectionMock, + $formatMock + ); + } + + public function testChildIsUsedForValidation() + { + $configurableProductMock = $this->createProductMock(); + $configurableProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); + $configurableProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(false); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct', 'getChildren']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($configurableProductMock); + + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + $childItem = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMockForAbstractClass(); + $childItem->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $item->expects($this->any()) + ->method('getChildren') + ->willReturn([$childItem]); + $item->expects($this->once()) + ->method('setProduct') + ->with($this->identicalTo($simpleProductMock)); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } + + /** + * @return Product|\PHPUnit_Framework_MockObject_MockObject + */ + private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject + { + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getAttribute', + 'getId', + 'setQuoteItemQty', + 'setQuoteItemPrice', + 'getTypeId', + 'hasData', + ]) + ->getMock(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemQty') + ->willReturnSelf(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemPrice') + ->willReturnSelf(); + + return $productMock; + } + + public function testChildIsNotUsedForValidation() + { + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php new file mode 100644 index 0000000000000..1a5c6c0003bfa --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector as CommonTaxCollectorPlugin; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Test for CommonTaxCollector plugin + */ +class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CommonTaxCollectorPlugin + */ + private $commonTaxCollectorPlugin; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->commonTaxCollectorPlugin = $this->objectManager->getObject(CommonTaxCollectorPlugin::class); + } + + /** + * Test to apply Tax Class Id from child item for configurable product + */ + public function testAfterMapItem() + { + $childTaxClassId = 10; + + /** @var Product|MockObject $childProductMock */ + $childProductMock = $this->createPartialMock( + Product::class, + ['getTaxClassId'] + ); + $childProductMock->method('getTaxClassId')->willReturn($childTaxClassId); + /* @var AbstractItem|MockObject $quoteItemMock */ + $childQuoteItemMock = $this->createMock( + AbstractItem::class + ); + $childQuoteItemMock->method('getProduct')->willReturn($childProductMock); + + /** @var Product|MockObject $productMock */ + $productMock = $this->createPartialMock( + Product::class, + ['getTypeId'] + ); + $productMock->method('getTypeId')->willReturn(Configurable::TYPE_CODE); + /* @var AbstractItem|MockObject $quoteItemMock */ + $quoteItemMock = $this->createPartialMock( + AbstractItem::class, + ['getProduct', 'getHasChildren', 'getChildren', 'getQuote', 'getAddress', 'getOptionByCode'] + ); + $quoteItemMock->method('getProduct')->willReturn($productMock); + $quoteItemMock->method('getHasChildren')->willReturn(true); + $quoteItemMock->method('getChildren')->willReturn([$childQuoteItemMock]); + + /* @var TaxClassKeyInterface|MockObject $taxClassObjectMock */ + $taxClassObjectMock = $this->createMock(TaxClassKeyInterface::class); + $taxClassObjectMock->expects($this->once())->method('setValue')->with($childTaxClassId); + + /* @var QuoteDetailsItemInterface|MockObject $quoteDetailsItemMock */ + $quoteDetailsItemMock = $this->createMock(QuoteDetailsItemInterface::class); + $quoteDetailsItemMock->method('getTaxClassKey')->willReturn($taxClassObjectMock); + + $this->commonTaxCollectorPlugin->afterMapItem( + $this->createMock(CommonTaxCollector::class), + $quoteDetailsItemMock, + $this->createMock(QuoteDetailsItemInterfaceFactory::class), + $quoteItemMock + ); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php index 99c31420473f5..189730e18080c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php @@ -55,24 +55,31 @@ protected function setUp() * situation: one product is supplying the price, which could be a price of zero (0) * * @dataProvider resolvePriceDataProvider + * + * @param $variantPrices + * @param $expectedPrice */ - public function testResolvePrice($expectedValue) + public function testResolvePrice($variantPrices, $expectedPrice) { - $price = $expectedValue; - $product = $this->getMockBuilder( \Magento\Catalog\Model\Product::class )->disableOriginalConstructor()->getMock(); $product->expects($this->never())->method('getSku'); - $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([$product]); - $this->priceResolver->expects($this->once()) + $products = array_map(function () { + return $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + }, $variantPrices); + + $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn($products); + $this->priceResolver ->method('resolvePrice') - ->with($product) - ->willReturn($price); + ->willReturnOnConsecutiveCalls(...$variantPrices); - $this->assertEquals($expectedValue, $this->resolver->resolvePrice($product)); + $actualPrice = $this->resolver->resolvePrice($product); + self::assertSame($expectedPrice, $actualPrice); } /** @@ -81,8 +88,40 @@ public function testResolvePrice($expectedValue) public function resolvePriceDataProvider() { return [ - 'price of zero' => [0.00], - 'price of five' => [5], + 'Single variant at price 0.00 (float), should return 0.00 (float)' => [ + $variantPrices = [ + 0.00, + ], + $expectedPrice = 0.00, + ], + 'Single variant at price 5 (integer), should return 5.00 (float)' => [ + $variantPrices = [ + 5, + ], + $expectedPrice = 5.00, + ], + 'Single variants at price null (null), should return 0.00 (float)' => [ + $variantPrices = [ + null, + ], + $expectedPrice = 0.00, + ], + 'Multiple variants at price 0, 10, 20, should return 0.00 (float)' => [ + $variantPrices = [ + 0, + 10, + 20, + ], + $expectedPrice = 0.00, + ], + 'Multiple variants at price 10, 0, 20, should return 0.00 (float)' => [ + $variantPrices = [ + 10, + 0, + 20, + ], + $expectedPrice = 0.00, + ], ]; } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/LowestPriceOptionsProviderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/LowestPriceOptionsProviderTest.php index ceeb242a750a2..7c83645a9fda3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/LowestPriceOptionsProviderTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/LowestPriceOptionsProviderTest.php @@ -9,6 +9,9 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; class LowestPriceOptionsProviderTest extends \PHPUnit\Framework\TestCase { @@ -42,6 +45,16 @@ class LowestPriceOptionsProviderTest extends \PHPUnit\Framework\TestCase */ private $productCollection; + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + protected function setUp() { $this->connection = $this @@ -68,6 +81,11 @@ protected function setUp() ->setMethods(['create']) ->getMock(); $this->collectionFactory->expects($this->once())->method('create')->willReturn($this->productCollection); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( @@ -76,6 +94,7 @@ protected function setUp() 'resourceConnection' => $this->resourceConnection, 'linkedProductSelectBuilder' => $this->linkedProductSelectBuilder, 'collectionFactory' => $this->collectionFactory, + 'storeManager' => $this->storeManagerMock, ] ); } @@ -94,6 +113,13 @@ public function testGetProducts() ->willReturnSelf(); $this->productCollection->expects($this->once())->method('addIdFilter')->willReturnSelf(); $this->productCollection->expects($this->once())->method('getItems')->willReturn($linkedProducts); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->with(Store::DEFAULT_STORE_ID) + ->willReturn($this->storeMock); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(Store::DEFAULT_STORE_ID); $this->assertEquals($linkedProducts, $this->model->getProducts($product)); } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php index e2e9fe9b2b1f9..535b75be0912b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CustomOptionsTest.php @@ -8,8 +8,19 @@ use Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier\AbstractModifierTest; use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\CustomOptions as CustomOptionsModifier; +/** + * Class for testing custom options in configurable product. + */ class CustomOptionsTest extends AbstractModifierTest { + protected function setUp() + { + parent::setUp(); + $this->arrayManagerMock->expects($this->any()) + ->method('merge') + ->willReturnArgument(1); + } + /** * {@inheritdoc} */ diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php index 0e03dfe3cde51..44d0ca86d98d0 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php @@ -5,13 +5,14 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product\Attribute\Backend\Sku; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; -use Magento\Ui\Component\Form; use Magento\Ui\Component\DynamicRows; +use Magento\Ui\Component\Form; use Magento\Ui\Component\Modal; -use Magento\Framework\UrlInterface; -use Magento\Catalog\Model\Locator\LocatorInterface; /** * Data provider for Configurable panel @@ -89,7 +90,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -97,7 +98,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -196,7 +197,7 @@ public function modifyMeta(array $meta) 'autoRender' => false, 'componentType' => 'insertListing', 'component' => 'Magento_ConfigurableProduct/js' - .'/components/associated-product-insert-listing', + . '/components/associated-product-insert-listing', 'dataScope' => $this->associatedListingPrefix . static::ASSOCIATED_PRODUCT_LISTING, 'externalProvider' => $this->associatedListingPrefix @@ -327,14 +328,12 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'trigger', 'params' => ['active', true], ], [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'openModal', ], ], @@ -466,7 +465,17 @@ protected function getRows() [], ['dataScope' => 'product_link'] ), - 'sku_container' => $this->getColumn('sku', __('SKU')), + 'sku_container' => $this->getColumn( + 'sku', + __('SKU'), + [ + 'validation' => + [ + 'required-entry' => true, + 'max_text_length' => Sku::SKU_MAX_LENGTH, + ] + ] + ), 'price_container' => $this->getColumn( 'price', __('Price'), @@ -563,6 +572,7 @@ protected function getColumn( 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => $name, 'visibleIfCanEdit' => false, + 'labelVisible' => false, 'imports' => [ 'visible' => '!${$.provider}:${$.parentScope}.canEdit' ], @@ -580,7 +590,9 @@ protected function getColumn( 'formElement' => Container::NAME, 'component' => 'Magento_Ui/js/form/components/group', 'label' => $label, + 'showLabel' => false, 'dataScope' => '', + 'showLabel' => false ]; $container['children'] = [ $name . '_edit' => $fieldEdit, diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php index 9150215e2c41e..31266df9e8b4a 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php @@ -17,8 +17,12 @@ use Magento\Framework\Json\Helper\Data as JsonHelper; use Magento\Framework\Locale\CurrencyInterface; use Magento\Framework\UrlInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; /** + * Loads data for product configurations. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AssociatedProducts @@ -83,6 +87,11 @@ class AssociatedProducts */ protected $imageHelper; + /** + * @var Escaper + */ + private $escaper; + /** * @param LocatorInterface $locator * @param UrlInterface $urlBuilder @@ -93,6 +102,8 @@ class AssociatedProducts * @param CurrencyInterface $localeCurrency * @param JsonHelper $jsonHelper * @param ImageHelper $imageHelper + * @param Escaper $escaper + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( LocatorInterface $locator, @@ -103,7 +114,8 @@ public function __construct( VariationMatrix $variationMatrix, CurrencyInterface $localeCurrency, JsonHelper $jsonHelper, - ImageHelper $imageHelper + ImageHelper $imageHelper, + Escaper $escaper = null ) { $this->locator = $locator; $this->urlBuilder = $urlBuilder; @@ -114,6 +126,7 @@ public function __construct( $this->localeCurrency = $localeCurrency; $this->jsonHelper = $jsonHelper; $this->imageHelper = $imageHelper; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); } /** @@ -202,6 +215,7 @@ public function getConfigurableAttributesData() 'code' => $attribute['code'], 'label' => $attribute['label'], 'position' => $attribute['position'], + '__disableTmpl' => true ]; foreach ($attribute['chosen'] as $chosenOption) { @@ -220,6 +234,7 @@ public function getConfigurableAttributesData() * * @return void * @throws \Zend_Currency_Exception + * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function prepareVariations() { @@ -250,6 +265,7 @@ protected function prepareVariations() 'id' => $attribute->getAttributeId(), 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], 'chosen' => [], + '__disableTmpl' => true ]; foreach ($attribute->getOptions() as $option) { if (!empty($option->getValue())) { @@ -259,6 +275,7 @@ protected function prepareVariations() 'id' => $option->getValue(), 'label' => $option->getLabel(), 'value' => $option->getValue(), + '__disableTmpl' => true ]; } } @@ -270,6 +287,7 @@ protected function prepareVariations() 'id' => $optionId, 'label' => $variation[$attribute->getId()]['label'], 'value' => $optionId, + '__disableTmpl' => true ]; $variationOptions[] = $variationOption; $attributes[$attribute->getAttributeId()]['chosen'][$optionId] = $variationOption; @@ -280,9 +298,9 @@ protected function prepareVariations() 'product_link' => '<a href="' . $this->urlBuilder->getUrl( 'catalog/product/edit', ['id' => $product->getId()] - ) . '" target="_blank">' . $product->getName() . '</a>', - 'sku' => $product->getSku(), - 'name' => $product->getName(), + ) . '" target="_blank">' . $this->escaper->escapeHtml($product->getName()) . '</a>', + 'sku' => $this->escaper->escapeHtml($product->getSku()), + 'name' => $this->escaper->escapeHtml($product->getName()), 'qty' => $this->getProductStockQty($product), 'price' => $price, 'price_string' => $currency->toCurrency(sprintf("%f", $price)), @@ -295,6 +313,7 @@ protected function prepareVariations() 'newProduct' => 0, 'attributes' => $this->getTextAttributes($variationOptions), 'thumbnail_image' => $this->imageHelper->init($product, 'product_thumbnail_image')->getUrl(), + '__disableTmpl' => true ]; $productIds[] = $product->getId(); } @@ -305,6 +324,7 @@ protected function prepareVariations() $this->productIds = $productIds; $this->productAttributes = array_values($attributes); } + //phpcs: enable /** * Get JSON string that contains attribute code and value diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 04725604a0859..55af76bc19906 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-configurable-product", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-catalog-inventory": "100.2.*", @@ -19,12 +19,14 @@ "suggest": { "magento/module-webapi": "100.2.*", "magento/module-sales": "101.0.*", + "magento/module-sales-rule": "101.0.*", "magento/module-product-video": "100.2.*", "magento/module-configurable-sample-data": "Sample Data version:100.2.*", - "magento/module-product-links-sample-data": "Sample Data version:100.2.*" + "magento/module-product-links-sample-data": "Sample Data version:100.2.*", + "magento/module-tax": "100.2.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml index a401dceaf5b99..fb6e55bf39d73 100644 --- a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml @@ -9,6 +9,7 @@ <type name="Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper"> <plugin name="configurable" type="Magento\ConfigurableProduct\Controller\Adminhtml\Product\Initialization\Helper\Plugin\Configurable" sortOrder="50" /> <plugin name="updateConfigurations" type="Magento\ConfigurableProduct\Controller\Adminhtml\Product\Initialization\Helper\Plugin\UpdateConfigurations" sortOrder="60" /> + <plugin name="cleanConfigurationTmpImages" type="Magento\ConfigurableProduct\Plugin\Controller\Adminhtml\Product\Initialization\Helper\CleanConfigurationTmpImages" sortOrder="999" /> </type> <type name="Magento\Catalog\Controller\Adminhtml\Product\Builder"> <plugin name="configurable" type="Magento\ConfigurableProduct\Controller\Adminhtml\Product\Builder\Plugin" sortOrder="50" /> diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 3f04081eaf645..fa9ed70cfb2cd 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -43,6 +43,13 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\Sales\Model\ResourceModel\Report\Bestsellers"> <arguments> <argument name="ignoredProductTypes" xsi:type="array"> @@ -125,6 +132,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="configurable" xsi:type="object">Magento\ConfigurableProduct\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <virtualType name="ConfigurableFinalPriceResolver" type="Magento\ConfigurableProduct\Pricing\Price\ConfigurablePriceResolver"> <arguments> <argument name="priceResolver" xsi:type="object">Magento\ConfigurableProduct\Pricing\Price\FinalPriceResolver</argument> @@ -168,13 +182,6 @@ </argument> </arguments> </type> - <type name="Magento\Framework\App\Cache\Tag\Strategy\Factory"> - <arguments> - <argument name="customStrategies" xsi:type="array"> - <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="object">\Magento\ConfigurableProduct\Model\Product\Cache\Tag\Configurable</item> - </argument> - </arguments> - </type> <type name="Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator"> <arguments> <argument name="estimators" xsi:type="array"> @@ -183,6 +190,14 @@ <argument name="batchSizeAdjusters" xsi:type="array"> <item name="configurable" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\CompositeProductBatchSizeAdjuster</item> </argument> + <!-- + real batch size will be smaller. + It depends on amount configurable product variations. + E.g for 100 variations real batch size will be 50000/100=500 + --> + <argument name="batchRowsCount" xsi:type="array"> + <item name="configurable" xsi:type="number">50000</item> + </argument> </arguments> </type> <type name="Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable"> @@ -196,6 +211,13 @@ <argument name="productIndexer" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Full</argument> </arguments> </type> + <virtualType name="LinkedProductSelectBuilderByIndexMinPrice" type="Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilderComposite"> + <arguments> + <argument name="linkedProductSelectBuilder" xsi:type="array"> + <item name="indexPrice" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\LinkedProductSelectBuilderByIndexPrice</item> + </argument> + </arguments> + </virtualType> <type name="Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProvider"> <arguments> <argument name="linkedProductSelectBuilder" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilder</argument> @@ -204,9 +226,40 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Product\LinkedProductSelectBuilder"> <arguments> <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\StockStatusBaseSelectProcessor</argument> + <argument name="linkedProductSelectBuilder" xsi:type="object">LinkedProductSelectBuilderByIndexMinPrice</argument> </arguments> </type> <type name="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver"> <plugin name="configurable" type="Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer\SalableResolver" /> </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\ProductIdentitiesExtender" /> + </type> + <type name="Magento\SalesRule\Model\Rule\Condition\Product"> + <plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" /> + </type> + <type name="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite"> + <arguments> + <argument name="itemResolvers" xsi:type="array"> + <item name="configurable" xsi:type="string">Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator"> + <arguments> + <argument name="productTypeChildrenValidationMap" xsi:type="array"> + <item name="configurable" xsi:type="boolean">false</item> + </argument> + </arguments> + </type> + <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> + <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> + </type> + <type name="Magento\Eav\Model\Entity\Attribute\Group"> + <arguments> + <argument name="reservedSystemNames" xsi:type="array"> + <item name="configurable" xsi:type="string">configurable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index 592b0292c98ab..bb830c36b929d 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -7,13 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Checkout\CustomerData\ItemPoolInterface"> - <arguments> - <argument name="itemMap" xsi:type="array"> - <item name="configurable" xsi:type="string">Magento\ConfigurableProduct\CustomerData\ConfigurableItem</item> - </argument> - </arguments> - </type> <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> diff --git a/app/code/Magento/ConfigurableProduct/etc/module.xml b/app/code/Magento/ConfigurableProduct/etc/module.xml index 188337fb0ed7c..b7d4c92aaa992 100644 --- a/app/code/Magento/ConfigurableProduct/etc/module.xml +++ b/app/code/Magento/ConfigurableProduct/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_ConfigurableProduct" setup_version="2.2.0"> + <module name="Magento_ConfigurableProduct" setup_version="2.2.2"> <sequence> <module name="Magento_Catalog"/> <module name="Magento_Msrp"/> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml index 110defd5248b9..9307da21e6659 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml @@ -8,7 +8,7 @@ <script> (function ($) { - var data = <?= /* @escapeNotVerified */ $block->getAttributesBlockJson() ?>; + var data = <?= /* @noEscape */ $block->getAttributesBlockJson() ?>; var set = data.set || {id: $('#attribute_set_id').val()}; if (data.tab == 'variations') { $('[data-role=product-variations-matrix]').trigger('add', data.attribute); diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml index 9c4612c972d96..5f49d5eb47442 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml @@ -5,8 +5,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <script> require([ diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183de..9c1cfdd0a1935 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -3,39 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - - ?> +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +?> <?php /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ ?> <?php $_product = $block->getProduct(); ?> <?php $_attributes = $block->decorateArray($block->getAllowAttributes()); ?> -<?php $_skipSaleableCheck = $this->helper('Magento\Catalog\Helper\Product')->getSkipSaleableCheck(); ?> -<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)):?> +<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> +<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)) :?> <fieldset id="catalog_product_composite_configure_fields_configurable" class="fieldset admin__fieldset"> <legend class="legend admin__legend"> - <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> + <span><?= $block->escapeHtml(__('Associated Products'))?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> - <label class="label admin__field-label"><?php - /* @escapeNotVerified */ echo $_attribute->getProductAttribute() - ->getStoreLabel($_product->getStoreId()); - ?></label> + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute) : ?> + <div class="field admin__field _required required"> + <label class="label admin__field-label"><?= + $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())) + ?></label> <div class="control admin__field-control <?php - if ($_attribute->getDecoratedIsLast()): - ?> last<?php + if ($_attribute->getDecoratedIsLast()) : + ?> last<?php endif; ?>"> - <select name="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" - id="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>" + <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" + id="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>" class="admin__control-select required-entry super-attribute-select"> - <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> + <option><?= $block->escapeHtml(__('Choose an Option...')) ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> @@ -44,7 +41,7 @@ require([ "Magento_Catalog/catalog/product/composite/configure" ], function(){ - var config = <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>; + var config = <?= /* @noEscape */ $block->getJsonConfig() ?>; if (window.productConfigure) { config.containerId = window.productConfigure.blockFormFields.id; if (window.productConfigure.restorePhase) { diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml index cc25474049190..44413c67ed5ba 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml @@ -4,18 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <h2 class="steps-wizard-title"><?= $block->escapeHtml( - __('Step 2: Attribute Values') - ); ?></h2> + __('Step 2: Attribute Values') + ); ?></h2> <div class="steps-wizard-info"> <span><?= $block->escapeHtml( - __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') - );?></span> + __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') + );?></span> </div> <div data-bind="foreach: attributes, sortableList: attributes"> @@ -41,24 +39,24 @@ data-bind="click: $parent.selectAllAttributes" title="<?= $block->escapeHtml(__('Select All')) ?>"> <span><?= $block->escapeHtml( - __('Select All') - ); ?></span> + __('Select All') + ); ?></span> </button> <button type="button" class="action-deselect-all action-tertiary" data-bind="click: $parent.deSelectAllAttributes" title="<?= $block->escapeHtml(__('Deselect All')) ?>"> <span><?= $block->escapeHtml( - __('Deselect All') - ); ?></span> + __('Deselect All') + ); ?></span> </button> <button type="button" class="action-remove-all action-tertiary" data-bind="click: $parent.removeAttribute.bind($parent)" title="<?= $block->escapeHtml(__('Remove Attribute')) ?>"> <span><?= $block->escapeHtml( - __('Remove Attribute') - ); ?></span> + __('Remove Attribute') + ); ?></span> </button> </div> </div> @@ -87,8 +85,8 @@ data-action="save" data-bind="click: $parents[1].saveOption.bind($parent)"> <span><?= $block->escapeHtml( - __('Save Option') - ); ?></span> + __('Save Option') + ); ?></span> </button> <button type="button" class="action-remove" @@ -96,8 +94,8 @@ data-action="remove" data-bind="click: $parents[1].removeOption.bind($parent)"> <span><?= $block->escapeHtml( - __('Remove Option') - ); ?></span> + __('Remove Option') + ); ?></span> </button> </div> </li> @@ -108,8 +106,8 @@ data-action="addOption" data-bind="click: $parent.createOption, visible: canCreateOption"> <span><?= $block->escapeHtml( - __('Create New Value') - ); ?></span> + __('Create New Value') + ); ?></span> </button> </div> </div> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml index 9d144b0f569e0..bee4a4ebe890e 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml @@ -3,24 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk */ ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'" data-role="bulk-step"> <h2 class="steps-wizard-title"><?= $block->escapeHtml(__('Step 3: Bulk Images, Price and Quantity')) ?></h2> <div class="steps-wizard-info"> - <?= /* @escapeNotVerified */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> + <?= /* @noEscape */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> </div> <div data-bind="with: sections().images" class="steps-wizard-section"> <div data-role="section"> <div class="steps-wizard-section-title"> - <span><?= $block->escapeHtml( - __('Images') - ); ?></span> + <span><?= $block->escapeHtml(__('Images')); ?></span> </div> <ul class="steps-wizard-section-list"> @@ -32,9 +28,7 @@ value="single" data-bind="checked:type"> <label for="apply-single-set-radio" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Apply single set of images to all SKUs') - ); ?></span> + <span><?= $block->escapeHtml(__('Apply single set of images to all SKUs')); ?></span> </label> </div> </li> @@ -71,9 +65,7 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml( - $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) - ) ?>" + data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>" > <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> @@ -92,8 +84,7 @@ </div> </div> - <?php foreach ($block->getImageTypes() as $typeData): - ?> + <?php foreach ($block->getImageTypes() as $typeData) : ?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" @@ -156,13 +147,9 @@ </div> <ul class="item-roles" data-role="roles-labels"> <?php - foreach ($block->getMediaAttributes() as $attribute): - ?> - <li data-role-code="<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" class="item-role item-role-<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>"> + foreach ($block->getMediaAttributes() as $attribute) :?> + <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= /* @noEscape */ $attribute->getFrontendLabel() ?> </li> <?php @@ -229,9 +216,7 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Role') - ); ?></span> + <span><?= $block->escapeHtml(__('Role')); ?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> @@ -243,13 +228,9 @@ <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" + value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" /> - <?= $block->escapeHtml( - $attribute->getFrontendLabel() - ); ?> + <?= $block->escapeHtml($attribute->getFrontendLabel()); ?> </label> </li> <?php @@ -261,24 +242,16 @@ <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Image Size') - ); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml( - __('{size}') - );?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> </div> <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Image Resolution') - ); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml( - __('{width}^{height} px') - );?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> </div> <div class="admin__field field-image-hide"> @@ -293,9 +266,7 @@ <% if (data.disabled == 1) { %>checked="checked"<% } %> /> <label for="hide-from-product-page" class="admin__field-label"> - <?= $block->escapeHtml( - __('Hide from Product Page') - ); ?> + <?= $block->escapeHtml(__('Hide from Product Page')); ?> </label> </div> </div> @@ -310,9 +281,7 @@ <fieldset class="admin__fieldset bulk-attribute-values"> <div class="admin__field _required"> <label class="admin__field-label" for="apply-images-attributes"> - <span><?= $block->escapeHtml( - __('Select attribute') - ); ?></span> + <span><?= $block->escapeHtml(__('Select attribute')); ?></span> </label> <div class="admin__field-control"> <select @@ -322,9 +291,7 @@ options: $parent.attributes, optionsText: 'label', value: attribute, - optionsCaption: '<?= $block->escapeHtml( - __("Select") - ); ?>' + optionsCaption: '<?= $block->escapeHtml(__("Select")); ?>' "> </select> </div> @@ -341,24 +308,18 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml( - $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) - ) ?>" + data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>" > <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> <div class="image-browse"> - <span><?= $block->escapeHtml( - __('Browse Files...') - ); ?></span> + <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> <input type="file" name="image" multiple="multiple" data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> </div> </div> <div class="product-image-wrapper"> - <p class="image-placeholder-text"><?= $block->escapeHtml( - __('Browse to find or drag image here') - ); ?></p> + <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')); ?></p> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> @@ -366,8 +327,7 @@ </div> </div> - <?php foreach ($block->getImageTypes() as $typeData): - ?> + <?php foreach ($block->getImageTypes() as $typeData) :?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" @@ -418,15 +378,11 @@ class="action-remove" data-role="delete-button" title="<?= $block->escapeHtml(__('Remove image')) ?>"> - <span><?= $block->escapeHtml( - __('Remove image') - ); ?></span> + <span><?= $block->escapeHtml(__('Remove image')); ?></span> </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml( - __('Hidden') - ); ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span></div> </div> <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> @@ -436,13 +392,10 @@ </div> <ul class="item-roles" data-role="roles-labels"> <?php - foreach ($block->getMediaAttributes() as $attribute): + foreach ($block->getMediaAttributes() as $attribute) : ?> - <li data-role-code="<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" class="item-role item-role-<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>"> + <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> </li> <?php @@ -492,9 +445,7 @@ <fieldset class="admin__fieldset fieldset-image-panel"> <div class="admin__field field-image-description"> <label class="admin__field-label" for="image-description"> - <span><?= $block->escapeHtml( - __('Alt Text') - );?></span> + <span><?= $block->escapeHtml(__('Alt Text'));?></span> </label> <div class="admin__field-control"> @@ -508,9 +459,7 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Role') - );?></span> + <span><?= $block->escapeHtml(__('Role'));?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> @@ -522,13 +471,9 @@ <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" + value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" /> - <?= $block->escapeHtml( - $attribute->getFrontendLabel() - ) ?> + <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> </label> </li> <?php @@ -540,24 +485,16 @@ <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Image Size') - ); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml( - __('{size}') - ); ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> </div> <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Image Resolution') - ); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml( - __('{width}^{height} px') - ); ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -572,9 +509,7 @@ <% if (data.disabled == 1) { %>checked="checked"<% } %> /> <label for="hide-from-product-page" class="admin__field-label"> - <?= $block->escapeHtml( - __('Hide from Product Page') - ); ?> + <?= $block->escapeHtml(__('Hide from Product Page')); ?> </label> </div> </div> @@ -593,9 +528,7 @@ <div data-bind="with: sections().price" class="steps-wizard-section"> <div data-role="section"> <div class="steps-wizard-section-title"> - <span><?= $block->escapeHtml( - __('Price') - ); ?></span> + <span><?= $block->escapeHtml(__('Price')); ?></span> </div> <ul class="steps-wizard-section-list"> <li> @@ -607,9 +540,7 @@ data-bind="checked:type" /> <label for="apply-single-price-radio" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Apply single price to all SKUs') - ); ?></span> + <span><?= $block->escapeHtml(__('Apply single price to all SKUs')); ?></span> </label> </div> </li> @@ -622,9 +553,7 @@ data-bind="checked:type" /> <label for="apply-unique-prices-radio" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Apply unique prices by attribute to each SKU') - ); ?></span> + <span><?= $block->escapeHtml(__('Apply unique prices by attribute to each SKU')); ?></span> </label> </div> </li> @@ -637,9 +566,7 @@ checked data-bind="checked:type" /> <label for="skip-pricing-radio" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Skip price at this time') - ); ?></span> + <span><?= $block->escapeHtml(__('Skip price at this time')); ?></span> </label> </div> </li> @@ -648,9 +575,7 @@ <fieldset class="admin__fieldset bulk-attribute-values" data-bind="visible: type() == 'single'"> <div class="admin__field _required"> <label for="apply-single-price-input" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Price') - ); ?></span> + <span><?= $block->escapeHtml(__('Price')); ?></span> </label> <div class="admin__field-control"> <div class="currency-addon"> @@ -667,9 +592,7 @@ <fieldset class="admin__fieldset bulk-attribute-values"> <div class="admin__field _required"> <label for="select-each-price" class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Select attribute') - ); ?></span> + <span><?= $block->escapeHtml(__('Select attribute')); ?></span> </label> <div class="admin__field-control"> <select id="select-each-price" class="admin__control-select" data-bind=" diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml index cfb742e80f719..c3dc614232201 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\SelectAttributes */ ?> <div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="select-attributes-step"> @@ -13,8 +11,8 @@ <?= /* @noEscape */ $block->getAddNewAttributeButton() ?> </div> <h2 class="steps-wizard-title"><?= $block->escapeHtml( - __('Step 1: Select Attributes') - ); ?></h2> + __('Step 1: Select Attributes') + ); ?></h2> <div class="selected-attributes" data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <?= $block->escapeHtml( __('Selected Attributes:') diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml index 2ded3aa1079a9..379e129b68c7e 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/summary.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Summary */ ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <h2 class="steps-wizard-title"><?= $block->escapeHtml( - __('Step 4: Summary') - ); ?></h2> + __('Step 4: Summary') + ); ?></h2> <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> <!-- ko if: gridNew().length --> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml index 07f4e39e43de6..c11a1adc19896 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml @@ -4,34 +4,32 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - - /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config */ +/** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config */ ?> -<div class="entry-edit form-inline" id="<?= /* @escapeNotVerified */ $block->getId() ?>" data-panel="product-variations"> +<div class="entry-edit form-inline" id="<?= $block->escapeHtmlAttr($block->getId()) ?>" data-panel="product-variations"> <div data-bind="scope: 'variation-steps-wizard'" class="product-create-configuration"> <div class="product-create-configuration-info"> <div class="note" data-role="product-create-configuration-info"> - <?= /* @escapeNotVerified */ __('Configurable products allow customers to choose options (Ex: shirt color). - You need to create a simple product for each configuration (Ex: a product for each color).');?> + <?= $block->escapeHtml(__('Configurable products allow customers to choose options (Ex: shirt color). + You need to create a simple product for each configuration (Ex: a product for each color).'));?> </div> </div> <div class="product-create-configuration-actions" data-action="product-create-configuration-buttons"> <div class="product-create-configuration-action"> <button type="button" data-action="open-steps-wizard" title="Create Product Configurations" class="action-secondary" data-bind="click: open"> - <span data-role="button-label" data-edit-label="<?= /* @escapeNotVerified */ __('Edit Configurations') ?>"> - <?= /* @escapeNotVerified */ $block->isHasVariations() + <span data-role="button-label" data-edit-label="<?= $block->escapeHtmlAttr(__('Edit Configurations')) ?>"> + <?= $block->escapeHtml($block->isHasVariations() ? __('Edit Configurations') - : __('Create Configurations') - ?> + : __('Create Configurations')) +?> </span> </button> </div> <div class="product-create-configuration-action" data-bind="scope: 'configurableProductGrid'"> <button class="action-tertiary action-menu-item" type="button" data-action="choose" data-bind="click: showManuallyGrid, visible: button"> - <?= /* @noEscape */ __('Add Products Manually') ?> + <?= $block->escapeHtml(__('Add Products Manually')) ?> </button> </div> </div> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml index 230e0fd14ccb6..71ae19d4a4813 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ ?> <?php @@ -17,7 +15,7 @@ $currencySymbol = $block->getCurrencySymbol(); <div id="product-variations-matrix" data-role="product-variations-matrix"> <div data-bind="scope: 'configurableVariations'"> <h3 class="hidden" data-bind="css: {hidden: !showVariations() }" class="title"> - <?= /* @escapeNotVerified */ __('Current Variations') ?> + <?= $block->escapeHtml(__('Current Variations')) ?> </h3> <script data-template-for="variation-image" type="text/x-magento-template"> @@ -47,22 +45,22 @@ $currencySymbol = $block->getCurrencySymbol(); <thead> <tr> <th class="data-grid-th data-grid-thumbnail-cell col-image" data-column="image"> - <?= /* @escapeNotVerified */ __('Image') ?> + <?= $block->escapeHtml(__('Image')) ?> </th> <th class="data-grid-th col-name" data-column="name"> - <?= /* @escapeNotVerified */ __('Name') ?> + <?= $block->escapeHtml(__('Name')) ?> </th> <th class="data-grid-th col-sku" data-column="sku"> - <?= /* @escapeNotVerified */ __('SKU') ?> + <?= $block->escapeHtml(__('SKU')) ?> </th> <th class="data-grid-th col-price" data-column="price"> - <?= /* @escapeNotVerified */ __('Price') ?> + <?= $block->escapeHtml(__('Price')) ?> </th> <th class="data-grid-th col-qty" data-column="qty"> - <?= /* @escapeNotVerified */ __('Quantity') ?> + <?= $block->escapeHtml(__('Quantity')) ?> </th> <th class="data-grid-th col-weight" data-column="weight"> - <?= /* @escapeNotVerified */ __('Weight') ?> + <?= $block->escapeHtml(__('Weight')) ?> </th> <!-- ko foreach: getAttributesOptions() --> <th data-bind="attr: {class:'data-grid-th col-' + $data.attribute_code}, @@ -70,7 +68,7 @@ $currencySymbol = $block->getCurrencySymbol(); </th> <!-- /ko --> <th class="data-grid-th"> - <?= /* @escapeNotVerified */ __('Actions') ?> + <?= $block->escapeHtml(__('Actions')) ?> </th> </tr> </thead> @@ -88,7 +86,7 @@ $currencySymbol = $block->getCurrencySymbol(); <input type="hidden" data-bind=" attr: {id: $parent.getRowId(variation, 'image'), name: $parent.getVariationRowName(variation, 'image')}"/> - <span><?= /* @escapeNotVerified */ __('Upload Image') ?></span> + <span><?= $block->escapeHtml(__('Upload Image')) ?></span> <input name="image" type="file" data-url="<?= $block->escapeHtml($block->getImageUploadUrl()) ?>" title="<?= $block->escapeHtml(__('Upload image')) ?>"/> @@ -102,11 +100,11 @@ $currencySymbol = $block->getCurrencySymbol(); <!-- /ko --> <button type="button" class="action toggle no-display" data-toggle="dropdown" data-mage-init='{"dropdown":{}}'> - <span><?= /* @escapeNotVerified */ __('Select') ?></span> + <span><?= $block->escapeHtml(__('Select')) ?></span> </button> <ul class="dropdown"> <li> - <a class="item" data-action="no-image"><?= /* @escapeNotVerified */ __('No Image') ?></a> + <a class="item" data-action="no-image"><?= $block->escapeHtml(__('No Image')) ?></a> </li> </ul> </div> @@ -208,7 +206,7 @@ $currencySymbol = $block->getCurrencySymbol(); " data-action="choose" href="#"> - <?= /* @escapeNotVerified */ __('Choose a different Product') ?> + <?= $block->escapeHtml(__('Choose a different Product')) ?> </a> </li> <li> @@ -219,7 +217,7 @@ $currencySymbol = $block->getCurrencySymbol(); </li> <li> <a class="action-menu-item" data-bind="click: $parent.removeProduct.bind($parent, $index())"> - <?= /* @escapeNotVerified */ __('Remove Product') ?> + <?= $block->escapeHtml(__('Remove Product')) ?> </a> </li> </ul> @@ -233,15 +231,14 @@ $currencySymbol = $block->getCurrencySymbol(); <!-- /ko --> </div> <div data-role="step-wizard-dialog" - data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= /* @escapeNotVerified */ __('Create Product Configurations') ?>", + data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= $block->escapeHtmlAttr(__('Create Product Configurations')) ?>", "buttons":[]}}' class="no-display"> - <?php - /* @escapeNotVerified */ echo $block->getVariationWizard([ + <?= /* @noEscape */ $block->getVariationWizard([ 'attributes' => $attributes, 'configurations' => $productMatrix ]); - ?> +?> </div> </div> @@ -252,8 +249,8 @@ $currencySymbol = $block->getCurrencySymbol(); "components": { "configurableVariations": { "component": "Magento_ConfigurableProduct/js/variations/variations", - "variations": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($productMatrix) ?>, - "productAttributes": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($attributes) ?>, + "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, + "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "configurableProductGrid": "configurableProductGrid" diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml index 2e38633218652..7b85efdbb73aa 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ $productMatrix = $block->getProductMatrix(); diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml index 3a23257cbbf9b..f009962bb97ff 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ ?> <?php @@ -48,9 +46,9 @@ $currencySymbol = $block->getCurrencySymbol(); "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() ?>.configurable_attribute_set_handler_modal", "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() ?>.configurable.configurable_products_button_set.create_configurable_products_button", "wizardModalButtonTitle": "<?= $block->escapeHtml(__('Edit Configurations')) ?>", - "productAttributes": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($attributes) ?>, + "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", - "variations": <?= /* @noEscape */ $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($productMatrix) ?>, + "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "attributeSetCreationUrl": "<?= /* @noEscape */ $block->getUrl('*/product_set/save') ?>" } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml index 6c993b243da23..6b30b3eba33b4 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/form.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\Framework\View\Element\Template */ ?> <div data-role="affected-attribute-set-selector" class="no-display affected-attribute-set"> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml index d70576d975ac3..6631a47e0a6a6 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ ?> <script> @@ -48,12 +46,12 @@ $form .modal({ - title: '<?= /* @escapeNotVerified */ __('Choose Affected Attribute Set') ?>', + title: '<?= $block->escapeJs(__('Choose Affected Attribute Set')) ?>', closed: function () { resetValidation(); }, buttons: [{ - text: '<?= /* @escapeNotVerified */ __('Confirm') ?>', + text: '<?= $block->escapeJs(__('Confirm')) ?>', attr: { 'data-action': 'confirm' }, @@ -77,12 +75,12 @@ $.ajax({ type: 'POST', - url: '<?= /* @escapeNotVerified */ $block->getAttributeSetCreationUrl() ?>', + url: '<?= $block->escapeUrl($block->getAttributeSetCreationUrl()) ?>', data: { gotoEdit: 1, attribute_set_name: $form.find('input[name=new-attribute-set-name]').val(), skeleton_set: $('#attribute_set_id').val(), - form_key: '<?= /* @escapeNotVerified */ $block->getFormKey() ?>', + form_key: '<?= $block->escapeJs($block->getFormKey()) ?>', return_session_messages_only: 1 }, dataType: 'json', @@ -101,8 +99,8 @@ return false; } },{ - text: '<?= /* @escapeNotVerified */ __('Cancel') ?>', - id: '<?= /* @escapeNotVerified */ $block->getJsId('close-button') ?>', + text: '<?= $block->escapeJs(__('Cancel')) ?>', + id: '<?= $block->escapeJs($block->getJsId('close-button')) ?>', 'class': 'action-close', click: function() { $form.modal('closeModal'); diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml index 4246d8f53a79c..e6cf1e9c6870d 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml @@ -3,16 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ ?> <script> require(["jquery","mage/mage","mage/backend/suggest"], function($){ - var options = <?php - /* @escapeNotVerified */ echo $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getSuggestWidgetOptions()) - ?>; + var options = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSuggestWidgetOptions()) ?>; $('#configurable-attribute-selector') .mage('suggest', options) .on('suggestselect', function (event, ui) { diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js index 01abce7696014..df800c9a64a39 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js @@ -6,8 +6,9 @@ define([ 'underscore', 'uiRegistry', - 'Magento_Ui/js/dynamic-rows/dynamic-rows' -], function (_, registry, dynamicRows) { + 'Magento_Ui/js/dynamic-rows/dynamic-rows', + 'jquery' +], function (_, registry, dynamicRows, $) { 'use strict'; return dynamicRows.extend({ @@ -217,6 +218,8 @@ define([ _.each(tmpData, function (row, index) { path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); + row.attributes = $('<i></i>').text(row.attributes).html(); + row.sku = $('<i></i>').text(row.sku).html(); this.source.set(path, row); }, this); @@ -376,8 +379,8 @@ define([ product = { 'id': row.productId, 'product_link': row.productUrl, - 'name': row.name, - 'sku': row.sku, + 'name': $('<i></i>').text(row.name).html(), + 'sku': $('<i></i>').text(row.sku).html(), 'status': row.status, 'price': row.price, 'price_currency': row.priceCurrency, diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 28e775b984b05..b2ef35546eea8 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js @@ -11,9 +11,6 @@ define([ return Abstract.extend({ defaults: { - listens: { - isConfigurable: 'handlePriceValue' - }, imports: { isConfigurable: '!ns = ${ $.ns }, index = configurable-matrix:isEmpty' }, @@ -22,12 +19,15 @@ define([ } }, - /** - * Invokes initialize method of parent class, - * contains initialization logic - */ + /** @inheritdoc */ initialize: function () { this._super(); + // resolve initial disable state + this.handlePriceValue(this.isConfigurable); + // add listener to track "configurable" type + this.setListeners({ + isConfigurable: 'handlePriceValue' + }); return this; }, @@ -50,8 +50,9 @@ define([ * @param {String} isConfigurable */ handlePriceValue: function (isConfigurable) { + this.disabled(!!this.isUseDefault() || isConfigurable); + if (isConfigurable) { - this.disable(); this.clear(); } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js index a8b8f95f6536f..155b176bd431b 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/paging/sizes.js @@ -9,21 +9,21 @@ define([ return Sizes.extend({ defaults: { - excludedOptions: ['100', '200'] - }, - - /** - * @override - */ - initialize: function () { - this._super(); - - this.excludedOptions.forEach(function (excludedOption) { - delete this.options[excludedOption]; - }, this); - this.updateArray(); - - return this; + options: { + '20': { + value: 20, + label: 20 + }, + '30': { + value: 30, + label: 30 + }, + '50': { + value: 50, + label: 50 + } + }, + value: 20 } }); }); diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js index 24fc24363562b..1d251f8ecc333 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js @@ -357,12 +357,12 @@ define([ var element; _.each(this.disabledAttributes, function (attribute) { - registry.get('index = ' + attribute).disabled(false); + registry.get('code = ' + attribute, 'index = ' + attribute).disabled(false); }); this.disabledAttributes = []; _.each(attributes, function (attribute) { - element = registry.get('index = ' + attribute.code); + element = registry.get('code = ' + attribute.code, 'index = ' + attribute.code); if (!_.isUndefined(element)) { element.disabled(true); @@ -383,7 +383,11 @@ define([ * Chose action for the form save button */ saveFormHandler: function () { - this.serializeData(); + this.formElement().validate(); + + if (this.formElement().source.get('params.invalid') === false) { + this.serializeData(); + } if (this.checkForNewAttributes()) { this.formSaveParams = arguments; @@ -407,15 +411,17 @@ define([ * - associated_product_ids_serialized. */ serializeData: function () { - this.source.data['configurable-matrix-serialized'] = - JSON.stringify(this.source.data['configurable-matrix']); - - delete this.source.data['configurable-matrix']; - - this.source.data['associated_product_ids_serialized'] = - JSON.stringify(this.source.data['associated_product_ids']); + if (this.source.data['configurable-matrix']) { + this.source.data['configurable-matrix-serialized'] = + JSON.stringify(this.source.data['configurable-matrix']); + delete this.source.data['configurable-matrix']; + } - delete this.source.data['associated_product_ids']; + if (this.source.data['associated_product_ids']) { + this.source.data['associated_product_ids_serialized'] = + JSON.stringify(this.source.data['associated_product_ids']); + delete this.source.data['associated_product_ids']; + } }, /** diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml index 98abb906f69d6..8ca80e2985a3a 100644 --- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/final_price.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php @@ -19,18 +16,24 @@ $finalPriceModel = $block->getPriceType('final_price'); $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; $schema = ($block->getZone() == 'item_view') ? true : false; ?> -<?php if (!$block->isProductList() && $block->hasSpecialPrice()): ?> - <span class="special-price"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ - 'display_label' => __('Special Price'), - 'price_id' => $block->getPriceId('product-price-' . $idSuffix), - 'price_type' => 'finalPrice', + +<span class="normal-price"> + <?php + $arguments = [ + 'display_label' => __('As low as'), + 'price_id' => $block->getPriceId('product-price-' . $idSuffix), + 'price_type' => 'finalPrice', 'include_container' => true, 'schema' => $schema - ]); ?> - </span> + ]; + + /* @noEscape */ echo $block->renderAmount($finalPriceModel->getAmount(), $arguments); + ?> +</span> + +<?php if (!$block->isProductList() && $block->hasSpecialPrice()) : ?> <span class="old-price sly-old-price no-display"> - <?php /* @escapeNotVerified */ echo $block->renderAmount($priceModel->getAmount(), [ + <?= /* @noEscape */ $block->renderAmount($priceModel->getAmount(), [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'price_type' => 'oldPrice', @@ -38,23 +41,16 @@ $schema = ($block->getZone() == 'item_view') ? true : false; 'skip_adjustments' => true ]); ?> </span> -<?php else: ?> - <?php /* @escapeNotVerified */ echo $block->renderAmount($finalPriceModel->getAmount(), [ - 'price_id' => $block->getPriceId('product-price-' . $idSuffix), - 'price_type' => 'finalPrice', - 'include_container' => true, - 'schema' => $schema - ]); ?> <?php endif; ?> -<?php if ($block->showMinimalPrice()): ?> - <?php if ($block->getUseLinkForAsLowAs()):?> - <a href="<?= /* @escapeNotVerified */ $block->getSaleableItem()->getProductUrl() ?>" class="minimal-price-link"> - <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> +<?php if ($block->showMinimalPrice()) : ?> + <?php if ($block->getUseLinkForAsLowAs()) :?> + <a href="<?= $block->escapeUrl($block->getSaleableItem()->getProductUrl()) ?>" class="minimal-price-link"> + <?= /* @noEscape */ $block->renderAmountMinimal() ?> </a> - <?php else:?> + <?php else :?> <span class="minimal-price-link"> - <?= /* @escapeNotVerified */ $block->renderAmountMinimal() ?> + <?= /* @noEscape */ $block->renderAmountMinimal() ?> </span> <?php endif?> <?php endif; ?> diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml index 18f96cfaaf398..c68419b955e6d 100644 --- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - ?> <script type="text/x-magento-template" id="tier-prices-template"> <ul class="prices-tier items"> @@ -15,10 +14,13 @@ + '</span>' + '</span>'; %> <li class="item"> - <%= $t('Buy %1 for %2 each and').replace('%1', item.qty).replace('%2', priceStr) %> - <strong class="benefit"> - <%= $t('save') %><span class="percent tier-<%= key %>"> <%= item.percentage %></span>% - </strong> + <%= '<?= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>' + .replace('%1', item.qty) + .replace('%2', priceStr) %> + <strong class="benefit"> + <?= $block->escapeHtml(__('save')) ?><span + class="percent tier-<%= key %>"> <%= item.percentage %></span>% + </strong> </li> <% }); %> </ul> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml b/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml index bad5acc209b5f..4ca5983081a6f 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/ConfigurableProduct/view/frontend/templates/js/components.phtml @@ -3,8 +3,5 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?= $block->getChildHtml() ?> +<?= $block->getChildHtml(); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml index 9b8e2c0c8c0bd..f7db41225c970 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php @@ -13,19 +10,19 @@ $_product = $block->getProduct(); $_attributes = $block->decorateArray($block->getAllowAttributes()); ?> -<?php if ($_product->isSaleable() && count($_attributes)):?> - <?php foreach ($_attributes as $_attribute): ?> +<?php if ($_product->isSaleable() && count($_attributes)) :?> + <?php foreach ($_attributes as $_attribute) : ?> <div class="field configurable required"> - <label class="label" for="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>"> + <label class="label" for="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>"> <span><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel()) ?></span> </label> <div class="control"> - <select name="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" - data-selector="super_attribute[<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>]" + <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" + data-selector="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" data-validate="{required:true}" - id="attribute<?= /* @escapeNotVerified */ $_attribute->getAttributeId() ?>" + id="attribute<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>" class="super-attribute-select"> - <option value=""><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> + <option value=""><?= $block->escapeHtml(__('Choose an Option...')) ?></option> </select> </div> </div> @@ -34,10 +31,15 @@ $_attributes = $block->decorateArray($block->getAllowAttributes()); { "#product_addtocart_form": { "configurable": { - "spConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig() ?>, - "gallerySwitchStrategy": "<?php /* @escapeNotVerified */ echo $block->getVar('gallery_switch_strategy', - 'Magento_ConfigurableProduct') ?: 'replace'; ?>" + "spConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, + "gallerySwitchStrategy": "<?= $block->escapeJs($block->getVar( + 'gallery_switch_strategy', + 'Magento_ConfigurableProduct' + ) ?: 'replace'); ?>" } + }, + "*" : { + "Magento_ConfigurableProduct/js/catalog-add-to-cart": {} } } </script> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js new file mode 100644 index 0000000000000..3e6a611c268af --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +require([ + 'jquery' +], function ($) { + 'use strict'; + + /** + * Add selected configurable attributes to redirect url + * + * @see Magento_Catalog/js/catalog-add-to-cart + */ + $('body').on('catalogCategoryAddToCartRedirect', function (event, data) { + $(data.form).find('select[name*="super"]').each(function (index, item) { + data.redirectParameters.push(item.config.id + '=' + $(item).val()); + }); + }); +}); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 545887d04c965..e1df9b323036f 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -32,6 +32,7 @@ define([ mediaGallerySelector: '[data-gallery-role=gallery-placeholder]', mediaGalleryInitial: null, slyOldPriceSelector: '.sly-old-price', + normalPriceLabelSelector: '.normal-price .price-label', /** * Defines the mechanism of how images of a gallery should be @@ -269,6 +270,7 @@ define([ this._reloadPrice(); this._displayRegularPriceBlock(this.simpleProduct); this._displayTierPriceBlock(this.simpleProduct); + this._displayNormalPriceLabel(); this._changeProductImage(); }, @@ -289,6 +291,8 @@ define([ images = this.options.spConfig.images[this.simpleProduct]; if (images) { + images = this._sortImages(images); + if (this.options.gallerySwitchStrategy === 'prepend') { images = images.concat(initialImages); } @@ -307,7 +311,17 @@ define([ $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); } - galleryObject.first(); + }, + + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return image.position; + }); }, /** @@ -358,7 +372,14 @@ define([ index = 1, allowedProducts, i, - j; + j, + finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount), + optionFinalPrice, + optionPriceDiff, + optionPrices = this.options.spConfig.optionPrices, + allowedOptions = [], + indexKey, + allowedProductMinPrice; this._clearSelect(element); element.options[0] = new Option('', ''); @@ -370,8 +391,16 @@ define([ } if (options) { + for (indexKey in this.options.spConfig.index) { + /* eslint-disable max-depth */ + if (this.options.spConfig.index.hasOwnProperty(indexKey)) { + allowedOptions = allowedOptions.concat(_.values(this.options.spConfig.index[indexKey])); + } + } + for (i = 0; i < options.length; i++) { allowedProducts = []; + optionPriceDiff = 0; /* eslint-disable max-depth */ if (prevConfig) { @@ -385,14 +414,32 @@ define([ } } else { allowedProducts = options[i].products.slice(0); + + if (typeof allowedProducts[0] !== 'undefined' && + typeof optionPrices[allowedProducts[0]] !== 'undefined') { + allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); + optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); + optionPriceDiff = optionFinalPrice - finalPrice; + + if (optionPriceDiff !== 0) { + options[i].label = options[i].label + ' ' + priceUtils.formatPrice( + optionPriceDiff, + this.options.priceFormat, + true); + } + } } - if (allowedProducts.length > 0) { + if (allowedProducts.length > 0 || _.include(allowedOptions, options[i].id)) { options[i].allowedProducts = allowedProducts; element.options[index] = new Option(this._getOptionLabel(options[i]), options[i].id); if (typeof options[i].price !== 'undefined') { - element.options[index].setAttribute('price', options[i].prices); + element.options[index].setAttribute('price', options[i].price); + } + + if (allowedProducts.length === 0) { + element.options[index].disabled = true; } element.options[index].config = options[i]; @@ -456,24 +503,56 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - hasProductPrice = false; + allowedProduct; _.each(elements, function (element) { var selected = element.options[element.selectedIndex], config = selected && selected.config, priceValue = {}; - if (config && config.allowedProducts.length === 1 && !hasProductPrice) { + if (config && config.allowedProducts.length === 1) { priceValue = this._calculatePrice(config); - hasProductPrice = true; + } else if (element.value) { + allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); } - prices[element.attributeId] = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } }, this); return prices; }, + /** + * Get product with minimum price from selected options. + * + * @param {Array} allowedProducts + * @returns {String} + * @private + */ + _getAllowedProductWithMinPrice: function (allowedProducts) { + var optionPrices = this.options.spConfig.optionPrices, + product = {}, + optionMinPrice, optionFinalPrice; + + _.each(allowedProducts, function (allowedProduct) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { + optionMinPrice = optionFinalPrice; + product = allowedProduct; + } + }, this); + + return product; + }, + /** * Returns prices for configured products * @@ -527,14 +606,50 @@ define([ * @private */ _displayRegularPriceBlock: function (optionId) { - if (typeof optionId != 'undefined' && - this.options.spConfig.optionPrices[optionId].oldPrice.amount != //eslint-disable-line eqeqeq + var shouldBeShown = true; + + _.each(this.options.settings, function (element) { + if (element.value === '') { + shouldBeShown = false; + } + }); + + if (shouldBeShown && + this.options.spConfig.optionPrices[optionId].oldPrice.amount !== this.options.spConfig.optionPrices[optionId].finalPrice.amount ) { $(this.options.slyOldPriceSelector).show(); } else { $(this.options.slyOldPriceSelector).hide(); } + + $(document).trigger('updateMsrpPriceBlock', + [ + optionId, + this.options.spConfig.optionPrices + ] + ); + }, + + /** + * Show or hide normal price label + * + * @private + */ + _displayNormalPriceLabel: function () { + var shouldBeShown = false; + + _.each(this.options.settings, function (element) { + if (element.value === '') { + shouldBeShown = true; + } + }); + + if (shouldBeShown) { + $(this.options.normalPriceLabelSelector).show(); + } else { + $(this.options.normalPriceLabelSelector).hide(); + } }, /** diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js index 64aefc27dc080..6f18798303151 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js @@ -1,15 +1,18 @@ define([ 'jquery', + 'underscore', 'Magento_Customer/js/customer-data' -], function ($, customerData) { +], function ($, _, customerData) { 'use strict'; var selectors = { formSelector: '#product_addtocart_form', - productIdSelector: '#product_addtocart_form [name="product"]' + productIdSelector: '#product_addtocart_form [name="product"]', + itemIdSelector: '#product_addtocart_form [name="item"]' }, cartData = customerData.get('cart'), productId = $(selectors.productIdSelector).val(), + itemId = $(selectors.itemIdSelector).val(), /** * set productOptions according to cart data from customer-data @@ -23,8 +26,10 @@ define([ if (!(data && data.items && data.items.length && productId)) { return false; } - changedProductOptions = data.items.find(function (item) { - return item['product_id'] === productId; + changedProductOptions = _.find(data.items, function (item) { + if (item['item_id'] === itemId) { + return item['product_id'] === productId; + } }); changedProductOptions = changedProductOptions && changedProductOptions.options && changedProductOptions.options.reduce(function (obj, val) { @@ -56,6 +61,7 @@ define([ this.setProductOptions(cartData()); this.updateOptions(); }.bind(this)); + this.updateOptions(); }, /** diff --git a/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php b/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php index dceb5767edae9..8ef1e24125981 100644 --- a/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php +++ b/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php @@ -5,11 +5,12 @@ */ namespace Magento\ConfigurableProductSales\Model\Order\Reorder; -use Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityCheckerInterface; -use Magento\Sales\Model\Order\Item; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityCheckerInterface; use Magento\Store\Model\Store; /** @@ -27,16 +28,24 @@ class OrderedProductAvailabilityChecker implements OrderedProductAvailabilityChe */ private $metadataPool; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @param ResourceConnection $resourceConnection * @param MetadataPool $metadataPool + * @param ProductRepositoryInterface $productRepository */ public function __construct( ResourceConnection $resourceConnection, - MetadataPool $metadataPool + MetadataPool $metadataPool, + ProductRepositoryInterface $productRepository ) { $this->resourceConnection = $resourceConnection; $this->metadataPool = $metadataPool; + $this->productRepository = $productRepository; } /** @@ -45,10 +54,12 @@ public function __construct( public function isAvailable(Item $item) { $buyRequest = $item->getBuyRequest(); - $superAttribute = $buyRequest->getData()['super_attribute']; + $superAttribute = $buyRequest->getData()['super_attribute'] ?? []; $connection = $this->getConnection(); $select = $connection->select(); - $orderItemParentId = $item->getParentItem()->getProductId(); + $linkField = $this->getMetadata()->getLinkField(); + $parentItem = $this->productRepository->getById($item->getParentItem()->getProductId()); + $orderItemParentId = $parentItem->getData($linkField); $select->from( ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], ['cpe.entity_id'] @@ -67,7 +78,7 @@ public function isAvailable(Item $item) ['cpid' . $attributeId => $this->resourceConnection->getTableName('catalog_product_entity_int')], sprintf( 'cpe.%1$s = cpid%2$d.%1$s AND cpid%2$d.attribute_id = %2$d AND cpid%2$d.store_id = %3$d', - $this->getMetadata()->getLinkField(), + $linkField, $attributeId, Store::DEFAULT_STORE_ID ), @@ -77,7 +88,7 @@ public function isAvailable(Item $item) ['cpis' . $attributeId => $this->resourceConnection->getTableName('catalog_product_entity_int')], sprintf( 'cpe.%1$s = cpis%2$d.%1$s AND cpis%2$d.attribute_id = %2$d AND cpis%2$d.store_id = %3$d', - $this->getMetadata()->getLinkField(), + $linkField, $attributeId, $item->getStoreId() ), diff --git a/app/code/Magento/ConfigurableProductSales/Test/Mftf/LICENSE.txt b/app/code/Magento/ConfigurableProductSales/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ConfigurableProductSales/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 <insert your license name here>" 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/app/code/Magento/ConfigurableProductSales/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ConfigurableProductSales/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ConfigurableProductSales/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 <insert your license name here>" 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/app/code/Magento/ConfigurableProductSales/Test/Mftf/README.md b/app/code/Magento/ConfigurableProductSales/Test/Mftf/README.md new file mode 100644 index 0000000000000..944286966a7ad --- /dev/null +++ b/app/code/Magento/ConfigurableProductSales/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Configurable Product Sales Functional Tests + +The Functional Test Module for **Magento Configurable Product Sales** module. diff --git a/app/code/Magento/ConfigurableProductSales/composer.json b/app/code/Magento/ConfigurableProductSales/composer.json index eaa97d8394321..9da2c75ea91c4 100644 --- a/app/code/Magento/ConfigurableProductSales/composer.json +++ b/app/code/Magento/ConfigurableProductSales/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-configurable-product-sales", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-sales": "101.0.*", "magento/module-store": "100.2.*", @@ -12,7 +12,7 @@ "magento/module-configurable-product": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Contact/Controller/Index/Post.php b/app/code/Magento/Contact/Controller/Index/Post.php index ee2d23b74df24..4392cb2148a51 100644 --- a/app/code/Magento/Contact/Controller/Index/Post.php +++ b/app/code/Magento/Contact/Controller/Index/Post.php @@ -12,7 +12,6 @@ use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\HTTP\PhpEnvironment\Request; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; @@ -67,7 +66,7 @@ public function __construct( */ public function execute() { - if (!$this->isPostRequest()) { + if (!$this->getRequest()->isPost()) { return $this->resultRedirectFactory->create()->setPath('*/*/'); } try { @@ -101,16 +100,6 @@ private function sendEmail($post) ); } - /** - * @return bool - */ - private function isPostRequest() - { - /** @var Request $request */ - $request = $this->getRequest(); - return !empty($request->getPostValue()); - } - /** * @return array * @throws \Exception diff --git a/app/code/Magento/Contact/Test/Mftf/LICENSE.txt b/app/code/Magento/Contact/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Contact/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 <insert your license name here>" 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/app/code/Magento/Contact/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Contact/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Contact/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 <insert your license name here>" 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/app/code/Magento/Contact/Test/Mftf/README.md b/app/code/Magento/Contact/Test/Mftf/README.md new file mode 100644 index 0000000000000..e2f9a58f72089 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Contact Functional Tests + +The Functional Test Module for **Magento Contact** module. diff --git a/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml new file mode 100644 index 0000000000000..288dbda7cb14d --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectContact"> + <annotations> + <features value="Contact"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Contact Pages"/> + <description value="Verify that the Secure URL configuration applies to the Contact pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15716"/> + <group value="contact"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/contact" stepKey="goToUnsecureContactURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/contact" stepKey="seeSecureContactURL"/> + <amOnUrl url="http://{$hostname}/contact/index/post" stepKey="goToUnsecureContactFormURL"/> + <seeInCurrentUrl url="https://{$hostname}/contact/index" stepKey="seeSecureContactFormURL"/> + </test> +</tests> diff --git a/app/code/Magento/Contact/Test/Unit/Controller/Index/PostTest.php b/app/code/Magento/Contact/Test/Unit/Controller/Index/PostTest.php index 0e1ddf21c2a08..f01922f42f40c 100644 --- a/app/code/Magento/Contact/Test/Unit/Controller/Index/PostTest.php +++ b/app/code/Magento/Contact/Test/Unit/Controller/Index/PostTest.php @@ -78,7 +78,7 @@ protected function setUp() $this->createMock(\Magento\Framework\Message\ManagerInterface::class); $this->requestStub = $this->createPartialMock( \Magento\Framework\App\Request\Http::class, - ['getPostValue', 'getParams', 'getParam'] + ['getPostValue', 'getParams', 'getParam', 'isPost'] ); $this->redirectResultMock = $this->createMock(\Magento\Framework\Controller\Result\Redirect::class); $this->redirectResultMock->method('setPath')->willReturnSelf(); @@ -144,6 +144,9 @@ public function testExecutePostValidation($postData, $exceptionExpected) $this->controller->execute(); } + /** + * @return array + */ public function postDataProvider() { return [ @@ -174,6 +177,10 @@ public function testExecuteValidPost() */ private function stubRequestPostData($post) { + $this->requestStub + ->expects($this->once()) + ->method('isPost') + ->willReturn(!empty($post)); $this->requestStub->method('getPostValue')->willReturn($post); $this->requestStub->method('getParams')->willReturn($post); $this->requestStub->method('getParam')->willReturnCallback( diff --git a/app/code/Magento/Contact/Test/Unit/Controller/Stub/IndexStub.php b/app/code/Magento/Contact/Test/Unit/Controller/Stub/IndexStub.php index a238daafaafaf..cabcebda061f9 100644 --- a/app/code/Magento/Contact/Test/Unit/Controller/Stub/IndexStub.php +++ b/app/code/Magento/Contact/Test/Unit/Controller/Stub/IndexStub.php @@ -8,6 +8,9 @@ class IndexStub extends \Magento\Contact\Controller\Index { + /** + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void + */ public function execute() { // Empty method stub for test diff --git a/app/code/Magento/Contact/composer.json b/app/code/Magento/Contact/composer.json index effec9f15a756..e931906081039 100644 --- a/app/code/Magento/Contact/composer.json +++ b/app/code/Magento/Contact/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-contact", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-customer": "101.0.*", @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Contact/view/frontend/email/submitted_form.html b/app/code/Magento/Contact/view/frontend/email/submitted_form.html index 1bce6159c586a..17146257aeff1 100644 --- a/app/code/Magento/Contact/view/frontend/email/submitted_form.html +++ b/app/code/Magento/Contact/view/frontend/email/submitted_form.html @@ -16,19 +16,19 @@ <table class="message-details"> <tr> - <td><b>{{trans "Name"}}</b></td> + <td><strong>{{trans "Name"}}</strong></td> <td>{{var data.name}}</td> </tr> <tr> - <td><b>{{trans "Email"}}</b></td> + <td><strong>{{trans "Email"}}</strong></td> <td>{{var data.email}}</td> </tr> <tr> - <td><b>{{trans "Phone"}}</b></td> + <td><strong>{{trans "Phone"}}</strong></td> <td>{{var data.telephone}}</td> </tr> </table> -<p><b>{{trans "Message"}}</b></p> +<p><strong>{{trans "Message"}}</strong></p> <p>{{var data.comment}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index d64a991bcafad..673bdc73840dc 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** @var \Magento\Contact\Block\ContactForm $block */ ?> <form class="form contact" @@ -19,25 +18,25 @@ <div class="field name required"> <label class="label" for="name"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> - <input name="name" id="name" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('name') ?: $this->helper('Magento\Contact\Helper\Data')->getUserName()) ?>" class="input-text" type="text" data-validate="{required:true}"/> + <input name="name" id="name" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('name') ?: $this->helper(\Magento\Contact\Helper\Data::class)->getUserName()) ?>" class="input-text" type="text" data-validate="{required:true}"/> </div> </div> <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="email" id="email" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('email') ?: $this->helper('Magento\Contact\Helper\Data')->getUserEmail()) ?>" class="input-text" type="email" data-validate="{required:true, 'validate-email':true}"/> + <input name="email" id="email" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('email') ?: $this->helper(\Magento\Contact\Helper\Data::class)->getUserEmail()) ?>" class="input-text" type="email" data-validate="{required:true, 'validate-email':true}"/> </div> </div> <div class="field telephone"> <label class="label" for="telephone"><span><?= $block->escapeHtml(__('Phone Number')) ?></span></label> <div class="control"> - <input name="telephone" id="telephone" title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $block->escapeHtmlAttr($this->helper('Magento\Contact\Helper\Data')->getPostValue('telephone')) ?>" class="input-text" type="text" /> + <input name="telephone" id="telephone" title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" value="<?= $block->escapeHtmlAttr($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('telephone')) ?>" class="input-text" type="text" /> </div> </div> <div class="field comment required"> <label class="label" for="comment"><span><?= $block->escapeHtml(__('What’s on your mind?')) ?></span></label> <div class="control"> - <textarea name="comment" id="comment" title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" class="input-text" cols="5" rows="3" data-validate="{required:true}"><?= $block->escapeHtml($this->helper('Magento\Contact\Helper\Data')->getPostValue('comment')) ?></textarea> + <textarea name="comment" id="comment" title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" class="input-text" cols="5" rows="3" data-validate="{required:true}"><?= $block->escapeHtml($this->helper(\Magento\Contact\Helper\Data::class)->getPostValue('comment')) ?></textarea> </div> </div> <?= $block->getChildHtml('form.additional.info') ?> diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less new file mode 100644 index 0000000000000..d79806eecbe9b --- /dev/null +++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less @@ -0,0 +1,52 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +*/ + +& when (@media-common = true) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 50%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 50%; + } + } + } +} + +// +// Desktop +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .contact-index-index .column:not(.sidebar-additional) .form.contact { + min-width: 600px; + } +} + +// Mobile +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 100%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 100%; + } + } + } +} + diff --git a/app/code/Magento/Cookie/Test/Mftf/LICENSE.txt b/app/code/Magento/Cookie/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Cookie/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 <insert your license name here>" 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/app/code/Magento/Cookie/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Cookie/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Cookie/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 <insert your license name here>" 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/app/code/Magento/Cookie/Test/Mftf/README.md b/app/code/Magento/Cookie/Test/Mftf/README.md new file mode 100644 index 0000000000000..c06fe5dcd60de --- /dev/null +++ b/app/code/Magento/Cookie/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Cookie Functional Tests + +The Functional Test Module for **Magento Cookie** module. diff --git a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php index 5694f3f3cab56..62ce6baf6c101 100644 --- a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php +++ b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php @@ -79,6 +79,9 @@ public function testGetCookieRestrictionLifetime() $this->assertEquals($this->_object->getCookieRestrictionLifetime(), 60 * 60 * 24 * 365); } + /** + * @return $this + */ protected function _initMock() { $scopeConfig = $this->_getConfigStub(); diff --git a/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/LifetimeTest.php b/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/LifetimeTest.php index c11bd741bc4b1..f4ec5c9fbc78e 100644 --- a/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/LifetimeTest.php +++ b/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/LifetimeTest.php @@ -1,18 +1,17 @@ <?php /** - * Unit test for Magento\Cookie\Model\Config\Backend\Lifetime - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Cookie\Test\Unit\Model\Config\Backend; use Magento\Framework\Session\Config\Validator\CookieLifetimeValidator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Unit test for Magento\Cookie\Model\Config\Backend\Lifetime. + */ class LifetimeTest extends \PHPUnit\Framework\TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | CookieLifetimeValidator */ diff --git a/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/PathTest.php b/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/PathTest.php index 705312425a75a..ed38dc15a8bd8 100644 --- a/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/PathTest.php +++ b/app/code/Magento/Cookie/Test/Unit/Model/Config/Backend/PathTest.php @@ -1,18 +1,18 @@ <?php /** - * Unit test for Magento\Cookie\Model\Config\Backend\Path * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Cookie\Test\Unit\Model\Config\Backend; use Magento\Framework\Session\Config\Validator\CookiePathValidator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Unit test for Magento\Cookie\Model\Config\Backend\Path + */ class PathTest extends \PHPUnit\Framework\TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | CookiePathValidator */ @@ -27,7 +27,8 @@ class PathTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->validatorMock = $this->getMockBuilder( - \Magento\Framework\Session\Config\Validator\CookiePathValidator::class) + \Magento\Framework\Session\Config\Validator\CookiePathValidator::class + ) ->disableOriginalConstructor() ->getMock(); $this->resourceMock = $this->getMockBuilder(\Magento\Framework\Module\ModuleResource::class) diff --git a/app/code/Magento/Cookie/composer.json b/app/code/Magento/Cookie/composer.json index 131b4684b8f1f..8c4ee14d1e242 100644 --- a/app/code/Magento/Cookie/composer.json +++ b/app/code/Magento/Cookie/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-cookie", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/framework": "101.0.*" }, @@ -10,7 +10,7 @@ "magento/module-backend": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Cookie/etc/adminhtml/system.xml b/app/code/Magento/Cookie/etc/adminhtml/system.xml index 26c963ddba76d..9790410969055 100644 --- a/app/code/Magento/Cookie/etc/adminhtml/system.xml +++ b/app/code/Magento/Cookie/etc/adminhtml/system.xml @@ -22,7 +22,7 @@ <label>Cookie Domain</label> <backend_model>Magento\Cookie\Model\Config\Backend\Domain</backend_model> </field> - <field id="cookie_httponly" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="cookie_httponly" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Use HTTP Only</label> <comment> <![CDATA[<strong style="color:red">Warning</strong>: Do not set to "No". User security could be compromised.]]> diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml index c5cfda8cd7d15..8712f31e71b36 100644 --- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Cookie\Block\Html\Notices $block */ ?> -<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()): ?> +<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()) : ?> <div role="alertdialog" tabindex="-1" class="message global cookie" diff --git a/app/code/Magento/Cookie/view/frontend/web/js/notices.js b/app/code/Magento/Cookie/view/frontend/web/js/notices.js index 253950747ce14..f1f3754ea54b1 100644 --- a/app/code/Magento/Cookie/view/frontend/web/js/notices.js +++ b/app/code/Magento/Cookie/view/frontend/web/js/notices.js @@ -29,7 +29,7 @@ define([ }); if ($.mage.cookies.get(this.options.cookieName)) { - window.location.reload(); + this.element.hide(); } else { window.location.href = this.options.noCookiesUrl; } diff --git a/app/code/Magento/Cron/Console/Command/CronCommand.php b/app/code/Magento/Cron/Console/Command/CronCommand.php index 78bbb2329f8dc..142a9a397eb5f 100644 --- a/app/code/Magento/Cron/Console/Command/CronCommand.php +++ b/app/code/Magento/Cron/Console/Command/CronCommand.php @@ -10,10 +10,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputOption; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ObjectManagerFactory; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManager; use Magento\Cron\Observer\ProcessCronQueueObserver; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Console\Cli; use Magento\Framework\Shell\ComplexParameter; @@ -35,13 +37,24 @@ class CronCommand extends Command private $objectManagerFactory; /** - * Constructor + * Application deployment configuration * + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** * @param ObjectManagerFactory $objectManagerFactory + * @param DeploymentConfig $deploymentConfig Application deployment configuration */ - public function __construct(ObjectManagerFactory $objectManagerFactory) - { + public function __construct( + ObjectManagerFactory $objectManagerFactory, + DeploymentConfig $deploymentConfig = null + ) { $this->objectManagerFactory = $objectManagerFactory; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get( + DeploymentConfig::class + ); parent::__construct(); } @@ -71,10 +84,16 @@ protected function configure() } /** + * Runs cron jobs if cron is not disabled in Magento configurations + * * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { + if (!$this->deploymentConfig->get('cron/enabled', 1)) { + $output->writeln('<info>' . 'Cron is disabled. Jobs were not run.' . '</info>'); + return; + } $omParams = $_SERVER; $omParams[StoreManager::PARAM_RUN_CODE] = 'admin'; $omParams[Store::CUSTOM_ENTRY_POINT_PARAM] = true; diff --git a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php index 2fc0f0ab4c1a0..87618785adb1d 100644 --- a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php +++ b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php @@ -72,14 +72,14 @@ public function afterSave() $frequency = $this->getData('groups/productalert_cron/fields/frequency/value'); $cronExprArray = [ - intval($time[1]), //Minute - intval($time[0]), //Hour + (int)$time[1], //Minute + (int)$time[0], //Hour $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month '*', //Month of the Year $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //Day of the Week ]; - $cronExprString = join(' ', $cronExprArray); + $cronExprString = implode(' ', $cronExprArray); try { $this->_configValueFactory->create()->load( diff --git a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php index 681129916647d..31d8ba59ee42d 100644 --- a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php +++ b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php @@ -70,8 +70,8 @@ public function afterSave() $frequency = $this->getData('groups/generate/fields/frequency/value'); $cronExprArray = [ - intval($time[1]), //Minute - intval($time[0]), //Hour + (int)$time[1], //Minute + (int)$time[0], //Hour $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month '*', //Month of the Year $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //# Day of the Week diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 39a58ef360cb3..a9ae04cb0c5d1 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\CronException; use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Intl\DateTimeFactory; /** * Crontab schedule model @@ -50,13 +51,19 @@ class Schedule extends \Magento\Framework\Model\AbstractModel */ private $timezoneConverter; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param TimezoneInterface $timezoneConverter + * @param TimezoneInterface|null $timezoneConverter + * @param DateTimeFactory|null $dateTimeFactory */ public function __construct( \Magento\Framework\Model\Context $context, @@ -64,10 +71,12 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - TimezoneInterface $timezoneConverter = null + TimezoneInterface $timezoneConverter = null, + DateTimeFactory $dateTimeFactory = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); } /** @@ -109,17 +118,20 @@ public function trySchedule() if (!$e || !$time) { return false; } + $configTimeZone = $this->timezoneConverter->getConfigTimezone(); + $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); if (!is_numeric($time)) { //convert time from UTC to admin store timezone //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone - $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); - $time = strtotime($time); + $dateTimeUtc = $this->dateTimeFactory->create($time); + $time = $dateTimeUtc->getTimestamp(); } - $match = $this->matchCronExpression($e[0], strftime('%M', $time)) - && $this->matchCronExpression($e[1], strftime('%H', $time)) - && $this->matchCronExpression($e[2], strftime('%d', $time)) - && $this->matchCronExpression($e[3], strftime('%m', $time)) - && $this->matchCronExpression($e[4], strftime('%w', $time)); + $time = $storeDateTime->setTimestamp($time); + $match = $this->matchCronExpression($e[0], $time->format('i')) + && $this->matchCronExpression($e[1], $time->format('H')) + && $this->matchCronExpression($e[2], $time->format('d')) + && $this->matchCronExpression($e[3], $time->format('m')) + && $this->matchCronExpression($e[4], $time->format('w')); return $match; } diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index f772a6c0c8493..6008d4ebce218 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -3,16 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - /** * Handling cron jobs */ namespace Magento\Cron\Observer; +use Magento\Cron\Model\Schedule; use Magento\Framework\App\State; use Magento\Framework\Console\Cli; use Magento\Framework\Event\ObserverInterface; -use \Magento\Cron\Model\Schedule; +use Magento\Framework\Profiler\Driver\Standard\Stat; +use Magento\Framework\Profiler\Driver\Standard\StatFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -56,6 +57,16 @@ class ProcessCronQueueObserver implements ObserverInterface */ const SECONDS_IN_MINUTE = 60; + /** + * How long to wait for cron group to become unlocked + */ + const LOCK_TIMEOUT = 5; + + /** + * Static lock prefix for cron group locking + */ + const LOCK_PREFIX = 'CRON_GROUP_'; + /** * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection */ @@ -116,15 +127,20 @@ class ProcessCronQueueObserver implements ObserverInterface */ private $state; + /** + * @var \Magento\Framework\Lock\LockManagerInterface + */ + private $lockManager; + /** * @var array */ private $invalid = []; /** - * @var array + * @var Stat */ - private $jobs; + private $statProfiler; /** * @param \Magento\Framework\ObjectManagerInterface $objectManager @@ -138,6 +154,7 @@ class ProcessCronQueueObserver implements ObserverInterface * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\App\State $state + * @param StatFactory $statFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -151,7 +168,9 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, \Psr\Log\LoggerInterface $logger, - \Magento\Framework\App\State $state + \Magento\Framework\App\State $state, + StatFactory $statFactory, + \Magento\Framework\Lock\LockManagerInterface $lockManager ) { $this->_objectManager = $objectManager; $this->_scheduleFactory = $scheduleFactory; @@ -164,6 +183,8 @@ public function __construct( $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); $this->logger = $logger; $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; } /** @@ -179,27 +200,25 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { - $pendingJobs = $this->_getPendingSchedules(); $currentTime = $this->dateTime->gmtTimestamp(); $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process + uksort( + $jobGroupsRoot, + function ($a, $b) { + return $this->getCronGroupConfigurationValue($b, 'use_separate_process') + - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); + } + ); $phpPath = $this->phpExecutableFinder->find() ?: 'php'; foreach ($jobGroupsRoot as $groupId => $jobsRoot) { - $this->_cleanup($groupId); - $this->_generate($groupId); - if ($this->_request->getParam('group') !== null - && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' - && $this->_request->getParam('group') !== $groupId - ) { + if (!$this->isGroupInFilter($groupId)) { continue; } - if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( - $this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/use_separate_process', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) == 1 - ) + if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' + && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ) { $this->_shell->execute( $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' @@ -211,42 +230,42 @@ public function execute(\Magento\Framework\Event\Observer $observer) continue; } - /** @var \Magento\Cron\Model\Schedule $schedule */ - foreach ($pendingJobs as $schedule) { - $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; - if (!$jobConfig) { - continue; - } - - $scheduledTime = strtotime($schedule->getScheduledAt()); - if ($scheduledTime > $currentTime) { - continue; + $this->lockGroup( + $groupId, + function ($groupId) use ($currentTime, $jobsRoot) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); + $this->processPendingJobs($groupId, $jobsRoot, $currentTime); } + ); + } + } - try { - if ($schedule->tryLockJob()) { - $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); - } - } catch (\Exception $e) { - $schedule->setMessages($e->getMessage()); - if ($schedule->getStatus() === Schedule::STATUS_ERROR) { - $this->logger->critical($e); - } - if ($schedule->getStatus() === Schedule::STATUS_MISSED - && $this->state->getMode() === State::MODE_DEVELOPER - ) { - $this->logger->info( - sprintf( - "%s Schedule Id: %s Job Code: %s", - $schedule->getMessages(), - $schedule->getScheduleId(), - $schedule->getJobCode() - ) - ); - } - } - $schedule->save(); - } + /** + * Lock group + * + * It should be taken by standalone (child) process, not by the parent process. + * + * @param int $groupId + * @param callable $callback + * + * @return void + */ + private function lockGroup($groupId, callable $callback) + { + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( + "Could not acquire lock for cron group: %s, skipping run", + $groupId + ) + ); + return; + } + try { + $callback($groupId); + } finally { + $this->lockManager->unlock(self::LOCK_PREFIX . $groupId); } } @@ -263,24 +282,25 @@ public function execute(\Magento\Framework\Event\Observer $observer) */ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) { - $scheduleLifetime = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $jobCode = $schedule->getJobCode(); + $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; if ($scheduledTime < $currentTime - $scheduleLifetime) { $schedule->setStatus(Schedule::STATUS_MISSED); - throw new \Exception('Too late for the schedule'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); } if (!isset($jobConfig['instance'], $jobConfig['method'])) { $schedule->setStatus(Schedule::STATUS_ERROR); - throw new \Exception('No callbacks found'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); } $model = $this->_objectManager->create($jobConfig['instance']); $callback = [$model, $jobConfig['method']]; if (!is_callable($callback)) { $schedule->setStatus(Schedule::STATUS_ERROR); + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method']) ); @@ -288,17 +308,82 @@ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + $this->startProfiling(); try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func_array($callback, [$schedule]); - } catch (\Exception $e) { + } catch (\Throwable $e) { $schedule->setStatus(Schedule::STATUS_ERROR); + $this->logger->error( + sprintf( + 'Cron Job %s has an error: %s. Statistics: %s', + $jobCode, + $e->getMessage(), + $this->getProfilingStat() + ) + ); + if (!$e instanceof \Exception) { + $e = new \RuntimeException( + 'Error when running a cron job', + 0, + $e + ); + } throw $e; + } finally { + $this->stopProfiling(); } - $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( - '%Y-%m-%d %H:%M:%S', - $this->dateTime->gmtTimestamp() - )); + $schedule->setStatus( + Schedule::STATUS_SUCCESS + )->setFinishedAt( + strftime( + '%Y-%m-%d %H:%M:%S', + $this->dateTime->gmtTimestamp() + ) + ); + + $this->logger->info( + sprintf( + 'Cron Job %s is successfully finished. Statistics: %s', + $jobCode, + $this->getProfilingStat() + ) + ); + } + + /** + * Starts profiling + * + * @return void + */ + private function startProfiling() + { + $this->statProfiler->clear(); + $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); + } + + /** + * Stops profiling + * + * @return void + */ + private function stopProfiling() + { + $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); + } + + /** + * Retrieves statistics in the JSON format + * + * @return string + */ + private function getProfilingStat() + { + $stat = $this->statProfiler->get('job'); + unset($stat[Stat::START]); + return json_encode($stat); } /** @@ -306,15 +391,35 @@ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, * * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection */ - protected function _getPendingSchedules() + private function getPendingSchedules($groupId) { - if (!$this->_pendingSchedules) { - $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( - 'status', - Schedule::STATUS_PENDING - )->load(); - } - return $this->_pendingSchedules; + $jobs = $this->_config->getJobs(); + $pendingJobs = $this->_scheduleFactory->create()->getCollection(); + $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); + $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); + return $pendingJobs; + } + + /** + * Return job collection from database with status 'pending', 'running' or 'success' + * + * @param string $groupId + * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection + */ + private function getNonExitedSchedules($groupId) + { + $jobs = $this->_config->getJobs(); + $pendingJobs = $this->_scheduleFactory->create()->getCollection(); + $pendingJobs->addFieldToFilter( + 'status', + [ + 'in' => [ + Schedule::STATUS_PENDING, Schedule::STATUS_RUNNING, Schedule::STATUS_SUCCESS + ] + ] + ); + $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); + return $pendingJobs; } /** @@ -323,22 +428,32 @@ protected function _getPendingSchedules() * @param string $groupId * @return $this */ - protected function _generate($groupId) + private function generateSchedules($groupId) { /** * check if schedule generation is needed */ $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); - $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( + $groupId, + self::XML_PATH_SCHEDULE_GENERATE_EVERY ); $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { return $this; } - $schedules = $this->_getPendingSchedules(); + /** + * save time schedules generation was ran with no expiration + */ + $this->_cache->save( + $this->dateTime->gmtTimestamp(), + self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, + ['crontab'], + null + ); + + $schedules = $this->getNonExitedSchedules($groupId); $exists = []; /** @var Schedule $schedule */ foreach ($schedules as $schedule) { @@ -348,21 +463,11 @@ protected function _generate($groupId) /** * generate global crontab jobs */ - $jobs = $this->getJobs(); + $jobs = $this->_config->getJobs(); $this->invalid = []; $this->_generateJobs($jobs[$groupId], $exists, $groupId); $this->cleanupScheduleMismatches(); - /** - * save time schedules generation was ran with no expiration - */ - $this->_cache->save( - $this->dateTime->gmtTimestamp(), - self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, - ['crontab'], - null - ); - return $this; } @@ -372,7 +477,7 @@ protected function _generate($groupId) * @param array $jobs * @param array $exists * @param string $groupId - * @return $this + * @return void */ protected function _generateJobs($jobs, $exists, $groupId) { @@ -385,77 +490,60 @@ protected function _generateJobs($jobs, $exists, $groupId) $timeInterval = $this->getScheduleTimeInterval($groupId); $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); } - return $this; } /** * Clean expired jobs * - * @param string $groupId - * @return $this + * @param $groupId + * @param $currentTime + * @return void */ - protected function _cleanup($groupId) + private function cleanupJobs($groupId, $currentTime) { - $this->cleanupDisabledJobs($groupId); - // check if history cleanup is needed $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); - $historyCleanUp = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { return $this; } - - // check how long the record should stay unprocessed before marked as MISSED - $scheduleLifetime = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + // save time history cleanup was ran with no expiration + $this->_cache->save( + $this->dateTime->gmtTimestamp(), + self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, + ['crontab'], + null ); - $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; - /** - * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history - */ - $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( - 'status', - ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] - )->load(); + $this->cleanupDisabledJobs($groupId); - $historySuccess = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $historyFailure = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); + $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); $historyLifetimes = [ Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, ]; - $now = $this->dateTime->gmtTimestamp(); - /** @var Schedule $record */ - foreach ($history as $record) { - $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : - strtotime($record->getScheduledAt()) + $scheduleLifetime; - if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { - $record->delete(); - } + $jobs = $this->_config->getJobs()[$groupId]; + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { + $count += $connection->delete( + $scheduleResource->getMainTable(), + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), + 'created_at < ?' => $connection->formatDate($currentTime - $time) + ] + ); } - // save time history cleanup was ran with no expiration - $this->_cache->save( - $this->dateTime->gmtTimestamp(), - self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, - ['crontab'], - null - ); - - return $this; + if ($count) { + $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); + } } /** @@ -486,7 +574,7 @@ protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exist for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); - $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); + $schedule = $this->createSchedule($jobCode, $cronExpression, $time); $valid = $schedule->trySchedule(); if (!$valid) { if ($alreadyScheduled) { @@ -510,7 +598,7 @@ protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exist * @param int $time * @return Schedule */ - protected function generateSchedule($jobCode, $cronExpression, $time) + protected function createSchedule($jobCode, $cronExpression, $time) { $schedule = $this->_scheduleFactory->create() ->setCronExpr($cronExpression) @@ -528,10 +616,7 @@ protected function generateSchedule($jobCode, $cronExpression, $time) */ protected function getScheduleTimeInterval($groupId) { - $scheduleAheadFor = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; return $scheduleAheadFor; @@ -546,17 +631,27 @@ protected function getScheduleTimeInterval($groupId) */ private function cleanupDisabledJobs($groupId) { - $jobs = $this->getJobs(); + $jobs = $this->_config->getJobs(); + $jobsToCleanup = []; foreach ($jobs[$groupId] as $jobCode => $jobConfig) { if (!$this->getCronExpression($jobConfig)) { /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ - $scheduleResource = $this->_scheduleFactory->create()->getResource(); - $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ - 'status=?' => Schedule::STATUS_PENDING, - 'job_code=?' => $jobCode, - ]); + $jobsToCleanup[] = $jobCode; } } + + if (count($jobsToCleanup) > 0) { + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $count = $scheduleResource->getConnection()->delete( + $scheduleResource->getMainTable(), + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, + ] + ); + + $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); + } } /** @@ -586,26 +681,103 @@ private function getCronExpression($jobConfig) */ private function cleanupScheduleMismatches() { + /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ + $scheduleResource = $this->_scheduleFactory->create()->getResource(); foreach ($this->invalid as $jobCode => $scheduledAtList) { - /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ - $scheduleResource = $this->_scheduleFactory->create()->getResource(); - $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ - 'status=?' => Schedule::STATUS_PENDING, - 'job_code=?' => $jobCode, - 'scheduled_at in (?)' => $scheduledAtList, - ]); + $scheduleResource->getConnection()->delete( + $scheduleResource->getMainTable(), + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code = ?' => $jobCode, + 'scheduled_at in (?)' => $scheduledAtList, + ] + ); } return $this; } /** - * @return array + * Get CronGroup Configuration Value + * + * @param $groupId + * @return int + */ + private function getCronGroupConfigurationValue($groupId, $path) + { + return $this->_scopeConfig->getValue( + 'system/cron/' . $groupId . '/' . $path, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is Group In Filter + * + * @param $groupId + * @return bool + */ + private function isGroupInFilter($groupId): bool + { + return !($this->_request->getParam('group') !== null + && trim($this->_request->getParam('group'), "'") !== $groupId); + } + + /** + * Process pending jobs + * + * @param $groupId + * @param $jobsRoot + * @param $currentTime + */ + private function processPendingJobs($groupId, $jobsRoot, $currentTime) + { + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); + /** @var \Magento\Cron\Model\Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run + continue; + } + $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; + if (!$jobConfig) { + continue; + } + + $scheduledTime = strtotime($schedule->getScheduledAt()); + if ($scheduledTime > $currentTime) { + continue; + } + + try { + if ($schedule->tryLockJob()) { + $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); + } + } catch (\Exception $e) { + $this->processError($schedule, $e); + } + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } + $schedule->save(); + } + } + + /** + * @param Schedule $schedule + * @param \Exception $exception + * @return void */ - private function getJobs() + private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) { - if ($this->jobs === null) { - $this->jobs = $this->_config->getJobs(); + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { + $this->logger->critical($exception); + } + if ($schedule->getStatus() === Schedule::STATUS_MISSED + && $this->state->getMode() === State::MODE_DEVELOPER + ) { + $this->logger->info($schedule->getMessages()); } - return $this->jobs; } } diff --git a/app/code/Magento/Cron/Test/Mftf/LICENSE.txt b/app/code/Magento/Cron/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Cron/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 <insert your license name here>" 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/app/code/Magento/Cron/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Cron/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Cron/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 <insert your license name here>" 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/app/code/Magento/Cron/Test/Mftf/README.md b/app/code/Magento/Cron/Test/Mftf/README.md new file mode 100644 index 0000000000000..76e02eadfb055 --- /dev/null +++ b/app/code/Magento/Cron/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Cron Functional Tests + +The Functional Test Module for **Magento Cron** module. diff --git a/app/code/Magento/Cron/Test/Unit/Console/Command/CronCommandTest.php b/app/code/Magento/Cron/Test/Unit/Console/Command/CronCommandTest.php index 8b3e50d6afb3a..6b1af9323cc93 100644 --- a/app/code/Magento/Cron/Test/Unit/Console/Command/CronCommandTest.php +++ b/app/code/Magento/Cron/Test/Unit/Console/Command/CronCommandTest.php @@ -6,19 +6,74 @@ namespace Magento\Cron\Test\Unit\Console\Command; use Magento\Cron\Console\Command\CronCommand; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManagerFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use Symfony\Component\Console\Tester\CommandTester; class CronCommandTest extends \PHPUnit\Framework\TestCase { + /** + * @var ObjectManagerFactory|MockObject + */ + private $objectManagerFactory; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + protected function setUp() + { + $this->objectManagerFactory = $this->createMock(ObjectManagerFactory::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + } + + /** + * Test command with disables cron + * + * @return void + */ + public function testExecuteWithDisabledCrons() + { + $this->objectManagerFactory->expects($this->never()) + ->method('create'); + $this->deploymentConfigMock->expects($this->once()) + ->method('get') + ->with('cron/enabled', 1) + ->willReturn(0); + $commandTester = new CommandTester( + new CronCommand($this->objectManagerFactory, $this->deploymentConfigMock) + ); + $commandTester->execute([]); + $expectedMsg = 'Cron is disabled. Jobs were not run.' . PHP_EOL; + $this->assertEquals($expectedMsg, $commandTester->getDisplay()); + } + + /** + * Test command with enabled cron + * + * @return void + */ public function testExecute() { - $objectManagerFactory = $this->createMock(\Magento\Framework\App\ObjectManagerFactory::class); $objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); $cron = $this->createMock(\Magento\Framework\App\Cron::class); - $objectManager->expects($this->once())->method('create')->willReturn($cron); - $cron->expects($this->once())->method('launch'); - $objectManagerFactory->expects($this->once())->method('create')->willReturn($objectManager); - $commandTester = new CommandTester(new CronCommand($objectManagerFactory)); + $objectManager->expects($this->once()) + ->method('create') + ->willReturn($cron); + $cron->expects($this->once()) + ->method('launch'); + $this->objectManagerFactory->expects($this->once()) + ->method('create') + ->willReturn($objectManager); + $this->deploymentConfigMock->expects($this->once()) + ->method('get') + ->with('cron/enabled', 1) + ->willReturn(1); + $commandTester = new CommandTester( + new CronCommand($this->objectManagerFactory, $this->deploymentConfigMock) + ); $commandTester->execute([]); $expectedMsg = 'Ran jobs by schedule.' . PHP_EOL; $this->assertEquals($expectedMsg, $commandTester->getDisplay()); diff --git a/app/code/Magento/Cron/Test/Unit/Model/CronJobException.php b/app/code/Magento/Cron/Test/Unit/Model/CronJobException.php index c50afa0e6f0d1..6954fe49fdc43 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/CronJobException.php +++ b/app/code/Magento/Cron/Test/Unit/Model/CronJobException.php @@ -12,8 +12,27 @@ class CronJobException { + /** + * @var \Throwable|null + */ + private $exception; + + /** + * @param \Throwable|null $exception + */ + public function __construct(\Throwable $exception = null) + { + $this->exception = $exception; + } + + /** + * @throws \Throwable + */ public function execute() { - throw new \Exception('Test exception'); + if (!$this->exception) { + $this->exception = new \Exception('Test exception'); + } + throw $this->exception; } } diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index e9f4c61c7f551..76e9627ad7098 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -6,6 +6,9 @@ namespace Magento\Cron\Test\Unit\Model; use Magento\Cron\Model\Schedule; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** * Class \Magento\Cron\Test\Unit\Model\ObserverTest @@ -18,11 +21,27 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase */ protected $helper; + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule + */ protected $resourceJobMock; + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $timezoneConverter; + + /** + * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFactory; + + /** + * @inheritdoc + */ protected function setUp() { - $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->helper = new ObjectManager($this); $this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class) ->disableOriginalConstructor() @@ -32,18 +51,30 @@ protected function setUp() $this->resourceJobMock->expects($this->any()) ->method('getIdFieldName') ->will($this->returnValue('id')); + + $this->timezoneConverter = $this->getMockBuilder(TimezoneInterface::class) + ->setMethods(['date']) + ->getMockForAbstractClass(); + + $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->setMethods(['create']) + ->getMock(); } /** + * Test for SetCronExpr + * * @param string $cronExpression * @param array $expected + * + * @return void * @dataProvider setCronExprDataProvider */ public function testSetCronExpr($cronExpression, $expected) { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); @@ -61,7 +92,7 @@ public function testSetCronExpr($cronExpression, $expected) * * @return array */ - public function setCronExprDataProvider() + public function setCronExprDataProvider(): array { return [ ['1 2 3 4 5', [1, 2, 3, 4, 5]], @@ -121,27 +152,33 @@ public function setCronExprDataProvider() } /** + * Test for SetCronExprException + * * @param string $cronExpression + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider setCronExprExceptionDataProvider */ public function testSetCronExprException($cronExpression) { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); } /** + * Data provider + * * Here is a list of allowed characters and values for Cron expression * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm * * @return array */ - public function setCronExprExceptionDataProvider() + public function setCronExprExceptionDataProvider(): array { return [ [''], @@ -153,17 +190,31 @@ public function setCronExprExceptionDataProvider() } /** + * Test for trySchedule + * * @param int $scheduledAt * @param array $cronExprArr * @param $expected + * + * @return void * @dataProvider tryScheduleDataProvider */ public function testTrySchedule($scheduledAt, $cronExprArr, $expected) { // 1. Create mocks + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); + /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( - \Magento\Cron\Model\Schedule::class + \Magento\Cron\Model\Schedule::class, + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -177,22 +228,29 @@ public function testTrySchedule($scheduledAt, $cronExprArr, $expected) $this->assertEquals($expected, $result); } + /** + * Test for tryScheduleWithConversionToAdminStoreTime + * + * @return void + */ public function testTryScheduleWithConversionToAdminStoreTime() { $scheduledAt = '2011-12-13 14:15:16'; $cronExprArr = ['*', '*', '*', '*', '*']; - // 1. Create mocks - $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); - $timezoneConverter->expects($this->once()) - ->method('date') - ->with($scheduledAt) - ->willReturn(new \DateTime($scheduledAt)); + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( \Magento\Cron\Model\Schedule::class, - ['timezoneConverter' => $timezoneConverter] + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -207,11 +265,15 @@ public function testTryScheduleWithConversionToAdminStoreTime() } /** + * Data provider + * * @return array */ - public function tryScheduleDataProvider() + public function tryScheduleDataProvider(): array { $date = '2011-12-13 14:15:16'; + $timestamp = (new \DateTime($date))->getTimestamp(); + $day = 'Monday'; return [ [$date, [], false], [$date, null, false], @@ -219,19 +281,23 @@ public function tryScheduleDataProvider() [$date, [], false], [$date, null, false], [$date, false, false], - [strtotime($date), ['*', '*', '*', '*', '*'], true], - [strtotime($date), ['15', '*', '*', '*', '*'], true], - [strtotime($date), ['*', '14', '*', '*', '*'], true], - [strtotime($date), ['*', '*', '13', '*', '*'], true], - [strtotime($date), ['*', '*', '*', '12', '*'], true], - [strtotime('Monday'), ['*', '*', '*', '*', '1'], true], + [$timestamp, ['*', '*', '*', '*', '*'], true], + [$timestamp, ['15', '*', '*', '*', '*'], true], + [$timestamp, ['*', '14', '*', '*', '*'], true], + [$timestamp, ['*', '*', '13', '*', '*'], true], + [$timestamp, ['*', '*', '*', '12', '*'], true], + [(new \DateTime($day))->getTimestamp(), ['*', '*', '*', '*', '1'], true], ]; } /** + * Test for matchCronExpression + * * @param string $cronExpressionPart * @param int $dateTimePart * @param bool $expectedResult + * + * @return void * @dataProvider matchCronExpressionDataProvider */ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult) @@ -248,9 +314,11 @@ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $exp } /** + * Data provider + * * @return array */ - public function matchCronExpressionDataProvider() + public function matchCronExpressionDataProvider(): array { return [ ['*', 0, true], @@ -287,7 +355,11 @@ public function matchCronExpressionDataProvider() } /** + * Test for matchCronExpressionException + * * @param string $cronExpressionPart + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider matchCronExpressionExceptionDataProvider */ @@ -304,9 +376,11 @@ public function testMatchCronExpressionException($cronExpressionPart) } /** + * Data provider + * * @return array */ - public function matchCronExpressionExceptionDataProvider() + public function matchCronExpressionExceptionDataProvider(): array { return [ ['1/2/3'], //Invalid cron expression, expecting 'match/modulus': 1/2/3 @@ -317,8 +391,12 @@ public function matchCronExpressionExceptionDataProvider() } /** + * Test for GetNumeric + * * @param mixed $param * @param int $expectedResult + * + * @return void * @dataProvider getNumericDataProvider */ public function testGetNumeric($param, $expectedResult) @@ -335,9 +413,11 @@ public function testGetNumeric($param, $expectedResult) } /** + * Data provider + * * @return array */ - public function getNumericDataProvider() + public function getNumericDataProvider(): array { return [ [null, false], @@ -362,6 +442,11 @@ public function getNumericDataProvider() ]; } + /** + * Test for tryLockJobSuccess + * + * @return void + */ public function testTryLockJobSuccess() { $scheduleId = 1; @@ -386,6 +471,11 @@ public function testTryLockJobSuccess() $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus()); } + /** + * Test for tryLockJobFailure + * + * @return void + */ public function testTryLockJobFailure() { $scheduleId = 1; diff --git a/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php new file mode 100644 index 0000000000000..703926b4c0116 --- /dev/null +++ b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cron\Test\Unit\Model\System\Config\Initial; + +use Magento\Cron\Model\Groups\Config\Data as GroupsConfigModel; +use Magento\Cron\Model\System\Config\Initial\Converter as ConverterPlugin; +use Magento\Framework\App\Config\Initial\Converter; + +/** + * Class ConverterTest + * + * Unit test for \Magento\Cron\Model\System\Config\Initial\Converter + */ +class ConverterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GroupsConfigModel|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupsConfigMock; + + /** + * @var Converter|\PHPUnit_Framework_MockObject_MockObject + */ + private $converterMock; + + /** + * @var ConverterPlugin + */ + private $converterPlugin; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->groupsConfigMock = $this->getMockBuilder( + GroupsConfigModel::class + )->disableOriginalConstructor()->getMock(); + $this->converterMock = $this->getMockBuilder(Converter::class)->getMock(); + $this->converterPlugin = new ConverterPlugin($this->groupsConfigMock); + } + + /** + * Tests afterConvert method with no $result['data']['default']['system'] set + */ + public function testAfterConvertWithNoData() + { + $expectedResult = ['test']; + $this->groupsConfigMock->expects($this->never()) + ->method('get'); + + $result = $this->converterPlugin->afterConvert($this->converterMock, $expectedResult); + + self::assertSame($expectedResult, $result); + } + + /** + * Tests afterConvert method with $result['data']['default']['system'] set + */ + public function testAfterConvertWithData() + { + $groups = [ + 'group1' => ['val1' => ['value' => '1']], + 'group2' => ['val2' => ['value' => '2']] + ]; + $expectedResult['data']['default']['system']['cron'] = [ + 'group1' => [ + 'val1' => '1' + ], + 'group2' => [ + 'val2' => '2' + ] + ]; + $result['data']['default']['system']['cron'] = '1'; + + $this->groupsConfigMock->expects($this->once()) + ->method('get') + ->willReturn($groups); + + $result = $this->converterPlugin->afterConvert($this->converterMock, $result); + + self::assertEquals($expectedResult, $result); + } +} diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index 0db6a598fb56f..213071f013a67 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -8,6 +8,7 @@ use Magento\Cron\Model\Schedule; use Magento\Cron\Observer\ProcessCronQueueObserver as ProcessCronQueueObserver; use Magento\Framework\App\State; +use Magento\Framework\Profiler\Driver\Standard\StatFactory; /** * Class \Magento\Cron\Test\Unit\Model\ObserverTest @@ -84,6 +85,11 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase */ protected $appStateMock; + /** + * @var \Magento\Framework\Lock\LockManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $lockManagerMock; + /** * @var \Magento\Cron\Model\ResourceModel\Schedule|\PHPUnit_Framework_MockObject_MockObject */ @@ -116,6 +122,7 @@ protected function setUp() )->disableOriginalConstructor()->getMock(); $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); + $this->_scheduleFactory = $this->getMockBuilder( \Magento\Cron\Model\ScheduleFactory::class )->setMethods( @@ -135,6 +142,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->lockManagerMock = $this->getMockBuilder(\Magento\Framework\Lock\LockManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->lockManagerMock->method('lock')->willReturn(true); + $this->lockManagerMock->method('unlock')->willReturn(true); + $this->observer = $this->createMock(\Magento\Framework\Event\Observer::class); $this->dateTimeMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) @@ -159,6 +172,16 @@ protected function setUp() $this->scheduleResource->method('getConnection')->willReturn($connection); $connection->method('delete')->willReturn(1); + $this->statFactory = $this->getMockBuilder(StatFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->stat = $this->getMockBuilder(\Magento\Framework\Profiler\Driver\Standard\Stat::class) + ->disableOriginalConstructor() + ->getMock(); + $this->statFactory->expects($this->any())->method('create')->willReturn($this->stat); + $this->_observer = new ProcessCronQueueObserver( $this->_objectManager, $this->_scheduleFactory, @@ -170,41 +193,23 @@ protected function setUp() $this->dateTimeMock, $phpExecutableFinderFactory, $this->loggerMock, - $this->appStateMock + $this->appStateMock, + $this->statFactory, + $this->lockManagerMock ); } - /** - * Test case without saved cron jobs in data base - */ - public function testDispatchNoPendingJobs() - { - $lastRun = $this->time + 10000000; - $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); - $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); - - $this->_config->expects($this->once())->method('getJobs')->will($this->returnValue([])); - - $scheduleMock = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } - /** * Test case for not existed cron jobs in files but in data base is presented */ public function testDispatchNoJobConfig() { $lastRun = $this->time + 10000000; - $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); - $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); + $this->_cache->expects($this->atLeastOnce())->method('load')->will($this->returnValue($lastRun)); + $this->_scopeConfig->expects($this->atLeastOnce())->method('getValue')->will($this->returnValue(0)); $this->_config->expects( - $this->any() + $this->atLeastOnce() )->method( 'getJobs' )->will( @@ -212,16 +217,21 @@ public function testDispatchNoJobConfig() ); $schedule = $this->createPartialMock(\Magento\Cron\Model\Schedule::class, ['getJobCode', '__wakeup']); - $schedule->expects($this->once())->method('getJobCode')->will($this->returnValue('not_existed_job_code')); + $schedule->expects($this->atLeastOnce()) + ->method('getJobCode') + ->will($this->returnValue('not_existed_job_code')); $this->_collection->addItem($schedule); $scheduleMock = $this->getMockBuilder( \Magento\Cron\Model\Schedule::class )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); + $scheduleMock->expects($this->atLeastOnce()) + ->method('getCollection') + ->will($this->returnValue($this->_collection)); + $this->_scheduleFactory->expects($this->atLeastOnce()) + ->method('create') + ->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -240,11 +250,13 @@ public function testDispatchCanNotLock() $schedule = $this->getMockBuilder( \Magento\Cron\Model\Schedule::class )->setMethods( - ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save'] + ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save', 'setFinishedAt'] )->disableOriginalConstructor()->getMock(); $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); - $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(false)); + $schedule->expects($this->never())->method('setFinishedAt'); + $abstractModel = $this->createMock(\Magento\Framework\Model\AbstractModel::class); $schedule->expects($this->any())->method('save')->will($this->returnValue($abstractModel)); $this->_collection->addItem($schedule); @@ -262,7 +274,9 @@ public function testDispatchCanNotLock() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->atLeastOnce()) + ->method('create') + ->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -272,10 +286,8 @@ public function testDispatchCanNotLock() */ public function testDispatchExceptionTooLate() { - $exceptionMessage = 'Too late for the schedule'; - $scheduleId = 42; + $exceptionMessage = 'Cron Job test_job1 is missed at 2017-07-30 15:00:00'; $jobCode = 'test_job1'; - $exception = $exceptionMessage . ' Schedule Id: ' . $scheduleId . ' Job Code: ' . $jobCode; $lastRun = $this->time + 10000000; $this->_cache->expects($this->any())->method('load')->willReturn($lastRun); @@ -299,25 +311,25 @@ public function testDispatchExceptionTooLate() 'getScheduleId', ] )->disableOriginalConstructor()->getMock(); - $schedule->expects($this->any())->method('getJobCode')->willReturn($jobCode); - $schedule->expects($this->once())->method('getScheduledAt')->willReturn($dateScheduledAt); + $schedule->expects($this->atLeastOnce())->method('getJobCode')->willReturn($jobCode); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->willReturn($dateScheduledAt); $schedule->expects($this->once())->method('tryLockJob')->willReturn(true); $schedule->expects( - $this->once() + $this->any() )->method( 'setStatus' )->with( $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_MISSED) )->willReturnSelf(); $schedule->expects($this->once())->method('setMessages')->with($this->equalTo($exceptionMessage)); - $schedule->expects($this->any())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); - $schedule->expects($this->once())->method('getMessages')->willReturn($exceptionMessage); - $schedule->expects($this->once())->method('getScheduleId')->willReturn($scheduleId); + $schedule->expects($this->atLeastOnce())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); + $schedule->expects($this->atLeastOnce())->method('getMessages')->willReturn($exceptionMessage); $schedule->expects($this->once())->method('save'); $this->appStateMock->expects($this->once())->method('getMode')->willReturn(State::MODE_DEVELOPER); - $this->loggerMock->expects($this->once())->method('info')->with($exception); + $this->loggerMock->expects($this->once())->method('info') + ->with('Cron Job test_job1 is missed at 2017-07-30 15:00:00'); $this->_collection->addItem($schedule); @@ -333,7 +345,7 @@ public function testDispatchExceptionTooLate() ->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->willReturn($this->_collection); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->willReturn($scheduleMock); + $this->_scheduleFactory->expects($this->atLeastOnce())->method('create')->willReturn($scheduleMock); $this->_observer->execute($this->observer); } @@ -343,7 +355,8 @@ public function testDispatchExceptionTooLate() */ public function testDispatchExceptionNoCallback() { - $exceptionMessage = 'No callbacks found'; + $jobCode = 'test_job1'; + $exceptionMessage = sprintf('No callbacks found for cron job %s', $jobCode); $exception = new \Exception(__($exceptionMessage)); $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); @@ -352,7 +365,7 @@ public function testDispatchExceptionNoCallback() )->setMethods( ['getJobCode', 'tryLockJob', 'getScheduledAt', 'save', 'setStatus', 'setMessages', '__wakeup', 'getStatus'] )->disableOriginalConstructor()->getMock(); - $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); + $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue($jobCode)); $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(true)); $schedule->expects( @@ -372,7 +385,7 @@ public function testDispatchExceptionNoCallback() $this->loggerMock->expects($this->once())->method('critical')->with($exception); - $jobConfig = ['test_group' => ['test_job1' => ['instance' => 'Some_Class']]]; + $jobConfig = ['test_group' => [$jobCode => ['instance' => 'Some_Class']]]; $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); @@ -388,7 +401,7 @@ public function testDispatchExceptionNoCallback() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -453,7 +466,7 @@ public function testDispatchExceptionInCallback( )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); $this->_objectManager ->expects($this->once()) ->method('create') @@ -468,6 +481,7 @@ public function testDispatchExceptionInCallback( */ public function dispatchExceptionInCallbackDataProvider() { + $throwable = new \TypeError(); return [ 'non-callable callback' => [ 'Not_Existed_Class', @@ -483,6 +497,19 @@ public function dispatchExceptionInCallbackDataProvider() 2, new \Exception(__('Test exception')) ], + 'throwable in execution' => [ + 'CronJobException', + new \Magento\Cron\Test\Unit\Model\CronJobException( + $throwable + ), + 'Error when running a cron job', + 2, + new \RuntimeException( + 'Error when running a cron job', + 0, + $throwable + ) + ], ]; } @@ -515,23 +542,22 @@ public function testDispatchRunJob() $scheduleMethods )->disableOriginalConstructor()->getMock(); $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); - $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(true)); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); + $schedule->expects($this->atLeastOnce())->method('tryLockJob')->will($this->returnValue(true)); + $schedule->expects($this->any())->method('setFinishedAt')->willReturnSelf(); // cron start to execute some job $schedule->expects($this->any())->method('setExecutedAt')->will($this->returnSelf()); - $schedule->expects($this->at(5))->method('save'); + $schedule->expects($this->atLeastOnce())->method('save'); // cron end execute some job $schedule->expects( - $this->at(6) + $this->atLeastOnce() )->method( 'setStatus' )->with( $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_SUCCESS) - )->will( - $this->returnSelf() - ); + )->willReturnSelf(); $schedule->expects($this->at(8))->method('save'); @@ -550,7 +576,7 @@ public function testDispatchRunJob() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once(2))->method('create')->will($this->returnValue($scheduleMock)); $testCronJob = $this->getMockBuilder('CronJob')->setMethods(['execute'])->getMock(); $testCronJob->expects($this->atLeastOnce())->method('execute')->with($schedule); @@ -585,6 +611,8 @@ public function testDispatchNotGenerate() )->will( $this->returnValue(['test_group' => []]) ); + $this->_config->expects($this->at(2))->method('getJobs')->will($this->returnValue($jobConfig)); + $this->_config->expects($this->at(3))->method('getJobs')->will($this->returnValue($jobConfig)); $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); $this->_cache->expects( $this->at(0) @@ -654,6 +682,8 @@ public function testDispatchGenerate() ]; $this->_config->expects($this->at(0))->method('getJobs')->willReturn($jobConfig); $this->_config->expects($this->at(1))->method('getJobs')->willReturn($jobs); + $this->_config->expects($this->at(2))->method('getJobs')->willReturn($jobs); + $this->_config->expects($this->at(3))->method('getJobs')->willReturn($jobs); $this->_request->expects($this->any())->method('getParam')->willReturn('default'); $this->_cache->expects( $this->at(0) @@ -730,7 +760,7 @@ public function testDispatchCleanup() $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); $this->_collection->addItem($schedule); - $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); + $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); $this->_cache->expects($this->at(0))->method('load')->will($this->returnValue($this->time + 10000000)); $this->_cache->expects($this->at(1))->method('load')->will($this->returnValue($this->time - 10000000)); @@ -757,7 +787,7 @@ public function testDispatchCleanup() )->setMethods(['getCollection', 'getResource'])->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->at(1))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -781,55 +811,17 @@ public function testMissedJobsCleanedInTime() $this->_cache->expects($this->at(2))->method('load')->will($this->returnValue($this->time + 10000000)); $this->_scheduleFactory->expects($this->at(2))->method('create')->will($this->returnValue($scheduleMock)); - // This item was scheduled 2 days and 2 hours ago - $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 180000); - /** @var \Magento\Cron\Model\Schedule|\PHPUnit_Framework_MockObject_MockObject $schedule1 */ - $schedule1 = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->setMethods( - ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] - )->getMock(); - $schedule1->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); - $schedule1->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule1->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); - //we expect this job be deleted from the list - $schedule1->expects($this->once())->method('delete')->will($this->returnValue(true)); - $this->_collection->addItem($schedule1); - - // This item was scheduled 1 day ago - $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); - $schedule2 = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->setMethods( - ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] - )->getMock(); - $schedule2->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); - $schedule2->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule2->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); - //we don't expect this job be deleted from the list - $schedule2->expects($this->never())->method('delete'); - $this->_collection->addItem($schedule2); - - $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); - - $this->_scopeConfig->expects($this->at(0))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_cleanup_every')) - ->will($this->returnValue(10)); - $this->_scopeConfig->expects($this->at(1))->method('getValue') - ->with($this->equalTo('system/cron/test_group/schedule_lifetime')) - ->will($this->returnValue(2*24*60)); - $this->_scopeConfig->expects($this->at(2))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_success_lifetime')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(3))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_failure_lifetime')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(4))->method('getValue') - ->with($this->equalTo('system/cron/test_group/schedule_generate_every')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(5))->method('getValue') - ->with($this->equalTo('system/cron/test_group/use_separate_process')) - ->will($this->returnValue(0)); + $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); + + $this->_scopeConfig->expects($this->any())->method('getValue') + ->willReturnMap([ + ['system/cron/test_group/use_separate_process', 0], + ['system/cron/test_group/history_cleanup_every', 10], + ['system/cron/test_group/schedule_lifetime', 2*24*60], + ['system/cron/test_group/history_success_lifetime', 0], + ['system/cron/test_group/history_failure_lifetime', 0], + ['system/cron/test_group/schedule_generate_every', 0], + ]); $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json index ef6b580bfe7d0..916ce4f964e78 100644 --- a/app/code/Magento/Cron/composer.json +++ b/app/code/Magento/Cron/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-cron", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/framework": "101.0.*" }, @@ -10,7 +10,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.9", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Cron/etc/cron_groups.xml b/app/code/Magento/Cron/etc/cron_groups.xml index a01426eab723e..9aa57662427c8 100644 --- a/app/code/Magento/Cron/etc/cron_groups.xml +++ b/app/code/Magento/Cron/etc/cron_groups.xml @@ -11,8 +11,8 @@ <schedule_ahead_for>20</schedule_ahead_for> <schedule_lifetime>15</schedule_lifetime> <history_cleanup_every>10</history_cleanup_every> - <history_success_lifetime>10080</history_success_lifetime> - <history_failure_lifetime>10080</history_failure_lifetime> + <history_success_lifetime>60</history_success_lifetime> + <history_failure_lifetime>4320</history_failure_lifetime> <use_separate_process>0</use_separate_process> </group> </config> diff --git a/app/code/Magento/Cron/etc/di.xml b/app/code/Magento/Cron/etc/di.xml index a37f3760b70a5..3e3bdc2053576 100644 --- a/app/code/Magento/Cron/etc/di.xml +++ b/app/code/Magento/Cron/etc/di.xml @@ -16,6 +16,18 @@ <type name="Magento\Framework\App\Config\Initial\Converter"> <plugin name="cron_system_config_initial_converter_plugin" type="Magento\Cron\Model\System\Config\Initial\Converter" /> </type> + <virtualType name="Magento\Cron\Model\VirtualLoggerHandler" type="Magento\Framework\Logger\Handler\Base"> + <arguments> + <argument name="fileName" xsi:type="string">/var/log/cron.log</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Cron\Model\VirtualLogger" type="Magento\Framework\Logger\Monolog"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="system" xsi:type="object">Magento\Cron\Model\VirtualLoggerHandler</item> + </argument> + </arguments> + </virtualType> <!-- @api --> <virtualType name="shellBackground" type="Magento\Framework\Shell"> <arguments> @@ -25,6 +37,7 @@ <type name="Magento\Cron\Observer\ProcessCronQueueObserver"> <arguments> <argument name="shell" xsi:type="object">shellBackground</argument> + <argument name="logger" xsi:type="object">Magento\Cron\Model\VirtualLogger</argument> </arguments> </type> <type name="Magento\Framework\Console\CommandListInterface"> diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php index c740b17ed008c..ec73ac0cf7aa5 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php @@ -20,7 +20,7 @@ class Currency extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'system/currency/rates.phtml'; + protected $_template = 'Magento_CurrencySymbol::system/currency/rates.phtml'; /** * Prepare layout diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php index 80415c9486898..e20054a5a8084 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Matrix.php @@ -16,7 +16,7 @@ class Matrix extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'system/currency/rate/matrix.phtml'; + protected $_template = 'Magento_CurrencySymbol::system/currency/rate/matrix.phtml'; /** * @var \Magento\Directory\Model\CurrencyFactory diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Services.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Services.php index 919a4d0ed6d7b..491ed93900bde 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Services.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency/Rate/Services.php @@ -16,7 +16,7 @@ class Services extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'system/currency/rate/services.phtml'; + protected $_template = 'Magento_CurrencySymbol::system/currency/rate/services.phtml'; /** * @var \Magento\Directory\Model\Currency\Import\Source\ServiceFactory diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php index ae13c4d399e47..3ab1bfc086721 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php @@ -7,15 +7,22 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\Exception\NotFoundException; + class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency { /** * Save rates action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $data = $this->getRequest()->getParam('rate'); if (is_array($data)) { try { diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php index eee7961b02f4a..ad80833d8da5d 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php @@ -6,15 +6,22 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; +use Magento\Framework\Exception\NotFoundException; + class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol { /** * Save custom Currency symbol * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $symbolsDataArray = $this->getRequest()->getParam('custom_currency_symbol', null); if (is_array($symbolsDataArray)) { foreach ($symbolsDataArray as &$symbolsData) { @@ -27,9 +34,9 @@ public function execute() try { $this->_objectManager->create(\Magento\CurrencySymbol\Model\System\Currencysymbol::class) ->setCurrencySymbolsData($symbolsDataArray); - $this->messageManager->addSuccess(__('You applied the custom currency symbols.')); + $this->messageManager->addSuccessMessage(__('You applied the custom currency symbols.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php index fcde688a1e145..518e7fcf4181f 100644 --- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php +++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php @@ -192,9 +192,11 @@ public function getCurrencySymbolsData() */ public function setCurrencySymbolsData($symbols = []) { - foreach ($this->getCurrencySymbolsData() as $code => $values) { - if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { - unset($symbols[$code]); + if (!$this->_storeManager->isSingleStoreMode()) { + foreach ($this->getCurrencySymbolsData() as $code => $values) { + if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { + unset($symbols[$code]); + } } } $value = []; diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetBaseCurrencyActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetBaseCurrencyActionGroup.xml new file mode 100644 index 0000000000000..1dc9b83b45008 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetBaseCurrencyActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Set base currency --> + <actionGroup name="AdminSetBaseCurrencyActionGroup" extends="AdminSaveConfigActionGroup"> + <arguments> + <argument name="currency" type="string"/> + </arguments> + <uncheckOption selector="{{CurrencySetupSection.baseCurrencyUseDefault}}" before="clickSaveConfigBtn" stepKey="uncheckUseDefaultOption"/> + <selectOption selector="{{CurrencySetupSection.baseCurrency}}" userInput="{{currency}}" after="uncheckUseDefaultOption" stepKey="setBaseCurrencyField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/LICENSE.txt b/app/code/Magento/CurrencySymbol/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/CurrencySymbol/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 <insert your license name here>" 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/app/code/Magento/CurrencySymbol/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/CurrencySymbol/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/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 <insert your license name here>" 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/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml new file mode 100644 index 0000000000000..e79ee3a8d0d6f --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="ConfigCurrencySetupPage" url="admin/system_config/edit/section/currency" area="admin" module="Magento_CurrencySymbol"> + <section name="CurrencySetupSection"/> + </page> +</pages> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/README.md b/app/code/Magento/CurrencySymbol/Test/Mftf/README.md new file mode 100644 index 0000000000000..5a927d934494a --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Currency Symbol Functional Tests + +The Functional Test Module for **Magento Currency Symbol** module. diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml new file mode 100644 index 0000000000000..b07b823200889 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CurrencySetupSection"> + <element name="baseCurrency" type="select" selector="#currency_options_base"/> + <element name="baseCurrencyUseDefault" type="checkbox" selector="#currency_options_base_inherit"/> + </section> +</sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 0863104a2bf8d..455449a449103 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -57,11 +57,18 @@ class SaveTest extends \PHPUnit\Framework\TestCase */ protected $filterManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->helperMock = $this->createMock(\Magento\Backend\Helper\Data::class); @@ -128,7 +135,7 @@ public function testExecute() ->willReturn($this->filterManagerMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You applied the custom currency symbols.')); $this->action->execute(); diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Model/System/CurrencysymbolTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Model/System/CurrencysymbolTest.php index 453a06651f354..0ae099fd78edc 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Model/System/CurrencysymbolTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Model/System/CurrencysymbolTest.php @@ -236,6 +236,9 @@ public function testGetCurrencySymbol( $this->assertEquals($expectedSymbol, $currencySymbol); } + /** + * @return array + */ public function getCurrencySymbolDataProvider() { return [ diff --git a/app/code/Magento/CurrencySymbol/composer.json b/app/code/Magento/CurrencySymbol/composer.json index e3548dba538e3..73fba69907c8d 100644 --- a/app/code/Magento/CurrencySymbol/composer.json +++ b/app/code/Magento/CurrencySymbol/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-currency-symbol", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-page-cache": "100.2.*", @@ -11,7 +11,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 7dca29263bae3..ddedf9ef7a467 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -3,64 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currencysymbol */ ?> - -<?php $block->getCurrencySymbolsData();?> - -<form id="currency-symbols-form" action="<?= /* @escapeNotVerified */ $block->getFormActionUrl() ?>" method="post"> - <input name="form_key" type="hidden" value="<?= /* @escapeNotVerified */ $block->getFormKey() ?>" /> +<form id="currency-symbols-form" action="<?= $block->escapeUrl($block->getFormActionUrl()) ?>" method="post"> + <input name="form_key" type="hidden" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> <fieldset class="admin__fieldset"> - <?php foreach ($block->getCurrencySymbolsData() as $code => $data): ?> + <?php foreach ($block->getCurrencySymbolsData() as $code => $data) : ?> <div class="admin__field _required"> - <label class="admin__field-label" for="custom_currency_symbol<?= /* @escapeNotVerified */ $code ?>"> - <span><?= /* @escapeNotVerified */ $code ?> (<?= /* @escapeNotVerified */ $data['displayName'] ?>)</span> + <label class="admin__field-label" for="custom_currency_symbol<?= $block->escapeHtmlAttr($code) ?>"> + <span><?= $block->escapeHtml($code) ?> (<?= $block->escapeHtml($data['displayName']) ?>)</span> </label> <div class="admin__field-control"> - <input id="custom_currency_symbol<?= /* @escapeNotVerified */ $code ?>" - class="required-entry admin__control-text" + <input id="custom_currency_symbol<?= $block->escapeHtmlAttr($code) ?>" + class="required-entry admin__control-text <?= $data['inherited'] ? 'disabled' : '' ?>" type="text" value="<?= $block->escapeHtmlAttr($data['displaySymbol']) ?>" - <?= $data['inherited'] ? ' disabled="disabled"' : '' ?> - name="custom_currency_symbol[<?= /* @escapeNotVerified */ $code ?>]"> + name="custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> <div class="admin__field admin__field-option"> - <input id="custom_currency_symbol_inherit<?= /* @escapeNotVerified */ $code ?>" + <input id="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>" class="admin__control-checkbox" type="checkbox" - onclick="toggleUseDefault(<?= /* @escapeNotVerified */ '\'' . $code . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" + onclick="toggleUseDefault(<?= '\'' . $block->escapeHtmlAttr($block->escapeJs($code)) . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" <?= $data['inherited'] ? ' checked="checked"' : '' ?> value="1" - name="inherit_custom_currency_symbol[<?= /* @escapeNotVerified */ $code ?>]"> - <label class="admin__field-label" for="custom_currency_symbol_inherit<?= /* @escapeNotVerified */ $code ?>"><span><?= /* @escapeNotVerified */ $block->getInheritText() ?></span></label> + name="inherit_custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> + <label class="admin__field-label" for="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>"><span><?= $block->escapeHtml($block->getInheritText()) ?></span></label> </div> </div> </div> <?php endforeach; ?> </fieldset> </form> -<script> -require(['jquery', "mage/mage", 'prototype'], function(jQuery){ - - jQuery('#currency-symbols-form').mage('form').mage('validation'); - - function toggleUseDefault(code, value) +<script type="text/x-magento-init"> { - checkbox = $('custom_currency_symbol_inherit'+code); - input = $('custom_currency_symbol'+code); - if (checkbox.checked) { - input.value = value; - input.disabled = true; - } else { - input.disabled = false; + "#currency-symbols-form": { + "Magento_CurrencySymbol/js/symbols-form": {} } } - - window.toggleUseDefault = toggleUseDefault; -}); </script> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml index 8e0abcb319764..b52be91a5b071 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml @@ -4,63 +4,58 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> -<?php /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate\Matrix */ -?> -<?php + $_oldRates = $block->getOldRates(); $_newRates = $block->getNewRates(); $_rates = ($_newRates) ? $_newRates : $_oldRates; ?> -<?php if (empty($_rates)): ?> - <div class="message message-warning warning"><p><?= /* @escapeNotVerified */ __('You must first configure currency options before being able to see currency rates.') ?></p></div> -<?php else: ?> - <form name="rateForm" id="rate-form" method="post" action="<?= /* @escapeNotVerified */ $block->getRatesFormAction() ?>"> +<?php if (empty($_rates)) : ?> + <div class="message message-warning warning"><p><?= $block->escapeHtml(__('You must first configure currency options before being able to see currency rates.')) ?></p></div> +<?php else : ?> + <form name="rateForm" id="rate-form" method="post" action="<?= $block->escapeUrl($block->getRatesFormAction()) ?>"> <?= $block->getBlockHtml('formkey') ?> <div class="admin__control-table-wrapper"> <table class="admin__control-table"> <thead> <tr> <th> </th> - <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode): ?> - <th><span><?= /* @escapeNotVerified */ $_currencyCode ?></span></th> + <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode) : ?> + <th><span><?= $block->escapeHtml($_currencyCode) ?></span></th> <?php endforeach; ?> </tr> </thead> - <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode): ?> + <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode) : ?> <tr> - <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])): ?> - <?php foreach ($_rates[$_currencyCode] as $_rate => $_value): ?> - <?php if (++$_j == 1): ?> - <td><span class="admin__control-support-text"><?= /* @escapeNotVerified */ $_currencyCode ?></span></td> + <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])) : ?> + <?php foreach ($_rates[$_currencyCode] as $_rate => $_value) : ?> + <?php if (++$_j == 1) : ?> + <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?></span></td> <td> <input type="text" - name="rate[<?= /* @escapeNotVerified */ $_currencyCode ?>][<?= /* @escapeNotVerified */ $_rate ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $_value : (isset($_oldRates[$_currencyCode][$_rate]) ? $_oldRates[$_currencyCode][$_rate] : '')) ?>" + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> <?php endif; ?> </td> - <?php else: ?> + <?php else : ?> <td> <input type="text" - name="rate[<?= /* @escapeNotVerified */ $_currencyCode ?>][<?= /* @escapeNotVerified */ $_rate ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $_value : (isset($_oldRates[$_currencyCode][$_rate]) ? $_oldRates[$_currencyCode][$_rate] : '')) ?>" - class="admin__control-text" - <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" + class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> + <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> <?php endif; ?> </td> <?php endif; ?> - <?php endforeach; $_j = 0; ?> + <?php endforeach; ?> + <?php $_j = 0; ?> <?php endif; ?> </tr> <?php endforeach; ?> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml index 985831c5de79f..916b6f8d54182 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/services.phtml @@ -4,16 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> -<?php /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate\Services */ ?> <div class="admin__field"> - <label class="admin__field-label"><span><?= /* @escapeNotVerified */ __('Import Service') ?></span></label> + <label class="admin__field-label"><span><?= $block->escapeHtml(__('Import Service')) ?></span></label> <div class="admin__field-control"> <?= $block->getChildHtml('import_services') ?> </div> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml index 42b2affa2c176..db00909e14892 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rates.phtml @@ -4,16 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> -<?php /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency */ ?> -<form action="<?= /* @escapeNotVerified */ $block->getImportFormAction() ?>" method="post" class="import-service"> +<form action="<?= $block->escapeUrl($block->getImportFormAction()) ?>" method="post" class="import-service"> <?= $block->getBlockHtml('formkey') ?> <fieldset class="admin__fieldset admin__fieldset-import-service"> <?= $block->getServicesHtml() ?> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js b/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js new file mode 100644 index 0000000000000..68f914ddb1b4d --- /dev/null +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js @@ -0,0 +1,39 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element) + .mage('form') + .mage('validation'); + + /** + * Toggle the field to use the default value + * + * @param {String} code + * @param {String} value + */ + function toggleUseDefault(code, value) { + var checkbox = $('#custom_currency_symbol_inherit' + code), + input = $('#custom_currency_symbol' + code); + + if (checkbox.is(':checked')) { + input.addClass('disabled'); + input.val(value); + input.prop('readonly', true); + } else { + input.removeClass('disabled'); + input.prop('readonly', false); + } + } + + window.toggleUseDefault = toggleUseDefault; + }; +}); diff --git a/app/code/Magento/Customer/Api/AccountDelegationInterface.php b/app/code/Magento/Customer/Api/AccountDelegationInterface.php new file mode 100644 index 0000000000000..f1b3cba769f8a --- /dev/null +++ b/app/code/Magento/Customer/Api/AccountDelegationInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Api; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Controller\Result\Redirect; + +/** + * Delegating account actions from outside of customer module. + */ +interface AccountDelegationInterface +{ + /** + * Create redirect to default new account form. + * + * @param CustomerInterface $customer Pre-filled customer data. + * @param array|null $mixedData Add this data to new-customer event + * if the new customer is created. + * + * @return Redirect + */ + public function createRedirectForNew( + CustomerInterface $customer, + array $mixedData = null + ): Redirect; +} diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index d2f9fb7ebc420..0bda1fc4bb815 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -7,6 +7,8 @@ namespace Magento\Customer\Api; +use Magento\Framework\Exception\InputException; + /** * Interface for managing customers accounts. * @api @@ -144,19 +146,24 @@ public function initiatePasswordReset($email, $template, $websiteId = null); /** * Reset customer password. * - * @param string $email + * @param string $email If empty value given then the customer + * will be matched by the RP token. * @param string $resetToken * @param string $newPassword + * * @return bool true on success * @throws \Magento\Framework\Exception\LocalizedException + * @throws InputException */ public function resetPassword($email, $resetToken, $newPassword); /** * Check if password reset token is valid. * - * @param int $customerId + * @param int $customerId If 0 is given then a customer + * will be matched by the RP token. * @param string $resetPasswordLinkToken + * * @return bool True if the token is valid * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired diff --git a/app/code/Magento/Customer/Block/Account/Dashboard/Info.php b/app/code/Magento/Customer/Block/Account/Dashboard/Info.php index ded7238edc755..87132c3afb8bc 100644 --- a/app/code/Magento/Customer/Block/Account/Dashboard/Info.php +++ b/app/code/Magento/Customer/Block/Account/Dashboard/Info.php @@ -102,7 +102,7 @@ public function getSubscriptionObject() $this->_subscription = $this->_createSubscriber(); $customer = $this->getCustomer(); if ($customer) { - $this->_subscription->loadByEmail($customer->getEmail()); + $this->_subscription->loadByCustomerId($customer->getId()); } } return $this->_subscription; diff --git a/app/code/Magento/Customer/Block/Account/Navigation.php b/app/code/Magento/Customer/Block/Account/Navigation.php index cb38d762769f2..64ced9d592e11 100644 --- a/app/code/Magento/Customer/Block/Account/Navigation.php +++ b/app/code/Magento/Customer/Block/Account/Navigation.php @@ -46,6 +46,10 @@ public function getLinks() */ private function compare(SortLinkInterface $firstLink, SortLinkInterface $secondLink) { - return ($firstLink->getSortOrder() < $secondLink->getSortOrder()); + if ($firstLink->getSortOrder() == $secondLink->getSortOrder()) { + return 0; + } + + return ($firstLink->getSortOrder() < $secondLink->getSortOrder()) ? 1 : -1; } } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit.php b/app/code/Magento/Customer/Block/Adminhtml/Edit.php index 973016baba29c..701e38bea6b58 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit.php @@ -122,7 +122,7 @@ protected function _construct() [ 'label' => __('Force Sign-In'), 'onclick' => 'deleteConfirm(\'' . $this->escapeJs($this->escapeHtml($deleteConfirmMsg)) . - '\', \'' . $url . '\')', + '\', \'' . $url . '\', {data: {}})', 'class' => 'invalidate-token' ], 10 diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php index 180cb3d66ea35..506ba3fb9bfda 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php @@ -25,7 +25,8 @@ public function getButtonData() $data = [ 'label' => __('Force Sign-In'), 'class' => 'invalidate-token', - 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")', + 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . + '", {data: {}})', 'sort_order' => 65, ]; } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 9a025211c9b0a..0aeed1562c51e 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $regionId = $element->getForm()->getElement('region_id')->getValue(); - $html = '<div class="field field-state required admin__field _required">'; + $html = '<div class="field field-state admin__field">'; $element->setClass('input-text admin__control-text'); $element->setRequired(true); $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php index 8506defbf9005..46a8dcfb28f1b 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php @@ -17,7 +17,7 @@ class Newsletter extends \Magento\Backend\Block\Widget\Form\Generic implements T /** * @var string */ - protected $_template = 'tab/newsletter.phtml'; + protected $_template = 'Magento_Customer::tab/newsletter.phtml'; /** * @var \Magento\Newsletter\Model\SubscriberFactory @@ -160,7 +160,7 @@ public function initForm() ] ); - if ($this->customerAccountManagement->isReadOnly($customerId)) { + if ($this->customerAccountManagement->isReadonly($customerId)) { $form->getElement('subscription')->setReadonly(true, true); } $isSubscribed = $subscriber->isSubscribed(); diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php index 43d9af36a5652..500646c8e05a7 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter/Grid/Renderer/Action.php @@ -10,6 +10,11 @@ */ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * Core registry * @@ -21,17 +26,24 @@ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstract * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param \Magento\Framework\Escaper|null $escaper */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + \Magento\Framework\Escaper $escaper = null ) { $this->_coreRegistry = $registry; + $this->escaper = $escaper ?? \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Escaper::class + ); parent::__construct($context, $data); } /** + * Render actions + * * @param \Magento\Framework\DataObject $row * @return string */ @@ -57,15 +69,20 @@ public function render(\Magento\Framework\DataObject $row) } /** + * Retrieve escaped value + * * @param string $value * @return string */ protected function _getEscapedValue($value) { - return addcslashes(htmlspecialchars($value), '\\\''); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return addcslashes($this->escaper->escapeHtml($value), '\\\''); } /** + * Actions to html + * * @param array $actions * @return string */ diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php index bb190260e4776..c1266febff99d 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php @@ -63,7 +63,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_orders_grid'); - $this->setDefaultSort('created_at', 'desc'); + $this->setDefaultSort('created_at'); + $this->setDefaultDir('desc'); $this->setUseAjax(true); } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php index 3f2c7cda7608d..988a157805b36 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -77,7 +77,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); @@ -94,7 +95,7 @@ protected function _prepareCollection() $quote = $this->getQuote(); if ($quote) { - $collection = $quote->getItemsCollection(false); + $collection = $quote->getItemsCollection(true); } else { $collection = $this->_dataCollectionFactory->create(); } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php index 81b7b8b3f96b5..c7023d0404f75 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php @@ -461,7 +461,7 @@ protected function getOnlineMinutesInterval() 'customer/online_customers/online_minutes_interval', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - return intval($configValue) > 0 ? intval($configValue) : self::DEFAULT_ONLINE_MINUTES_INTERVAL; + return (int)$configValue > 0 ? (int)$configValue : self::DEFAULT_ONLINE_MINUTES_INTERVAL; } /** diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php index d0b47886dca7e..aef91184fc782 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Wishlist/Grid/Renderer/Description.php @@ -18,6 +18,6 @@ class Description extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abs */ public function render(\Magento\Framework\DataObject $row) { - return nl2br(htmlspecialchars($row->getData($this->getColumn()->getIndex()))); + return nl2br($this->escapeHtml($row->getData($this->getColumn()->getIndex()))); } } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Group/Edit.php b/app/code/Magento/Customer/Block/Adminhtml/Group/Edit.php index be2d143e7f864..0bf7f607a531b 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Group/Edit.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Group/Edit.php @@ -57,6 +57,8 @@ public function __construct( * Update Save and Delete buttons. Remove Delete button if group can't be deleted. * * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _construct() { @@ -68,6 +70,23 @@ protected function _construct() $this->buttonList->update('save', 'label', __('Save Customer Group')); $this->buttonList->update('delete', 'label', __('Delete Customer Group')); + $this->buttonList->update( + 'delete', + 'onclick', + sprintf( + "deleteConfirm('%s','%s', %s)", + 'Are you sure?', + $this->getDeleteUrl(), + json_encode( + [ + 'action' => '', + 'data' => [ + 'form_key' => $this->getFormKey() + ] + ] + ) + ) + ); $groupId = $this->coreRegistry->registry(RegistryConstants::CURRENT_GROUP_ID); if (!$groupId || $this->groupManagement->isReadonly($groupId)) { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php index 4a0d0f66425bb..9ee856f6e0af9 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php @@ -24,7 +24,7 @@ class Vat extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element /** * @var string */ - protected $_template = 'sales/order/create/address/form/renderer/vat.phtml'; + protected $_template = 'Magento_Customer::sales/order/create/address/form/renderer/vat.phtml'; /** * @var \Magento\Framework\Json\EncoderInterface diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php new file mode 100644 index 0000000000000..2be340c8ccca4 --- /dev/null +++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\DataProviders; + +use Magento\Framework\Escaper; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Provides address attribute data into template. + */ +class AddressAttributeData implements ArgumentInterface +{ + /** + * @var AddressMetadataInterface + */ + private $addressMetadata; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @param AddressMetadataInterface $addressMetadata + * @param Escaper $escaper + */ + public function __construct( + AddressMetadataInterface $addressMetadata, + Escaper $escaper + ) { + + $this->addressMetadata = $addressMetadata; + $this->escaper = $escaper; + } + + /** + * Returns frontend label for attribute. + * + * @param string $attributeCode + * @return string + * @throws LocalizedException + */ + public function getFrontendLabel(string $attributeCode): string + { + try { + $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); + $frontendLabel = $attribute->getFrontendLabel(); + } catch (NoSuchEntityException $e) { + $frontendLabel = ''; + } + + return $this->escaper->escapeHtml(__($frontendLabel)); + } +} diff --git a/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php new file mode 100644 index 0000000000000..280948439e1f8 --- /dev/null +++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\DataProviders; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Directory\Model\Country\Postcode\Config as PostCodeConfig; + +/** + * Provides postcodes patterns into template. + */ +class PostCodesPatternsAttributeData implements ArgumentInterface +{ + /** + * @var PostCodeConfig + */ + private $postCodeConfig; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * Constructor + * + * @param PostCodeConfig $postCodeConfig + * @param SerializerInterface $serializer + */ + public function __construct(PostCodeConfig $postCodeConfig, SerializerInterface $serializer) + { + $this->postCodeConfig = $postCodeConfig; + $this->serializer = $serializer; + } + + /** + * Get serialized post codes + * + * @return string + */ + public function getSerializedPostCodes(): string + { + return $this->serializer->serialize($this->postCodeConfig->getPostCodes()); + } +} diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php index 7b265ae1f0f32..d3d3306a49b44 100644 --- a/app/code/Magento/Customer/Block/Form/Login.php +++ b/app/code/Magento/Customer/Block/Form/Login.php @@ -47,15 +47,6 @@ public function __construct( $this->_customerSession = $customerSession; } - /** - * @return $this - */ - protected function _prepareLayout() - { - $this->pageConfig->getTitle()->set(__('Customer Login')); - return parent::_prepareLayout(); - } - /** * Retrieve form posting url * diff --git a/app/code/Magento/Customer/Block/Newsletter.php b/app/code/Magento/Customer/Block/Newsletter.php index 7a34b1d892bc1..a5e768915f91b 100644 --- a/app/code/Magento/Customer/Block/Newsletter.php +++ b/app/code/Magento/Customer/Block/Newsletter.php @@ -20,7 +20,7 @@ class Newsletter extends \Magento\Customer\Block\Account\Dashboard /** * @var string */ - protected $_template = 'form/newsletter.phtml'; + protected $_template = 'Magento_Customer::form/newsletter.phtml'; /** * @return bool diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 0d282042b1d94..46187f4ff7704 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -127,7 +127,8 @@ protected function getFormFilter() protected function applyOutputFilter($value) { $filter = $this->getFormFilter(); - if ($filter) { + if ($filter && $value) { + $value = date('Y-m-d', $this->getTime()); $value = $filter->outputFilter($value); } return $value; diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php index ecd09319cd85e..2576545601c73 100644 --- a/app/code/Magento/Customer/Block/Widget/Name.php +++ b/app/code/Magento/Customer/Block/Widget/Name.php @@ -106,8 +106,11 @@ public function getPrefixOptions() $prefixOptions = $this->options->getNamePrefixOptions(); if ($this->getObject() && !empty($prefixOptions)) { - $oldPrefix = $this->escapeHtml(trim($this->getObject()->getPrefix())); - $prefixOptions[$oldPrefix] = $oldPrefix; + $prefixOption = $this->getObject()->getPrefix(); + $oldPrefix = $this->escapeHtml(trim($prefixOption)); + if ($prefixOption !== null && !isset($prefixOptions[$oldPrefix]) && !isset($prefixOptions[$prefixOption])) { + $prefixOptions[$oldPrefix] = $oldPrefix; + } } return $prefixOptions; } @@ -161,8 +164,11 @@ public function getSuffixOptions() { $suffixOptions = $this->options->getNameSuffixOptions(); if ($this->getObject() && !empty($suffixOptions)) { - $oldSuffix = $this->escapeHtml(trim($this->getObject()->getSuffix())); - $suffixOptions[$oldSuffix] = $oldSuffix; + $suffixOption = $this->getObject()->getSuffix(); + $oldSuffix = $this->escapeHtml(trim($suffixOption)); + if ($suffixOption !== null && !isset($suffixOptions[$oldSuffix]) && !isset($suffixOptions[$suffixOption])) { + $suffixOptions[$oldSuffix] = $oldSuffix; + } } return $suffixOptions; } @@ -239,10 +245,14 @@ public function getStoreLabel($attributeCode) */ public function getAttributeValidationClass($attributeCode) { - return $this->_addressHelper->getAttributeValidationClass($attributeCode); + $attributeMetadata = $this->_getAttribute($attributeCode); + + return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; } /** + * Check if attribute is required + * * @param string $attributeCode * @return bool */ @@ -253,6 +263,8 @@ private function _isAttributeRequired($attributeCode) } /** + * Check if attribute is visible + * * @param string $attributeCode * @return bool */ diff --git a/app/code/Magento/Customer/Controller/Account/Confirmation.php b/app/code/Magento/Customer/Controller/Account/Confirmation.php index e7d23cac8d62a..a3e2db0207630 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirmation.php +++ b/app/code/Magento/Customer/Controller/Account/Confirmation.php @@ -6,8 +6,10 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Model\Url; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Result\PageFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\Customer\Api\AccountManagementInterface; @@ -35,24 +37,32 @@ class Confirmation extends \Magento\Customer\Controller\AbstractAccount */ protected $resultPageFactory; + /** + * @var Url + */ + private $customerUrl; + /** * @param Context $context * @param Session $customerSession * @param PageFactory $resultPageFactory * @param StoreManagerInterface $storeManager * @param AccountManagementInterface $customerAccountManagement + * @param Url $customerUrl */ public function __construct( Context $context, Session $customerSession, PageFactory $resultPageFactory, StoreManagerInterface $storeManager, - AccountManagementInterface $customerAccountManagement + AccountManagementInterface $customerAccountManagement, + Url $customerUrl = null ) { $this->session = $customerSession; $this->resultPageFactory = $resultPageFactory; $this->storeManager = $storeManager; $this->customerAccountManagement = $customerAccountManagement; + $this->customerUrl = $customerUrl ?: ObjectManager::getInstance()->get(Url::class); parent::__construct($context); } @@ -98,6 +108,8 @@ public function execute() $resultPage = $this->resultPageFactory->create(); $resultPage->getLayout()->getBlock('accountConfirmation')->setEmail( $this->getRequest()->getParam('email', $email) + )->setLoginUrl( + $this->customerUrl->getLoginUrl() ); return $resultPage; } diff --git a/app/code/Magento/Customer/Controller/Account/CreatePassword.php b/app/code/Magento/Customer/Controller/Account/CreatePassword.php index fb2e3dd42908b..a86f42c14a027 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePassword.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePassword.php @@ -54,27 +54,30 @@ public function __construct( public function execute() { $resetPasswordToken = (string)$this->getRequest()->getParam('token'); - $customerId = (int)$this->getRequest()->getParam('id'); - $isDirectLink = $resetPasswordToken != '' && $customerId != 0; + $isDirectLink = $resetPasswordToken != ''; if (!$isDirectLink) { $resetPasswordToken = (string)$this->session->getRpToken(); - $customerId = (int)$this->session->getRpCustomerId(); } try { - $this->accountManagement->validateResetPasswordLinkToken($customerId, $resetPasswordToken); + $this->accountManagement->validateResetPasswordLinkToken( + 0, + $resetPasswordToken + ); if ($isDirectLink) { $this->session->setRpToken($resetPasswordToken); - $this->session->setRpCustomerId($customerId); $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/createpassword'); + return $resultRedirect; } else { /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); - $resultPage->getLayout()->getBlock('resetPassword')->setCustomerId($customerId) + $resultPage->getLayout() + ->getBlock('resetPassword') ->setResetPasswordLinkToken($resetPasswordToken); + return $resultPage; } } catch (\Exception $exception) { diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index 27d8ddd99344c..1d67a6b5d5eba 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Model\Account\Redirect as AccountRedirect; use Magento\Customer\Api\Data\AddressInterface; use Magento\Framework\Api\DataObjectHelper; @@ -31,6 +32,8 @@ use Magento\Framework\Data\Form\FormKey\Validator; /** + * Post create customer action + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -126,6 +129,11 @@ class CreatePost extends \Magento\Customer\Controller\AbstractAccount */ private $formKeyValidator; + /** + * @var CustomerRepository + */ + private $customerRepository; + /** * @param Context $context * @param Session $customerSession @@ -145,6 +153,7 @@ class CreatePost extends \Magento\Customer\Controller\AbstractAccount * @param CustomerExtractor $customerExtractor * @param DataObjectHelper $dataObjectHelper * @param AccountRedirect $accountRedirect + * @param CustomerRepository $customerRepository * @param Validator $formKeyValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -168,6 +177,7 @@ public function __construct( CustomerExtractor $customerExtractor, DataObjectHelper $dataObjectHelper, AccountRedirect $accountRedirect, + CustomerRepository $customerRepository = null, Validator $formKeyValidator = null ) { $this->session = $customerSession; @@ -188,6 +198,7 @@ public function __construct( $this->dataObjectHelper = $dataObjectHelper; $this->accountRedirect = $accountRedirect; $this->formKeyValidator = $formKeyValidator ?: ObjectManager::getInstance()->get(Validator::class); + $this->customerRepository = $customerRepository ?: ObjectManager::getInstance()->get(CustomerRepository::class); parent::__construct($context); } @@ -294,34 +305,29 @@ public function execute() $resultRedirect->setUrl($this->_redirect->error($url)); return $resultRedirect; } - $this->session->regenerateId(); - try { $address = $this->extractAddress(); $addresses = $address === null ? [] : [$address]; - $customer = $this->customerExtractor->extract('customer_account_create', $this->_request); $customer->setAddresses($addresses); - $password = $this->getRequest()->getParam('password'); $confirmation = $this->getRequest()->getParam('password_confirmation'); $redirectUrl = $this->session->getBeforeAuthUrl(); - $this->checkPasswordConfirmation($password, $confirmation); - $customer = $this->accountManagement ->createAccount($customer, $password, $redirectUrl); if ($this->getRequest()->getParam('is_subscribed', false)) { - $this->subscriberFactory->create()->subscribeCustomerById($customer->getId()); + $extensionAttributes = $customer->getExtensionAttributes(); + $extensionAttributes->setIsSubscribed(true); + $customer->setExtensionAttributes($extensionAttributes); + $this->customerRepository->save($customer); } - $this->_eventManager->dispatch( 'customer_register_success', ['account_controller' => $this, 'customer' => $customer] ); - $confirmationStatus = $this->accountManagement->getConfirmationStatus($customer->getId()); if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { $email = $this->customerUrl->getEmailConfirmationUrl($customer->getEmail()); diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 3f895ad2f17ac..4d9ec962c292d 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -6,6 +6,8 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\Customer\Mapper; use Magento\Customer\Model\EmailNotificationInterface; @@ -20,9 +22,11 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; use Magento\Framework\Exception\State\UserLockedException; +use Magento\Framework\Escaper; /** - * Class EditPost + * Class to editing post. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EditPost extends \Magento\Customer\Controller\AbstractAccount @@ -72,6 +76,16 @@ class EditPost extends \Magento\Customer\Controller\AbstractAccount */ private $customerMapper; + /** + * @var Escaper + */ + private $escaper; + + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param Context $context * @param Session $customerSession @@ -79,6 +93,8 @@ class EditPost extends \Magento\Customer\Controller\AbstractAccount * @param CustomerRepositoryInterface $customerRepository * @param Validator $formKeyValidator * @param CustomerExtractor $customerExtractor + * @param Escaper|null $escaper + * @param AddressRegistry|null $addressRegistry */ public function __construct( Context $context, @@ -86,7 +102,9 @@ public function __construct( AccountManagementInterface $customerAccountManagement, CustomerRepositoryInterface $customerRepository, Validator $formKeyValidator, - CustomerExtractor $customerExtractor + CustomerExtractor $customerExtractor, + Escaper $escaper = null, + AddressRegistry $addressRegistry = null ) { parent::__construct($context); $this->session = $customerSession; @@ -94,6 +112,8 @@ public function __construct( $this->customerRepository = $customerRepository; $this->formKeyValidator = $formKeyValidator; $this->customerExtractor = $customerExtractor; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); } /** @@ -131,7 +151,7 @@ private function getEmailNotification() } /** - * Change customer email or password action + * Change customer email or password action. * * @return \Magento\Framework\Controller\Result\Redirect */ @@ -155,6 +175,9 @@ public function execute() // whether a customer enabled change password option $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($customerCandidateDataObject); + $this->customerRepository->save($customerCandidateDataObject); $this->getEmailNotification()->credentialsChanged( $customerCandidateDataObject, @@ -163,6 +186,7 @@ public function execute() ); $this->dispatchSuccessEvent($customerCandidateDataObject); $this->messageManager->addSuccess(__('You saved the account information.')); + return $resultRedirect->setPath('customer/account'); } catch (InvalidEmailOrPasswordException $e) { $this->messageManager->addError($e->getMessage()); @@ -173,11 +197,12 @@ public function execute() $this->session->logout(); $this->session->start(); $this->messageManager->addError($message); + return $resultRedirect->setPath('customer/account/login'); } catch (InputException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); foreach ($e->getErrors() as $error) { - $this->messageManager->addError($error->getMessage()); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($error->getMessage())); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($e->getMessage()); @@ -306,4 +331,18 @@ private function getCustomerMapper() } return $this->customerMapper; } + + /** + * Disable Customer Address Validation. + * + * @param CustomerInterface $customer + * @return void + */ + private function disableAddressValidation(CustomerInterface $customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php index f115b64efebdd..cd2c46ff53043 100644 --- a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php +++ b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php @@ -40,10 +40,17 @@ public function __construct( /** * Forgot customer password page * - * @return \Magento\Framework\View\Result\Page + * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page */ public function execute() { + if ($this->session->isLoggedIn()) { + /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setPath('*/*/'); + return $resultRedirect; + } + /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $resultPage->getLayout()->getBlock('forgotPassword')->setEmailValue($this->session->getForgottenEmail()); diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php index fe92032b1b75e..0e58fbca688ce 100644 --- a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php @@ -10,6 +10,8 @@ use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\SecurityViolationException; @@ -35,21 +37,29 @@ class ForgotPasswordPost extends \Magento\Customer\Controller\AbstractAccount */ protected $session; + /** + * @var Validator + */ + private $formKeyValidator; + /** * @param Context $context * @param Session $customerSession * @param AccountManagementInterface $customerAccountManagement * @param Escaper $escaper + * @param Validator|null $formKeyValidator */ public function __construct( Context $context, Session $customerSession, AccountManagementInterface $customerAccountManagement, - Escaper $escaper + Escaper $escaper, + Validator $formKeyValidator = null ) { $this->session = $customerSession; $this->customerAccountManagement = $customerAccountManagement; $this->escaper = $escaper; + $this->formKeyValidator = $formKeyValidator ?? ObjectManager::getInstance()->get(Validator::class); parent::__construct($context); } @@ -57,11 +67,20 @@ public function __construct( * Forgot customer password action * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); + + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if (!$this->formKeyValidator->validate($this->getRequest())) { + return $resultRedirect->setPath('*/*/forgotpassword'); + } + $email = (string)$this->getRequest()->getPost('email'); if ($email) { if (!\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { diff --git a/app/code/Magento/Customer/Controller/Account/Index.php b/app/code/Magento/Customer/Controller/Account/Index.php index f734660fc3a77..2ecf79d35b11f 100644 --- a/app/code/Magento/Customer/Controller/Account/Index.php +++ b/app/code/Magento/Customer/Controller/Account/Index.php @@ -35,9 +35,6 @@ public function __construct( */ public function execute() { - /** @var \Magento\Framework\View\Result\Page $resultPage */ - $resultPage = $this->resultPageFactory->create(); - $resultPage->getConfig()->getTitle()->set(__('My Account')); - return $resultPage; + return $this->resultPageFactory->create(); } } diff --git a/app/code/Magento/Customer/Controller/Account/LoginPost.php b/app/code/Magento/Customer/Controller/Account/LoginPost.php index b55863a3b486a..6c352bdb4c0d7 100644 --- a/app/code/Magento/Customer/Controller/Account/LoginPost.php +++ b/app/code/Magento/Customer/Controller/Account/LoginPost.php @@ -18,6 +18,8 @@ use Magento\Framework\App\Config\ScopeConfigInterface; /** + * Post login customer action. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class LoginPost extends \Magento\Customer\Controller\AbstractAccount @@ -151,7 +153,6 @@ public function execute() try { $customer = $this->customerAccountManagement->authenticate($login['username'], $login['password']); $this->session->setCustomerDataAsLoggedIn($customer); - $this->session->regenerateId(); if ($this->getCookieManager()->getCookie('mage-cache-sessid')) { $metadata = $this->getCookieMetadataFactory()->createCookieMetadata(); $metadata->setPath('/'); @@ -171,27 +172,24 @@ public function execute() 'This account is not confirmed. <a href="%1">Click here</a> to resend confirmation email.', $value ); - $this->messageManager->addError($message); - $this->session->setUsername($login['username']); } catch (UserLockedException $e) { $message = __( 'You did not sign in correctly or your account is temporarily disabled.' ); - $this->messageManager->addError($message); - $this->session->setUsername($login['username']); } catch (AuthenticationException $e) { $message = __('You did not sign in correctly or your account is temporarily disabled.'); - $this->messageManager->addError($message); - $this->session->setUsername($login['username']); } catch (LocalizedException $e) { $message = $e->getMessage(); - $this->messageManager->addError($message); - $this->session->setUsername($login['username']); } catch (\Exception $e) { // PA DSS violation: throwing or logging an exception here can disclose customer password $this->messageManager->addError( __('An unspecified error occurred. Please contact us for assistance.') ); + } finally { + if (isset($message)) { + $this->messageManager->addError($message); + $this->session->setUsername($login['username']); + } } } else { $this->messageManager->addError(__('A login and a password are required.')); diff --git a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php index 3de44e35d2447..ab6944a995d84 100644 --- a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php @@ -12,7 +12,6 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\Exception\InputException; use Magento\Customer\Model\Customer\CredentialsValidator; -use Magento\Framework\App\ObjectManager; class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount { @@ -31,17 +30,14 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount */ protected $session; - /** - * @var CredentialsValidator - */ - private $credentialsValidator; - /** * @param Context $context * @param Session $customerSession * @param AccountManagementInterface $accountManagement * @param CustomerRepositoryInterface $customerRepository * @param CredentialsValidator|null $credentialsValidator + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -53,8 +49,6 @@ public function __construct( $this->session = $customerSession; $this->accountManagement = $accountManagement; $this->customerRepository = $customerRepository; - $this->credentialsValidator = $credentialsValidator ?: ObjectManager::getInstance() - ->get(CredentialsValidator::class); parent::__construct($context); } @@ -70,27 +64,33 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resetPasswordToken = (string)$this->getRequest()->getQuery('token'); - $customerId = (int)$this->getRequest()->getQuery('id'); $password = (string)$this->getRequest()->getPost('password'); $passwordConfirmation = (string)$this->getRequest()->getPost('password_confirmation'); if ($password !== $passwordConfirmation) { $this->messageManager->addError(__("New Password and Confirm New Password values didn't match.")); - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + $resultRedirect->setPath( + '*/*/createPassword', + ['token' => $resetPasswordToken] + ); return $resultRedirect; } if (iconv_strlen($password) <= 0) { $this->messageManager->addError(__('Please enter a new password.')); - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + $resultRedirect->setPath( + '*/*/createPassword', + ['token' => $resetPasswordToken] + ); return $resultRedirect; } try { - $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); - $this->credentialsValidator->checkPasswordDifferentFromEmail($customerEmail, $password); - $this->accountManagement->resetPassword($customerEmail, $resetPasswordToken, $password); + $this->accountManagement->resetPassword( + '', + $resetPasswordToken, + $password + ); $this->session->unsRpToken(); - $this->session->unsRpCustomerId(); $this->messageManager->addSuccess(__('You updated your password.')); $resultRedirect->setPath('*/*/login'); return $resultRedirect; @@ -102,7 +102,11 @@ public function execute() } catch (\Exception $exception) { $this->messageManager->addError(__('Something went wrong while saving the new password.')); } - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + + $resultRedirect->setPath( + '*/*/createPassword', + ['token' => $resetPasswordToken] + ); return $resultRedirect; } } diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php index ef92bd2ef533b..d287808b4056d 100644 --- a/app/code/Magento/Customer/Controller/Address/Delete.php +++ b/app/code/Magento/Customer/Controller/Address/Delete.php @@ -6,13 +6,20 @@ */ namespace Magento\Customer\Controller\Address; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Customer\Controller\Address { /** * @return \Magento\Framework\Controller\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $addressId = $this->getRequest()->getParam('id', false); if ($addressId && $this->_formKeyValidator->validate($this->getRequest())) { @@ -20,12 +27,12 @@ public function execute() $address = $this->_addressRepository->getById($addressId); if ($address->getCustomerId() === $this->_getSession()->getCustomerId()) { $this->_addressRepository->deleteById($addressId); - $this->messageManager->addSuccess(__('You deleted the address.')); + $this->messageManager->addSuccessMessage(__('You deleted the address.')); } else { - $this->messageManager->addError(__('We can\'t delete the address right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete the address right now.')); } } catch (\Exception $other) { - $this->messageManager->addException($other, __('We can\'t delete the address right now.')); + $this->messageManager->addExceptionMessage($other, __('We can\'t delete the address right now.')); } } return $this->resultRedirectFactory->create()->setPath('*/*/index'); diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index 21334f51b1752..3d995fef4c5f7 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -197,17 +197,17 @@ public function execute() try { $address = $this->_extractAddress(); $this->_addressRepository->save($address); - $this->messageManager->addSuccess(__('You saved the address.')); + $this->messageManager->addSuccessMessage(__('You saved the address.')); $url = $this->_buildUrl('*/*/index', ['_secure' => true]); return $this->resultRedirectFactory->create()->setUrl($this->_redirect->success($url)); } catch (InputException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); foreach ($e->getErrors() as $error) { - $this->messageManager->addError($error->getMessage()); + $this->messageManager->addErrorMessage($error->getMessage()); } } catch (\Exception $e) { $redirectUrl = $this->_buildUrl('*/*/index'); - $this->messageManager->addException($e, __('We can\'t save the address.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t save the address.')); } $url = $redirectUrl; diff --git a/app/code/Magento/Customer/Controller/Address/Index.php b/app/code/Magento/Customer/Controller/Address/Index.php index ad04c7bd5c71b..674d3bcf0e0d3 100644 --- a/app/code/Magento/Customer/Controller/Address/Index.php +++ b/app/code/Magento/Customer/Controller/Address/Index.php @@ -28,9 +28,9 @@ class Index extends \Magento\Customer\Controller\Address * @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionDataFactory * @param \Magento\Framework\Reflection\DataObjectProcessor $dataProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param CustomerRepositoryInterface $customerRepository * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param CustomerRepositoryInterface $customerRepository * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php index 571ef57702bc3..6fc9f45ab62ff 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php @@ -7,6 +7,7 @@ namespace Magento\Customer\Controller\Adminhtml\Group; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\NotFoundException; class Delete extends \Magento\Customer\Controller\Adminhtml\Group { @@ -14,9 +15,14 @@ class Delete extends \Magento\Customer\Controller\Adminhtml\Group * Delete customer group. * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $id = $this->getRequest()->getParam('id'); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 936d9cdbc1704..6b35397d9be13 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\Data\GroupInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Exception\NotFoundException; class Save extends \Magento\Customer\Controller\Adminhtml\Group { @@ -66,9 +67,14 @@ protected function storeCustomerGroupDataToSession($customerGroupData) * Create or save customer group. * * @return \Magento\Backend\Model\View\Result\Redirect|\Magento\Backend\Model\View\Result\Forward + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $taxClass = (int)$this->getRequest()->getParam('tax_class'); /** @var \Magento\Customer\Api\Data\GroupInterface $customerGroup */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php index ebab2a42a02ec..6b80cd5b3a6a5 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/AbstractMassAction.php @@ -10,6 +10,7 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\ResultInterface; use Magento\Backend\App\Action\Context; +use Magento\Framework\Exception\NotFoundException; use Magento\Ui\Component\MassAction\Filter; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; @@ -60,6 +61,10 @@ public function __construct(Context $context, Filter $filter, CollectionFactory */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $collection = $this->filter->getCollection($this->collectionFactory->create()); return $this->massAction($collection); @@ -73,7 +78,7 @@ public function execute() /** * Return component referer url - * TODO: Technical dept referer url should be implement as a part of Action configuration in in appropriate way + * TODO: Technical dept referer url should be implement as a part of Action configuration in appropriate way * * @return null|string */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php index 2d0ee3ae13da4..1984c3bc0d536 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php @@ -6,13 +6,17 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\App\Action; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; -use Magento\Customer\Test\Block\Form\Login; use Magento\Customer\Ui\Component\Listing\AttributeRepository; -use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\ObjectManager; /** + * Customer inline edit action. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InlineEdit extends \Magento\Backend\App\Action @@ -59,6 +63,16 @@ class InlineEdit extends \Magento\Backend\App\Action */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param Action\Context $context * @param CustomerRepositoryInterface $customerRepository @@ -66,6 +80,8 @@ class InlineEdit extends \Magento\Backend\App\Action * @param \Magento\Customer\Model\Customer\Mapper $customerMapper * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param \Psr\Log\LoggerInterface $logger + * @param AddressRegistry|null $addressRegistry + * @param \Magento\Framework\Escaper $escaper */ public function __construct( Action\Context $context, @@ -73,13 +89,17 @@ public function __construct( \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Customer\Model\Customer\Mapper $customerMapper, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + AddressRegistry $addressRegistry = null, + \Magento\Framework\Escaper $escaper = null ) { $this->customerRepository = $customerRepository; $this->resultJsonFactory = $resultJsonFactory; $this->customerMapper = $customerMapper; $this->dataObjectHelper = $dataObjectHelper; $this->logger = $logger; + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); parent::__construct($context); } @@ -101,6 +121,8 @@ private function getEmailNotification() } /** + * Inline edit action execute + * * @return \Magento\Framework\Controller\Result\Json */ public function execute() @@ -109,7 +131,7 @@ public function execute() $resultJson = $this->resultJsonFactory->create(); $postItems = $this->getRequest()->getParam('items', []); - if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { + if (!($this->getRequest()->getParam('isAjax') && $this->getRequest()->isPost() && count($postItems))) { return $resultJson->setData([ 'messages' => [__('Please correct the data sent.')], 'error' => true, @@ -204,7 +226,7 @@ protected function updateDefaultBilling(array $data) } /** - * Save customer with error catching + * Save customer with error catching. * * @param CustomerInterface $customer * @return void @@ -212,15 +234,20 @@ protected function updateDefaultBilling(array $data) protected function saveCustomer(CustomerInterface $customer) { try { + // No need to validate customer address during inline edit action + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); } catch (\Magento\Framework\Exception\InputException $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId($this->escaper->escapeHtml($e->getMessage()))); $this->logger->critical($e); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId($this->escaper->escapeHtml($e->getMessage()))); $this->logger->critical($e); } catch (\Exception $e) { - $this->getMessageManager()->addError($this->getErrorWithCustomerId('We can\'t save the customer.')); + $this->getMessageManager() + ->addError($this->getErrorWithCustomerId('We can\'t save the customer.')); $this->logger->critical($e); } } @@ -249,7 +276,7 @@ protected function processAddressData(array $data) protected function getErrorMessages() { $messages = []; - foreach ($this->getMessageManager()->getMessages()->getItems() as $error) { + foreach ($this->getMessageManager()->getMessages()->getErrors() as $error) { $messages[] = $error->getText(); } return $messages; @@ -262,7 +289,7 @@ protected function getErrorMessages() */ protected function isErrorExists() { - return (bool)$this->getMessageManager()->getMessages(true)->getCount(); + return (bool)$this->getMessageManager()->getMessages(true)->getCountByType(MessageInterface::TYPE_ERROR); } /** @@ -297,4 +324,18 @@ protected function getErrorWithCustomerId($errorText) { return '[Customer ID: ' . $this->getCustomer()->getId() . '] ' . __($errorText); } + + /** + * Disable Customer Address Validation. + * + * @param CustomerInterface $customer + * @return void + */ + private function disableAddressValidation(CustomerInterface $customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php index 762b872b97b6d..49a51052beb90 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Backend\App\Action\Context; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Eav\Model\Entity\Collection\AbstractCollection; @@ -13,7 +14,7 @@ use Magento\Framework\Controller\ResultFactory; /** - * Class MassAssignGroup + * Class to execute MassAssignGroup action. */ class MassAssignGroup extends AbstractMassAction { @@ -39,7 +40,7 @@ public function __construct( } /** - * Customer mass assign group action + * Customer mass assign group action. * * @param AbstractCollection $collection * @return \Magento\Backend\Model\View\Result\Redirect @@ -51,6 +52,8 @@ protected function massAction(AbstractCollection $collection) // Verify customer exists $customer = $this->customerRepository->getById($customerId); $customer->setGroupId($this->getRequest()->getParam('group')); + // No need to validate customer and customer address during assigning customer to the group + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $customersUpdated++; } @@ -64,4 +67,15 @@ protected function massAction(AbstractCollection $collection) return $resultRedirect; } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation. + * + * @param CustomerInterface $customer + * @return void + */ + private function setIgnoreValidationFlag(CustomerInterface $customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php index 37c8ed5a252f8..eab18520e69a7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php @@ -31,7 +31,9 @@ public function execute() \Magento\Customer\Model\AccountManagement::EMAIL_REMINDER, $customer->getWebsiteId() ); - $this->messageManager->addSuccess(__('The customer will receive an email with a link to reset password.')); + $this->messageManager->addSuccessMessage( + __('The customer will receive an email with a link to reset password.') + ); } catch (NoSuchEntityException $exception) { $resultRedirect->setPath('customer/index'); return $resultRedirect; @@ -44,7 +46,7 @@ public function execute() } catch (SecurityViolationException $exception) { $this->messageManager->addErrorMessage($exception->getMessage()); } catch (\Exception $exception) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $exception, __('Something went wrong while resetting customer password.') ); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 44eba83d96d7e..561039990f705 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -5,6 +5,15 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\DataObjectFactory as ObjectFactory; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\CustomerInterface; @@ -12,7 +21,13 @@ use Magento\Customer\Model\EmailNotificationInterface; use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ObjectManager; +/** + * Class to Save customer. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Save extends \Magento\Customer\Controller\Adminhtml\Index { /** @@ -20,6 +35,98 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + + /** + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\Registry $coreRegistry + * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory + * @param \Magento\Customer\Model\CustomerFactory $customerFactory + * @param \Magento\Customer\Model\AddressFactory $addressFactory + * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory + * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory + * @param \Magento\Customer\Helper\View $viewHelper + * @param \Magento\Framework\Math\Random $random + * @param CustomerRepositoryInterface $customerRepository + * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param Mapper $addressMapper + * @param AccountManagementInterface $customerAccountManagement + * @param AddressRepositoryInterface $addressRepository + * @param CustomerInterfaceFactory $customerDataFactory + * @param AddressInterfaceFactory $addressDataFactory + * @param \Magento\Customer\Model\Customer\Mapper $customerMapper + * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor + * @param DataObjectHelper $dataObjectHelper + * @param ObjectFactory $objectFactory + * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param AddressRegistry|null $addressRegistry + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\Registry $coreRegistry, + \Magento\Framework\App\Response\Http\FileFactory $fileFactory, + \Magento\Customer\Model\CustomerFactory $customerFactory, + \Magento\Customer\Model\AddressFactory $addressFactory, + \Magento\Customer\Model\Metadata\FormFactory $formFactory, + \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory, + \Magento\Customer\Helper\View $viewHelper, + \Magento\Framework\Math\Random $random, + CustomerRepositoryInterface $customerRepository, + \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + Mapper $addressMapper, + AccountManagementInterface $customerAccountManagement, + AddressRepositoryInterface $addressRepository, + CustomerInterfaceFactory $customerDataFactory, + AddressInterfaceFactory $addressDataFactory, + \Magento\Customer\Model\Customer\Mapper $customerMapper, + \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, + DataObjectHelper $dataObjectHelper, + ObjectFactory $objectFactory, + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory, + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + AddressRegistry $addressRegistry = null + ) { + parent::__construct( + $context, + $coreRegistry, + $fileFactory, + $customerFactory, + $addressFactory, + $formFactory, + $subscriberFactory, + $viewHelper, + $random, + $customerRepository, + $extensibleDataObjectConverter, + $addressMapper, + $customerAccountManagement, + $addressRepository, + $customerDataFactory, + $addressDataFactory, + $customerMapper, + $dataObjectProcessor, + $dataObjectHelper, + $objectFactory, + $layoutFactory, + $resultLayoutFactory, + $resultPageFactory, + $resultForwardFactory, + $resultJsonFactory + ); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + } + /** * Reformat customer account data to be compatible with customer service interface * @@ -166,7 +273,7 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) } /** - * Save customer action + * Save customer action. * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -176,11 +283,9 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) public function execute() { $returnToEdit = false; - $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = $this->getCurrentCustomerId(); - if ($originalRequestData) { + if ($this->getRequest()->getPostValue()) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); @@ -188,6 +293,8 @@ public function execute() if ($customerId) { $currentCustomer = $this->_customerRepository->getById($customerId); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($currentCustomer); $customerData = array_merge( $this->customerMapper->toFlatArray($currentCustomer), $customerData @@ -266,18 +373,28 @@ public function execute() $messages = $exception->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); + $returnToEdit = true; + } catch (\Magento\Framework\Exception\AbstractAggregateException $exception) { + $errors = $exception->getErrors(); + $messages = []; + foreach ($errors as $error) { + $messages[] = $error->getMessage(); + } + $this->_addSessionErrorMessages($messages); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (LocalizedException $exception) { $this->_addSessionErrorMessages($exception->getMessage()); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Exception $exception) { $this->messageManager->addException($exception, __('Something went wrong while saving the customer.')); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } } + $resultRedirect = $this->resultRedirectFactory->create(); if ($returnToEdit) { if ($customerId) { @@ -294,6 +411,7 @@ public function execute() } else { $resultRedirect->setPath('customer/index'); } + return $resultRedirect; } @@ -368,4 +486,43 @@ private function getCurrentCustomerId() return $customerId; } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @return void + */ + private function disableAddressValidation(CustomerInterface $customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + + /** + * Retrieve formatted form data + * + * @return array + */ + private function retrieveFormattedFormData(): array + { + $originalRequestData = $this->getRequest()->getPostValue(); + + /* Customer data filtration */ + if (isset($originalRequestData['customer'])) { + $customerData = $this->_extractData( + 'adminhtml_customer', + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + [], + 'customer' + ); + + $customerData = array_intersect_key($customerData, $originalRequestData['customer']); + $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); + } + + return $originalRequestData; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php index 711fab9e608bf..c515e1151f7e6 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php @@ -132,30 +132,18 @@ public function __construct( */ public function execute() { - $file = null; - $plain = false; - if ($this->getRequest()->getParam('file')) { - // download file - $file = $this->urlDecoder->decode( - $this->getRequest()->getParam('file') - ); - } elseif ($this->getRequest()->getParam('image')) { - // show plain image - $file = $this->urlDecoder->decode( - $this->getRequest()->getParam('image') - ); - $plain = true; - } else { - throw new NotFoundException(__('Page not found.')); - } + list($file, $plain) = $this->getFileParams(); /** @var \Magento\Framework\Filesystem $filesystem */ $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryRead(DirectoryList::MEDIA); $fileName = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . ltrim($file, '/'); $path = $directory->getAbsolutePath($fileName); - if (!$directory->isFile($fileName) - && !$this->_objectManager->get(\Magento\MediaStorage\Helper\File\Storage::class)->processStorageFile($path) + if (mb_strpos($path, '..') !== false + || (!$directory->isFile($fileName) + && !$this->_objectManager->get( + \Magento\MediaStorage\Helper\File\Storage::class + )->processStorageFile($path)) ) { throw new NotFoundException(__('Page not found.')); } @@ -198,4 +186,31 @@ public function execute() ); } } + + /** + * Get parameters from request. + * + * @return array + * @throws NotFoundException + */ + private function getFileParams() + { + if ($this->getRequest()->getParam('file')) { + // download file + $file = $this->urlDecoder->decode( + $this->getRequest()->getParam('file') + ); + + return [$file, false]; + } elseif ($this->getRequest()->getParam('image')) { + // show plain image + $file = $this->urlDecoder->decode( + $this->getRequest()->getParam('image') + ); + + return [$file, true]; + } else { + throw new NotFoundException(__('Page not found.')); + } + } } diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php index f1384ba188a0a..96b782b07a3e0 100644 --- a/app/code/Magento/Customer/Controller/Ajax/Login.php +++ b/app/code/Magento/Customer/Controller/Ajax/Login.php @@ -7,12 +7,12 @@ namespace Magento\Customer\Controller\Ajax; use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\EmailNotConfirmedException; -use Magento\Framework\Exception\InvalidEmailOrPasswordException; use Magento\Framework\App\ObjectManager; use Magento\Customer\Model\Account\Redirect as AccountRedirect; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; /** * Login controller @@ -58,6 +58,21 @@ class Login extends \Magento\Framework\App\Action\Action */ protected $scopeConfig; + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var CookieMetadataFactory + */ + private $cookieMetadataFactory; + + /** + * @var \Magento\Customer\Model\Session + */ + private $customerSession; + /** * Initialize Login controller * @@ -67,6 +82,8 @@ class Login extends \Magento\Framework\App\Action\Action * @param AccountManagementInterface $customerAccountManagement * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + * @param CookieManagerInterface $cookieManager + * @param CookieMetadataFactory $cookieMetadataFactory */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -74,7 +91,9 @@ public function __construct( \Magento\Framework\Json\Helper\Data $helper, AccountManagementInterface $customerAccountManagement, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, + CookieManagerInterface $cookieManager = null, + CookieMetadataFactory $cookieMetadataFactory = null ) { parent::__construct($context); $this->customerSession = $customerSession; @@ -82,11 +101,16 @@ public function __construct( $this->customerAccountManagement = $customerAccountManagement; $this->resultJsonFactory = $resultJsonFactory; $this->resultRawFactory = $resultRawFactory; + $this->cookieManager = $cookieManager ?: ObjectManager::getInstance()->get( + CookieManagerInterface::class + ); + $this->cookieMetadataFactory = $cookieMetadataFactory ?: ObjectManager::getInstance()->get( + CookieMetadataFactory::class + ); } /** * Get account redirect. - * For release backward compatibility. * * @deprecated 100.0.10 * @return AccountRedirect @@ -112,6 +136,8 @@ public function setAccountRedirect($value) } /** + * Initializes config dependency. + * * @deprecated 100.0.10 * @return ScopeConfigInterface */ @@ -124,6 +150,8 @@ protected function getScopeConfig() } /** + * Sets config dependency. + * * @deprecated 100.0.10 * @param ScopeConfigInterface $value * @return void @@ -167,31 +195,27 @@ public function execute() $credentials['password'] ); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $this->customerSession->regenerateId(); $redirectRoute = $this->getAccountRedirect()->getRedirectCookie(); + if ($this->cookieManager->getCookie('mage-cache-sessid')) { + $metadata = $this->cookieMetadataFactory->createCookieMetadata(); + $metadata->setPath('/'); + $this->cookieManager->deleteCookie('mage-cache-sessid', $metadata); + } if (!$this->getScopeConfig()->getValue('customer/startup/redirect_dashboard') && $redirectRoute) { $response['redirectUrl'] = $this->_redirect->success($redirectRoute); $this->getAccountRedirect()->clearRedirectCookie(); } - } catch (EmailNotConfirmedException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; - } catch (InvalidEmailOrPasswordException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; } catch (LocalizedException $e) { $response = [ 'errors' => true, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'captcha' => $this->customerSession->getData('user_login_show_captcha') ]; } catch (\Exception $e) { $response = [ 'errors' => true, - 'message' => __('Invalid login or password.') + 'message' => __('Invalid login or password.'), + 'captcha' => $this->customerSession->getData('user_login_show_captcha') ]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php index 6e73e070c790d..e37461d20f5de 100644 --- a/app/code/Magento/Customer/Controller/Section/Load.php +++ b/app/code/Magento/Customer/Controller/Section/Load.php @@ -64,17 +64,17 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - $resultJson->setHeader('Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store'); - $resultJson->setHeader('Pragma', 'no-cache'); + $resultJson->setHeader('Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store', true); + $resultJson->setHeader('Pragma', 'no-cache', true); try { $sectionNames = $this->getRequest()->getParam('sections'); $sectionNames = $sectionNames ? array_unique(\explode(',', $sectionNames)) : null; - $updateSectionId = $this->getRequest()->getParam('update_section_id'); - if ('false' === $updateSectionId) { - $updateSectionId = false; + $forceNewSectionTimestamp = $this->getRequest()->getParam('force_new_section_timestamp'); + if ('false' === $forceNewSectionTimestamp) { + $forceNewSectionTimestamp = false; } - $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$updateSectionId); + $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$forceNewSectionTimestamp); } catch (\Exception $e) { $resultJson->setStatusHeader( \Zend\Http\Response::STATUS_CODE_400, diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php index aa73e275ee0ca..f82a4d15ae8bf 100644 --- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php +++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php @@ -5,10 +5,13 @@ */ namespace Magento\Customer\CustomerData\Plugin; -use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\Cookie\PhpCookieManager; +/** + * Class SessionChecker + */ class SessionChecker { /** @@ -36,10 +39,12 @@ public function __construct( /** * Delete frontend session cookie if customer session is expired * - * @param SessionManager $sessionManager + * @param SessionManagerInterface $sessionManager * @return void + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException */ - public function beforeStart(SessionManager $sessionManager) + public function beforeStart(SessionManagerInterface $sessionManager) { if (!$this->cookieManager->getCookie($sessionManager->getName()) && $this->cookieManager->getCookie('mage-cache-sessid') diff --git a/app/code/Magento/Customer/CustomerData/Section/Identifier.php b/app/code/Magento/Customer/CustomerData/Section/Identifier.php index 2a770925d1c37..54d7cee2d90bd 100644 --- a/app/code/Magento/Customer/CustomerData/Section/Identifier.php +++ b/app/code/Magento/Customer/CustomerData/Section/Identifier.php @@ -43,12 +43,12 @@ public function __construct( /** * Init mark(identifier) for sections * - * @param bool $forceUpdate + * @param bool $forceNewTimestamp * @return int */ - public function initMark($forceUpdate) + public function initMark($forceNewTimestamp) { - if ($forceUpdate) { + if ($forceNewTimestamp) { $this->markId = time(); return $this->markId; } @@ -68,18 +68,18 @@ public function initMark($forceUpdate) * * @param array $sectionsData * @param null $sectionNames - * @param bool $updateIds + * @param bool $forceNewTimestamp * @return array */ - public function markSections(array $sectionsData, $sectionNames = null, $updateIds = false) + public function markSections(array $sectionsData, $sectionNames = null, $forceNewTimestamp = false) { if (!$sectionNames) { $sectionNames = array_keys($sectionsData); } - $markId = $this->initMark($updateIds); + $markId = $this->initMark($forceNewTimestamp); foreach ($sectionNames as $name) { - if ($updateIds || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { + if ($forceNewTimestamp || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { $sectionsData[$name][self::SECTION_KEY] = $markId; } } diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index 26e9140c63df5..be5ea09c0db33 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -55,10 +55,10 @@ public function __construct( /** * {@inheritdoc} */ - public function getSectionsData(array $sectionNames = null, $updateIds = false) + public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false) { $sectionsData = $sectionNames ? $this->getSectionDataByNames($sectionNames) : $this->getAllSectionData(); - $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $updateIds); + $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $forceNewTimestamp); return $sectionsData; } diff --git a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php index c308804fd0f8d..ad73b9722b133 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php +++ b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php @@ -14,8 +14,8 @@ interface SectionPoolInterface * Get section data by section names. If $sectionNames is null then return all sections data * * @param array $sectionNames - * @param bool $updateIds + * @param bool $forceNewTimestamp * @return array */ - public function getSectionsData(array $sectionNames = null, $updateIds = false); + public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false); } diff --git a/app/code/Magento/Customer/Model/Account/Redirect.php b/app/code/Magento/Customer/Model/Account/Redirect.php index 2e8d596474e96..6b897cb11f718 100644 --- a/app/code/Magento/Customer/Model/Account/Redirect.php +++ b/app/code/Magento/Customer/Model/Account/Redirect.php @@ -206,6 +206,10 @@ protected function processLoggedCustomer() $referer = $this->request->getParam(CustomerUrl::REFERER_QUERY_PARAM_NAME); if ($referer) { $referer = $this->urlDecoder->decode($referer); + preg_match('/logoutSuccess/', $referer, $matches, PREG_OFFSET_CAPTURE); + if (!empty($matches)) { + $referer = str_replace('logoutSuccess', '', $referer); + } if ($this->hostChecker->isOwnOrigin($referer)) { $this->applyRedirect($referer); } diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php new file mode 100644 index 0000000000000..7d01ff0efc411 --- /dev/null +++ b/app/code/Magento/Customer/Model/AccountConfirmation.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model; + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Registry; + +/** + * Class AccountConfirmation. + * Checks if email confirmation required for customer. + */ +class AccountConfirmation +{ + /** + * Configuration path for email confirmation. + */ + const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Registry + */ + private $registry; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Registry $registry + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Registry $registry + ) { + $this->scopeConfig = $scopeConfig; + $this->registry = $registry; + } + + /** + * Check if accounts confirmation is required. + * + * @param int|null $websiteId + * @param int|null $customerId + * @param string $customerEmail + * @return bool + */ + public function isConfirmationRequired($websiteId, $customerId, $customerEmail): bool + { + if ($this->canSkipConfirmation($customerId, $customerEmail)) { + return false; + } + + return (bool)$this->scopeConfig->getValue( + self::XML_PATH_IS_CONFIRM, + ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); + } + + /** + * Check whether confirmation may be skipped when registering using certain email address. + * + * @param int|null $customerId + * @param string $customerEmail + * @return bool + */ + private function canSkipConfirmation($customerId, $customerEmail): bool + { + if (!$customerId) { + return false; + } + + /* If an email was used to start the registration process and it is the same email as the one + used to register, then this can skip confirmation. + */ + $skipConfirmationIfEmail = $this->registry->registry("skip_confirmation_if_email"); + if (!$skipConfirmationIfEmail) { + return false; + } + + return strtolower($skipConfirmationIfEmail) === strtolower($customerEmail); + } +} diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index ba646549f6919..e252ae57ad595 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -16,9 +16,13 @@ use Magento\Customer\Model\Config\Share as ConfigShare; use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\CredentialsValidator; +use Magento\Customer\Model\Data\Customer; use Magento\Customer\Model\Metadata\Validator; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; +use Magento\Directory\Model\AllowedCountries; use Magento\Eav\Model\Validator\Attribute\Backend; use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -40,8 +44,11 @@ use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Math\Random; +use Magento\Framework\Phrase; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Registry; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\StringUtils as StringHelper; use Magento\Store\Model\ScopeInterface; @@ -49,11 +56,12 @@ use Psr\Log\LoggerInterface as PsrLogger; /** - * Handle various customer account actions + * Handle various customer account actions. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AccountManagement implements AccountManagementInterface { @@ -89,6 +97,10 @@ class AccountManagement implements AccountManagementInterface */ const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; + /** + * @deprecated + * @see AccountConfirmation::XML_PATH_IS_CONFIRM + */ const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; /** @@ -238,6 +250,21 @@ class AccountManagement implements AccountManagementInterface */ private $transportBuilder; + /** + * @var SessionManagerInterface + */ + private $sessionManager; + + /** + * @var SaveHandlerInterface + */ + private $saveHandler; + + /** + * @var CollectionFactory + */ + private $visitorCollectionFactory; + /** * @var DataObjectProcessor */ @@ -298,6 +325,26 @@ class AccountManagement implements AccountManagementInterface */ private $dateTimeFactory; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var AddressRegistry + */ + private $addressRegistry; + + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -323,8 +370,17 @@ class AccountManagement implements AccountManagementInterface * @param ObjectFactory $objectFactory * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param CredentialsValidator|null $credentialsValidator - * @param DateTimeFactory $dateTimeFactory + * @param DateTimeFactory|null $dateTimeFactory + * @param AccountConfirmation|null $accountConfirmation + * @param SessionManagerInterface|null $sessionManager + * @param SaveHandlerInterface|null $saveHandler + * @param CollectionFactory|null $visitorCollectionFactory + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param AddressRegistry|null $addressRegistry + * @param AllowedCountries|null $allowedCountriesReader * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( CustomerFactory $customerFactory, @@ -351,7 +407,14 @@ public function __construct( ObjectFactory $objectFactory, ExtensibleDataObjectConverter $extensibleDataObjectConverter, CredentialsValidator $credentialsValidator = null, - DateTimeFactory $dateTimeFactory = null + DateTimeFactory $dateTimeFactory = null, + AccountConfirmation $accountConfirmation = null, + SessionManagerInterface $sessionManager = null, + SaveHandlerInterface $saveHandler = null, + CollectionFactory $visitorCollectionFactory = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + AddressRegistry $addressRegistry = null, + AllowedCountries $allowedCountriesReader = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -379,6 +442,20 @@ public function __construct( $this->credentialsValidator = $credentialsValidator ?: ObjectManager::getInstance()->get(CredentialsValidator::class); $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); + $this->sessionManager = $sessionManager + ?: ObjectManager::getInstance()->get(SessionManagerInterface::class); + $this->saveHandler = $saveHandler + ?: ObjectManager::getInstance()->get(SaveHandlerInterface::class); + $this->visitorCollectionFactory = $visitorCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder + ?: ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); + $this->addressRegistry = $addressRegistry + ?: ObjectManager::getInstance()->get(AddressRegistry::class); + $this->allowedCountriesReader = $allowedCountriesReader + ?: ObjectManager::getInstance()->get(AllowedCountries::class); } /** @@ -398,7 +475,7 @@ private function getAuthentication() } /** - * {@inheritdoc} + * @inheritdoc */ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '') { @@ -421,7 +498,7 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '') } /** - * {@inheritdoc} + * @inheritdoc */ public function activate($email, $confirmationKey) { @@ -430,7 +507,7 @@ public function activate($email, $confirmationKey) } /** - * {@inheritdoc} + * @inheritdoc */ public function activateById($customerId, $confirmationKey) { @@ -444,8 +521,11 @@ public function activateById($customerId, $confirmationKey) * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @param string $confirmationKey * @return \Magento\Customer\Api\Data\CustomerInterface - * @throws \Magento\Framework\Exception\State\InvalidTransitionException - * @throws \Magento\Framework\Exception\State\InputMismatchException + * @throws InputException + * @throws InputMismatchException + * @throws InvalidTransitionException + * @throws LocalizedException + * @throws NoSuchEntityException */ private function activateCustomer($customer, $confirmationKey) { @@ -459,13 +539,15 @@ private function activateCustomer($customer, $confirmationKey) } $customer->setConfirmation(null); + // No need to validate customer and customer address while activating customer + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $this->getEmailNotification()->newAccount($customer, 'confirmed', '', $this->storeManager->getStore()->getId()); return $customer; } /** - * {@inheritdoc} + * @inheritdoc */ public function authenticate($username, $password) { @@ -500,7 +582,7 @@ public function authenticate($username, $password) } /** - * {@inheritdoc} + * @inheritdoc */ public function validateResetPasswordLinkToken($customerId, $resetPasswordLinkToken) { @@ -509,7 +591,7 @@ public function validateResetPasswordLinkToken($customerId, $resetPasswordLinkTo } /** - * {@inheritdoc} + * @inheritdoc */ public function initiatePasswordReset($email, $template, $websiteId = null) { @@ -519,6 +601,9 @@ public function initiatePasswordReset($email, $template, $websiteId = null) // load customer by email $customer = $this->customerRepository->get($email, $websiteId); + // No need to validate customer address while saving customer reset password token + $this->disableAddressValidation($customer); + $newPasswordToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newPasswordToken); @@ -531,51 +616,104 @@ public function initiatePasswordReset($email, $template, $websiteId = null) $this->getEmailNotification()->passwordResetConfirmation($customer); break; default: - $this->handleUnknownTemplate($template); - break; + throw new InputException(__( + 'Invalid value of "%value" provided for the %fieldName field. '. + 'Possible values: %template1 or %template2.', + [ + 'value' => $template, + 'fieldName' => 'template', + 'template1' => AccountManagement::EMAIL_REMINDER, + 'template2' => AccountManagement::EMAIL_RESET + ] + )); } + return true; } catch (MailException $e) { // If we are not able to send a reset password email, this should be ignored $this->logger->critical($e); } + return false; } /** - * Handle not supported template + * Match a customer by their RP token. * - * @param string $template - * @throws InputException + * @param string $rpToken + * @return CustomerInterface + * @throws ExpiredException + * @throws LocalizedException + * @throws NoSuchEntityException */ - private function handleUnknownTemplate($template) + private function matchCustomerByRpToken(string $rpToken): CustomerInterface { - throw new InputException(__( - 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.', - [ - 'value' => $template, - 'fieldName' => 'template', - 'template1' => AccountManagement::EMAIL_REMINDER, - 'template2' => AccountManagement::EMAIL_RESET - ] - )); + + $this->searchCriteriaBuilder->addFilter( + 'rp_token', + $rpToken + ); + $this->searchCriteriaBuilder->setPageSize(1); + $found = $this->customerRepository->getList( + $this->searchCriteriaBuilder->create() + ); + + if ($found->getTotalCount() > 1) { + //Failed to generated unique RP token + throw new ExpiredException( + new Phrase('Reset password token expired.') + ); + } + if ($found->getTotalCount() === 0) { + //Customer with such token not found. + throw NoSuchEntityException::singleField( + 'rp_token', + $rpToken + ); + } + + //Unique customer found. + return $found->getItems()[0]; } /** - * {@inheritdoc} + * @inheritdoc */ public function resetPassword($email, $resetToken, $newPassword) { - $customer = $this->customerRepository->get($email); + if (!$email) { + $customer = $this->matchCustomerByRpToken($resetToken); + $email = $customer->getEmail(); + } else { + $customer = $this->customerRepository->get($email); + } + + // No need to validate customer and customer address while saving customer reset password token + $this->disableAddressValidation($customer); + $this->setIgnoreValidationFlag($customer); + //Validate Token and new password strength $this->validateResetPasswordToken($customer->getId(), $resetToken); + $this->credentialsValidator->checkPasswordDifferentFromEmail( + $email, + $newPassword + ); $this->checkPasswordStrength($newPassword); //Update secure data $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); + $this->getAuthentication()->unlock($customer->getId()); + $this->destroyCustomerSessions($customer->getId()); + if ($this->sessionManager->isSessionExists() && !headers_sent()) { + //delete old session and move data to the new session + //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session + // phpcs:ignore Magento2.Functions.DiscouragedFunction + session_regenerate_id(true); + } $this->customerRepository->save($customer); + return true; } @@ -665,7 +803,7 @@ protected function getMinPasswordLength() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfirmationStatus($customerId) { @@ -681,9 +819,9 @@ public function getConfirmationStatus($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ - public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '', $extensions = []) { if ($password !== null) { $this->checkPasswordStrength($password); @@ -697,16 +835,21 @@ public function createAccount(CustomerInterface $customer, $password = null, $re } else { $hash = null; } - return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl); + + return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl, $extensions); } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function createAccountWithPasswordHash(CustomerInterface $customer, $hash, $redirectUrl = '') - { + public function createAccountWithPasswordHash( + CustomerInterface $customer, + $hash, + $redirectUrl = '', + $extensions = [] + ) { // This logic allows an existing customer to be added to a different store. No new account is created. // The plan is to move this logic into a new method called something like 'registerAccountWithStore' if ($customer->getId()) { @@ -755,6 +898,9 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash } try { foreach ($customerAddresses as $address) { + if (!$this->isAddressAllowedForWebsite($address, $customer->getStoreId())) { + continue; + } if ($address->getId()) { $newAddress = clone $address; $newAddress->setId(null); @@ -773,13 +919,13 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash $customer = $this->customerRepository->getById($customer->getId()); $newLinkToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newLinkToken); - $this->sendEmailConfirmation($customer, $redirectUrl); + $this->sendEmailConfirmation($customer, $redirectUrl, $extensions); return $customer; } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultBillingAddress($customerId) { @@ -788,7 +934,7 @@ public function getDefaultBillingAddress($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultShippingAddress($customerId) { @@ -801,9 +947,12 @@ public function getDefaultShippingAddress($customerId) * * @param CustomerInterface $customer * @param string $redirectUrl + * @param array $extensions * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ - protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) + protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl, $extensions = []) { try { $hash = $this->customerRegistry->retrieveSecureData($customer->getId())->getPasswordHash(); @@ -813,15 +962,25 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU } elseif ($hash == '') { $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } - $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); + $this->getEmailNotification()->newAccount( + $customer, + $templateType, + $redirectUrl, + $customer->getStoreId(), + null, + $extensions + ); + $customer->setConfirmation(null); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); + } catch (\UnexpectedValueException $e) { + $this->logger->error($e); } } /** - * {@inheritdoc} + * @inheritdoc */ public function changePassword($email, $currentPassword, $newPassword) { @@ -834,7 +993,7 @@ public function changePassword($email, $currentPassword, $newPassword) } /** - * {@inheritdoc} + * @inheritdoc */ public function changePasswordById($customerId, $currentPassword, $newPassword) { @@ -847,14 +1006,17 @@ public function changePasswordById($customerId, $currentPassword, $newPassword) } /** - * Change customer password + * Change customer password. * * @param CustomerInterface $customer * @param string $currentPassword * @param string $newPassword * @return bool true on success * @throws InputException + * @throws InputMismatchException * @throws InvalidEmailOrPasswordException + * @throws LocalizedException + * @throws NoSuchEntityException * @throws UserLockedException */ private function changePasswordForCustomer($customer, $currentPassword, $newPassword) @@ -871,7 +1033,10 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass $customerSecure->setRpTokenCreatedAt(null); $this->checkPasswordStrength($newPassword); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); + $this->destroyCustomerSessions($customer->getId()); + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); + return true; } @@ -887,6 +1052,8 @@ protected function createPasswordHash($password) } /** + * Get EAV validator + * * @return Backend */ private function getEavValidator() @@ -898,7 +1065,7 @@ private function getEavValidator() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate(CustomerInterface $customer) { @@ -923,7 +1090,7 @@ public function validate(CustomerInterface $customer) } /** - * {@inheritdoc} + * @inheritdoc */ public function isEmailAvailable($customerEmail, $websiteId = null) { @@ -939,7 +1106,7 @@ public function isEmailAvailable($customerEmail, $websiteId = null) } /** - * {@inheritDoc} + * @inheritdoc */ public function isCustomerInStore($customerWebsiteId, $storeId) { @@ -961,20 +1128,18 @@ public function isCustomerInStore($customerWebsiteId, $storeId) * @param int $customerId * @param string $resetPasswordLinkToken * @return bool - * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched - * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired - * @throws \Magento\Framework\Exception\InputException If token or customer id is invalid - * @throws \Magento\Framework\Exception\NoSuchEntityException If customer doesn't exist + * @throws ExpiredException + * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ private function validateResetPasswordToken($customerId, $resetPasswordLinkToken) { if (empty($customerId) || $customerId < 0) { - throw new InputException( - __( - 'Invalid value of "%value" provided for the %fieldName field.', - ['value' => $customerId, 'fieldName' => 'customerId'] - ) - ); + //Looking for the customer. + $customerId = $this->matchCustomerByRpToken($resetPasswordLinkToken) + ->getId(); } if (!is_string($resetPasswordLinkToken) || empty($resetPasswordLinkToken)) { $params = ['fieldName' => 'resetPasswordLinkToken']; @@ -1057,6 +1222,8 @@ protected function sendNewAccountEmail( * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function sendPasswordResetNotificationEmail($customer) @@ -1070,6 +1237,7 @@ protected function sendPasswordResetNotificationEmail($customer) * @param CustomerInterface $customer * @param int|string|null $defaultStoreId * @return int + * @throws LocalizedException * @deprecated 100.1.0 */ protected function getWebsiteStoreId($customer, $defaultStoreId = null) @@ -1083,6 +1251,8 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null) } /** + * Get email template types + * * @return array * @deprecated 100.1.0 */ @@ -1116,6 +1286,7 @@ protected function getTemplateTypes() * @param int|null $storeId * @param string $email * @return $this + * @throws MailException * @deprecated 100.1.0 */ protected function sendEmailTemplate( @@ -1147,17 +1318,15 @@ protected function sendEmailTemplate( * * @param CustomerInterface $customer * @return bool + * @deprecated + * @see AccountConfirmation::isConfirmationRequired */ protected function isConfirmationRequired($customer) { - if ($this->canSkipConfirmation($customer)) { - return false; - } - - return (bool)$this->scopeConfig->getValue( - self::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, - $customer->getWebsiteId() + return $this->accountConfirmation->isConfirmationRequired( + $customer->getWebsiteId(), + $customer->getId(), + $customer->getEmail() ); } @@ -1166,6 +1335,8 @@ protected function isConfirmationRequired($customer) * * @param CustomerInterface $customer * @return bool + * @deprecated + * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation($customer) { @@ -1214,14 +1385,15 @@ public function isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt) } /** - * Change reset password link token - * - * Stores new reset password link token + * Set a new reset password link token. * * @param CustomerInterface $customer * @param string $passwordLinkToken * @return bool * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) { @@ -1239,8 +1411,10 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) $customerSecure->setRpTokenCreatedAt( $this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT) ); + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } + return true; } @@ -1249,6 +1423,8 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordReminderEmail($customer) @@ -1276,6 +1452,8 @@ public function sendPasswordReminderEmail($customer) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordResetConfirmationEmail($customer) @@ -1320,6 +1498,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId) * * @param CustomerInterface $customer * @return Data\CustomerSecure + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function getFullCustomerObject($customer) @@ -1345,6 +1524,20 @@ public function getPasswordHash($password) return $this->encryptor->getHash($password); } + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + /** * Get email notification * @@ -1361,4 +1554,59 @@ private function getEmailNotification() return $this->emailNotification; } } + + /** + * Destroy all active customer sessions by customer id (current session will not be destroyed). + * + * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering + * configured session lifetime. + * + * @param string|int $customerId + * @return void + */ + private function destroyCustomerSessions($customerId) + { + $sessionLifetime = $this->scopeConfig->getValue( + \Magento\Framework\Session\Config::XML_PATH_COOKIE_LIFETIME, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + $dateTime = $this->dateTimeFactory->create(); + $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime) + ->format(DateTime::DATETIME_PHP_FORMAT); + /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */ + $visitorCollection = $this->visitorCollectionFactory->create(); + $visitorCollection->addFieldToFilter('customer_id', $customerId); + $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); + $visitorCollection->addFieldToFilter('session_id', ['neq' => $this->sessionManager->getSessionId()]); + /** @var \Magento\Customer\Model\Visitor $visitor */ + foreach ($visitorCollection->getItems() as $visitor) { + $sessionId = $visitor->getSessionId(); + $this->saveHandler->destroy($sessionId); + } + } + + /** + * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation. + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag(Customer $customer) + { + $customer->setData('ignore_validation_flag', true); + } + + /** + * Check is address allowed for store + * + * @param AddressInterface $address + * @param int|null $storeId + * @return bool + */ + private function isAddressAllowedForWebsite(AddressInterface $address, $storeId): bool + { + $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); + + return in_array($address->getCountryId(), $allowedCountries); + } } diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index 1dcd8516af69f..c39420542248e 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -154,9 +154,6 @@ public function updateData(AddressInterface $address) // Need to explicitly set this due to discrepancy in the keys between model and data object $this->setIsDefaultBilling($address->isDefaultBilling()); $this->setIsDefaultShipping($address->isDefaultShipping()); - if (!$this->getAttributeSetId()) { - $this->setAttributeSetId(AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS); - } $customAttributes = $address->getCustomAttributes(); if ($customAttributes !== null) { foreach ($customAttributes as $attribute) { diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index a6ba510932d3d..09b3540f2b689 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -30,6 +30,7 @@ * @method string getPostcode() * @method bool getShouldIgnoreValidation() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * * @api * @since 100.0.2 @@ -118,6 +119,11 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt */ protected $dataObjectHelper; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -135,6 +141,7 @@ class AbstractAddress extends AbstractExtensibleModel implements AddressModelInt * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\Escaper|null $escaper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -153,7 +160,8 @@ public function __construct( \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Framework\Escaper $escaper = null ) { $this->_directoryData = $directoryData; $data = $this->_implodeArrayField($data); @@ -165,6 +173,9 @@ public function __construct( $this->addressDataFactory = $addressDataFactory; $this->regionDataFactory = $regionDataFactory; $this->dataObjectHelper = $dataObjectHelper; + $this->escaper = $escaper ?? \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Escaper::class + ); parent::__construct( $context, $registry, @@ -263,7 +274,7 @@ public function setStreet($street) * * @param array|string $key * @param null $value - * @return \Magento\Framework\DataObject + * @return $this */ public function setData($key, $value = null) { @@ -562,9 +573,7 @@ public function getDataModel($defaultBillingAddressId = null, $defaultShippingAd } /** - * Validate address attribute values - * - * + * Validate address attribute values. * * @return bool|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -619,23 +628,53 @@ public function validate() $errors[] = __('%fieldName is a required field.', ['fieldName' => 'postcode']); } - if (!\Zend_Validate::is($this->getCountryId(), 'NotEmpty')) { + $countryId = $this->getCountryId(); + if (!\Zend_Validate::is($countryId, 'NotEmpty')) { $errors[] = __('%fieldName is a required field.', ['fieldName' => 'countryId']); - } - - if ($this->getCountryModel()->getRegionCollection()->getSize() && !\Zend_Validate::is( - $this->getRegionId(), - 'NotEmpty' - ) && $this->_directoryData->isRegionRequired( - $this->getCountryId() - ) - ) { - $errors[] = __('%fieldName is a required field.', ['fieldName' => 'regionId']); + } else { + //Checking if such country exists. + $countryCollection = $this->_directoryData->getCountryCollection($this->getStoreId()); + if (!in_array($countryId, $countryCollection->getAllIds(), true)) { + $errors[] = __( + 'Invalid value of "%value" provided for the %fieldName field.', + [ + 'fieldName' => 'countryId', + 'value' => $this->escaper->escapeHtml($countryId) + ] + ); + } else { + //If country is valid then validating selected region ID. + $countryModel = $this->getCountryModel(); + $regionCollection = $countryModel->getRegionCollection(); + $region = $this->getRegion(); + $regionId = (string)$this->getRegionId(); + $allowedRegions = $regionCollection->getAllIds(); + $isRegionRequired = $this->_directoryData->isRegionRequired($countryId); + if ($isRegionRequired && empty($allowedRegions) && !\Zend_Validate::is($region, 'NotEmpty')) { + //If region is required for country and country doesn't provide regions list + //region must be provided. + $errors[] = __('%fieldName is a required field.', ['fieldName' => 'region']); + } elseif ($allowedRegions && !\Zend_Validate::is($regionId, 'NotEmpty') && $isRegionRequired) { + //If country actually has regions and requires you to + //select one then it must be selected. + $errors[] = __('%fieldName is a required field.', ['fieldName' => 'regionId']); + } elseif ($allowedRegions && $regionId && !in_array($regionId, $allowedRegions, true)) { + //If a region is selected then checking if it exists. + $errors[] = __( + 'Invalid value of "%value" provided for the %fieldName field.', + [ + 'fieldName' => 'regionId', + 'value' => $this->escaper->escapeHtml($regionId) + ] + ); + } + } } if (empty($errors) || $this->getShouldIgnoreValidation()) { return true; } + return $errors; } diff --git a/app/code/Magento/Customer/Model/Address/Mapper.php b/app/code/Magento/Customer/Model/Address/Mapper.php index f1128b5d37def..629673ca7189b 100644 --- a/app/code/Magento/Customer/Model/Address/Mapper.php +++ b/app/code/Magento/Customer/Model/Address/Mapper.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Model\Address; use Magento\Customer\Api\Data\AddressInterface; @@ -39,7 +37,11 @@ public function __construct(ExtensibleDataObjectConverter $extensibleDataObjectC */ public function toFlatArray($addressDataObject) { - $flatAddressArray = $this->extensibleDataObjectConverter->toFlatArray($addressDataObject, [], \Magento\Customer\Api\Data\AddressInterface::class); + $flatAddressArray = $this->extensibleDataObjectConverter->toFlatArray( + $addressDataObject, + [], + \Magento\Customer\Api\Data\AddressInterface::class + ); //preserve street $street = $addressDataObject->getStreet(); if (!empty($street) && is_array($street)) { diff --git a/app/code/Magento/Customer/Model/Authentication.php b/app/code/Magento/Customer/Model/Authentication.php index 0967f1a0189e3..b0729647d7eec 100644 --- a/app/code/Magento/Customer/Model/Authentication.php +++ b/app/code/Magento/Customer/Model/Authentication.php @@ -167,7 +167,7 @@ public function authenticate($customerId, $password) { $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); $hash = $customerSecure->getPasswordHash(); - if (!$this->encryptor->validateHash($password, $hash)) { + if (!$hash || !$this->encryptor->validateHash($password, $hash)) { $this->processAuthenticationFailure($customerId); if ($this->isLocked($customerId)) { throw new UserLockedException(__('The account is locked.')); diff --git a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php index a333fe8df594a..36eabe3571ceb 100644 --- a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php +++ b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php @@ -7,6 +7,7 @@ use Magento\Checkout\Model\ConfigProviderInterface; use Magento\Customer\Model\Url; +use Magento\Framework\App\ObjectManager; use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -22,6 +23,7 @@ class ConfigProvider implements ConfigProviderInterface /** * @var UrlInterface + * @deprecated */ protected $urlBuilder; @@ -30,19 +32,28 @@ class ConfigProvider implements ConfigProviderInterface */ protected $scopeConfig; + /** + * @var Url + */ + private $customerUrl; + /** * @param UrlInterface $urlBuilder * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $scopeConfig + * @param Url|null $customerUrl */ public function __construct( UrlInterface $urlBuilder, StoreManagerInterface $storeManager, - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + Url $customerUrl = null ) { $this->urlBuilder = $urlBuilder; $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; + $this->customerUrl = $customerUrl ?? ObjectManager::getInstance() + ->get(Url::class); } /** @@ -78,7 +89,7 @@ protected function isAutocompleteEnabled() */ protected function getLoginUrl() { - return $this->urlBuilder->getUrl(Url::ROUTE_ACCOUNT_LOGIN); + return $this->customerUrl->getLoginUrl(); } /** diff --git a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php index fc0fa3ebc073d..40a10a1db0935 100644 --- a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php +++ b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php @@ -87,15 +87,20 @@ public function afterDelete() { $result = parent::afterDelete(); - if ($this->getScope() == \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) { - $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); - $website = $this->_storeManager->getWebsite($this->getScopeCode()); - $attribute->setWebsite($website); - $attribute->load($attribute->getId()); - $attribute->setData('scope_multiline_count', null); - $attribute->save(); - } + $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); + switch ($this->getScope()) { + case \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES: + $website = $this->_storeManager->getWebsite($this->getScopeCode()); + $attribute->setWebsite($website); + $attribute->load($attribute->getId()); + $attribute->setData('scope_multiline_count', null); + break; + case ScopeConfigInterface::SCOPE_TYPE_DEFAULT: + $attribute->setData('multiline_count', 2); + break; + } + $attribute->save(); return $result; } } diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 2e2260f16ff91..ccecb7ba54f1b 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -18,6 +18,8 @@ use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; /** * Customer model @@ -58,6 +60,10 @@ class Customer extends \Magento\Framework\Model\AbstractModel const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; + /** + * @deprecated + * @see AccountConfirmation::XML_PATH_IS_CONFIRM + */ const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; @@ -174,7 +180,7 @@ class Customer extends \Magento\Framework\Model\AbstractModel protected $_encryptor; /** - * @var \Magento\Framework\Math\Random + * @var Random */ protected $mathRandom; @@ -208,6 +214,11 @@ class Customer extends \Magento\Framework\Model\AbstractModel */ protected $indexerRegistry; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -229,6 +240,8 @@ class Customer extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data + * @param AccountConfirmation|null $accountConfirmation + * @param Random|null $mathRandom * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -252,7 +265,9 @@ public function __construct( \Magento\Customer\Api\CustomerMetadataInterface $metadataService, \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + AccountConfirmation $accountConfirmation = null, + Random $mathRandom = null ) { $this->metadataService = $metadataService; $this->_scopeConfig = $scopeConfig; @@ -269,6 +284,9 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->dataObjectHelper = $dataObjectHelper; $this->indexerRegistry = $indexerRegistry; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); + $this->mathRandom = $mathRandom ?: ObjectManager::getInstance()->get(Random::class); parent::__construct( $context, $registry, @@ -344,13 +362,6 @@ public function updateData($customer) $this->setId($customerId); } - // Need to use attribute set or future updates can cause data loss - if (!$this->getAttributeSetId()) { - $this->setAttributeSetId( - CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER - ); - } - return $this; } @@ -770,20 +781,14 @@ public function sendNewAccountEmail($type = 'registered', $backUrl = '', $storeI * Check if accounts confirmation is required in config * * @return bool + * @deprecated + * @see AccountConfirmation::isConfirmationRequired */ public function isConfirmationRequired() { - if ($this->canSkipConfirmation()) { - return false; - } - $websiteId = $this->getWebsiteId() ? $this->getWebsiteId() : null; - return (bool)$this->_scopeConfig->getValue( - self::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, - $websiteId - ); + return $this->accountConfirmation->isConfirmationRequired($websiteId, $this->getId(), $this->getEmail()); } /** @@ -793,7 +798,7 @@ public function isConfirmationRequired() */ public function getRandomConfirmationKey() { - return md5(uniqid()); + return $this->mathRandom->getRandomString(32); } /** @@ -952,6 +957,16 @@ public function getSharedWebsiteIds() return $ids; } + /** + * Retrieve attribute set id for customer. + * + * @return int + */ + public function getAttributeSetId() + { + return parent::getAttributeSetId() ?: CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; + } + /** * Set store to customer * @@ -1156,6 +1171,8 @@ public function setIsReadonly($value) * Check whether confirmation may be skipped when registering using certain email address * * @return bool + * @deprecated + * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation() { diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php index e70fc30d7a528..296d2877df8ea 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php @@ -42,13 +42,21 @@ public function __construct( } /** - * @return array + * @inheritdoc */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $groups = $this->_groupManagement->getLoggedInGroups(); + $this->_options = $this->_converter->toOptionArray($groups, 'id', 'code'); + + array_walk( + $this->_options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); } return $this->_options; diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Store.php b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Store.php index 1845ba36d46c1..e730033641da0 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Store.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Store.php @@ -40,9 +40,9 @@ public function __construct( } /** - * @return array + * @inheritdoc */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $collection = $this->_createStoresCollection(); diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Website.php b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Website.php index d670ed73db654..43c68f06d5f6c 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Website.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Website.php @@ -32,9 +32,9 @@ public function __construct( } /** - * @return array + * @inheritdoc */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $this->_options = $this->_store->getWebsiteValuesForForm(); diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index ce976d3f62c74..a629e4ae9d29e 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -27,6 +27,7 @@ use Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool; use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Ui\Component\Form\Element\Multiline; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -343,7 +344,12 @@ protected function getAttributesMeta(Type $entityType) // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value foreach ($this->metaProperties as $metaName => $origName) { $value = $attribute->getDataUsingMethod($origName); - $meta[$code]['arguments']['data']['config'][$metaName] = ($metaName === 'label') ? __($value) : $value; + if ($metaName === 'label') { + $meta[$code]['arguments']['data']['config'][$metaName] = __($value); + $meta[$code]['arguments']['data']['config']['__disableTmpl'] = [$metaName => true]; + } else { + $meta[$code]['arguments']['data']['config'][$metaName] = $value; + } if ('frontend_input' === $origName) { $meta[$code]['arguments']['data']['config']['formElement'] = isset($this->formElement[$value]) ? $this->formElement[$value] @@ -356,7 +362,14 @@ protected function getAttributesMeta(Type $entityType) $meta[$code]['arguments']['data']['config']['options'] = $this->getCountryWithWebsiteSource() ->getAllOptions(); } else { - $meta[$code]['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); + $options = $attribute->getSource()->getAllOptions(); + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = ['label' => true]; + } + ); + $meta[$code]['arguments']['data']['config']['options'] = $options; } } @@ -370,50 +383,21 @@ protected function getAttributesMeta(Type $entityType) $this->overrideFileUploaderMetadata($entityType, $attribute, $meta[$code]['arguments']['data']['config']); } - $this->processWebsiteMeta($meta); return $meta; } - /** - * Check whether the specific attribute can be shown in form: customer registration, customer edit, etc... - * - * @param Attribute $customerAttribute - * @return bool - */ - private function canShowAttributeInForm(AbstractAttribute $customerAttribute) - { - $isRegistration = $this->context->getRequestParam($this->getRequestFieldName()) === null; - - if ($customerAttribute->getEntityType()->getEntityTypeCode() === 'customer') { - return is_array($customerAttribute->getUsedInForms()) && - ( - (in_array('customer_account_create', $customerAttribute->getUsedInForms()) && $isRegistration) || - (in_array('customer_account_edit', $customerAttribute->getUsedInForms()) && !$isRegistration) - ); - } else { - return is_array($customerAttribute->getUsedInForms()) && - in_array('customer_address_edit', $customerAttribute->getUsedInForms()); - } - } - /** * Detect can we show attribute on specific form or not * * @param Attribute $customerAttribute * @return bool */ - private function canShowAttribute(AbstractAttribute $customerAttribute) + private function canShowAttribute(AbstractAttribute $customerAttribute): bool { - $userDefined = (bool) $customerAttribute->getIsUserDefined(); - if (!$userDefined) { - return $customerAttribute->getIsVisible(); - } - - $canShowOnForm = $this->canShowAttributeInForm($customerAttribute); - - return ($this->allowToShowHiddenAttributes && $canShowOnForm) || - (!$this->allowToShowHiddenAttributes && $canShowOnForm && $customerAttribute->getIsVisible()); + return $this->allowToShowHiddenAttributes && (bool) $customerAttribute->getIsUserDefined() + ? true + : (bool) $customerAttribute->getIsVisible(); } /** @@ -503,6 +487,7 @@ private function overrideFileUploaderMetadata( $url = $this->getFileUploadUrl($entityTypeCode); $config = [ + 'dataType' => $this->getMetadataValue($config, 'dataType'), 'formElement' => 'fileUploader', 'componentType' => 'fileUploader', 'maxFileSize' => $maxFileSize, @@ -596,8 +581,14 @@ protected function prepareAddressData($addressId, array &$addresses, array $cust ) { $addresses[$addressId]['default_shipping'] = $customer['default_shipping']; } - if (isset($addresses[$addressId]['street']) && !is_array($addresses[$addressId]['street'])) { - $addresses[$addressId]['street'] = explode("\n", $addresses[$addressId]['street']); + + foreach ($this->meta['address']['children'] as $attributeName => $attributeMeta) { + if ($attributeMeta['arguments']['data']['config']['dataType'] === Multiline::NAME + && isset($addresses[$addressId][$attributeName]) + && !\is_array($addresses[$addressId][$attributeName]) + ) { + $addresses[$addressId][$attributeName] = explode("\n", $addresses[$addressId][$attributeName]); + } } } diff --git a/app/code/Magento/Customer/Model/Customer/Mapper.php b/app/code/Magento/Customer/Model/Customer/Mapper.php index 6512e6ca897d3..7e4ca6c8cd04d 100644 --- a/app/code/Magento/Customer/Model/Customer/Mapper.php +++ b/app/code/Magento/Customer/Model/Customer/Mapper.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Model\Customer; use Magento\Customer\Api\Data\CustomerInterface; @@ -38,8 +36,13 @@ public function __construct(ExtensibleDataObjectConverter $extensibleDataObjectC */ public function toFlatArray(CustomerInterface $customer) { - $flatArray = $this->extensibleDataObjectConverter->toNestedArray($customer, [], \Magento\Customer\Api\Data\CustomerInterface::class); + $flatArray = $this->extensibleDataObjectConverter->toNestedArray( + $customer, + [], + \Magento\Customer\Api\Data\CustomerInterface::class + ); unset($flatArray["addresses"]); + return ConvertArray::toFlatArray($flatArray); } } diff --git a/app/code/Magento/Customer/Model/Customer/NotificationStorage.php b/app/code/Magento/Customer/Model/Customer/NotificationStorage.php index 7054324851f34..11e0b9b916559 100644 --- a/app/code/Magento/Customer/Model/Customer/NotificationStorage.php +++ b/app/code/Magento/Customer/Model/Customer/NotificationStorage.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Model\Customer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Serialize\SerializerInterface; @@ -18,21 +19,21 @@ class NotificationStorage private $cache; /** - * @param FrontendInterface $cache - */ - - /** - * @param FrontendInterface $cache + * @var SerializerInterface */ private $serializer; /** * NotificationStorage constructor. * @param FrontendInterface $cache + * @param SerializerInterface $serializer */ - public function __construct(FrontendInterface $cache) - { + public function __construct( + FrontendInterface $cache, + SerializerInterface $serializer = null + ) { $this->cache = $cache; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); } /** @@ -45,7 +46,7 @@ public function __construct(FrontendInterface $cache) public function add($notificationType, $customerId) { $this->cache->save( - $this->getSerializer()->serialize([ + $this->serializer->serialize([ 'customer_id' => $customerId, 'notification_type' => $notificationType ]), @@ -88,19 +89,4 @@ private function getCacheKey($notificationType, $customerId) { return 'notification_' . $notificationType . '_' . $customerId; } - - /** - * Get serializer - * - * @return SerializerInterface - * @deprecated 100.2.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance() - ->get(SerializerInterface::class); - } - return $this->serializer; - } } diff --git a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php index 06de649524e71..bc9bffb6ffdf0 100644 --- a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php +++ b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php @@ -6,31 +6,43 @@ namespace Magento\Customer\Model; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; + /** * Customer Authentication update model. */ class CustomerAuthUpdate { /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ protected $customerRegistry; /** - * @var \Magento\Customer\Model\ResourceModel\Customer + * @var CustomerResourceModel */ protected $customerResourceModel; /** - * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry - * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel + * @var Customer + */ + private $customerModel; + + /** + * @param CustomerRegistry $customerRegistry + * @param CustomerResourceModel $customerResourceModel + * @param Customer|null $customerModel */ public function __construct( - \Magento\Customer\Model\CustomerRegistry $customerRegistry, - \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel + CustomerRegistry $customerRegistry, + CustomerResourceModel $customerResourceModel, + Customer $customerModel = null ) { $this->customerRegistry = $customerRegistry; $this->customerResourceModel = $customerResourceModel; + $this->customerModel = $customerModel ?: ObjectManager::getInstance()->get(Customer::class); } /** @@ -38,21 +50,30 @@ public function __construct( * * @param int $customerId * @return $this + * @throws NoSuchEntityException */ public function saveAuth($customerId) { $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); + $this->customerResourceModel->load($this->customerModel, $customerId); + $currentLockExpiresVal = $this->customerModel->getData('lock_expires'); + $newLockExpiresVal = $customerSecure->getData('lock_expires'); + $this->customerResourceModel->getConnection()->update( $this->customerResourceModel->getTable('customer_entity'), [ 'failures_num' => $customerSecure->getData('failures_num'), 'first_failure' => $customerSecure->getData('first_failure'), - 'lock_expires' => $customerSecure->getData('lock_expires'), + 'lock_expires' => $newLockExpiresVal, ], $this->customerResourceModel->getConnection()->quoteInto('entity_id = ?', $customerId) ); + if ($currentLockExpiresVal !== $newLockExpiresVal) { + $this->customerModel->reindex(); + } + return $this; } } diff --git a/app/code/Magento/Customer/Model/Data/Address.php b/app/code/Magento/Customer/Model/Data/Address.php index f4cf228fb557c..b399a144368d3 100644 --- a/app/code/Magento/Customer/Model/Data/Address.php +++ b/app/code/Magento/Customer/Model/Data/Address.php @@ -42,7 +42,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function getCustomAttributesCodes() { @@ -327,7 +327,7 @@ public function setCompany($company) */ public function setTelephone($telephone) { - return $this->setData(self::TELEPHONE, $telephone); + return $this->setData(self::TELEPHONE, trim($telephone)); } /** @@ -452,7 +452,7 @@ public function setIsDefaultBilling($isDefaultBilling) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Customer\Api\Data\AddressExtensionInterface|null */ @@ -462,7 +462,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Customer\Api\Data\AddressExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php b/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php new file mode 100644 index 0000000000000..cb78fe3eafc96 --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php @@ -0,0 +1,56 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Delegation; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\AccountDelegationInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\Result\RedirectFactory; + +/** + * @inheritDoc + */ +class AccountDelegation implements AccountDelegationInterface +{ + /** + * @var RedirectFactory + */ + private $redirectFactory; + + /** + * @var Storage + */ + private $storage; + + /** + * @param RedirectFactory $redirectFactory + * @param Storage $storage + */ + public function __construct( + RedirectFactory $redirectFactory, + Storage $storage + ) { + $this->redirectFactory = $redirectFactory; + $this->storage = $storage; + } + + /** + * @inheritDoc + */ + public function createRedirectForNew( + CustomerInterface $customer, + array $mixedData = null + ): Redirect { + $this->storage->storeNewOperation($customer, $mixedData); + + return $this->redirectFactory->create() + ->setPath('customer/account/create'); + } +} diff --git a/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php b/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php new file mode 100644 index 0000000000000..b94b56bacf379 --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php @@ -0,0 +1,56 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Delegation\Data; + +use Magento\Customer\Api\Data\CustomerInterface; + +/** + * Data required for delegated new-account operation. + */ +class NewOperation +{ + /** + * @var CustomerInterface + */ + private $customer; + + /** + * @var array + */ + private $additionalData; + + /** + * @param CustomerInterface $customer + * @param array $additionalData + */ + public function __construct( + CustomerInterface $customer, + array $additionalData + ) { + $this->customer = $customer; + $this->additionalData = $additionalData; + } + + /** + * @return CustomerInterface + */ + public function getCustomer(): CustomerInterface + { + return $this->customer; + } + + /** + * @return array + */ + public function getAdditionalData(): array + { + return $this->additionalData; + } +} diff --git a/app/code/Magento/Customer/Model/Delegation/Storage.php b/app/code/Magento/Customer/Model/Delegation/Storage.php new file mode 100644 index 0000000000000..08aa7700a8b21 --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/Storage.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Delegation; + +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\RegionInterface; +use Magento\Customer\Api\Data\RegionInterfaceFactory; +use Magento\Customer\Model\Delegation\Data\NewOperation; +use Magento\Customer\Model\Data\Customer; +use Magento\Customer\Model\Data\Address; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\Session\Proxy as SessionProxy; +use Magento\Customer\Model\Delegation\Data\NewOperationFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Psr\Log\LoggerInterface; + +/** + * Store data for delegated operations. + */ +class Storage +{ + /** + * @var Session + */ + private $session; + + /** + * @var NewOperationFactory + */ + private $newFactory; + + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var AddressInterfaceFactory + */ + private $addressFactory; + + /** + * @var RegionInterfaceFactory + */ + private $regionFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param NewOperationFactory $newFactory + * @param CustomerInterfaceFactory $customerFactory + * @param AddressInterfaceFactory $addressFactory + * @param RegionInterfaceFactory $regionFactory + * @param LoggerInterface $logger + * @param SessionProxy $session + */ + public function __construct( + NewOperationFactory $newFactory, + CustomerInterfaceFactory $customerFactory, + AddressInterfaceFactory $addressFactory, + RegionInterfaceFactory $regionFactory, + LoggerInterface $logger, + SessionProxy $session + ) { + $this->newFactory = $newFactory; + $this->customerFactory = $customerFactory; + $this->addressFactory = $addressFactory; + $this->regionFactory = $regionFactory; + $this->logger = $logger; + $this->session = $session; + } + + /** + * Store data for new account operation. + * + * @param CustomerInterface $customer + * @param array $delegatedData + * + * @return void + */ + public function storeNewOperation( + CustomerInterface $customer, + array $delegatedData + ) { + /** @var Customer $customer */ + $customerData = $customer->__toArray(); + $addressesData = []; + if ($customer->getAddresses()) { + /** @var Address $address */ + foreach ($customer->getAddresses() as $address) { + $addressesData[] = $address->__toArray(); + } + } + $this->session->setCustomerFormData($customerData); + $this->session->setDelegatedNewCustomerData( + [ + 'customer' => $customerData, + 'addresses' => $addressesData, + 'delegated_data' => $delegatedData, + ] + ); + } + + /** + * Retrieve delegated new operation data and mark it as used. + * + * @return NewOperation|null + */ + public function consumeNewOperation() + { + try { + $serialized = $this->session->getDelegatedNewCustomerData(true); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $serialized = null; + } + if (!$serialized) { + return null; + } + + /** @var AddressInterface[] $addresses */ + $addresses = []; + foreach ($serialized['addresses'] as $addressData) { + if (isset($addressData['region'])) { + /** @var RegionInterface $region */ + $region = $this->regionFactory->create( + ['data' => $addressData['region']] + ); + $addressData['region'] = $region; + } + + $customAttributes = []; + if (!empty($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { + $customAttributes = $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]; + unset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]); + } + + $address = $this->addressFactory->create( + ['data' => $addressData] + ); + + foreach ($customAttributes as $attributeCode => $attributeValue) { + $address->setCustomAttribute($attributeCode, $attributeValue); + } + + $addresses[] = $address; + } + $customerData = $serialized['customer']; + $customerData['addresses'] = $addresses; + + return $this->newFactory->create( + [ + 'customer' => $this->customerFactory->create(['data' => $customerData]), + 'additionalData' => $serialized['delegated_data'], + ] + ); + } +} diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 14ae9a885c7b1..679c108ab1d30 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Model; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Customer\Helper\View as CustomerViewHelper; @@ -14,6 +17,8 @@ use Magento\Framework\Exception\LocalizedException; /** + * Class for notification customer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailNotification implements EmailNotificationInterface @@ -60,6 +65,8 @@ class EmailNotification implements EmailNotificationInterface self::NEW_ACCOUNT_EMAIL_CONFIRMATION => self::XML_PATH_CONFIRM_EMAIL_TEMPLATE, ]; + const CUSTOMER_CONFIRM_URL = 'customer/account/confirm/'; + /**#@-*/ /**#@-*/ @@ -90,6 +97,11 @@ class EmailNotification implements EmailNotificationInterface */ private $scopeConfig; + /** + * @var SenderResolverInterface + */ + private $senderResolver; + /** * @param CustomerRegistry $customerRegistry * @param StoreManagerInterface $storeManager @@ -97,6 +109,7 @@ class EmailNotification implements EmailNotificationInterface * @param CustomerViewHelper $customerViewHelper * @param DataObjectProcessor $dataProcessor * @param ScopeConfigInterface $scopeConfig + * @param SenderResolverInterface|null $senderResolver */ public function __construct( CustomerRegistry $customerRegistry, @@ -104,7 +117,8 @@ public function __construct( TransportBuilder $transportBuilder, CustomerViewHelper $customerViewHelper, DataObjectProcessor $dataProcessor, - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + SenderResolverInterface $senderResolver = null ) { $this->customerRegistry = $customerRegistry; $this->storeManager = $storeManager; @@ -112,6 +126,7 @@ public function __construct( $this->customerViewHelper = $customerViewHelper; $this->dataProcessor = $dataProcessor; $this->scopeConfig = $scopeConfig; + $this->senderResolver = $senderResolver ?: ObjectManager::getInstance()->get(SenderResolverInterface::class); } /** @@ -230,6 +245,7 @@ private function passwordReset(CustomerInterface $customer) * @param int|null $storeId * @param string $email * @return void + * @throws \Magento\Framework\Exception\MailException */ private function sendEmailTemplate( $customer, @@ -243,10 +259,17 @@ private function sendEmailTemplate( if ($email === null) { $email = $customer->getEmail(); } + + /** @var array $from */ + $from = $this->senderResolver->resolve( + $this->scopeConfig->getValue($sender, 'store', $storeId), + $storeId + ); + $transport = $this->transportBuilder->setTemplateIdentifier($templateId) ->setTemplateOptions(['area' => 'frontend', 'store' => $storeId]) ->setTemplateVars($templateParams) - ->setFrom($this->scopeConfig->getValue($sender, 'store', $storeId)) + ->setFrom($from) ->addTo($email, $this->customerViewHelper->getCustomerName($customer)) ->getTransport(); @@ -295,9 +318,9 @@ private function getWebsiteStoreId($customer, $defaultStoreId = null) */ public function passwordReminder(CustomerInterface $customer) { - $storeId = $this->getWebsiteStoreId($customer); + $storeId = $customer->getStoreId(); if (!$storeId) { - $storeId = $this->storeManager->getStore()->getId(); + $storeId = $this->getWebsiteStoreId($customer); } $customerEmailData = $this->getFullCustomerObject($customer); @@ -343,6 +366,7 @@ public function passwordResetConfirmation(CustomerInterface $customer) * @param string $backUrl * @param string $storeId * @param string $sendemailStoreId + * @param array $extensions * @return void * @throws LocalizedException */ @@ -351,7 +375,8 @@ public function newAccount( $type = self::NEW_ACCOUNT_EMAIL_REGISTERED, $backUrl = '', $storeId = 0, - $sendemailStoreId = null + $sendemailStoreId = null, + $extensions = [] ) { $types = self::TEMPLATE_TYPES; @@ -367,11 +392,26 @@ public function newAccount( $customerEmailData = $this->getFullCustomerObject($customer); + $templateVars = [ + 'customer' => $customerEmailData, + 'back_url' => $backUrl, + 'store' => $store, + ]; + if ($type == self::NEW_ACCOUNT_EMAIL_CONFIRMATION) { + if (empty($extensions)) { + $templateVars['url'] = self::CUSTOMER_CONFIRM_URL; + $templateVars['extensions'] = $extensions; + } else { + $templateVars['url'] = $extensions['url']; + $templateVars['extensions'] = $extensions['extension_info']; + } + } + $this->sendEmailTemplate( $customer, $types[$type], self::XML_PATH_REGISTER_EMAIL_IDENTITY, - ['customer' => $customerEmailData, 'back_url' => $backUrl, 'store' => $store], + $templateVars, $storeId ); } diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index 2d6917efdaf56..6a8472758c169 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -202,7 +202,7 @@ public function moveTemporaryFile($fileName) { $fileName = ltrim($fileName, '/'); - $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispretionPath($fileName); + $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); $destinationPath = $this->entityTypeCode . $dispersionPath; if (!$this->mediaDirectory->create($destinationPath)) { diff --git a/app/code/Magento/Customer/Model/FileUploader.php b/app/code/Magento/Customer/Model/FileUploader.php index b94eff6bdff44..c425ac06666c5 100644 --- a/app/code/Magento/Customer/Model/FileUploader.php +++ b/app/code/Magento/Customer/Model/FileUploader.php @@ -110,7 +110,7 @@ public function upload() $result = $fileProcessor->saveTemporaryFile($this->scope . '[' . $this->getAttributeCode() . ']'); // Update tmp_name param. Required for attribute validation! - $result['tmp_name'] = $result['path'] . '/' . ltrim($result['file'], '/'); + $result['tmp_name'] = ltrim($result['file'], '/'); $result['url'] = $fileProcessor->getViewUrl( FileProcessor::TMP_DIR . '/' . ltrim($result['name'], '/'), diff --git a/app/code/Magento/Customer/Model/GroupManagement.php b/app/code/Magento/Customer/Model/GroupManagement.php index 47d7d7ad1ac41..48cb5d55061c5 100644 --- a/app/code/Magento/Customer/Model/GroupManagement.php +++ b/app/code/Magento/Customer/Model/GroupManagement.php @@ -8,16 +8,19 @@ namespace Magento\Customer\Model; use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\Data\GroupInterfaceFactory; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; -use Magento\Customer\Api\GroupRepositoryInterface; -use Magento\Customer\Api\Data\GroupInterfaceFactory; -use Magento\Customer\Model\GroupFactory; /** + * The class contains methods for getting information about a customer group + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface @@ -65,6 +68,11 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface */ protected $filterBuilder; + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + /** * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $scopeConfig @@ -73,6 +81,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface * @param GroupInterfaceFactory $groupDataFactory * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param FilterBuilder $filterBuilder + * @param SortOrderBuilder $sortOrderBuilder */ public function __construct( StoreManagerInterface $storeManager, @@ -81,7 +90,8 @@ public function __construct( GroupRepositoryInterface $groupRepository, GroupInterfaceFactory $groupDataFactory, SearchCriteriaBuilder $searchCriteriaBuilder, - FilterBuilder $filterBuilder + FilterBuilder $filterBuilder, + SortOrderBuilder $sortOrderBuilder = null ) { $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; @@ -90,10 +100,12 @@ public function __construct( $this->groupDataFactory = $groupDataFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterBuilder = $filterBuilder; + $this->sortOrderBuilder = $sortOrderBuilder ?: ObjectManager::getInstance() + ->get(SortOrderBuilder::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function isReadonly($groupId) { @@ -107,7 +119,7 @@ public function isReadonly($groupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultGroup($storeId = null) { @@ -133,7 +145,7 @@ public function getDefaultGroup($storeId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getNotLoggedInGroup() { @@ -141,7 +153,7 @@ public function getNotLoggedInGroup() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLoggedInGroups() { @@ -155,15 +167,20 @@ public function getLoggedInGroups() ->setConditionType('neq') ->setValue(self::CUST_GROUP_ALL) ->create(); + $groupNameSortOrder = $this->sortOrderBuilder + ->setField('customer_group_code') + ->setAscendingDirection() + ->create(); $searchCriteria = $this->searchCriteriaBuilder ->addFilters($notLoggedInFilter) ->addFilters($groupAll) + ->addSortOrder($groupNameSortOrder) ->create(); return $this->groupRepository->getList($searchCriteria)->getItems(); } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllCustomersGroup() { diff --git a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php new file mode 100644 index 0000000000000..7c224d29f1dc1 --- /dev/null +++ b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Model\Indexer; + +use Magento\Customer\Model\ResourceModel\Group\CollectionFactory as CustomerGroupCollectionFactory; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Indexer\DimensionProviderInterface; + +class CustomerGroupDimensionProvider implements DimensionProviderInterface +{ + /** + * Name for customer group dimension for multidimensional indexer + * 'cg' - stands for 'customer_group' + */ + const DIMENSION_NAME = 'cg'; + + /** + * @var CustomerGroupCollectionFactory + */ + private $collectionFactory; + + /** + * @var \SplFixedArray + */ + private $customerGroupsDataIterator; + + /** + * @var DimensionFactory + */ + private $dimensionFactory; + + public function __construct(CustomerGroupCollectionFactory $collectionFactory, DimensionFactory $dimensionFactory) + { + $this->dimensionFactory = $dimensionFactory; + $this->collectionFactory = $collectionFactory; + } + + public function getIterator(): \Traversable + { + foreach ($this->getCustomerGroups() as $customerGroup) { + yield $this->dimensionFactory->create(self::DIMENSION_NAME, (string)$customerGroup); + } + } + + /** + * @return array + */ + private function getCustomerGroups(): array + { + if ($this->customerGroupsDataIterator === null) { + $customerGroups = $this->collectionFactory->create()->getAllIds(); + $this->customerGroupsDataIterator = is_array($customerGroups) ? $customerGroups : []; + } + + return $this->customerGroupsDataIterator; + } +} diff --git a/app/code/Magento/Customer/Model/Indexer/Source.php b/app/code/Magento/Customer/Model/Indexer/Source.php index e4bf03e08a9ad..983cda7063903 100644 --- a/app/code/Magento/Customer/Model/Indexer/Source.php +++ b/app/code/Magento/Customer/Model/Indexer/Source.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Model\Indexer; +use Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory; use Magento\Customer\Model\ResourceModel\Customer\Indexer\Collection; use Magento\Framework\App\ResourceConnection\SourceProviderInterface; use Traversable; @@ -25,11 +26,11 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface private $batchSize; /** - * @param \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collection + * @param CollectionFactory $collectionFactory * @param int $batchSize */ public function __construct( - \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collectionFactory, + CollectionFactory $collectionFactory, $batchSize = 10000 ) { $this->customerCollection = $collectionFactory->create(); @@ -37,7 +38,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getMainTable() { @@ -45,7 +46,7 @@ public function getMainTable() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIdFieldName() { @@ -53,7 +54,7 @@ public function getIdFieldName() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToSelect($fieldName, $alias = null) { @@ -62,7 +63,7 @@ public function addFieldToSelect($fieldName, $alias = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelect() { @@ -70,7 +71,7 @@ public function getSelect() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToFilter($attribute, $condition = null) { @@ -79,7 +80,7 @@ public function addFieldToFilter($attribute, $condition = null) } /** - * @return int + * @inheritdoc */ public function count() { @@ -105,4 +106,22 @@ public function getIterator() $pageNumber++; } while ($pageNumber <= $lastPage); } + + /** + * Joins Attribute + * + * @param string $alias alias for the joined attribute + * @param string|\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * @param string $bind attribute of the main entity to link with joined $filter + * @param string $filter primary key for the joined entity (entity_id default) + * @param string $joinType inner|left + * @param int|null $storeId + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @see Collection::joinAttribute() + */ + public function joinAttribute($alias, $attribute, $bind, $filter = null, $joinType = 'inner', $storeId = null) + { + $this->customerCollection->joinAttribute($alias, $attribute, $bind, $filter, $joinType, $storeId); + } } diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php index 5a46fdb9defc4..71f0b393e4a5d 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Cache for attribute metadata @@ -53,6 +54,11 @@ class AttributeMetadataCache */ private $serializer; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Constructor * @@ -60,17 +66,21 @@ class AttributeMetadataCache * @param StateInterface $state * @param SerializerInterface $serializer * @param AttributeMetadataHydrator $attributeMetadataHydrator + * @param StoreManagerInterface|null $storeManager */ public function __construct( CacheInterface $cache, StateInterface $state, SerializerInterface $serializer, - AttributeMetadataHydrator $attributeMetadataHydrator + AttributeMetadataHydrator $attributeMetadataHydrator, + StoreManagerInterface $storeManager = null ) { $this->cache = $cache; $this->state = $state; $this->serializer = $serializer; $this->attributeMetadataHydrator = $attributeMetadataHydrator; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** @@ -82,11 +92,12 @@ public function __construct( */ public function load($entityType, $suffix = '') { - if (isset($this->attributes[$entityType . $suffix])) { - return $this->attributes[$entityType . $suffix]; + $storeId = $this->storeManager->getStore()->getId(); + if (isset($this->attributes[$entityType . $suffix . $storeId])) { + return $this->attributes[$entityType . $suffix . $storeId]; } if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedData = $this->cache->load($cacheKey); if ($serializedData) { $attributesData = $this->serializer->unserialize($serializedData); @@ -94,7 +105,7 @@ public function load($entityType, $suffix = '') foreach ($attributesData as $key => $attributeData) { $attributes[$key] = $this->attributeMetadataHydrator->hydrate($attributeData); } - $this->attributes[$entityType . $suffix] = $attributes; + $this->attributes[$entityType . $suffix . $storeId] = $attributes; return $attributes; } } @@ -111,9 +122,10 @@ public function load($entityType, $suffix = '') */ public function save($entityType, array $attributes, $suffix = '') { - $this->attributes[$entityType . $suffix] = $attributes; + $storeId = $this->storeManager->getStore()->getId(); + $this->attributes[$entityType . $suffix . $storeId] = $attributes; if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $attributesData = []; foreach ($attributes as $key => $attribute) { $attributesData[$key] = $this->attributeMetadataHydrator->extract($attribute); diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataHydrator.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataHydrator.php index 190a3a38e0bf0..f61f064e3f97e 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataHydrator.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataHydrator.php @@ -11,6 +11,7 @@ use Magento\Customer\Api\Data\OptionInterfaceFactory; use Magento\Customer\Api\Data\ValidationRuleInterface; use Magento\Customer\Api\Data\ValidationRuleInterfaceFactory; +use Magento\Customer\Model\Data\AttributeMetadata; use Magento\Framework\Reflection\DataObjectProcessor; /** @@ -120,7 +121,7 @@ public function extract($attributeMetadata) { return $this->dataObjectProcessor->buildOutputDataArray( $attributeMetadata, - AttributeMetadataInterface::class + AttributeMetadata::class ); } } diff --git a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php index 7ed806e657e82..a9753fc810a99 100644 --- a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php +++ b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php @@ -33,16 +33,26 @@ class CustomerMetadata implements CustomerMetadataInterface */ private $attributeMetadataDataProvider; + /** + * List of system attributes which should be available to the clients. + * + * @var string[] + */ + private $systemAttributes; + /** * @param AttributeMetadataConverter $attributeMetadataConverter * @param AttributeMetadataDataProvider $attributeMetadataDataProvider + * @param string[] $systemAttributes */ public function __construct( AttributeMetadataConverter $attributeMetadataConverter, - AttributeMetadataDataProvider $attributeMetadataDataProvider + AttributeMetadataDataProvider $attributeMetadataDataProvider, + array $systemAttributes = [] ) { $this->attributeMetadataConverter = $attributeMetadataConverter; $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; + $this->systemAttributes = $systemAttributes; } /** @@ -116,7 +126,7 @@ public function getAllAttributesMetadata() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_INTERFACE_NAME) { @@ -134,9 +144,10 @@ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_IN $isDataObjectMethod = isset($this->customerDataObjectMethods['get' . $camelCaseKey]) || isset($this->customerDataObjectMethods['is' . $camelCaseKey]); - /** Even though disable_auto_group_change is system attribute, it should be available to the clients */ if (!$isDataObjectMethod - && (!$attributeMetadata->isSystem() || $attributeCode == 'disable_auto_group_change') + && (!$attributeMetadata->isSystem() + || in_array($attributeCode, $this->systemAttributes) + ) ) { $customAttributes[] = $attributeMetadata; } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php index f28cce0ea2ae1..d79c6a960f833 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php @@ -1,19 +1,17 @@ <?php /** - * Form Element Abstract Data Model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Model\Metadata\Form; use Magento\Framework\Api\ArrayObjectSearch; use Magento\Framework\Validator\EmailAddress; /** + * Form Element Abstract Data Model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractData @@ -138,7 +136,8 @@ public function setRequestScope($scope) } /** - * Set scope visibility + * Set scope visibility. + * * Search value only in scope or search value in scope and global * * @param boolean $flag @@ -170,7 +169,7 @@ public function setExtractedData(array $data) */ public function getExtractedData($index = null) { - if (!is_null($index)) { + if ($index !== null) { if (isset($this->_extractedData[$index])) { return $this->_extractedData[$index]; } @@ -227,9 +226,9 @@ protected function _getFormFilter() */ protected function _dateFilterFormat($format = null) { - if (is_null($format)) { + if ($format === null) { // get format - if (is_null($this->_dateFilterFormat)) { + if ($this->_dateFilterFormat === null) { $this->_dateFilterFormat = \IntlDateFormatter::SHORT; } return $this->_localeDate->getDateFormat($this->_dateFilterFormat); @@ -282,10 +281,14 @@ protected function _validateInputRule($value) 'input_validation' ); - if (!is_null($inputValidation)) { + if ($inputValidation !== null) { + $allowWhiteSpace = false; switch ($inputValidation) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), @@ -329,7 +332,8 @@ protected function _validateInputRule($value) __("Invalid type given. String expected") __("'%value%' appears to be a DNS hostname but contains a dash in an invalid position") __("'%value%' does not match the expected structure for a DNS hostname") - __("'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'") + __("'%value%' appears to be a DNS hostname but cannot match + * against hostname schema for TLD '%tld%'") __("'%value%' does not appear to be a valid local network name") __("'%value%' does not appear to be a valid URI hostname") __("'%value%' appears to be an IP address, but IP addresses are not allowed") @@ -384,7 +388,8 @@ protected function _validateInputRule($value) ); $validator->setMessage( __( - "'%value%' looks like a DNS hostname but we cannot match it against the hostname schema for TLD '%tld%'." + "'%value%' looks like a DNS hostname but we cannot match it against " + . "the hostname schema for TLD '%tld%'." ), \Zend_Validate_Hostname::INVALID_HOSTNAME_SCHEMA ); diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Date.php b/app/code/Magento/Customer/Model/Metadata/Form/Date.php index b27f6627439e4..6f14b2e6f1dad 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Date.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Date.php @@ -12,7 +12,7 @@ class Date extends AbstractData { /** - * {@inheritdoc} + * @inheritdoc */ public function extractValue(\Magento\Framework\App\RequestInterface $request) { @@ -21,7 +21,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -95,21 +95,15 @@ public function validateValue($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function compactValue($value) { - if ($value !== false) { - if (empty($value)) { - $value = null; - } - return $value; - } - return false; + return $value; } /** - * {@inheritdoc} + * @inheritdoc */ public function restoreValue($value) { @@ -117,7 +111,7 @@ public function restoreValue($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFactory::OUTPUT_FORMAT_TEXT) { diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Text.php b/app/code/Magento/Customer/Model/Metadata/Form/Text.php index 9ef6df0a6d36e..ecbe1976f1cfa 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Text.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Text.php @@ -5,8 +5,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Framework\Api\ArrayObjectSearch; class Text extends AbstractData @@ -19,7 +21,7 @@ class Text extends AbstractData /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute + * @param AttributeMetadataInterface $attribute * @param \Magento\Framework\Locale\ResolverInterface $localeResolver * @param string $value * @param string $entityTypeCode @@ -29,7 +31,7 @@ class Text extends AbstractData public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Psr\Log\LoggerInterface $logger, - \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute, + AttributeMetadataInterface $attribute, \Magento\Framework\Locale\ResolverInterface $localeResolver, $value, $entityTypeCode, @@ -72,26 +74,7 @@ public function validateValue($value) return true; } - // validate length - $length = $this->_string->strlen(trim($value)); - - $validateRules = $attribute->getValidationRules(); - - $minTextLength = ArrayObjectSearch::getArrayElementByName( - $validateRules, - 'min_text_length' - ); - if ($minTextLength !== null && $length < $minTextLength) { - $errors[] = __('"%1" length must be equal or greater than %2 characters.', $label, $minTextLength); - } - - $maxTextLength = ArrayObjectSearch::getArrayElementByName( - $validateRules, - 'max_text_length' - ); - if ($maxTextLength !== null && $length > $maxTextLength) { - $errors[] = __('"%1" length must be equal or less than %2 characters.', $label, $maxTextLength); - } + $errors = $this->validateLength($value, $attribute, $errors); $result = $this->_validateInputRule($value); if ($result !== true) { @@ -127,4 +110,42 @@ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFa { return $this->_applyOutputFilter($this->_value); } + + /** + * Length validation + * + * @param mixed $value + * @param AttributeMetadataInterface $attribute + * @param array $errors + * @return array + */ + protected function validateLength($value, AttributeMetadataInterface $attribute, array $errors): array + { + // validate length + $label = __($attribute->getStoreLabel()); + + $length = $this->_string->strlen(trim($value)); + + $validateRules = $attribute->getValidationRules(); + + if (!empty(ArrayObjectSearch::getArrayElementByName($validateRules, 'input_validation'))) { + $minTextLength = ArrayObjectSearch::getArrayElementByName( + $validateRules, + 'min_text_length' + ); + if ($minTextLength !== null && $length < $minTextLength) { + $errors[] = __('"%1" length must be equal or greater than %2 characters.', $label, $minTextLength); + } + + $maxTextLength = ArrayObjectSearch::getArrayElementByName( + $validateRules, + 'max_text_length' + ); + if ($maxTextLength !== null && $length > $maxTextLength) { + $errors[] = __('"%1" length must be equal or less than %2 characters.', $label, $maxTextLength); + } + } + + return $errors; + } } diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index ee109dac08104..f8761b4888a32 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -5,9 +5,14 @@ */ namespace Magento\Customer\Model; +use Magento\Config\Model\Config\Source\Nooptreq as NooptreqSource; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Framework\Escaper; +use Magento\Store\Api\Data\StoreInterface; +/** + * Customer Options. + */ class Options { /** @@ -37,43 +42,70 @@ public function __construct( /** * Retrieve name prefix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNamePrefixOptions($store = null) { - return $this->_prepareNamePrefixSuffixOptions($this->addressHelper->getConfig('prefix_options', $store)); + return $this->prepareNamePrefixSuffixOptions( + $this->addressHelper->getConfig('prefix_options', $store), + $this->addressHelper->getConfig('prefix_show', $store) == NooptreqSource::VALUE_OPTIONAL + ); } /** * Retrieve name suffix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNameSuffixOptions($store = null) { - return $this->_prepareNamePrefixSuffixOptions($this->addressHelper->getConfig('suffix_options', $store)); + return $this->prepareNamePrefixSuffixOptions( + $this->addressHelper->getConfig('suffix_options', $store), + $this->addressHelper->getConfig('suffix_show', $store) == NooptreqSource::VALUE_OPTIONAL + ); + } + + /** + * Unserialize and clear name prefix or suffix options. + * + * @param string $options + * @param bool $isOptional + * @return array|bool + * + * @deprecated + * @see prepareNamePrefixSuffixOptions() + */ + protected function _prepareNamePrefixSuffixOptions($options, $isOptional = false) + { + return $this->prepareNamePrefixSuffixOptions($options, $isOptional); } /** * Unserialize and clear name prefix or suffix options + * If field is optional, add an empty first option. * * @param string $options + * @param bool $isOptional * @return array|bool */ - protected function _prepareNamePrefixSuffixOptions($options) + private function prepareNamePrefixSuffixOptions($options, $isOptional = false) { $options = trim($options); if (empty($options)) { return false; } $result = []; - $options = explode(';', $options); + $options = array_filter(explode(';', $options)); foreach ($options as $value) { $value = $this->escaper->escapeHtml(trim($value)); $result[$value] = $value; } + if ($isOptional && trim(current($options))) { + $result = array_merge([' ' => ' '], $result); + } + return $result; } } diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerFlushFormKey.php b/app/code/Magento/Customer/Model/Plugin/CustomerFlushFormKey.php new file mode 100644 index 0000000000000..2d000ccfb4b93 --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/CustomerFlushFormKey.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Model\Plugin; + +use Magento\Customer\Model\Session; +use Magento\Framework\Data\Form\FormKey as DataFormKey; +use Magento\PageCache\Observer\FlushFormKey; + +class CustomerFlushFormKey +{ + /** + * @var Session + */ + private $session; + + /** + * @var DataFormKey + */ + private $dataFormKey; + + /** + * Initialize dependencies. + * + * @param Session $session + * @param DataFormKey $dataFormKey + */ + public function __construct(Session $session, DataFormKey $dataFormKey) + { + $this->session = $session; + $this->dataFormKey = $dataFormKey; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param FlushFormKey $subject + * @param callable $proceed + * @param $args + */ + public function aroundExecute(FlushFormKey $subject, callable $proceed, ...$args) + { + $currentFormKey = $this->dataFormKey->getFormKey(); + $proceed(...$args); + $beforeParams = $this->session->getBeforeRequestParams(); + if (isset($beforeParams['form_key']) && $beforeParams['form_key'] === $currentFormKey) { + $beforeParams['form_key'] = $this->dataFormKey->getFormKey(); + $this->session->setBeforeRequestParams($beforeParams); + } + } +} diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php index 3f73c8cdaeed4..281f9e4febddf 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php @@ -15,6 +15,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Psr\Log\LoggerInterface; +/** + * Plugin before \Magento\Framework\App\Action\AbstractAction::dispatch. + * + * Plugin to remove notifications from cache. + */ class CustomerNotification { /** @@ -66,6 +71,8 @@ public function __construct( } /** + * Removes notifications from cache. + * * @param AbstractAction $subject * @param RequestInterface $request * @return void @@ -82,10 +89,10 @@ public function beforeDispatch(AbstractAction $subject, RequestInterface $reques ) ) { try { + $this->session->regenerateId(); $customer = $this->customerRepository->getById($customerId); $this->session->setCustomerData($customer); $this->session->setCustomerGroupId($customer->getGroupId()); - $this->session->regenerateId(); $this->notificationStorage->remove(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId); } catch (NoSuchEntityException $e) { $this->logger->error($e); diff --git a/app/code/Magento/Customer/Model/Renderer/Region.php b/app/code/Magento/Customer/Model/Renderer/Region.php index 5c7fcd38d6c52..ad620e4e4b3f2 100644 --- a/app/code/Magento/Customer/Model/Renderer/Region.php +++ b/app/code/Magento/Customer/Model/Renderer/Region.php @@ -80,7 +80,7 @@ public function render(AbstractElement $element) $regionCollection = self::$_regionCollections[$countryId]; } - $regionId = intval($element->getForm()->getElement('region_id')->getValue()); + $regionId = (int)$element->getForm()->getElement('region_id')->getValue(); $htmlAttributes = $element->getHtmlAttributes(); foreach ($htmlAttributes as $key => $attribute) { diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php index a52c372310843..8b5c9a08931b5 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php @@ -13,7 +13,8 @@ use Magento\Framework\App\ObjectManager; /** - * Class Address + * Customer Address resource model. + * * @package Magento\Customer\Model\ResourceModel * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -31,8 +32,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity /** * @param \Magento\Eav\Model\Entity\Context $context - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite * @param \Magento\Framework\Validator\Factory $validatorFactory * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data @@ -90,7 +91,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $address) } /** - * Validate customer address entity + * Validate customer address entity. * * @param \Magento\Framework\DataObject $address * @return void @@ -98,6 +99,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $address) */ protected function _validate($address) { + if ($address->getDataByKey('should_ignore_validation')) { + return; + }; $validator = $this->_validatorFactory->createValidator('customer_address', 'save'); if (!$validator->isValid($address)) { diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php index cf7105c1519af..37b3b1af0a42d 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php @@ -45,11 +45,9 @@ public function __construct( } /** - * Retrieve all options - * - * @return array + * @inheritdoc */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $this->_options = $this->_createCountriesCollection()->loadByStore( diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php index 3f45993a3e143..b25d11e424660 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -70,8 +70,9 @@ public function __construct( * Retrieve all options * * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->options) { $allowedCountries = []; diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Region.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Region.php index 8e45e4a91ecd4..2cafdab077d67 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Region.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Region.php @@ -33,11 +33,9 @@ public function __construct( } /** - * Retrieve all region options - * - * @return array + * @inheritdoc */ - public function getAllOptions() + public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $this->_options = $this->_createRegionsCollection()->load()->toOptionArray(); diff --git a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php index 2c7b778f5f485..2c84a9fb0b1c5 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php @@ -7,6 +7,7 @@ */ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Model\Address as CustomerAddressModel; use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\ResourceModel\Address\Collection; @@ -92,7 +93,7 @@ public function __construct( $this->addressFactory = $addressFactory; $this->addressRegistry = $addressRegistry; $this->customerRegistry = $customerRegistry; - $this->addressResource = $addressResourceModel; + $this->addressResourceModel = $addressResourceModel; $this->directoryData = $directoryData; $this->addressSearchResultsFactory = $addressSearchResultsFactory; $this->addressCollectionFactory = $addressCollectionFactory; @@ -123,6 +124,7 @@ public function save(\Magento\Customer\Api\Data\AddressInterface $address) } else { $addressModel->updateData($address); } + $addressModel->setStoreId($customerModel->getStoreId()); $errors = $addressModel->validate(); if ($errors !== true) { @@ -236,7 +238,7 @@ public function delete(\Magento\Customer\Api\Data\AddressInterface $address) $address = $this->addressRegistry->retrieve($addressId); $customerModel = $this->customerRegistry->retrieve($address->getCustomerId()); $customerModel->getAddressesCollection()->clear(); - $this->addressResource->delete($address); + $this->addressResourceModel->delete($address); $this->addressRegistry->remove($addressId); return true; } @@ -254,7 +256,7 @@ public function deleteById($addressId) $address = $this->addressRegistry->retrieve($addressId); $customerModel = $this->customerRegistry->retrieve($address->getCustomerId()); $customerModel->getAddressesCollection()->removeItemByKey($addressId); - $this->addressResource->delete($address); + $this->addressResourceModel->delete($address); $this->addressRegistry->remove($addressId); return true; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 7e5f9d51549ec..daacb2655f588 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -11,7 +11,7 @@ use Magento\Framework\Exception\AlreadyExistsException; /** - * Customer entity resource model + * Customer entity resource model. * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -92,7 +92,7 @@ protected function _getDefaultAttributes() } /** - * Check customer scope, email and confirmation key before saving + * Check customer scope, email and confirmation key before saving. * * @param \Magento\Framework\DataObject $customer * @return $this @@ -150,7 +150,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) $customer->setConfirmation(null); } - $this->_validate($customer); + if (!$customer->getData('ignore_validation_flag')) { + $this->_validate($customer); + } return $this; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php index e55c5d443c9d1..d55a5c0aea2be 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php @@ -7,12 +7,12 @@ namespace Magento\Customer\Model\ResourceModel\Customer; /** - * Class Relation + * Class to process object relations. */ class Relation implements \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationInterface { /** - * Save relations for Customer + * Save relations for Customer. * * @param \Magento\Framework\Model\AbstractModel $customer * @return void @@ -23,41 +23,43 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $customer $defaultBillingId = $customer->getData('default_billing'); $defaultShippingId = $customer->getData('default_shipping'); - /** @var \Magento\Customer\Model\Address $address */ - foreach ($customer->getAddresses() as $address) { - if ($address->getData('_deleted')) { - if ($address->getId() == $defaultBillingId) { - $customer->setData('default_billing', null); - } + if (!$customer->getData('ignore_validation_flag')) { + /** @var \Magento\Customer\Model\Address $address */ + foreach ($customer->getAddresses() as $address) { + if ($address->getData('_deleted')) { + if ($address->getId() == $defaultBillingId) { + $customer->setData('default_billing', null); + } - if ($address->getId() == $defaultShippingId) { - $customer->setData('default_shipping', null); - } + if ($address->getId() == $defaultShippingId) { + $customer->setData('default_shipping', null); + } - $removedAddressId = $address->getId(); - $address->delete(); + $removedAddressId = $address->getId(); + $address->delete(); - // Remove deleted address from customer address collection - $customer->getAddressesCollection()->removeItemByKey($removedAddressId); - } else { - $address->setParentId( - $customer->getId() - )->setStoreId( - $customer->getStoreId() - )->setIsCustomerSaveTransaction( - true - )->save(); + // Remove deleted address from customer address collection + $customer->getAddressesCollection()->removeItemByKey($removedAddressId); + } else { + $address->setParentId( + $customer->getId() + )->setStoreId( + $customer->getStoreId() + )->setIsCustomerSaveTransaction( + true + )->save(); - if (($address->getIsPrimaryBilling() || - $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId - ) { - $customer->setData('default_billing', $address->getId()); - } + if (($address->getIsPrimaryBilling() || + $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId + ) { + $customer->setData('default_billing', $address->getId()); + } - if (($address->getIsPrimaryShipping() || - $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId - ) { - $customer->setData('default_shipping', $address->getId()); + if (($address->getIsPrimaryShipping() || + $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId + ) { + $customer->setData('default_shipping', $address->getId()); + } } } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index e66caeeb33707..cda132d4b4ddc 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -7,65 +7,81 @@ namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\CustomerRegistry; +use \Magento\Customer\Model\Customer as CustomerModel; +use Magento\Customer\Model\Data\CustomerSecureFactory; use Magento\Customer\Model\Customer\NotificationStorage; +use Magento\Customer\Model\Delegation\Data\NewOperation; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Event\ManagerInterface; +use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; /** * Customer repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ -class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface +class CustomerRepository implements CustomerRepositoryInterface { /** - * @var \Magento\Customer\Model\CustomerFactory + * @var CustomerFactory */ protected $customerFactory; /** - * @var \Magento\Customer\Model\Data\CustomerSecureFactory + * @var CustomerSecureFactory */ protected $customerSecureFactory; /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ protected $customerRegistry; /** - * @var \Magento\Customer\Model\ResourceModel\AddressRepository + * @var AddressRepository */ protected $addressRepository; /** - * @var \Magento\Customer\Model\ResourceModel\Customer + * @var Customer */ protected $customerResourceModel; /** - * @var \Magento\Customer\Api\CustomerMetadataInterface + * @var CustomerMetadataInterface */ protected $customerMetadata; /** - * @var \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory + * @var CustomerSearchResultsInterfaceFactory */ protected $searchResultsFactory; /** - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ protected $eventManager; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; @@ -80,7 +96,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte protected $imageProcessor; /** - * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface + * @var JoinProcessorInterface */ protected $extensionAttributesJoinProcessor; @@ -95,39 +111,46 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte private $notificationStorage; /** - * @param \Magento\Customer\Model\CustomerFactory $customerFactory - * @param \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory - * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry - * @param \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository - * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel - * @param \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata - * @param \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @var DelegatedStorage + */ + private $delegatedStorage; + + /** + * @param CustomerFactory $customerFactory + * @param CustomerSecureFactory $customerSecureFactory + * @param CustomerRegistry $customerRegistry + * @param AddressRepository $addressRepository + * @param Customer $customerResourceModel + * @param CustomerMetadataInterface $customerMetadata + * @param CustomerSearchResultsInterfaceFactory $searchResultsFactory + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param DataObjectHelper $dataObjectHelper * @param ImageProcessorInterface $imageProcessor - * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor + * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface $collectionProcessor * @param NotificationStorage $notificationStorage + * @param DelegatedStorage|null $delegatedStorage * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Customer\Model\CustomerFactory $customerFactory, - \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory, - \Magento\Customer\Model\CustomerRegistry $customerRegistry, - \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository, - \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel, - \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata, - \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + CustomerFactory $customerFactory, + CustomerSecureFactory $customerSecureFactory, + CustomerRegistry $customerRegistry, + AddressRepository $addressRepository, + Customer $customerResourceModel, + CustomerMetadataInterface $customerMetadata, + CustomerSearchResultsInterfaceFactory $searchResultsFactory, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + ExtensibleDataObjectConverter $extensibleDataObjectConverter, DataObjectHelper $dataObjectHelper, ImageProcessorInterface $imageProcessor, - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, + JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor, - NotificationStorage $notificationStorage + NotificationStorage $notificationStorage, + DelegatedStorage $delegatedStorage = null ) { $this->customerFactory = $customerFactory; $this->customerSecureFactory = $customerSecureFactory; @@ -144,15 +167,21 @@ public function __construct( $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->collectionProcessor = $collectionProcessor; $this->notificationStorage = $notificationStorage; + $this->delegatedStorage = $delegatedStorage ?? ObjectManager::getInstance()->get(DelegatedStorage::class); } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) { + /** @var NewOperation|null $delegatedNewOperation */ + $delegatedNewOperation = !$customer->getId() + ? $this->delegatedStorage->consumeNewOperation() : null; $prevCustomerData = null; $prevCustomerDataArr = null; if ($customer->getId()) { @@ -174,21 +203,22 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa [], \Magento\Customer\Api\Data\CustomerInterface::class ); - $customer->setAddresses($origAddresses); - $customerModel = $this->customerFactory->create(['data' => $customerData]); - $storeId = $customerModel->getStoreId(); - if ($storeId === null) { - $customerModel->setStoreId($this->storeManager->getStore()->getId()); - } + /** @var Customer $customerModel */ + $customerModel = $this->customerFactory->create( + ['data' => $customerData] + ); + //Model's actual ID field maybe different than "id" + //so "id" field from $customerData may be ignored. $customerModel->setId($customer->getId()); - // Need to use attribute set or future updates can cause data loss - if (!$customerModel->getAttributeSetId()) { - $customerModel->setAttributeSetId( - \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER + $storeId = $customerModel->getStoreId(); + if ($storeId === null) { + $customerModel->setStoreId( + $prevCustomerData ? $prevCustomerData->getStoreId() : $this->storeManager->getStore()->getId() ); } + $this->populateCustomerWithSecureData($customerModel, $passwordHash); // If customer email was changed, reset RpToken info @@ -198,25 +228,36 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $customerModel->setRpToken(null); $customerModel->setRpTokenCreatedAt(null); } - if (!array_key_exists('default_billing', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_billing', $prevCustomerDataArr) + if (!array_key_exists('addresses', $customerArr) + && null !== $prevCustomerDataArr + && array_key_exists('default_billing', $prevCustomerDataArr) ) { - $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); + $customerModel->setDefaultBilling( + $prevCustomerDataArr['default_billing'] + ); } - - if (!array_key_exists('default_shipping', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_shipping', $prevCustomerDataArr) + if (!array_key_exists('addresses', $customerArr) + && null !== $prevCustomerDataArr + && array_key_exists('default_shipping', $prevCustomerDataArr) ) { - $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); + $customerModel->setDefaultShipping( + $prevCustomerDataArr['default_shipping'] + ); } - + $this->setIgnoreValidationFlag($customerArr, $customerModel); $customerModel->save(); $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); - if ($customer->getAddresses() !== null) { + if (!$customer->getAddresses() + && $delegatedNewOperation + && $delegatedNewOperation->getCustomer()->getAddresses() + ) { + $customer->setAddresses( + $delegatedNewOperation->getCustomer()->getAddresses() + ); + } + if ($customer->getAddresses() !== null && !$customerModel->getData('ignore_validation_flag')) { if ($customer->getId()) { $existingAddresses = $this->getById($customer->getId())->getAddresses(); $getIdFunc = function ($address) { @@ -226,7 +267,6 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa } else { $existingAddressIds = []; } - $savedAddressIds = []; foreach ($customer->getAddresses() as $address) { $address->setCustomerId($customerId) @@ -236,7 +276,6 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $savedAddressIds[] = $address->getId(); } } - $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); foreach ($addressIdsToDelete as $addressId) { $this->addressRepository->deleteById($addressId); @@ -244,10 +283,17 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa } $this->customerRegistry->remove($customerId); $savedCustomer = $this->get($customer->getEmail(), $customer->getWebsiteId()); + $this->eventManager->dispatch( 'customer_save_after_data_object', - ['customer_data_object' => $savedCustomer, 'orig_customer_data_object' => $prevCustomerData] + [ + 'customer_data_object' => $savedCustomer, + 'orig_customer_data_object' => $prevCustomerData, + 'delegate_data' => $delegatedNewOperation + ? $delegatedNewOperation->getAdditionalData() : [] + ] ); + return $savedCustomer; } @@ -323,7 +369,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) ->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left') ->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left') ->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left') - ->joinAttribute('company', 'customer_address/company', 'default_billing', null, 'left'); + ->joinAttribute('billing_company', 'customer_address/company', 'default_billing', null, 'left'); $this->collectionProcessor->process($searchCriteria, $collection); @@ -363,15 +409,12 @@ public function deleteById($customerId) * Helper function that adds a FilterGroup to the collection. * * @deprecated 100.2.0 - * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup - * @param \Magento\Customer\Model\ResourceModel\Customer\Collection $collection + * @param FilterGroup $filterGroup + * @param Collection $collection * @return void - * @throws \Magento\Framework\Exception\InputException */ - protected function addFilterGroupToCollection( - \Magento\Framework\Api\Search\FilterGroup $filterGroup, - \Magento\Customer\Model\ResourceModel\Customer\Collection $collection - ) { + protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) + { $fields = []; foreach ($filterGroup->getFilters() as $filter) { $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; @@ -381,4 +424,18 @@ protected function addFilterGroupToCollection( $collection->addFieldToFilter($fields); } } + + /** + * Set ignore_validation_flag to skip model validation. + * + * @param array $customerArray + * @param CustomerModel $customerModel + * @return void + */ + private function setIgnoreValidationFlag(array $customerArray, CustomerModel $customerModel) + { + if (isset($customerArray['ignore_validation_flag'])) { + $customerModel->setData('ignore_validation_flag', true); + } + } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/ServiceCollection.php b/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/ServiceCollection.php index c0afd353fa7a7..05dffbce2d754 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/ServiceCollection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/ServiceCollection.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Model\ResourceModel\Group\Grid; use Magento\Framework\Data\Collection\EntityFactory; @@ -71,7 +69,12 @@ public function loadData($printQuery = false, $logQuery = false) $groups = $searchResults->getItems(); foreach ($groups as $group) { $groupItem = new \Magento\Framework\DataObject(); - $groupItem->addData($this->simpleDataObjectConverter->toFlatArray($group, \Magento\Customer\Api\Data\GroupInterface::class)); + $groupItem->addData( + $this->simpleDataObjectConverter->toFlatArray( + $group, + \Magento\Customer\Api\Data\GroupInterface::class + ) + ); $this->_addItem($groupItem); } $this->_setIsLoaded(); diff --git a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php index d004b99c5a3c9..4350ff6720619 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php @@ -109,7 +109,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Customer\Api\Data\GroupInterface $group) { @@ -156,11 +156,16 @@ public function save(\Magento\Customer\Api\Data\GroupInterface $group) ->setCode($groupModel->getCode()) ->setTaxClassId($groupModel->getTaxClassId()) ->setTaxClassName($groupModel->getTaxClassName()); + + if ($group->getExtensionAttributes()) { + $groupDataObject->setExtensionAttributes($group->getExtensionAttributes()); + } + return $groupDataObject; } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($id) { @@ -174,7 +179,7 @@ public function getById($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -296,6 +301,7 @@ public function deleteById($id) * * @param \Magento\Customer\Api\Data\GroupInterface $group * @throws InputException + * @throws \Zend_Validate_Exception * @return void * * @SuppressWarnings(PHPMD.NPathComplexity) diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index 71b0297fdd114..7ebc8e5c9fdee 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -354,8 +354,9 @@ public function setCustomerGroupId($id) } /** - * Get customer group id - * If customer is not logged in system, 'not logged in' group id will be returned + * Get customer group id. + * + * If customer is not logged in system, 'not logged in' group id will be returned. * * @return int */ @@ -407,24 +408,29 @@ public function checkCustomerId($customerId) } /** + * Sets customer as logged in. + * * @param Customer $customer * @return $this */ public function setCustomerAsLoggedIn($customer) { + $this->regenerateId(); $this->setCustomer($customer); $this->_eventManager->dispatch('customer_login', ['customer' => $customer]); $this->_eventManager->dispatch('customer_data_object_login', ['customer' => $this->getCustomerDataObject()]); - $this->regenerateId(); return $this; } /** + * Sets customer as logged in. + * * @param CustomerData $customer * @return $this */ public function setCustomerDataAsLoggedIn($customer) { + $this->regenerateId(); $this->_httpContext->setValue(Context::CONTEXT_AUTH, true, false); $this->setCustomerData($customer); @@ -487,7 +493,7 @@ public function authenticate($loginUrl = null) $this->response->setRedirect($loginUrl); } else { $arguments = $this->_customerUrl->getLoginUrlParams(); - if ($this->_session->getCookieShouldBeReceived() && $this->_createUrl()->getUseSession()) { + if ($this->_createUrl()->getUseSession()) { $arguments += [ '_query' => [ $this->sidResolver->getSessionIdQueryParam($this->_session) => $this->_session->getSessionId(), @@ -555,7 +561,7 @@ public function setAfterAuthUrl($url) } /** - * Reset core session hosts after reseting session ID + * Reset core session hosts after resetting session ID * * @return $this */ @@ -567,6 +573,8 @@ public function regenerateId() } /** + * Creates \Magento\Framework\UrlInterface object. + * * @return \Magento\Framework\UrlInterface */ protected function _createUrl() diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index 9822e2ad1b80e..123a9eef4b75a 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -179,15 +179,23 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = return $gatewayResponse; } + $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode); + $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode); + try { $soapClient = $this->createVatNumberValidationSoapClient(); $requestParams = []; - $requestParams['countryCode'] = $countryCode; - $requestParams['vatNumber'] = str_replace([' ', '-'], ['', ''], $vatNumber); - $requestParams['requesterCountryCode'] = $requesterCountryCode; - $requestParams['requesterVatNumber'] = str_replace([' ', '-'], ['', ''], $requesterVatNumber); - + $requestParams['countryCode'] = $countryCodeForVatNumber; + $vatNumberSanitized = $this->isCountryInEU($countryCode) + ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber) + : str_replace([' ', '-'], ['', ''], $vatNumber); + $requestParams['vatNumber'] = $vatNumberSanitized; + $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber; + $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode) + ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber) + : str_replace([' ', '-'], ['', ''], $requesterVatNumber); + $requestParams['requesterVatNumber'] = $reqVatNumSanitized; // Send request to service $result = $soapClient->checkVatApprox($requestParams); @@ -296,4 +304,22 @@ public function isCountryInEU($countryCode, $storeId = null) ); return in_array($countryCode, $euCountries); } + + /** + * Returns the country code to use in the VAT number which is not always the same as the normal country code + * + * @param string $countryCode + * @return string + */ + private function getCountryCodeForVatNumber(string $countryCode): string + { + // Greece uses a different code for VAT numbers then its country code + // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 + // And https://en.wikipedia.org/wiki/VAT_identification_number: + // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code + // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language, + // instead of its ISO 3166-1 alpha-2 country code GR)" + + return $countryCode === 'GR' ? 'EL' : $countryCode; + } } diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index b4bad240bc825..d144b7f6b70ec 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -6,7 +6,8 @@ namespace Magento\Customer\Model; -use Magento\Framework\Indexer\StateInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestSafetyInterface; /** * Class Visitor @@ -67,6 +68,11 @@ class Visitor extends \Magento\Framework\Model\AbstractModel */ protected $indexerRegistry; + /** + * @var RequestSafetyInterface + */ + private $requestSafety; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -95,7 +101,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $ignoredUserAgents = [], array $ignores = [], - array $data = [] + array $data = [], + RequestSafetyInterface $requestSafety = null ) { $this->session = $session; $this->httpHeader = $httpHeader; @@ -105,6 +112,7 @@ public function __construct( $this->scopeConfig = $scopeConfig; $this->dateTime = $dateTime; $this->indexerRegistry = $indexerRegistry; + $this->requestSafety = $requestSafety ?? ObjectManager::getInstance()->get(RequestSafetyInterface::class); } /** @@ -151,10 +159,17 @@ public function initByRequest($observer) if ($this->session->getVisitorData()) { $this->setData($this->session->getVisitorData()); + if ($this->getSessionId() != $this->session->getSessionId()) { + $this->setSessionId($this->session->getSessionId()); + } } $this->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); + // prevent saving Visitor for safe methods, e.g. GET request + if ($this->requestSafety->isSafeMethod()) { + return $this; + } if (!$this->getId()) { $this->setSessionId($this->session->getSessionId()); $this->save(); @@ -174,7 +189,8 @@ public function initByRequest($observer) */ public function saveByRequest($observer) { - if ($this->skipRequestLogging || $this->isModuleIgnored($observer)) { + // prevent saving Visitor for safe methods, e.g. GET request + if ($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) { return $this; } @@ -304,11 +320,9 @@ public function clean() */ public function getOnlineInterval() { - $configValue = intval( - $this->scopeConfig->getValue( - static::XML_PATH_ONLINE_INTERVAL, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) + $configValue = (int)$this->scopeConfig->getValue( + static::XML_PATH_ONLINE_INTERVAL, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); return $configValue ?: static::DEFAULT_ONLINE_MINUTES_INTERVAL; } diff --git a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php index eb7e81009c92c..831506af17cf6 100644 --- a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php +++ b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php @@ -6,11 +6,15 @@ namespace Magento\Customer\Observer; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\CustomerRegistry; +/** + * Observer to execute upgrading customer password hash when customer has logged in. + */ class UpgradeCustomerPasswordObserver implements ObserverInterface { /** @@ -46,7 +50,7 @@ public function __construct( } /** - * Upgrade customer password hash when customer has logged in + * Upgrade customer password hash when customer has logged in. * * @param \Magento\Framework\Event\Observer $observer * @return void @@ -61,7 +65,20 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$this->encryptor->validateHashVersion($customerSecure->getPasswordHash(), true)) { $customerSecure->setPasswordHash($this->encryptor->getHash($password, true)); + // No need to validate customer and customer address while upgrading customer password + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation. + * + * @param CustomerInterface $customer + * @return void + */ + private function setIgnoreValidationFlag(CustomerInterface $customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Setup/UpgradeData.php b/app/code/Magento/Customer/Setup/UpgradeData.php index b5aba18a92f28..0ad36b1d6d11c 100644 --- a/app/code/Magento/Customer/Setup/UpgradeData.php +++ b/app/code/Magento/Customer/Setup/UpgradeData.php @@ -159,6 +159,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $this->upgradeVersionTwoZeroTwelve($customerSetup); } + if (version_compare($context->getVersion(), '2.0.13', '<')) { + $this->upgradeVersionTwoZeroThirteen($customerSetup); + } + $indexer = $this->indexerRegistry->get(Customer::CUSTOMER_GRID_INDEXER_ID); $indexer->reindexAll(); $this->eavConfig->clear(); @@ -663,4 +667,36 @@ private function upgradeCustomerPasswordResetlinkExpirationPeriodConfig($setup) ['path = ?' => \Magento\Customer\Model\Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] ); } + + /** + * @param CustomerSetup $customerSetup + */ + private function upgradeVersionTwoZeroThirteen(CustomerSetup $customerSetup) + { + $entityAttributes = [ + 'customer_address' => [ + 'firstname' => [ + 'input_filter' => 'trim' + ], + 'lastname' => [ + 'input_filter' => 'trim' + ], + 'middlename' => [ + 'input_filter' => 'trim' + ], + ], + 'customer' => [ + 'firstname' => [ + 'input_filter' => 'trim' + ], + 'lastname' => [ + 'input_filter' => 'trim' + ], + 'middlename' => [ + 'input_filter' => 'trim' + ], + ], + ]; + $this->upgradeAttributes($entityAttributes, $customerSetup); + } } diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml new file mode 100644 index 0000000000000..1ffc258e78a43 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddCustomerAddressWithRegionTypeSelectActionGroup" > + <arguments> + <argument name="customerAddress" defaultValue="CustomerAddressSimple"/> + </arguments> + <click selector="{{AdminCustomerAccountAddressSection.addresses}}" stepKey="proceedToAddresses"/> + <click selector="{{AdminCustomerAccountAddressSection.addNewAddress}}" stepKey="addNewAddresses"/> + <fillField userInput="{{customerAddress.street[0]}}" selector="{{AdminCustomerAccountNewAddressSection.street}}" stepKey="fillStreetAddress"/> + <fillField userInput="{{customerAddress.city}}" selector="{{AdminCustomerAccountNewAddressSection.city}}" stepKey="fillCity"/> + <selectOption userInput="{{customerAddress.country_id}}" selector="{{AdminCustomerAccountNewAddressSection.country}}" stepKey="selectCountry"/> + <selectOption userInput="{{customerAddress.state}}" selector="{{AdminCustomerAccountNewAddressSection.regionId}}" stepKey="selectState"/> + <fillField userInput="{{customerAddress.postcode}}" selector="{{AdminCustomerAccountNewAddressSection.zip}}" stepKey="fillZipCode"/> + <fillField userInput="{{customerAddress.telephone}}" selector="{{AdminCustomerAccountNewAddressSection.phone}}" stepKey="fillPhone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCustomer"/> + <see userInput="You saved the customer." selector="{{AdminMessagesSection.success}}" stepKey="customerIsSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml new file mode 100644 index 0000000000000..d7529b3bdd58e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Assert customer info in customers grid row --> + <actionGroup name="AdminAssertCustomerInCustomersGrid"> + <arguments> + <argument name="text" type="string"/> + <argument name="row" type="string"/> + </arguments> + <see selector="{{AdminCustomerGridSection.gridRow(row)}}" userInput="{{text}}" stepKey="seeCustomerInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCheckCustomerInGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCheckCustomerInGridActionGroup.xml new file mode 100644 index 0000000000000..59925da01a7ad --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCheckCustomerInGridActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCheckCustomerInGridActionGroup"> + <arguments> + <argument name="customer"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFilter"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{customer.email}}" stepKey="filterEmail"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="applyFilter"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{customer.firstname}}" stepKey="assertFirstName"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{customer.lastname}}" stepKey="assertLastName"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{customer.email}}" stepKey="assertEmail"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClearCustomersFiltersActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClearCustomersFiltersActionGroup.xml new file mode 100644 index 0000000000000..68cbd52461d15 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClearCustomersFiltersActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Action group clears filters on Admin Customers index page --> + <actionGroup name="AdminClearCustomersFiltersActionGroup"> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomerIndexPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml new file mode 100644 index 0000000000000..3fc25ecf57faa --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetCustomerDataLifetimeActionGroup"> + <arguments> + <argument name="minutes" defaultValue="60" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerConfigPage.url('#customer_online_customers-link')}}" stepKey="openCustomerConfigPage"/> + <fillField userInput="{{minutes}}" selector="{{AdminCustomerConfigSection.customerDataLifetime}}" stepKey="fillCustomerDataLifetime"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml new file mode 100644 index 0000000000000..8b908fa1456a9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCustomerWithWebsiteAndStoreViewActionGroup"> + <arguments> + <argument name="customer"/> + <argument name="address"/> + <argument name="websiteName" type="string"/> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> + <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="addNewCustomer"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{websiteName}}" stepKey="selectWebSite"/> + <fillField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customer.firstname}}" stepKey="fillFirstName"/> + <fillField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customer.lastname}}" stepKey="fillLastName"/> + <fillField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customer.email}}" stepKey="fillEmail"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeViewName}}" stepKey="selectStoreView"/> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="goToAddresses"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <click selector="{{AdminCustomerEditAddressesSection.addNewAddress}}" stepKey="clickOnAddNewAddress"/> + <waitForPageLoad stepKey="waitForAddressFields"/> + <click selector="{{AdminCustomerEditAddressesSection.defaultBillingAddress}}" stepKey="tickBillingAddress"/> + <click selector="{{AdminCustomerEditAddressesSection.defaultShippingAddress}}" stepKey="tickShippingAddress"/> + <fillField selector="{{AdminCustomerEditAddressesSection.firstName}}" userInput="{{address.firstname}}" stepKey="fillFirstNameForAddress"/> + <fillField selector="{{AdminCustomerEditAddressesSection.lastName}}" userInput="{{address.lastname}}" stepKey="fillLastNameForAddress"/> + <fillField selector="{{AdminCustomerEditAddressesSection.streetAddress}}" userInput="{{address.street[0]}}" stepKey="fillStreetAddress"/> + <fillField selector="{{AdminCustomerEditAddressesSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> + <selectOption selector="{{AdminCustomerEditAddressesSection.country}}" userInput="{{address.country}}" stepKey="selectCountry"/> + <selectOption selector="{{AdminCustomerEditAddressesSection.state}}" userInput="{{address.state}}" stepKey="selectState"/> + <fillField selector="{{AdminCustomerEditAddressesSection.zip}}" userInput="{{address.postcode}}" stepKey="fillZip"/> + <fillField selector="{{AdminCustomerEditAddressesSection.phoneNumber}}" userInput="{{address.telephone}}" stepKey="fillPhoneNumber"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="save"/> + <see userInput="You saved the customer." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml new file mode 100644 index 0000000000000..b9bac35b503b3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveCustomerForm"> + <scrollToTopOfPage stepKey="scrollToPageTop"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the customer." stepKey="seeSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml new file mode 100644 index 0000000000000..5dd54e21d84a9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Action group deletes customer by customer email field --> + <actionGroup name="AdminDeleteCustomerActionGroup"> + <arguments> + <argument name="customerEmail"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomersPage"/> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFilters"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{customerEmail}}" stepKey="filterCustomersByEmail"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="applyFilters"/> + <click selector="{{AdminCustomerGridMainActionsSection.customerCheckbox(customerEmail)}}" stepKey="chooseCustomer"/> + <click selector="{{AdminCustomerGridMainActionsSection.actions}}" stepKey="openActions"/> + <waitForPageLoad stepKey="waitActions"/> + <click selector="{{AdminCustomerGridMainActionsSection.delete}}" stepKey="deleteCustomer"/> + <waitForPageLoad stepKey="waitForConfirmationAlert"/> + <click selector="{{AdminCustomerGridMainActionsSection.ok}}" stepKey="acceptDeletion"/> + <waitForPageLoad stepKey="waitForDelete"/> + <see userInput="were deleted." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.xml new file mode 100644 index 0000000000000..94cd759e5cef5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerSavedCardActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontCustomerSavedCardActionGroup"> + <arguments> + <argument name="card" type="entity" defaultValue="StoredPaymentMethods"/> + </arguments> + <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.cardNumber}}" userInput="{{card.cardNumberEnding}}" stepKey="verifyCardNumber"/> + <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.expirationDate}}" userInput="{{card.cardExpire}}" stepKey="verifyCardExpire"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml new file mode 100644 index 0000000000000..031e1c679933e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CustomerActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLoginOnStorefront"> + <arguments> + <argument name="customer" defaultValue="customer"/> + </arguments> + <amOnPage stepKey="loginPage" url="customer/account/login/"/> + <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + </actionGroup> + + <actionGroup name="CustomerLogoutStorefrontActionGroup"> + <amOnPage url="customer/account/logout/" stepKey="storefrontSignOut"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml new file mode 100644 index 0000000000000..a77f17ed4382c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginToStorefrontActionGroup"> + <arguments> + <argument name="Customer"/> + </arguments> + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad stepKey="waitPageFullyLoaded"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="waitFormAppears"/> + <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <waitForPageLoad stepKey="waitForCustomerLoggedIn" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml new file mode 100644 index 0000000000000..3b7aab22f749e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenEditCustomerFromAdminActionGroup" extends="clearFiltersAdminDataGrid"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" before="waitForPageLoad" stepKey="navigateToCustomers"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> + <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml new file mode 100644 index 0000000000000..9975431e9fe6e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenStorefrontStoredPaymentMethodsPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenStorefrontCustomerStoredPaymentMethodsPageActionGroup"> + <amOnPage url="{{StorefrontCustomerStoredPaymentMethodsPage.url}}" stepKey="goToCustomerStoredPaymentMethodsPage"/> + <waitForPageLoad stepKey="waitForCustomerStoredPaymentMethodsPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml new file mode 100644 index 0000000000000..f4d73157eb0b3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Delete a customer by Email by filtering grid and using delete action--> + <actionGroup name="RemoveCustomerFromAdminActionGroup"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearFilters}}" dependentSelector="{{AdminCustomerFiltersSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> + <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <see selector="{{AdminCustomerGridSection.customerGridCell('1', 'Email')}}" userInput="{{customer.email}}" stepKey="seeCustomerInGrid"/> + <click selector="{{AdminCustomerGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminCustomerGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminCustomerGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminCustomerGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="A total of 1 record(s) were deleted." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearFilters}}" dependentSelector="{{AdminCustomerFiltersSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml new file mode 100644 index 0000000000000..d86591b799a33 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SignUpNewUserFromStorefrontActionGroup"> + <arguments> + <argument name="Customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <click selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}" stepKey="clickOnCreateAccountLink"/> + <fillField userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" stepKey="fillFirstName"/> + <fillField userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" stepKey="fillLastName"/> + <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}" stepKey="fillPassword"/> + <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" stepKey="fillConfirmPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <see userInput="Thank you for registering with Main Website Store." stepKey="seeThankYouMessage"/> + <see userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" stepKey="seeFirstName"/> + <see userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" stepKey="seeLastName"/> + <see userInput="{{Customer.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" stepKey="seeEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerWelcomeMessageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerWelcomeMessageActionGroup.xml new file mode 100644 index 0000000000000..01f80f4c17d2c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerWelcomeMessageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCustomerWelcomeMessageActionGroup"> + <arguments> + <argument name="customerFullName" type="string" /> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" userInput="Welcome, {{customerFullName}}!" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertMessageCustomerCreateAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertMessageCustomerCreateAccountActionGroup.xml new file mode 100644 index 0000000000000..37a9a0ec23e03 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertMessageCustomerCreateAccountActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertMessageCustomerCreateAccountActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Thank you for registering with Main Website Store." /> + </arguments> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="{{message}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.xml new file mode 100644 index 0000000000000..d76277d2e5e45 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertRegistrationPageFieldsActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertRegistrationPageFields"> + <seeInCurrentUrl url="{{StorefrontCustomerCreatePage.url}}" stepKey="seeCreateNewCustomerAccountPage"/> + <seeElement selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" stepKey="seeFirstNameField"/> + <seeElement selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" stepKey="seeFLastNameField"/> + <seeElement selector="{{StorefrontCustomerCreateFormSection.emailField}}" stepKey="seeEmailField"/> + <seeElement selector="{{StorefrontCustomerCreateFormSection.passwordField}}" stepKey="seePasswordField"/> + <seeElement selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" stepKey="seeConfirmPasswordField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml new file mode 100644 index 0000000000000..aa764e5f51de1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerAccountCheckTab"> + <arguments> + <argument name="tabName" type="string"/> + </arguments> + + <see selector="{{StorefrontCustomerSidebarSection.sidebarTab(tabName)}}" userInput="{{tabName}}" stepKey="checkTabExists"/> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(tabName)}}" stepKey="clickToOpenTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontHeaderSection.mainTitle}}" userInput="{{tabName}}" stepKey="checkTabTitle"/> + </actionGroup> + + <!--Go to Storefront > Account Information--> + <actionGroup name="StorefrontStartCustomerAccountInformationEdit"> + <amOnPage url="{{StorefrontCustomerAccountInformationPage.url}}" stepKey="goToAccountInformationEditPage"/> + <see selector="{{StorefrontCustomerAccountInformationSection.title}}" userInput="Edit Account Information" stepKey="seeEditAccountInformationPageTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 0000000000000..1199d0f80948f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLogoutStorefrontByMenuItemsActionGroup"> + <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcome}}" + dependentSelector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" + visible="false" + stepKey="clickHeaderCustomerMenuButton" /> + <click selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="clickSignOutButton" /> + </actionGroup> + + <actionGroup name="StorefrontWaitCustomerLoggedOut"> + <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomer"/> + <waitForPageLoad stepKey="waitForRedirectToHomePage"/> + <waitForText selector="{{StorefrontCmsPageSection.mainContent}}" userInput="CMS homepage content goes here." stepKey="waitForLoadContentMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml new file mode 100644 index 0000000000000..de97bb47de796 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerLogoutActionGroup"> + <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="storefrontSignOut"/> + </actionGroup> + + <actionGroup name="StorefrontSignOutActionGroup"> + <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> + <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillRegistrationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillRegistrationFormActionGroup.xml new file mode 100644 index 0000000000000..c1a1920b3c734 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillRegistrationFormActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillRegistrationFormActionGroup"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> + <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> + <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> + <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> + <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> + <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml new file mode 100644 index 0000000000000..a97ad2a8b1907 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontRegisterCustomerFromOrderSuccessPage"> + <arguments> + <argument name="customer" /> + </arguments> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{customer.password}}" stepKey="typePassword"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{customer.password}}" stepKey="typeConfirmationPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="Thank you for registering" stepKey="verifyAccountCreated"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontSaveRegistrationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontSaveRegistrationFormActionGroup.xml new file mode 100644 index 0000000000000..e5af973e78851 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontSaveRegistrationFormActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSaveRegistrationFormActionGroup"> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <waitForPageLoad stepKey="waitForCreateAccountButtonToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml new file mode 100644 index 0000000000000..ebce94a10f9e5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerAddressSimple" type="address"> + <data key="id">0</data> + <data key="customer_id">12</data> + <requiredEntity type="region">CustomerRegionOne</requiredEntity> + <data key="region_id">0</data> + <data key="country_id">US</data> + <array key="street"> + <item>7700 W Parmer Ln</item> + <item>Bld D</item> + </array> + <data key="company">Magento</data> + <data key="telephone">1234568910</data> + <data key="fax">1234568910</data> + <data key="postcode">78729</data> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="middlename">string</data> + <data key="prefix">Mr</data> + <data key="suffix">Sr</data> + <data key="vat_id">vatData</data> + <data key="default_shipping">true</data> + <data key="default_billing">true</data> + </entity> + <entity name="US_Address_TX" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> + <!--If required other field can be added to UK_Address entity, don't modify any existing data--> + <entity name="UK_Address" type="address"> + <data key="country_id">GB</data> + </entity> + <entity name="US_Address_CA" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">qa</data> + <data key="telephone">13456768768</data> + <data key="fax">987654321</data> + <array key="street"> + <item>Main st1</item> + <item>ap.66</item> + </array> + <data key="city">Culver City</data> + <data key="state">California</data> + <data key="postcode">90230</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionCA</requiredEntity> + </entity> + <entity name="US_Address_NY" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">qa</data> + <data key="telephone">13456768768</data> + <data key="fax">987654321</data> + <array key="street"> + <item>Main st1</item> + <item>ap.66</item> + </array> + <data key="city">New York</data> + <data key="state">New York</data> + <data key="postcode">11001</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionNY</requiredEntity> + </entity> + <entity name="UK_Default_Address" type="address"> + <data key="firstname">Jane</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>172, Westminster Bridge Rd</item> + </array> + <data key="city">London</data> + <data key="state"></data> + <data key="postcode">SE1 7RW</data> + <data key="country_id">GB</data> + <data key="country">United Kingdom</data> + <data key="telephone">444-44-444-44</data> + </entity> + <entity name="US_Default_Billing_Address_TX" type="address" extends="US_Address_TX"> + <data key="default_billing">false</data> + <data key="default_shipping">true</data> + </entity> + <entity name="US_Default_Shipping_Address_CA" type="address" extends="US_Address_CA"> + <data key="default_billing">true</data> + <data key="default_shipping">false</data> + </entity> + <entity name="Canada_Address"> + <data key="firstname">Robert</data> + <data key="lastname">Roe</data> + <array key="street"> + <item>3197 rue Parc</item> + </array> + <data key="city">Sherbrooke</data> + <data key="state">Quebec</data> + <data key="postcode">J1L 1C9</data> + <data key="country_id">CA</data> + <data key="country">Canada</data> + <data key="telephone">999-99-99-99</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml new file mode 100644 index 0000000000000..8a25815c06dfd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerAccountSharingDefault" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">CustomerAccountSharingPerWebsite</requiredEntity> + </entity> + <entity name="CustomerAccountSharingPerWebsite" type="account_share_scope_value"> + <data key="value">1</data> + </entity> + + <entity name="CustomerAccountSharingGlobal" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">GlobalCustomerAccountSharing</requiredEntity> + </entity> + <entity name="GlobalCustomerAccountSharing" type="account_share_scope_value"> + <data key="value">0</data> + </entity> + + <entity name="CustomerAccountSharingSystemValue" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">CustomerAccountSharingInherit</requiredEntity> + </entity> + <entity name="CustomerAccountSharingInherit" type="account_share_scope_value"> + <data key="inherit">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml new file mode 100644 index 0000000000000..7cd8ca0a3690c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerEntityOne" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">defaultBillingValue</data> + <data key="default_shipping">defaultShippingValue</data> + <data key="confirmation">confirmationData</data> + <data key="created_at">12:00</data> + <data key="updated_at">12:00</data> + <data key="created_in">createdInData</data> + <data key="dob">01-01-1970</data> + <data key="email" unique="prefix">test@email.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="middlename">S</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="prefix">Mr</data> + <data key="suffix">Sr</data> + <data key="gender">0</data> + <data key="store_id">0</data> + <data key="taxvat">taxValue</data> + <data key="website_id">0</data> + <requiredEntity type="address">CustomerAddressSimple</requiredEntity> + <data key="disable_auto_group_change">0</data> + <!--requiredEntity type="extension_attribute">ExtensionAttributeSimple</requiredEntity--> + </entity> + <entity name="Simple_US_Customer" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + </entity> + <entity name="Simple_Customer_Without_Address" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> + <entity name="Simple_US_Customer_For_Update" type="customer"> + <var key="id" entityKey="id" entityType="customer"/> + <data key="firstname">Jane</data> + </entity> + <entity name="Simple_US_CA_Customer" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_CA</requiredEntity> + </entity> + <entity name="Simple_US_NY_Customer" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_NY</requiredEntity> + </entity> + <entity name="Customer_With_Different_Default_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> + <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> + </entity> + <entity name="John_Smith_Customer" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">john.smith@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Smith</data> + <data key="fullname">John Smith</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> + <entity name="Simple_US_Customer_Incorrect_Email" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email">><script>alert(1);</script>@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_CA</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml new file mode 100644 index 0000000000000..d7fc22f085344 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiCustomerGroup" type="customerGroup"> + <data key="code" unique="suffix">ApiCustomerGroup</data> + <data key="tax_class_id">0</data> + <data key="tax_class_name">Retail Customer</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml new file mode 100644 index 0000000000000..1331f288286e5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerPrefixOptions" type="customer_name_address_options"> + <requiredEntity type="prefix_options">PrefixOptions</requiredEntity> + </entity> + <entity name="PrefixOptions" type="prefix_options"> + <data key="value">Mr;Mrs;Ms;Dr</data> + </entity> + + <entity name="DefaultCustomerPrefixOptions" type="customer_name_address_options"> + <requiredEntity type="prefix_options">DefaultPrefixOptions</requiredEntity> + </entity> + <entity name="DefaultPrefixOptions" type="prefix_options"> + <data key="value"></data> + </entity> + + <entity name="CustomerSuffixOptions" type="customer_name_address_options"> + <requiredEntity type="suffix_options">SuffixOptions</requiredEntity> + </entity> + <entity name="SuffixOptions" type="suffix_options"> + <data key="value">Jr;Sr</data> + </entity> + + <entity name="DefaultCustomerSuffixOptions" type="customer_name_address_options"> + <requiredEntity type="suffix_options">DefaultSuffixOptions</requiredEntity> + </entity> + <entity name="DefaultSuffixOptions" type="suffix_options"> + <data key="value"></data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml new file mode 100644 index 0000000000000..fee4463709dd5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ExtensionAttributeSimple" type="extension_attribute"> + <data key="is_subscribed">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml new file mode 100644 index 0000000000000..0a357c86ed9f8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Simple_UK_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoe@example.com</data> + <data key="company">Test Company</data> + <data key="streetFirstLine">39 St Maurices Road</data> + <data key="streetSecondLine">ap. 654</data> + <data key="city">PULDAGON</data> + <data key="telephone">077 5866 0667</data> + <data key="country">United Kingdom</data> + <data key="region"> </data> + <data key="postcode">KW1 7NQ</data> + </entity> + <entity name="Simple_US_CA_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoeusca@example.com</data> + <data key="company">Magento</data> + <data key="streetFirstLine">123 Alphabet Drive</data> + <data key="streetSecondLine">ap. 350</data> + <data key="city">Los Angeles</data> + <data key="telephone">555-55-555-55</data> + <data key="country">United States</data> + <data key="region">California</data> + <data key="postcode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml new file mode 100644 index 0000000000000..9f31e5994eaae --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerRegionOne" type="region"> + <data key="region_code">100</data> + <data key="region_id">12</data> + </entity> + <entity name="RegionTX" type="region"> + <data key="region">Texas</data> + <data key="region_code">TX</data> + <data key="region_id">57</data> + </entity> + <entity name="RegionCA" type="region"> + <data key="region">California</data> + <data key="region_code">CA</data> + <data key="region_id">12</data> + </entity> + <entity name="RegionNY" type="region"> + <data key="region">New York</data> + <data key="region_code">NY</data> + <data key="region_id">43</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/LICENSE.txt b/app/code/Magento/Customer/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Customer/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 <insert your license name here>" 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/app/code/Magento/Customer/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Customer/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Customer/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 <insert your license name here>" 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/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml new file mode 100644 index 0000000000000..deb911f244f11 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/address-meta.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateAddress" dataType="address" type="create"> + <field key="region">region</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="company">string</field> + <field key="telephone">string</field> + <field key="fax">string</field> + <field key="postcode">string</field> + <field key="city">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="middlename">string</field> + <field key="prefix">string</field> + <field key="suffix">string</field> + <field key="vat_id">string</field> + <field key="default_shipping">boolean</field> + <field key="default_billing">boolean</field> + <field key="extension_attributes">empty_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </operation> + <operation name="UpdateAddress" dataType="address" type="update"> + <field key="id">integer</field> + <field key="customer_id">integer</field> + <field key="region">region</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="company">string</field> + <field key="telephone">string</field> + <field key="fax">string</field> + <field key="postcode">string</field> + <field key="city">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="middlename">string</field> + <field key="prefix">string</field> + <field key="suffix">string</field> + <field key="vat_id">string</field> + <field key="default_shipping">boolean</field> + <field key="default_billing">boolean</field> + <field key="extension_attributes">empty_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml new file mode 100644 index 0000000000000..a45bf1645d088 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerGroup" dataType="customerGroup" type="create" auth="adminOauth" url="/V1/customerGroups" method="POST"> + <contentType>application/json</contentType> + <object dataType="customerGroup" key="group"> + <field key="code">string</field> + <field key="tax_class_id">integer</field> + <field key="tax_class_name">string</field> + <array key="extension_attributes"> + <value>extension_attributes</value> + </array> + </object> + </operation> + <operation name="DeleteCustomerGroup" dataType="customerGroup" type="delete" auth="adminOauth" url="/V1/customerGroups/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml new file mode 100644 index 0000000000000..ab2ee2aeddb54 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-meta.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomer" dataType="customer" type="create" auth="adminOauth" url="/V1/customers" method="POST"> + <contentType>application/json</contentType> + <object dataType="customer" key="customer"> + <field key="group_id">integer</field> + <field key="default_billing">string</field> + <field key="default_shipping">string</field> + <field key="confirmation">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="created_in">string</field> + <field key="dob">string</field> + <field key="email">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="middlename">string</field> + <field key="prefix">string</field> + <field key="suffix">string</field> + <field key="gender">integer</field> + <field key="store_id">integer</field> + <field key="taxvat">string</field> + <field key="website_id">integer</field> + <array key="addresses"> + <value>address</value> + </array> + <field key="disable_auto_group_change">integer</field> + <field key="extension_attributes">customer_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </object> + <field key="password">string</field> + <field key="redirectUrl">string</field> + </operation> + <operation name="DeleteCustomer" dataType="customer" type="delete" auth="adminOauth" url="/V1/customers/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml new file mode 100644 index 0000000000000..b52eda0c4cfdd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CustomerAccountShareConfig" dataType="customer_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" + successRegex="/messages-message-success/" returnRegex="" method="POST"> + <object key="groups" dataType="customer_account_sharing_config"> + <object key="account_share" dataType="customer_account_sharing_config"> + <object key="fields" dataType="customer_account_sharing_config"> + <object key="scope" dataType="account_share_scope_value"> + <field key="value">string</field> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml new file mode 100644 index 0000000000000..8561e937221a9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_extension_attribute-meta.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="create"> + <field key="is_subscribed">boolean</field> + <field key="extension_attribute">customer_nested_extension_attribute</field> + </operation> + <operation name="UpdateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="update"> + <field key="is_subscribed">boolean</field> + <field key="extension_attribute">customer_nested_extension_attribute</field> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml new file mode 100644 index 0000000000000..07175a09fe3e1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CustomerConfigNameAddressOptions" dataType="customer_name_address_options" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="customer_name_address_options"> + <object key="address" dataType="customer_name_address_options"> + <object key="fields" dataType="customer_name_address_options"> + <object key="prefix_options" dataType="prefix_options"> + <field key="value">string</field> + </object> + <object key="suffix_options" dataType="suffix_options"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml new file mode 100644 index 0000000000000..eb9829cca4981 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_nested_extension_attribute-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateNestedExtensionAttribute" dataType="customer_nested_extension_attribute" type="create"> + <field key="id">integer</field> + <field key="customer_id">integer</field> + <field key="value">string</field> + </operation> + <operation name="UpdateNestedExtensionAttribute" dataType="customer_nested_extension_attribute" type="update"> + <field key="id">integer</field> + <field key="customer_id">integer</field> + <field key="value">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml new file mode 100644 index 0000000000000..3dd019462c846 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/region-meta.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateRegion" dataType="region" type="create"> + <field key="region_code">string</field> + <field key="region">string</field> + <field key="region_id">string</field> + <field key="extension_attributes">extension_attributes</field> + </operation> + <operation name="UpdateRegion" dataType="region" type="update"> + <field key="region_code">string</field> + <field key="region">string</field> + <field key="region_id">string</field> + <field key="extension_attributes">empty_extension_attribute</field> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml new file mode 100644 index 0000000000000..3cf8490ec4af1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCustomerConfigPage" url="admin/system_config/edit/section/customer/{{tabLink}}" area="admin" parameterized="true" module="Magento_Customer"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml new file mode 100644 index 0000000000000..06ab646aa4c75 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCustomerPage" url="/customer/index/" area="admin" module="Magento_Customer"> + <section name="AdminCustomerGridMainActionsSection"/> + <section name="AdminCustomerMessagesSection"/> + <section name="AdminCustomerGridSection"/> + <section name="AdminCustomerFiltersSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml new file mode 100644 index 0000000000000..7cd36c12c80bd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminEditCustomerPage" url="/customer/index/edit/id/{{var1}}" area="admin" module="Magento_Customer" parameterized="true"> + <section name="AdminCustomerAccountInformationSection"/> + <section name="AdminCustomerMainActionsSection"/> + <section name="AdminCustomerAccountAddressSection"/> + <section name="AdminCustomerAccountEditAddressSection"/> + <section name="AdminCustomerAccountNewAddressSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml new file mode 100644 index 0000000000000..1b73441dd149d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerPage.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminNewCustomerPage" url="/customer/index/new" area="admin" module="Customer"> + <section name="AdminNewCustomerAccountInformationSection"/> + <section name="AdminNewCustomerMainActionsSection"/> + <section name="AdminCustomerAccountInformationEditAddressSection"/> + <section name="AdminCustomerAccountAddressSection"/> + <section name="AdminMainActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml new file mode 100644 index 0000000000000..80caea5a1f541 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAccountInformationPage" url="/customer/account/edit" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerSidebarSection"/> + <section name="StorefrontCustomerAccountInformationSection" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml new file mode 100644 index 0000000000000..8fbc7b10fdd95 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAddressNewPage" url="/customer/address/new" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAddressEditFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml new file mode 100644 index 0000000000000..bdbbcab67da67 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAddressesPage" url="/customer/address/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAddressesSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml new file mode 100644 index 0000000000000..ba61cbb0bca42 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerCreatePage" url="/customer/account/create/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerCreateFormSection" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml new file mode 100644 index 0000000000000..788a23fb539c5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerDashboardPage" url="/customer/account/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerDashboardAccountInformationSection" /> + <section name="StorefrontCustomerSidebarSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml new file mode 100644 index 0000000000000..b3cea8f2c2939 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerLogoutPage" url="customer/account/logout/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml new file mode 100644 index 0000000000000..9c1fc7aa8a88d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerLogoutSuccessPage" url="customer/account/logoutSuccess/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml new file mode 100644 index 0000000000000..9cd6dc1e69e35 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrderViewPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerOrderViewPage" url="sales/order/view/order_id/{{var1}}" area="storefront" module="Magento_Customer" parameterized="true"> + <section name="StorefrontCustomerOrderSection" /> + <section name="StorefrontCustomerOrderViewSection" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml new file mode 100644 index 0000000000000..effc07d00f408 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerOrdersGridPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerOrdersGridPage" url="/sales/order/history/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerOrdersGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml new file mode 100644 index 0000000000000..f6673227beada --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerSignInPage" url="/customer/account/login/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerSignInFormSection" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml new file mode 100644 index 0000000000000..4e89e5476c3bc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerSignOutPage" url="/customer/account/logout/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.xml new file mode 100644 index 0000000000000..bec802689a6cc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerStoredPaymentMethodsPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerStoredPaymentMethodsPage" url="/vault/cards/listaction/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerStoredPaymentMethodsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml new file mode 100644 index 0000000000000..6b65bd97e8cb3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontHomePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontHomePage" url="/" area="storefront" module="Magento_Customer"> + <section name="StorefrontPanelHeader" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/README.md b/app/code/Magento/Customer/Test/Mftf/README.md new file mode 100644 index 0000000000000..f9fe1cd5b4a39 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Customer Functional Tests + +The Functional Test Module for **Magento Customer** module. diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml new file mode 100644 index 0000000000000..376b0b9f66db9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCreateUserSection"> + <element name="system" type="input" selector="#menu-magento-backend-system"/> + <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> + <element name="create" type="input" selector="#add"/> + <element name="usernameTextField" type="input" selector="#user_username"/> + <element name="firstNameTextField" type="input" selector="#user_firstname"/> + <element name="lastNameTextField" type="input" selector="#user_lastname"/> + <element name="emailTextField" type="input" selector="#user_email"/> + <element name="passwordTextField" type="input" selector="#user_password"/> + <element name="pwConfirmationTextField" type="input" selector="#user_confirmation"/> + <element name="currentPasswordField" type="input" selector="#user_current_password"/> + <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> + <element name="saveButton" type="button" selector="#save"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml new file mode 100644 index 0000000000000..70042e2a71467 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml @@ -0,0 +1,13 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAccountAddressSection"> + <element name="addresses" type="button" selector="#tab_address" timeout="30"/> + <element name="addNewAddress" type="button" selector=".address-list-actions button.scalable.add span" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountEditAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountEditAddressSection.xml new file mode 100644 index 0000000000000..0f64c675ead49 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountEditAddressSection.xml @@ -0,0 +1,18 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAccountEditAddressSection"> + <element name="firstName" type="button" selector="input[name*='address'][name*='firstname']"/> + <element name="lastName" type="button" selector="input[name*='address'][name*='lastname']"/> + <element name="street" type="button" selector="input[name*='street']"/> + <element name="city" type="input" selector="input[name*='city']"/> + <element name="country" type="select" selector="select[name*='address'][name*='country']" timeout="10"/> + <element name="region" type="select" selector="select[name*='address'][name*='region_id']"/> + <element name="zip" type="input" selector="input[name*='postcode']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..6b988b067043a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -0,0 +1,23 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAccountInformationSection"> + <element name="accountInformationTitle" type="text" selector=".admin__page-nav-title"/> + <element name="accountInformationButton" type="text" selector="//a/span[text()='Account Information']"/> + <element name="addressesButton" type="select" selector="//a//span[contains(text(), 'Addresses')]"/> + <element name="firstName" type="input" selector="input[name='customer[firstname]']"/> + <element name="lastName" type="input" selector="input[name='customer[lastname]']"/> + <element name="email" type="input" selector="input[name='customer[email]']"/> + <element name="group" type="select" selector="[name='customer[group_id]']"/> + <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> + <element name="associateToWebsite" type="select" selector="select[name='customer[website_id]']"/> + <element name="storeView" type="select" selector="select[name='customer[sendemail_store_id]']"/> + <element name="accountInformationTab" type="button" selector="#tab_customer"/> + <element name="attributeImage" type="block" selector=".file-uploader-preview img"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml new file mode 100644 index 0000000000000..8445343c9b9c0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml @@ -0,0 +1,19 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAccountNewAddressSection"> + <element name="firstName" type="button" selector="input[name*='address'][name*='new'][name*='firstname']"/> + <element name="lastName" type="button" selector="input[name*='address'][name*='new'][name*='lastname']"/> + <element name="street" type="button" selector="input[name*='new'][name*='street']"/> + <element name="city" type="input" selector="input[name*='new'][name*='city']"/> + <element name="country" type="select" selector="select[name*='address'][name*='new'][name*='country']" timeout="10"/> + <element name="regionId" type="select" selector="select[name*='address'][name*='new'][name*='region_id']"/> + <element name="zip" type="input" selector="input[name*='address'][name*='new'][name*='postcode']"/> + <element name="phone" type="text" selector="input[name*='address'][name*='new'][name*='telephone']" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml new file mode 100644 index 0000000000000..22eae72c880f9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAddressFiltersSection"> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml new file mode 100644 index 0000000000000..33696fbd616e3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerConfigSection"> + <element name="customerDataLifetime" type="input" selector="#customer_online_customers_section_data_lifetime"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerEditAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerEditAddressesSection.xml new file mode 100644 index 0000000000000..3c9e14033fb0d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerEditAddressesSection.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerEditAddressesSection"> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Addresses']"/> + <element name="defaultBillingAddress" type="button" selector="//label[text()='Default Billing Address']"/> + <element name="defaultShippingAddress" type="button" selector="//label[text()='Default Shipping Address']"/> + <element name="firstName" type="button" selector="input[name*='address'][name*=firstname]"/> + <element name="lastName" type="button" selector="input[name*='address'][name*=lastname]"/> + <element name="streetAddress" type="button" selector="input[name*='address'][name*=street]"/> + <element name="city" type="input" selector="input[name*='address'][name*=city]"/> + <element name="country" type="select" selector="select[name*='address'][name*=country_id]"/> + <element name="state" type="select" selector="select[name*=address][name*=region_id]"/> + <element name="zip" type="input" selector="input[name*=address][name*=postcode]"/> + <element name="phoneNumber" type="input" selector="input[name*=address][name*=telephone]"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml new file mode 100644 index 0000000000000..564fc69f58704 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerFiltersSection"> + <element name="filtersButton" type="button" selector="#container > div > div.admin__data-grid-header > div:nth-child(1) > div.data-grid-filters-actions-wrap > div > button" timeout="30"/> + <element name="nameInput" type="input" selector="input[name=name]"/> + <element name="emailInput" type="input" selector="input[name=email]"/> + <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> + <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="countryOptions" type="button" selector=".admin__data-grid-filters select[name=billing_country_id] option"/> + <element name="countryOptionsWithoutEmptyOption" type="button" selector=".admin__data-grid-filters select[name=billing_country_id] option:not([value=''])"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml new file mode 100644 index 0000000000000..c1553de65111e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerGridMainActionsSection"> + <element name="addNewCustomer" type="button" selector="#add" timeout="30"/> + <element name="actions" type="text" selector=".action-select"/> + <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/../preceding-sibling::td[contains(@class, 'data-grid-checkbox-cell')]//input" parameterized="true"/> + <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> + <element name="ok" type="button" selector=".modal-footer button.action-accept"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml new file mode 100644 index 0000000000000..265b520bc91cc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCustomerGridSection"> + <element name="customerGrid" type="text" selector="table[data-role='grid']"/> + <element name="firstRowEditLink" type="checkbox" selector="tr[data-repeat-index='0'] .action-menu-item" timeout="30"/> + <element name="customerGridCell" type="text" selector="//div[@data-role='grid-wrapper']//tr[{{row}}+1]//td[count(//div[@data-role='grid-wrapper']//tr//th//span[contains(., '{{column}}')]/../preceding-sibling::th) + 1]//div" parameterized="true"/> + <element name="multicheckDropdown" type="button" selector="div[data-role='grid-wrapper'] th.data-grid-multicheck-cell button.action-multicheck-toggle"/> + <element name="multicheckOption" type="button" selector="//div[@data-role='grid-wrapper']//th[contains(@class, data-grid-multicheck-cell)]//li//span[text() = '{{label}}']" parameterized="true"/> + <element name="bulkActionDropdown" type="button" selector="div.admin__data-grid-header-row.row div.action-select-wrap button.action-select"/> + <element name="bulkActionOption" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-select-wrap')]//ul/li/span[text() = '{{label}}']" parameterized="true"/> + <element name="gridRow" type="text" selector="//*[@data-role='sticky-el-root']/parent::div/parent::div/following-sibling::div//tbody//*[@class='data-row'][{{row}}]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml new file mode 100644 index 0000000000000..9553752539757 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerMainActionsSection"> + <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="deleteButton" type="button" selector="div.page-actions button.delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml new file mode 100644 index 0000000000000..9e8bbfb0e18cf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMessagesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCustomerMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml new file mode 100644 index 0000000000000..76feb2624b5ed --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminEditCustomerInformationSection"> + <element name="orders" type="button" selector="#tab_orders_content" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml new file mode 100644 index 0000000000000..bce4a7e848c13 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerOrdersSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminEditCustomerOrdersSection"> + <element name="orderGrid" type="text" selector="#customer_orders_grid_table"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..868ffd8e034db --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerAccountInformationSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewCustomerAccountInformationSection"> + <element name="accountInformationTitle" type="text" selector=".admin__page-nav-title"/> + <element name="firstName" type="input" selector="input[name='customer[firstname]']"/> + <element name="lastName" type="input" selector="input[name='customer[lastname]']"/> + <element name="email" type="input" selector="input[name='customer[email]']"/> + <element name="group" type="select" selector="[name='customer[group_id]']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerMainActionsSection.xml new file mode 100644 index 0000000000000..400b028d57186 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerMainActionsSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewCustomerMainActionsSection"> + <element name="saveButton" type="button" selector="#save" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml new file mode 100644 index 0000000000000..7386d711513ef --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StoreFrontSignOutSection"> + <element name="customerAccount" type="button" selector=".customer-name"/> + <element name="signOut" type="button" selector="div.customer-menu li.authorization-link"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..f6706d0e16ab3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountInformationSection"> + <element name="title" type="text" selector=".page-title span"/> + <element name="firstName" type="input" selector="#firstname"/> + <element name="lastName" type="input" selector="#lastname"/> + <element name="changeEmail" type="checkbox" selector="#change_email"/> + <element name="changePassword" type="checkbox" selector="#change_password"/> + <element name="customAttributeFiled" type="input" selector="#{{attribute_code}}" parameterized="true"/> + <element name="saveButton" type="button" selector="#form-validate .action.save.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml new file mode 100644 index 0000000000000..2af00532301ed --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressEditFormSection"> + <element name="country" type="select" selector=".form-address-edit select#country" /> + <element name="countryEmptyOption" type="select" selector=".form-address-edit select#country option[value='']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml new file mode 100644 index 0000000000000..88b46d245105f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressesSection"> + <element name="addressesList" type="text" selector=".block-addresses-list" /> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml new file mode 100644 index 0000000000000..adf898a65f212 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerCreateFormSection"> + <element name="firstnameField" type="input" selector="#firstname"/> + <element name="lastnameField" type="input" selector="#lastname"/> + <element name="emailField" type="input" selector="#email_address"/> + <element name="passwordField" type="input" selector="#password"/> + <element name="confirmPasswordField" type="input" selector="#password-confirmation"/> + <element name="createAccountButton" type="button" selector="button.action.submit.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml new file mode 100644 index 0000000000000..83d671d30da3f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerDashboardAccountInformationSection"> + <element name="ContactInformation" type="textarea" selector=".box.box-information .box-content"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml new file mode 100644 index 0000000000000..7957fa0350fa5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerOrderSection"> + <element name="isMyOrdersSection" type="text" selector="//*[@class='page-title']//*[contains(text(), 'My Orders')]"/> + <element name="productCustomOptions" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[normalize-space(.)='{{var3}}']" parameterized="true"/> + <element name="productCustomOptionsFile" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[contains(.,'{{var3}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml new file mode 100644 index 0000000000000..f6a6cb2d457e1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerOrderViewSection"> + <element name="orderTitle" type="text" selector=".page-title span"/> + <element name="myOrdersTable" type="text" selector="#my-orders-table"/> + <element name="subtotal" type="text" selector=".subtotal .amount"/> + <element name="paymentMethod" type="text" selector=".payment-method dt"/> + <element name="printOrderLink" type="text" selector="a.action.print" timeout="30"/> + <element name="shippingAddress" type="text" selector=".box.box-order-shipping-address"/> + <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml new file mode 100644 index 0000000000000..419387fd92d1f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerOrdersGridSection"> + <element name="viewOrderAction" type="text" selector="//*[@id='my-orders-table']//*[text()='{{orderId}}']/..//a[contains(@class, 'action view')]" parameterized="true"/> + <element name="orderStatus" type="text" selector="//*[@id='my-orders-table']//*[text()='{{orderId}}']/..//td[contains(@class,'status')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml new file mode 100644 index 0000000000000..7482193031091 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSidebarSection"> + <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{var1}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 0000000000000..d5eed446750bf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSignInFormSection"> + <element name="emailField" type="input" selector="#email"/> + <element name="passwordField" type="input" selector="#pass"/> + <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> + <element name="customerLoginBlock" type="text" selector=".login-container .block.block-customer-login"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml new file mode 100644 index 0000000000000..d6b586e42f28c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerStoredPaymentMethodsSection"> + <element name="cardNumber" type="text" selector="td.card-number"/> + <element name="expirationDate" type="text" selector="td.card-expire"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml new file mode 100644 index 0000000000000..575eabe68bd5c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontPanelHeaderSection"> + <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)"/> + <element name="customerWelcome" type="text" selector=".panel.header .customer-welcome"/> + <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-menu"/> + <element name="customerLogoutLink" type="text" selector=".panel.header .customer-welcome .customer-menu .authorization-link a" timeout="30"/> + <element name="welcomeMessage" type="text" selector=".greet.welcome span"/> + <element name="notYouLink" type="button" selector=".greet.welcome span a"/> + <element name="welcomeMessageFullText" type="button" selector=".greet.welcome"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.xml new file mode 100644 index 0000000000000..7951c91aab61a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AddingProductWithExpiredSessionTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AddingProductWithExpiredSessionTest"> + <annotations> + <title value="Adding a product to cart from category page with an expired session"/> + <features value="Module/ Catalog"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94337"/> + <stories value="MAGETWO-73443: Adding a product to cart from category page with an expired session does not allow product to be added"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <!--Delete created product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!--Navigate to a category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!-- Remove PHPSESSID and form_key to replicate an expired session--> + <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + <resetCookie userInput="form_key" stepKey="resetCookieForCart2"/> + <!-- "Add to Cart" created product--> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName($$createSimpleProduct.name$$)}}" stepKey="moveMouseOverProduct" /> + <click selector="{{StorefrontCategoryProductSection.productAddToCartByName($$createSimpleProduct.name$$)}}" stepKey="clickAddToCart" /> + <waitForPageLoad stepKey="waitForPageLoad1"/> + + <see stepKey="assertErrorMessage" userInput="Your session has expired"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml new file mode 100644 index 0000000000000..c354d0647da64 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCustomerTest"> + <annotations> + <features value="Customer Creation"/> + <stories value="Create a Customer via the Admin"/> + <title value="You should be able to create a customer in the Admin back-end."/> + <description value="You should be able to create a customer in the Admin back-end."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-72095"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + </before> + <after> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCustomerGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToCreateCustomer"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminNewCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminNewCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminNewCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminNewCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner1"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCustomerGridFilter"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner2"/> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml new file mode 100644 index 0000000000000..757504fe25627 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExactMatchSearchInCustomerGridTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Search"/> + <title value="Admin customer grid exact match searching"/> + <description value="Admin customer grid exact match searching with quotes in keyword"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17026"/> + <useCaseId value="MAGETWO-99569"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createSecondCustomer"> + <field key="firstname">"Jane Doe"</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCustomerGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Step 1: Go to Customers > All Customers--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <!--Step 2: On Customers grid page search customer by keyword with quotes--> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchCustomer"> + <argument name="keyword" value="$$createSecondCustomer.firstname$$"/> + </actionGroup> + <!--Step 3: Check if customer is placed in a first row and clear grid filter--> + <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="checkCustomerInGrid"> + <argument name="text" value="$$createSecondCustomer.fullname$$"/> + <argument name="row" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml new file mode 100644 index 0000000000000..3b45dc286f410 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AllowedCountriesRestrictionApplyOnBackendTest"> + <annotations> + <title value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <description value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <features value="Customer"/> + <stories value="Admin Customer Grid"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17282"/> + <useCaseId value="MAGETWO-86624"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="CustomerAccountSharingDefault" stepKey="setCustomerAccountSharingToDefault"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="loginToAdmin"/> + + <!--Create new website,store and store view--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> + <argument name="newWebsiteName" value="{{CustomWebSite.name}}"/> + <argument name="websiteCode" value="{{CustomWebSite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="adminCreateNewStore"> + <argument name="website" value="{{CustomWebSite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="adminCreateNewStoreView"> + <argument name="storeGroup" value="customStore"/> + </actionGroup> + </before> + <after> + <!--Delete all created data and set main website country options to default--> + <comment userInput="Delete all created data and set main website country options to default" stepKey="resetConfigToDefault"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{CustomWebSite.name}}"/> + </actionGroup> + <amOnPage url="{{AdminConfigurationGeneralSectionPage.url('#general_country-link')}}" stepKey="goToConfigurationSectionPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> + <argument name="scopeName" value="_defaultWebsite.name"/> + </actionGroup> + <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Check that all countries are allowed initially and get amount--> + <amOnPage url="{{AdminConfigurationGeneralSectionPage.url('#general_country-link')}}" stepKey="goToCountryConfigurationSectionPage"/> + <executeJS function="return document.querySelectorAll('{{AdminConfigurationGeneralCountryOptionsSection.allowedCountriesOptions}}').length" stepKey="countriesAmount"/> + + <!-- Switch to first website, allow only Canada and set Canada as default country --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsite"> + <argument name="scopeName" value="_defaultWebsite.name"/> + </actionGroup> + <actionGroup ref="AllowOnlyOneCountryActionGroup" stepKey="allowCanadaCountry"> + <argument name="country" value="Canada"/> + </actionGroup> + + <!--Switch to second website and allow all countries except Canada--> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchToSecondWebsite"> + <argument name="scopeName" value="CustomWebSite.name"/> + </actionGroup> + <actionGroup ref="AllowAllCountriesExceptOneActionGroup" stepKey="allowAllCountriesExceptCanada"> + <argument name="country" value="Canada"/> + </actionGroup> + + <!--Open created customer details page and add Canada address--> + <amOnPage url="{{AdminEditCustomerPage.url($$createCustomer.id$$)}}" stepKey="goToCustomerEditPage"/> + <actionGroup ref="AdminAddCustomerAddressWithRegionTypeSelectActionGroup" stepKey="addAddressForCustomer"> + <argument name="customerAddress" value="Canada_Address"/> + </actionGroup> + + <!--Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGrid"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomersGrid"/> + <seeNumberOfElements userInput="$countriesAmount" selector="{{AdminCustomerFiltersSection.countryOptionsWithoutEmptyOption}}" stepKey="assertCountryAmounts"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml new file mode 100644 index 0000000000000..bc10969944c60 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SearchByEmailInCustomerGridTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer grid search"/> + <title value="Admin customer grid email searching"/> + <description value="Admin customer grid searching by email in keyword"/> + <severity value="MAJOR"/> + <testCaseId value="MC-18137"/> + <useCaseId value="MC-17940"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createSecondCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCustomerGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Step 1: Go to Customers > All Customers--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <!--Step 2: On Customers grid page search customer by keyword--> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchCustomer"> + <argument name="keyword" value="$$createSecondCustomer.email$$"/> + </actionGroup> + <!--Step 3: Check if customer is placed in a first row and clear grid filter--> + <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="checkCustomerInGrid"> + <argument name="text" value="$$createSecondCustomer.email$$"/> + <argument name="row" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml new file mode 100644 index 0000000000000..faf1815eb4984 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontCreateCustomerTest"> + <annotations> + <features value="Customer Creation"/> + <stories value="Create a Customer via the Storefront"/> + <title value="You should be able to create a customer via the storefront"/> + <description value="You should be able to create a customer via the storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23546"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <amOnPage stepKey="amOnStorefrontPage" url="/"/> + <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> + <fillField stepKey="fillFirstName" userInput="{{CustomerEntityOne.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> + <fillField stepKey="fillLastName" userInput="{{CustomerEntityOne.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> + <fillField stepKey="fillEmail" userInput="{{CustomerEntityOne.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{CustomerEntityOne.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> + <fillField stepKey="fillConfirmPassword" userInput="{{CustomerEntityOne.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}"/> + <see stepKey="seeThankYouMessage" userInput="Thank you for registering with Main Website Store."/> + <see stepKey="seeFirstName" userInput="{{CustomerEntityOne.firstname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeLastName" userInput="{{CustomerEntityOne.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeEmail" userInput="{{CustomerEntityOne.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml new file mode 100644 index 0000000000000..0859a0439660e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontPersistedCustomerLoginTest"> + <annotations> + + <features value="Persisted customer can login from storefront."/> + <stories value="Persisted customer can login from storefront."/> + <title value="Persisted customer can login from storefront."/> + <description value="Persisted customer can login from storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-72103"/> + <group value="customer"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="$$customer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <see stepKey="seeFirstName" userInput="$$customer.firstname$$" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeLastName" userInput="$$customer.lastname$$" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml new file mode 100644 index 0000000000000..485ab61c5f171 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectCustomer"> + <annotations> + <features value="Customer"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Customer Pages"/> + <description value="Verify that the Secure URL configuration applies to the Customer pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15697"/> + <group value="customer"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/customer" stepKey="goToUnsecureCustomerURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/customer" stepKey="seeSecureCustomerURL"/> + <amOnUrl url="http://{$hostname}/customer/section/load" stepKey="goToUnsecureCustomerSectionLoadURL"/> + <seeCurrentUrlEquals url="http://{$hostname}/customer/section/load" stepKey="seeUnsecureCustomerSectionLoadURL"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Account/AuthenticationPopupTest.php b/app/code/Magento/Customer/Test/Unit/Block/Account/AuthenticationPopupTest.php index b43b1d1aa39a9..618173e886e66 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Account/AuthenticationPopupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Account/AuthenticationPopupTest.php @@ -128,6 +128,9 @@ public function testGetConfig($isAutocomplete, $baseUrl, $registerUrl, $forgotUr $this->assertEquals($result, $this->model->getConfig()); } + /** + * @return array + */ public function dataProviderGetConfig() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Block/Account/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Block/Account/CustomerTest.php index 6489fea91e43e..793975c0b3191 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Account/CustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Account/CustomerTest.php @@ -22,6 +22,9 @@ protected function setUp() ->getObject(\Magento\Customer\Block\Account\Customer::class, ['httpContext' => $this->httpContext]); } + /** + * @return array + */ public function customerLoggedInDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Block/Account/Dashboard/InfoTest.php b/app/code/Magento/Customer/Test/Unit/Block/Account/Dashboard/InfoTest.php index 02d31d69e73df..346218e73bfb3 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Account/Dashboard/InfoTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Account/Dashboard/InfoTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Account\Dashboard; use Magento\Framework\Exception\NoSuchEntityException; @@ -64,15 +62,10 @@ protected function setUp() $layout = $this->getMockForAbstractClass(\Magento\Framework\View\LayoutInterface::class, [], '', false); $this->_formRegister = $this->createMock(\Magento\Customer\Block\Form\Register::class); - $layout->expects( - $this->any() - )->method( - 'getBlockSingleton' - )->with( - \Magento\Customer\Block\Form\Register::class - )->will( - $this->returnValue($this->_formRegister) - ); + $layout->expects($this->any()) + ->method('getBlockSingleton') + ->with(\Magento\Customer\Block\Form\Register::class) + ->willReturn($this->_formRegister); $this->_context = $this->getMockBuilder(\Magento\Framework\View\Element\Template\Context::class) ->disableOriginalConstructor()->getMock(); @@ -87,16 +80,15 @@ protected function setUp() $this->_helperView = $this->getMockBuilder( \Magento\Customer\Helper\View::class )->disableOriginalConstructor()->getMock(); - $this->_subscriberFactory = $this->createPartialMock(\Magento\Newsletter\Model\SubscriberFactory::class, ['create']); + $this->_subscriberFactory = $this->createPartialMock( + \Magento\Newsletter\Model\SubscriberFactory::class, + ['create'] + ); $this->_subscriber = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); $this->_subscriber->expects($this->any())->method('loadByEmail')->will($this->returnSelf()); - $this->_subscriberFactory->expects( - $this->any() - )->method( - 'create' - )->will( - $this->returnValue($this->_subscriber) - ); + $this->_subscriberFactory->expects($this->any()) + ->method('create') + ->willReturn($this->_subscriber); $this->_block = new \Magento\Customer\Block\Account\Dashboard\Info( $this->_context, @@ -108,13 +100,9 @@ protected function setUp() public function testGetCustomer() { - $this->currentCustomer->expects( - $this->once() - )->method( - 'getCustomer' - )->will( - $this->returnValue($this->_customer) - ); + $this->currentCustomer->expects($this->once()) + ->method('getCustomer') + ->willReturn($this->_customer); $customer = $this->_block->getCustomer(); $this->assertEquals($customer, $this->_customer); @@ -140,13 +128,9 @@ public function testGetName() { $expectedValue = 'John Q Doe Jr'; - $this->currentCustomer->expects( - $this->once() - )->method( - 'getCustomer' - )->will( - $this->returnValue($this->_customer) - ); + $this->currentCustomer->expects($this->once()) + ->method('getCustomer') + ->willReturn($this->_customer); /** * Called three times, once for each attribute (i.e. prefix, middlename, and suffix) @@ -194,16 +178,15 @@ public function getIsSubscribedProvider() */ public function testIsNewsletterEnabled($isNewsletterEnabled, $expectedValue) { - $this->_formRegister->expects( - $this->once() - )->method( - 'isNewsletterEnabled' - )->will( - $this->returnValue($isNewsletterEnabled) - ); + $this->_formRegister->expects($this->once()) + ->method('isNewsletterEnabled') + ->willReturn($isNewsletterEnabled); $this->assertEquals($expectedValue, $this->_block->isNewsletterEnabled()); } + /** + * @return array + */ public function isNewsletterEnabledProvider() { return [[true, true], [false, false]]; diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php index 9973137511a42..1c252bfc75a53 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Adminhtml\Edit\Tab; use Magento\Backend\Model\Session; @@ -61,7 +59,10 @@ public function setUp() $this->contextMock = $this->createMock(\Magento\Backend\Block\Template\Context::class); $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $this->formFactoryMock = $this->createMock(\Magento\Framework\Data\FormFactory::class); - $this->subscriberFactoryMock = $this->createPartialMock(\Magento\Newsletter\Model\SubscriberFactory::class, ['create']); + $this->subscriberFactoryMock = $this->createPartialMock( + \Magento\Newsletter\Model\SubscriberFactory::class, + ['create'] + ); $this->accountManagementMock = $this->createMock(\Magento\Customer\Api\AccountManagementInterface::class); $this->urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); $this->backendSessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session::class) @@ -94,7 +95,10 @@ public function testInitForm() $subscriberMock = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); $fieldsetMock = $this->createMock(\Magento\Framework\Data\Form\Element\Fieldset::class); $elementMock = $this->createPartialMock(\Magento\Framework\Data\Form\Element\Checkbox::class, ['setIsChecked']); - $formMock = $this->createPartialMock(\Magento\Framework\Data\Form::class, ['setHtmlIdPrefix', 'addFieldset', 'setValues', 'getElement', 'setForm', 'setParent', 'setBaseUrl']); + $formMock = $this->createPartialMock( + \Magento\Framework\Data\Form::class, + ['setHtmlIdPrefix', 'addFieldset', 'setValues', 'getElement', 'setForm', 'setParent', 'setBaseUrl'] + ); $this->registryMock->expects($this->exactly(3)) ->method('registry') ->willReturnMap( @@ -139,7 +143,10 @@ public function testInitFormWithCustomerFormData() $subscriberMock = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); $fieldsetMock = $this->createMock(\Magento\Framework\Data\Form\Element\Fieldset::class); $elementMock = $this->createPartialMock(\Magento\Framework\Data\Form\Element\Checkbox::class, ['setIsChecked']); - $formMock = $this->createPartialMock(\Magento\Framework\Data\Form::class, ['setHtmlIdPrefix', 'addFieldset', 'setValues', 'getElement', 'setForm', 'setParent', 'setBaseUrl']); + $formMock = $this->createPartialMock( + \Magento\Framework\Data\Form::class, + ['setHtmlIdPrefix', 'addFieldset', 'setValues', 'getElement', 'setForm', 'setParent', 'setBaseUrl'] + ); $this->registryMock->expects($this->exactly(3)) ->method('registry') ->willReturnMap( diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php index c12da66cdc616..92c2bcfeb8e59 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php @@ -14,6 +14,10 @@ class ItemTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item */ protected $itemBlock; + /** + * @param $amountOption + * @param bool $withoutOptions + */ public function configure($amountOption, $withoutOptions = false) { $options = []; @@ -95,6 +99,9 @@ public function testRender($amountOption, $expectedHtml) $this->assertXmlStringEqualsXmlString($expectedHtml, $realHtml); } + /** + * @return array + */ public function optionHtmlProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 74b2c119784e6..d09243f64b575 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Widget; use Magento\Framework\Exception\NoSuchEntityException; @@ -350,13 +348,9 @@ public function testGetSortedDateInputsWithoutStrippingNonInputChars() */ public function testGetMinDateRange($validationRules, $expectedValue) { - $this->attribute->expects( - $this->once() - )->method( - 'getValidationRules' - )->will( - $this->returnValue($validationRules) - ); + $this->attribute->expects($this->once()) + ->method('getValidationRules') + ->willReturn($validationRules); $this->assertEquals($expectedValue, $this->_block->getMinDateRange()); } @@ -420,13 +414,9 @@ public function testGetMinDateRangeWithException() */ public function testGetMaxDateRange($validationRules, $expectedValue) { - $this->attribute->expects( - $this->once() - )->method( - 'getValidationRules' - )->will( - $this->returnValue($validationRules) - ); + $this->attribute->expects($this->once()) + ->method('getValidationRules') + ->willReturn($validationRules); $this->assertEquals($expectedValue, $this->_block->getMaxDateRange()); } @@ -509,7 +499,6 @@ public function testGetHtmlExtraParamsWithRequiredOption() ->with('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}') ->will($this->returnValue('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}')); - $this->context->expects($this->any())->method('getEscaper')->will($this->returnValue($this->escaper)); $this->assertEquals( diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/GenderTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/GenderTest.php index 5eaf54810addb..10927fc2093d8 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/GenderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/GenderTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Widget; use Magento\Customer\Block\Widget\Gender; diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/NameTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/NameTest.php index 3f174484df3e1..21c4a8e53c3b8 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/NameTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/NameTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Widget; use Magento\Customer\Api\Data\AttributeMetadataInterface; @@ -159,6 +157,9 @@ public function testMethodWithNoSuchEntityException($method) $this->assertFalse($this->_block->{$method}()); } + /** + * @return array + */ public function methodDataProvider() { return [ @@ -210,9 +211,8 @@ public function testGetPrefixOptionsNotEmpty() * Added some padding so that the trim() call on Customer::getPrefix() will remove it. Also added * special characters so that the escapeHtml() method returns a htmlspecialchars translated value. */ - $customer = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class)->getMockForAbstractClass( - ); + $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->getMockForAbstractClass(); $customer->expects($this->once())->method('getPrefix')->willReturn(' <' . self::PREFIX . '> '); $this->_block->setObject($customer); @@ -237,18 +237,13 @@ public function testGetPrefixOptionsNotEmpty() public function testGetPrefixOptionsEmpty() { - $customer = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class)->getMockForAbstractClass( - ); + $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->getMockForAbstractClass(); $this->_block->setObject($customer); - $this->_options->expects( - $this->once() - )->method( - 'getNamePrefixOptions' - )->will( - $this->returnValue([]) - ); + $this->_options->expects($this->once()) + ->method('getNamePrefixOptions') + ->willReturn([]); $this->assertEmpty($this->_block->getPrefixOptions()); } @@ -259,9 +254,8 @@ public function testGetSuffixOptionsNotEmpty() * Added padding and special characters to show that trim() works on Customer::getSuffix() and that * a properly htmlspecialchars translated value is returned. */ - $customer = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class)->getMockForAbstractClass( - ); + $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->getMockForAbstractClass(); $customer->expects($this->once())->method('getSuffix')->willReturn(' <' . self::SUFFIX . '> '); $this->_block->setObject($customer); @@ -271,13 +265,9 @@ public function testGetSuffixOptionsNotEmpty() $expectedOptions = $suffixOptions; $expectedOptions[$suffix] = $suffix; - $this->_options->expects( - $this->once() - )->method( - 'getNameSuffixOptions' - )->will( - $this->returnValue($suffixOptions) - ); + $this->_options->expects($this->once()) + ->method('getNameSuffixOptions') + ->willReturn($suffixOptions); $this->_escaper->expects($this->once())->method('escapeHtml')->will($this->returnValue($suffix)); $this->assertSame($expectedOptions, $this->_block->getSuffixOptions()); @@ -285,18 +275,13 @@ public function testGetSuffixOptionsNotEmpty() public function testGetSuffixOptionsEmpty() { - $customer = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class)->getMockForAbstractClass( - ); + $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->getMockForAbstractClass(); $this->_block->setObject($customer); - $this->_options->expects( - $this->once() - )->method( - 'getNameSuffixOptions' - )->will( - $this->returnValue([]) - ); + $this->_options->expects($this->once()) + ->method('getNameSuffixOptions') + ->willReturn([]); $this->assertEmpty($this->_block->getSuffixOptions()); } @@ -453,7 +438,7 @@ private function _setUpIsAttributeRequired() */ $this->_block->setForceUseCustomerAttributes(false); $this->_block->setForceUseCustomerRequiredAttributes(true); - $this->_block->setObject(new \StdClass()); + $this->_block->setObject(new \stdClass()); /** * The first call to isRequired() is false so that the second if conditional in the other code path diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/TaxvatTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/TaxvatTest.php index e132d10ae8667..afe3d1d87dbc8 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/TaxvatTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/TaxvatTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Block\Widget; use Magento\Framework\Exception\NoSuchEntityException; diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index 991faf55a68f0..01fc465d4ae84 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Controller\Account; use Magento\Customer\Controller\Account\Confirm; @@ -103,7 +101,10 @@ protected function setUp() { $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); - $this->responseMock = $this->createPartialMock(\Magento\Framework\App\Response\Http::class, ['setRedirect', '__wakeup']); + $this->responseMock = $this->createPartialMock( + \Magento\Framework\App\Response\Http::class, + ['setRedirect', '__wakeup'] + ); $viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmationTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmationTest.php new file mode 100644 index 0000000000000..113f8c104a4ea --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmationTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Unit\Controller\Account; + +use Magento\Customer\Controller\Account\Confirmation; +use Magento\Framework\App\Request\Http; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ConfirmationTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Confirmation + */ + private $model; + + /** + * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var \Magento\Framework\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultPageFactoryMock; + + /** + * @var \Magento\Customer\Model\Url|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerUrlMock; + + /** + * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + public function setUp() + { + $this->customerSessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->disableOriginalConstructor() + ->setMethods(['isLoggedIn']) + ->getMock(); + $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) + ->disableOriginalConstructor() + ->setMethods(['getRequest']) + ->getMock(); + $this->requestMock = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->setMethods(['getPost', 'getParam']) + ->getMock(); + $this->contextMock->expects($this->any()) + ->method('getRequest') + ->willReturn($this->requestMock); + + $this->resultPageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->customerUrlMock = $this->getMockBuilder(\Magento\Customer\Model\Url::class) + ->disableOriginalConstructor() + ->setMethods(['getLoginUrl']) + ->getMock(); + $this->model = (new ObjectManagerHelper($this))->getObject( + Confirmation::class, + [ + 'context' => $this->contextMock, + 'customerSession' => $this->customerSessionMock, + 'resultPageFactory' => $this->resultPageFactoryMock, + 'customerUrl' => $this->customerUrlMock, + ] + ); + } + + public function testGetLoginUrl() + { + $this->customerSessionMock->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->requestMock->expects($this->once())->method('getPost')->with('email')->willReturn(null); + + $resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) + ->disableOriginalConstructor() + ->setMethods(['getLayout']) + ->getMock(); + + $this->resultPageFactoryMock->expects($this->once())->method('create')->willReturn($resultPageMock); + + $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + ->disableOriginalConstructor() + ->setMethods(['getBlock']) + ->getMock(); + + $resultPageMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); + + $blockMock = $this->getMockBuilder(\Magento\Framework\View\Element\Template::class) + ->disableOriginalConstructor() + ->setMethods(['setEmail', 'setLoginUrl']) + ->getMock(); + + $layoutMock->expects($this->once())->method('getBlock')->with('accountConfirmation')->willReturn($blockMock); + + $blockMock->expects($this->once())->method('setEmail')->willReturnSelf(); + $blockMock->expects($this->once())->method('setLoginUrl')->willReturnSelf(); + + $this->model->execute(); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php deleted file mode 100644 index 77f41024ba02f..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php +++ /dev/null @@ -1,233 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Customer\Test\Unit\Controller\Account; - -use Magento\Framework\Controller\Result\Redirect; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class CreatePasswordTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Customer\Controller\Account\CreatePassword */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $sessionMock; - - /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageFactoryMock; - - /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $accountManagementMock; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectFactoryMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - protected function setUp() - { - $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['setRpToken', 'setRpCustomerId', 'getRpToken', 'getRpCustomerId']) - ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) - ->getMockForAbstractClass(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->getMockForAbstractClass(); - $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\Customer\Controller\Account\CreatePassword::class, - [ - 'customerSession' => $this->sessionMock, - 'resultPageFactory' => $this->pageFactoryMock, - 'accountManagement' => $this->accountManagementMock, - 'request' => $this->requestMock, - 'resultRedirectFactory' => $this->redirectFactoryMock, - 'messageManager' => $this->messageManagerMock, - ] - ); - } - - public function testExecuteWithLink() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, $token], - ['id', null, $customerId], - ] - ); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willReturn(true); - - $this->sessionMock->expects($this->once()) - ->method('setRpToken') - ->with($token); - $this->sessionMock->expects($this->once()) - ->method('setRpCustomerId') - ->with($customerId); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createpassword', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithSession() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, null], - ['id', null, $customerId], - ] - ); - - $this->sessionMock->expects($this->once()) - ->method('getRpToken') - ->willReturn($token); - $this->sessionMock->expects($this->once()) - ->method('getRpCustomerId') - ->willReturn($customerId); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willReturn(true); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->pageFactoryMock->expects($this->once()) - ->method('create') - ->with(false, []) - ->willReturn($pageMock); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - /** @var \Magento\Customer\Block\Account\Resetpassword|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $blockMock = $this->getMockBuilder(\Magento\Customer\Block\Account\Resetpassword::class) - ->disableOriginalConstructor() - ->setMethods(['setCustomerId', 'setResetPasswordLinkToken']) - ->getMock(); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('resetPassword') - ->willReturn($blockMock); - - $blockMock->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $blockMock->expects($this->once()) - ->method('setResetPasswordLinkToken') - ->with($token) - ->willReturnSelf(); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithException() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, $token], - ['id', null, null], - ] - ); - - $this->sessionMock->expects($this->once()) - ->method('getRpToken') - ->willReturn($token); - $this->sessionMock->expects($this->once()) - ->method('getRpCustomerId') - ->willReturn($customerId); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willThrowException(new \Exception('Exception.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Your password reset link has expired.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/forgotpassword', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php index 759d5f661c509..f6182ee693069 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePostTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Controller\Account; use Magento\Customer\Api\AccountManagementInterface; @@ -154,7 +151,9 @@ protected function setUp() $this->customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); $this->customerDetailsMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerDetailsFactoryMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); + $this->customerDetailsFactoryMock = $this->createMock( + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class + ); $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -168,7 +167,10 @@ protected function setUp() $formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); $this->subscriberMock = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); - $subscriberFactoryMock = $this->createPartialMock(\Magento\Newsletter\Model\SubscriberFactory::class, ['create']); + $subscriberFactoryMock = $this->createPartialMock( + \Magento\Newsletter\Model\SubscriberFactory::class, + ['create'] + ); $subscriberFactoryMock->expects($this->any()) ->method('create') ->will($this->returnValue($this->subscriberMock)); @@ -183,8 +185,8 @@ protected function setUp() $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->resultRedirectFactoryMock = $this->getMockBuilder( - \Magento\Framework\Controller\Result\RedirectFactory::class) + $this->resultRedirectFactoryMock = + $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); @@ -529,6 +531,9 @@ public function testSuccessRedirect( $this->model->execute(); } + /** + * @return array + */ public function getSuccessRedirectDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreateTest.php index 4fa63fcc2e624..7398f2ee7964d 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreateTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Controller\Account; class CreateTest extends \PHPUnit\Framework\TestCase @@ -67,7 +64,10 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $this->redirectResultMock = $this->createMock(\Magento\Framework\Controller\Result\Redirect::class); - $this->redirectFactoryMock = $this->createPartialMock(\Magento\Framework\Controller\Result\RedirectFactory::class, ['create']); + $this->redirectFactoryMock = $this->createPartialMock( + \Magento\Framework\Controller\Result\RedirectFactory::class, + ['create'] + ); $this->resultPageMock = $this->createMock(\Magento\Framework\View\Result\Page::class); $this->pageFactoryMock = $this->createMock(\Magento\Framework\View\Result\PageFactory::class); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php index f2860725dbbae..0e8cffc0bf434 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/EditPostTest.php @@ -19,6 +19,8 @@ use Magento\Framework\Message\ManagerInterface; /** + * Unit tests for Magento\Customer\Controller\Account\EditPost. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EditPostTest extends \PHPUnit\Framework\TestCase @@ -98,6 +100,9 @@ class EditPostTest extends \PHPUnit\Framework\TestCase */ private $customerMapperMock; + /** + * @inheritdoc + */ protected function setUp() { $this->prepareContext(); @@ -707,6 +712,8 @@ protected function prepareContext() } /** + * Executes methods needed for new Customer. + * * @param int $customerId * @param \PHPUnit_Framework_MockObject_MockObject $address * @return \PHPUnit_Framework_MockObject_MockObject @@ -720,9 +727,9 @@ protected function getNewCustomerMock($customerId, $address) ->method('setId') ->with($customerId) ->willReturnSelf(); - $newCustomerMock->expects($this->once()) + $newCustomerMock->expects($this->atLeastOnce()) ->method('getAddresses') - ->willReturn(null); + ->willReturn([]); $newCustomerMock->expects($this->once()) ->method('setAddresses') ->with([$address]) @@ -732,6 +739,8 @@ protected function getNewCustomerMock($customerId, $address) } /** + * Executes methods needed for existing Customer. + * * @param int $customerId * @param \PHPUnit_Framework_MockObject_MockObject $address * @return \PHPUnit_Framework_MockObject_MockObject @@ -741,7 +750,7 @@ protected function getCurrentCustomerMock($customerId, $address) $currentCustomerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMockForAbstractClass(); - $currentCustomerMock->expects($this->once()) + $currentCustomerMock->expects($this->atLeastOnce()) ->method('getAddresses') ->willReturn([$address]); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php index ec88784892e6c..9f28e553ee1e5 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php @@ -13,6 +13,7 @@ use Magento\Framework\App\Request\Http as Request; use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\Result\RedirectFactory as ResultRedirectFactory; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; @@ -67,6 +68,11 @@ class ForgotPasswordPostTest extends \PHPUnit\Framework\TestCase */ protected $messageManager; + /** + * @var Validator|\PHPUnit_Framework_MockObject_MockObject + */ + private $formKeyValidatorMock; + protected function setUp() { $this->prepareContext(); @@ -81,12 +87,20 @@ protected function setUp() $this->escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->disableOriginalConstructor() ->getMock(); + $this->formKeyValidatorMock = $this->createMock(Validator::class); + + $this->request->expects($this->once())->method('isPost')->willReturn(true); + $this->formKeyValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->request) + ->willReturn(true); $this->controller = new ForgotPasswordPost( $this->context, $this->session, $this->accountManagement, - $this->escaper + $this->escaper, + $this->formKeyValidatorMock ); } @@ -228,9 +242,7 @@ protected function prepareContext() $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() - ->setMethods([ - 'getPost', - ]) + ->setMethods(['getPost', 'isPost']) ->getMock(); $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php index 8e07f41cff5a7..9da7e8a656fc1 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Test\Unit\Controller\Account; use Magento\Customer\Api\AccountManagementInterface; @@ -291,9 +292,8 @@ public function testExecuteSuccessCustomRedirect() ->method('setCustomerDataAsLoggedIn') ->with($customerMock) ->willReturnSelf(); - $this->session->expects($this->once()) - ->method('regenerateId') - ->willReturnSelf(); + $this->session->expects($this->never()) + ->method('regenerateId'); $this->accountRedirect->expects($this->never()) ->method('getRedirect') @@ -356,9 +356,8 @@ public function testExecuteSuccess() ->method('setCustomerDataAsLoggedIn') ->with($customerMock) ->willReturnSelf(); - $this->session->expects($this->once()) - ->method('regenerateId') - ->willReturnSelf(); + $this->session->expects($this->never()) + ->method('regenerateId'); $this->accountRedirect->expects($this->once()) ->method('getRedirect') diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php deleted file mode 100644 index b79ad008e5e44..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php +++ /dev/null @@ -1,379 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Customer\Test\Unit\Controller\Account; - -use Magento\Framework\Controller\Result\Redirect; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ResetPasswordPostTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Customer\Controller\Account\ResetPasswordPost */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $sessionMock; - - /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageFactoryMock; - - /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $accountManagementMock; - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRepositoryMock; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectFactoryMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - protected function setUp() - { - $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['unsRpToken', 'unsRpCustomerId']) - ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) - ->getMockForAbstractClass(); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getQuery', 'getPost']) - ->getMockForAbstractClass(); - $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\Customer\Controller\Account\ResetPasswordPost::class, - [ - 'customerSession' => $this->sessionMock, - 'resultPageFactory' => $this->pageFactoryMock, - 'accountManagement' => $this->accountManagementMock, - 'customerRepository' => $this->customerRepositoryMock, - 'request' => $this->requestMock, - 'resultRedirectFactory' => $this->redirectFactoryMock, - 'messageManager' => $this->messageManagerMock, - ] - ); - } - - public function testExecute() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willReturn(true); - - $this->sessionMock->expects($this->once()) - ->method('unsRpToken'); - $this->sessionMock->expects($this->once()) - ->method('unsRpCustomerId'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('You updated your password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/login', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithException() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willThrowException(new \Exception('Exception.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Something went wrong while saving the new password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * Test for InputException - */ - public function testExecuteWithInputException() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willThrowException(new \Magento\Framework\Exception\InputException(__('InputException.'))); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('InputException.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithWrongConfirmation() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'wrong_password'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('New Password and Confirm New Password values didn\'t match.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithEmptyPassword() - { - $token = 'token'; - $customerId = '11'; - $password = ''; - $passwordConfirmation = ''; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Please enter a new password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php index 4064b8586257d..f28053a6611fc 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php @@ -78,7 +78,9 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) ->getMockForAbstractClass(); $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) @@ -146,7 +148,7 @@ public function testExecute() ->method('deleteById') ->with($addressId); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You deleted the address.')); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -183,11 +185,11 @@ public function testExecuteWithException() ->willReturn(34); $exception = new \Exception('Exception'); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t delete the address right now.')) ->willThrowException($exception); $this->messageManager->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, __('We can\'t delete the address right now.')); $this->resultRedirect->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php index 2b5438991b113..a2766d42403ba 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php @@ -549,7 +549,7 @@ public function testExecute( ->willReturnSelf(); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the address.')) ->willReturnSelf(); @@ -578,6 +578,9 @@ public function testExecute( $this->assertEquals($this->resultRedirect, $this->model->execute()); } + /** + * @return array + */ public function dataProviderTestExecute() { return [ @@ -637,7 +640,7 @@ public function testExecuteInputException() ->willThrowException(new InputException(__('InputException'))); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('InputException') ->willReturnSelf(); @@ -700,7 +703,7 @@ public function testExecuteException() ->willThrowException($exception); $this->messageManager->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, __('We can\'t save the address.')) ->willReturnSelf(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php index 5f7064d5b124b..55967854f97ca 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php @@ -88,7 +88,9 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php index 22c5003544bed..4a76f33d82084 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php @@ -5,9 +5,15 @@ */ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Escaper; /** + * Unit tests for Inline customer edit. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -67,14 +73,28 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase /** @var EmailNotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $emailNotification; + /** @var AddressRegistry|\PHPUnit_Framework_MockObject_MockObject */ + private $addressRegistry; + /** @var array */ private $items; + /** @var \Magento\Framework\Escaper */ + private $escaper; + + /** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class, [], '', false); + $this->escaper = new Escaper(); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->messageManager = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, [], @@ -124,8 +144,12 @@ protected function setUp() '', false ); - $this->logger = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class, [], '', false); - + $this->logger = $this->getMockForAbstractClass( + \Psr\Log\LoggerInterface::class, + [], + '', + false + ); $this->emailNotification = $this->getMockBuilder(EmailNotificationInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -137,6 +161,7 @@ protected function setUp() 'messageManager' => $this->messageManager, ] ); + $this->addressRegistry = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); $this->controller = $objectManager->getObject( \Magento\Customer\Controller\Adminhtml\Index\InlineEdit::class, [ @@ -149,6 +174,8 @@ protected function setUp() 'addressDataFactory' => $this->addressDataFactory, 'addressRepository' => $this->addressRepository, 'logger' => $this->logger, + 'addressRegistry' => $this->addressRegistry, + 'escaper' => $this->escaper, ] ); $reflection = new \ReflectionClass(get_class($this->controller)); @@ -164,6 +191,9 @@ protected function setUp() ]; } + /** + * @param int $populateSequence + */ protected function prepareMocksForTesting($populateSequence = 0) { $this->resultJsonFactory->expects($this->once()) @@ -200,6 +230,11 @@ protected function prepareMocksForTesting($populateSequence = 0) ->willReturn(12); } + /** + * Prepare mocks for update customers default billing address use case. + * + * @return void + */ protected function prepareMocksForUpdateDefaultBilling() { $this->prepareMocksForProcessAddressData(); @@ -208,12 +243,15 @@ protected function prepareMocksForUpdateDefaultBilling() 'firstname' => 'Firstname', 'lastname' => 'Lastname', ]; - $this->customerData->expects($this->once()) + $this->customerData->expects($this->exactly(2)) ->method('getAddresses') ->willReturn([$this->address]); $this->address->expects($this->once()) ->method('isDefaultBilling') ->willReturn(true); + $this->addressRegistry->expects($this->once()) + ->method('retrieve') + ->willReturn(new DataObject()); $this->dataObjectHelper->expects($this->at(0)) ->method('populateWithArray') ->with( @@ -239,10 +277,11 @@ protected function prepareMocksForErrorMessagesProcessing() ->method('getMessages') ->willReturn($this->messageCollection); $this->messageCollection->expects($this->once()) - ->method('getItems') + ->method('getErrors') ->willReturn([$this->message]); $this->messageCollection->expects($this->once()) - ->method('getCount') + ->method('getCountByType') + ->with(MessageInterface::TYPE_ERROR) ->willReturn(1); $this->message->expects($this->once()) ->method('getText') @@ -300,6 +339,11 @@ public function testExecuteWithoutItems() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Localized Exception during inline edit. + * + * @return void + */ public function testExecuteLocalizedException() { $exception = new \Magento\Framework\Exception\LocalizedException(__('Exception message')); @@ -307,6 +351,9 @@ public function testExecuteLocalizedException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) @@ -322,6 +369,11 @@ public function testExecuteLocalizedException() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Execute Exception during inline edit. + * + * @return void + */ public function testExecuteException() { $exception = new \Exception('Exception message'); @@ -329,6 +381,9 @@ public function testExecuteException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php index 884aab711d168..01f26a8906cc7 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -7,10 +7,13 @@ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; use Magento\Framework\App\Action\Context; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; +use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** - * Class MassAssignGroupTest + * Unit tests for Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase @@ -18,118 +21,141 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup */ - protected $massAction; + private $massAction; /** * @var Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $contextMock; + private $contextMock; /** * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectMock; + private $resultRedirectMock; /** * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + private $requestMock; /** * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $responseMock; + private $responseMock; /** * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; /** * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** - * @var \Magento\Customer\Model\ResourceModel\Customer\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerCollectionMock; + private $customerCollectionMock; /** - * @var \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerCollectionFactoryMock; + private $customerCollectionFactoryMock; /** * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject */ - protected $filterMock; + private $filterMock; /** * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRepositoryMock; + private $customerRepositoryMock; + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestInterfaceMock; + + /** + * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $this->resultRedirectFactoryMock = $this->createMock( + \Magento\Backend\Model\View\Result\RedirectFactory::class + ); $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); + ->disableOriginalConstructor() + ->getMock(); $this->objectManagerMock = $this->createPartialMock( \Magento\Framework\ObjectManager\ObjectManager::class, ['create'] ); + $this->requestInterfaceMock = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + true, + true, + ['isPost'] + ); $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->customerCollectionMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $this->customerCollectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $this->customerCollectionFactoryMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) + $this->customerCollectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) + $this->redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) ->disableOriginalConstructor() ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - $this->resultRedirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) ->disableOriginalConstructor() ->getMock(); + $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); + $this->resultRedirectFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->resultRedirectMock); $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any()) + $this->contextMock->expects($this->once()) ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) + ->willReturn($this->resultRedirectFactoryMock); + $this->contextMock->expects($this->once()) ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->customerCollectionMock) - ->willReturnArgument(0); - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + ->willReturn($this->resultFactoryMock); + $this->customerRepositoryMock = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->massAction = $objectManagerHelper->getObject( \Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup::class, @@ -142,13 +168,43 @@ protected function setUp() ); } + /** + * Execute Create resultFactory and Create and Get customerCollectionFactory. + * + * @return void + */ + private function expectsCreateAndGetCollectionMethods() + { + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) + ->willReturn($this->redirectMock); + $this->customerCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->customerCollectionMock); + $this->filterMock->expects($this->once()) + ->method('getCollection') + ->with($this->customerCollectionMock) + ->willReturnArgument(0); + } + + /** + * Unit test to verify mass customer group assignment use case. + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecute() { + $customersIds = [10, 11, 12]; - $customerMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class - )->getMockForAbstractClass(); - $this->customerCollectionMock->expects($this->any()) + $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->expectsCreateAndGetCollectionMethods(); + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); + $this->customerCollectionMock->expects($this->once()) ->method('getAllIds') ->willReturn($customersIds); @@ -168,15 +224,22 @@ public function testExecute() $this->massAction->execute(); } + /** + * Unit test to verify expected error during mass customer group assignment use case. + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecuteWithException() { $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) + $this->expectsCreateAndGetCollectionMethods(); + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); + $this->customerCollectionMock->expects($this->once()) ->method('getAllIds') ->willReturn($customersIds); - $this->customerRepositoryMock->expects($this->any()) + $this->customerRepositoryMock->expects($this->once()) ->method('getById') ->willThrowException(new \Exception('Some message.')); @@ -186,4 +249,17 @@ public function testExecuteWithException() $this->massAction->execute(); } + + /** + * Check that error throws when request is not a POST. + * + * @return void + * @expectedException \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithNotPostRequest() + { + $this->requestMock->expects($this->once())->method('isPost')->willReturn(false); + + $this->massAction->execute(); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php deleted file mode 100644 index 190ff2c06618f..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassDeleteTest.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassDeleteTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassDeleteTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Customer\Controller\Adminhtml\Index\MassDelete - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRepositoryMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create'] - ); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->customerCollectionMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->customerCollectionFactoryMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $this->resultRedirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->customerCollectionMock) - ->willReturnArgument(0); - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->massAction = $objectManagerHelper->getObject( - \Magento\Customer\Controller\Adminhtml\Index\MassDelete::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->customerCollectionFactoryMock, - 'customerRepository' => $this->customerRepositoryMock, - ] - ); - } - - public function testExecute() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('deleteById') - ->willReturnMap([[10, true], [11, true], [12, true]]); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('A total of %1 record(s) were deleted.', count($customersIds))); - - $this->resultRedirectMock->expects($this->any()) - ->method('setPath') - ->with('customer/*/index') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - public function testExecuteWithException() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('deleteById') - ->willThrowException(new \Exception('Some message.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('Some message.'); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php deleted file mode 100644 index daf9c64fe7b7b..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassSubscribeTest.php +++ /dev/null @@ -1,203 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassSubscribeTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassSubscribeTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Customer\Controller\Adminhtml\Index\MassSubscribe - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRepositoryMock; - - /** - * @var \Magento\Newsletter\Model\Subscriber|\PHPUnit_Framework_MockObject_MockObject - */ - protected $subscriberMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create'] - ); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->customerCollectionMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->customerCollectionFactoryMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - $this->subscriberMock = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); - $subscriberFactoryMock = $this->getMockBuilder(\Magento\Newsletter\Model\SubscriberFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $subscriberFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->subscriberMock); - - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->customerCollectionMock) - ->willReturnArgument(0); - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->massAction = $objectManagerHelper->getObject( - \Magento\Customer\Controller\Adminhtml\Index\MassSubscribe::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->customerCollectionFactoryMock, - 'customerRepository' => $this->customerRepositoryMock, - 'subscriberFactory' => $subscriberFactoryMock, - ] - ); - } - - public function testExecute() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('getById') - ->willReturnMap([[10, true], [11, true], [12, true]]); - - $this->subscriberMock->expects($this->any()) - ->method('subscribeCustomerById') - ->willReturnMap([[10, true], [11, true], [12, true]]); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('A total of %1 record(s) were updated.', count($customersIds))); - - $this->resultRedirectMock->expects($this->any()) - ->method('setPath') - ->with('customer/*/index') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - public function testExecuteWithException() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('getById') - ->willThrowException(new \Exception('Some message.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('Some message.'); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php deleted file mode 100644 index 05624661a2de4..0000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassUnsubscribeTest.php +++ /dev/null @@ -1,203 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassUnsubscribeTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassUnsubscribeTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Customer\Controller\Adminhtml\Index\MassUnsubscribe - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionMock; - - /** - * @var \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRepositoryMock; - - /** - * @var \Magento\Newsletter\Model\Subscriber|\PHPUnit_Framework_MockObject_MockObject - */ - protected $subscriberMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create'] - ); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->customerCollectionMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->customerCollectionFactoryMock = - $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - $this->subscriberMock = $this->createMock(\Magento\Newsletter\Model\Subscriber::class); - $subscriberFactoryMock = $this->getMockBuilder(\Magento\Newsletter\Model\SubscriberFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $subscriberFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->subscriberMock); - - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->customerCollectionMock) - ->willReturnArgument(0); - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->massAction = $objectManagerHelper->getObject( - \Magento\Customer\Controller\Adminhtml\Index\MassUnsubscribe::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->customerCollectionFactoryMock, - 'customerRepository' => $this->customerRepositoryMock, - 'subscriberFactory' => $subscriberFactoryMock, - ] - ); - } - - public function testExecute() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('getById') - ->willReturnMap([[10, true], [11, true], [12, true]]); - - $this->subscriberMock->expects($this->any()) - ->method('unsubscribeCustomerById') - ->willReturnMap([[10, true], [11, true], [12, true]]); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('A total of %1 record(s) were updated.', count($customersIds))); - - $this->resultRedirectMock->expects($this->any()) - ->method('setPath') - ->with('customer/*/index') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - public function testExecuteWithException() - { - $customersIds = [10, 11, 12]; - - $this->customerCollectionMock->expects($this->any()) - ->method('getAllIds') - ->willReturn($customersIds); - - $this->customerRepositoryMock->expects($this->any()) - ->method('getById') - ->willThrowException(new \Exception('Some message.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('Some message.'); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php index 02d071ab394a5..8e81db656b581 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; use Magento\Customer\Api\Data\CustomerInterface; @@ -143,11 +141,11 @@ protected function setUp() $this->messageManager = $this->getMockBuilder( \Magento\Framework\Message\Manager::class )->disableOriginalConstructor()->setMethods( - ['addSuccess', 'addMessage', 'addException', 'addErrorMessage'] + ['addSuccessMessage', 'addMessage', 'addExceptionMessage', 'addErrorMessage'] )->getMock(); - $this->resultRedirectFactoryMock = $this->getMockBuilder( - \Magento\Backend\Model\View\Result\RedirectFactory::class) + $this->resultRedirectFactoryMock = + $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -235,7 +233,7 @@ public function testResetPasswordActionNoCustomer() ->with($this->equalTo($redirectLink)); $this->assertInstanceOf( - \Magento\Backend\Model\View\Result\Redirect::class, + \Magento\Backend\Model\View\Result\Redirect::class, $this->_testedObject->execute() ); } @@ -289,7 +287,7 @@ public function testResetPasswordActionInvalidCustomerId() ->with($this->equalTo($redirectLink)); $this->assertInstanceOf( - \Magento\Backend\Model\View\Result\Redirect::class, + \Magento\Backend\Model\View\Result\Redirect::class, $this->_testedObject->execute() ); } @@ -443,7 +441,7 @@ public function testResetPasswordActionException() $this->messageManager->expects( $this->once() )->method( - 'addException' + 'addExceptionMessage' )->with( $this->equalTo($exception), $this->equalTo('Something went wrong while resetting customer password.') @@ -503,7 +501,7 @@ public function testResetPasswordActionSendEmail() $this->messageManager->expects( $this->once() )->method( - 'addSuccess' + 'addSuccessMessage' )->with( $this->equalTo('The customer will receive an email with a link to reset password.') ); @@ -528,7 +526,7 @@ public function testResetPasswordActionSendEmail() ); $this->assertInstanceOf( - \Magento\Backend\Model\View\Result\Redirect::class, + \Magento\Backend\Model\View\Result\Redirect::class, $this->_testedObject->execute() ); } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 5372bb11a89b5..09082a0a9de53 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -15,6 +15,8 @@ use Magento\Framework\Controller\Result\Redirect; /** + * Testing Save Customer use case from admin page. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @covers \Magento\Customer\Controller\Adminhtml\Index\Save @@ -275,6 +277,8 @@ protected function setUp() } /** + * Test for Execute method with existent customer. + * * @covers \Magento\Customer\Controller\Adminhtml\Index\Index::execute * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -540,6 +544,10 @@ public function testExecuteWithExistentCustomer() $customerEmail = 'customer@email.com'; $customerMock->expects($this->once())->method('getEmail')->willReturn($customerEmail); + $customerMock->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); + $this->emailNotificationMock->expects($this->once()) ->method('credentialsChanged') ->with($customerMock, $customerEmail) @@ -878,22 +886,24 @@ public function testExecuteWithNewCustomerAndValidationException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -904,7 +914,7 @@ public function testExecuteWithNewCustomerAndValidationException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -917,12 +927,12 @@ public function testExecuteWithNewCustomerAndValidationException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -930,19 +940,19 @@ public function testExecuteWithNewCustomerAndValidationException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -990,7 +1000,10 @@ public function testExecuteWithNewCustomerAndValidationException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -1021,22 +1034,24 @@ public function testExecuteWithNewCustomerAndLocalizedException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1047,7 +1062,7 @@ public function testExecuteWithNewCustomerAndLocalizedException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -1060,12 +1075,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1074,19 +1089,19 @@ public function testExecuteWithNewCustomerAndLocalizedException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1133,7 +1148,10 @@ public function testExecuteWithNewCustomerAndLocalizedException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -1164,22 +1182,24 @@ public function testExecuteWithNewCustomerAndException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1190,7 +1210,7 @@ public function testExecuteWithNewCustomerAndException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -1203,12 +1223,12 @@ public function testExecuteWithNewCustomerAndException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1216,19 +1236,19 @@ public function testExecuteWithNewCustomerAndException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1277,7 +1297,10 @@ public function testExecuteWithNewCustomerAndException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php index 59c940bb85297..328f1bef3a905 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php @@ -101,6 +101,49 @@ public function testExecuteNoParamsShouldThrowException() $controller->execute(); } + /** + * @expectedException \Magento\Framework\Exception\NotFoundException + * @expectedExceptionMessage Page not found. + */ + public function testExecuteInvalidFile() + { + $file = '../../../app/etc/env.php'; + $encodedFile = base64_encode($file); + $fileName = 'customer/' . $file; + $path = 'path'; + + $this->requestMock->expects($this->atLeastOnce())->method('getParam')->with('file')->willReturn($encodedFile); + + $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($fileName)->willReturn($path); + + $this->fileSystemMock->expects($this->once())->method('getDirectoryRead') + ->with(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA) + ->willReturn($this->directoryMock); + + $this->storage->expects($this->once())->method('processStorageFile')->with($path)->willReturn(false); + + $this->objectManagerMock->expects($this->any())->method('get') + ->willReturnMap( + [ + [\Magento\Framework\Filesystem::class, $this->fileSystemMock], + [\Magento\MediaStorage\Helper\File\Storage::class, $this->storage], + ] + ); + + $this->urlDecoderMock->expects($this->once())->method('decode')->with($encodedFile)->willReturn($file); + $fileFactoryMock = $this->createMock(\Magento\Framework\App\Response\Http\FileFactory::class); + + $controller = $this->objectManager->getObject( + \Magento\Customer\Controller\Adminhtml\Index\Viewfile::class, + [ + 'context' => $this->contextMock, + 'urlDecoder' => $this->urlDecoderMock, + 'fileFactory' => $fileFactoryMock, + ] + ); + $controller->execute(); + } + public function testExecuteParamFile() { $decodedFile = 'decoded_file'; @@ -122,7 +165,7 @@ public function testExecuteParamFile() ->willReturnMap( [ [\Magento\Framework\Filesystem::class, $this->fileSystemMock], - [\Magento\MediaStorage\Helper\File\Storage::class, $this->storage] + [\Magento\MediaStorage\Helper\File\Storage::class, $this->storage], ] ); @@ -142,7 +185,7 @@ public function testExecuteParamFile() [ 'context' => $this->contextMock, 'urlDecoder' => $this->urlDecoderMock, - 'fileFactory' => $fileFactoryMock + 'fileFactory' => $fileFactoryMock, ] ); $controller->execute(); @@ -172,7 +215,7 @@ public function testExecuteGetParamImage() ->willReturnMap( [ [\Magento\Framework\Filesystem::class, $this->fileSystemMock], - [\Magento\MediaStorage\Helper\File\Storage::class, $this->storage] + [\Magento\MediaStorage\Helper\File\Storage::class, $this->storage], ] ); @@ -201,7 +244,7 @@ public function testExecuteGetParamImage() [ 'context' => $this->contextMock, 'urlDecoder' => $this->urlDecoderMock, - 'resultRawFactory' => $this->resultRawFactoryMock + 'resultRawFactory' => $this->resultRawFactoryMock, ] ); $this->assertSame($this->resultRawMock, $controller->execute()); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php index b759b1a62573f..54a02664141ee 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php @@ -4,14 +4,30 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -/** - * Test customer ajax login controller - */ namespace Magento\Customer\Test\Unit\Controller\Ajax; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Controller\Ajax\Login; +use Magento\Customer\Model\Account\Redirect; +use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\Result\Raw; +use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\ObjectManager\ObjectManager as FakeObjectManager; +use Magento\Framework\Stdlib\Cookie\CookieMetadata; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,174 +35,190 @@ class LoginTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Customer\Controller\Ajax\Login + * @var Login + */ + private $controller; + + /** + * @var Http|MockObject + */ + private $request; + + /** + * @var ResponseInterface|MockObject */ - protected $object; + private $response; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - protected $request; + private $customerSession; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var FakeObjectManager|MockObject */ - protected $response; + private $objectManager; /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + * @var AccountManagement|MockObject */ - protected $customerSession; + private $accountManagement; /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - protected $objectManager; + private $jsonHelper; /** - * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Json|MockObject */ - protected $customerAccountManagementMock; + private $resultJson; /** - * @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ - protected $jsonHelperMock; + private $resultJsonFactory; /** - * @var \Magento\Framework\Controller\Result\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Raw|MockObject */ - protected $resultJson; + private $resultRaw; /** - * @var \Magento\Framework\Controller\Result\JsonFactory| \PHPUnit_Framework_MockObject_MockObject + * @var RedirectInterface|MockObject */ - protected $resultJsonFactory; + private $redirect; /** - * @var \Magento\Framework\Controller\Result\Raw| \PHPUnit_Framework_MockObject_MockObject + * @var CookieManagerInterface|MockObject */ - protected $resultRaw; + private $cookieManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CookieMetadataFactory|MockObject */ - protected $redirectMock; + private $cookieMetadataFactory; + /** + * @inheritdoc + */ protected function setUp() { - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->response = $this->createPartialMock(\Magento\Framework\App\ResponseInterface::class, ['setRedirect', 'sendResponse', 'representJson', 'setHttpResponseCode']); - $this->customerSession = $this->createPartialMock(\Magento\Customer\Model\Session::class, [ + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + $this->response = $this->createPartialMock( + ResponseInterface::class, + ['setRedirect', 'sendResponse', 'representJson', 'setHttpResponseCode'] + ); + $this->customerSession = $this->createPartialMock(Session::class, [ 'isLoggedIn', 'getLastCustomerId', 'getBeforeAuthUrl', 'setBeforeAuthUrl', 'setCustomerDataAsLoggedIn', - 'regenerateId' + 'regenerateId', + 'getData' ]); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['get']); - $this->customerAccountManagementMock = - $this->createPartialMock(\Magento\Customer\Model\AccountManagement::class, ['authenticate']); + $this->objectManager = $this->createPartialMock(FakeObjectManager::class, ['get']); + $this->accountManagement = $this->createPartialMock(AccountManagement::class, ['authenticate']); - $this->jsonHelperMock = $this->createPartialMock(\Magento\Framework\Json\Helper\Data::class, ['jsonDecode']); + $this->jsonHelper = $this->createPartialMock(Data::class, ['jsonDecode']); - $this->resultJson = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $this->resultJson = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->resultRaw = $this->getMockBuilder(\Magento\Framework\Controller\Result\Raw::class) + $this->cookieManager = $this->getMockBuilder(CookieManagerInterface::class) + ->setMethods(['getCookie', 'deleteCookie']) + ->getMockForAbstractClass(); + $this->cookieMetadataFactory = $this->getMockBuilder(CookieMetadataFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadata = $this->getMockBuilder(CookieMetadata::class) ->disableOriginalConstructor() ->getMock(); - $resultRawFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\RawFactory::class) + + $this->resultRaw = $this->getMockBuilder(Raw::class) + ->disableOriginalConstructor() + ->getMock(); + $resultRawFactory = $this->getMockBuilder(RawFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultRawFactory->expects($this->atLeastOnce()) - ->method('create') + $resultRawFactory->method('create') ->willReturn($this->resultRaw); - $contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); - $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $contextMock->expects($this->atLeastOnce())->method('getRedirect')->willReturn($this->redirectMock); - $contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->request); - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->object = $objectManager->getObject( - \Magento\Customer\Controller\Ajax\Login::class, + /** @var Context|MockObject $context */ + $context = $this->createMock(Context::class); + $this->redirect = $this->createMock(RedirectInterface::class); + $context->method('getRedirect') + ->willReturn($this->redirect); + $context->method('getRequest') + ->willReturn($this->request); + + $objectManager = new ObjectManager($this); + $this->controller = $objectManager->getObject( + Login::class, [ - 'context' => $contextMock, + 'context' => $context, 'customerSession' => $this->customerSession, - 'helper' => $this->jsonHelperMock, + 'helper' => $this->jsonHelper, 'response' => $this->response, 'resultRawFactory' => $resultRawFactory, 'resultJsonFactory' => $this->resultJsonFactory, 'objectManager' => $this->objectManager, - 'customerAccountManagement' => $this->customerAccountManagementMock, + 'customerAccountManagement' => $this->accountManagement, + 'cookieManager' => $this->cookieManager, + 'cookieMetadataFactory' => $this->cookieMetadataFactory ] ); } + /** + * Checks successful login. + */ public function testLogin() { $jsonRequest = '{"username":"customer@example.com", "password":"password"}'; $loginSuccessResponse = '{"errors": false, "message":"Login successful."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->atLeastOnce()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'customer@example.com', 'password' => 'password']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('customer@example.com', 'password') - ->willReturn($customerMock); + ->willReturn($customer); - $this->customerSession->expects($this->once()) - ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); + $this->customerSession->method('setCustomerDataAsLoggedIn') + ->with($customer); + $this->customerSession->method('regenerateId'); - $this->customerSession->expects($this->once())->method('regenerateId'); + /** @var Redirect|MockObject $redirect */ + $redirect = $this->createMock(Redirect::class); + $this->controller->setAccountRedirect($redirect); + $redirect->method('getRedirectCookie') + ->willReturn('some_url1'); - $redirectMock = $this->createMock(\Magento\Customer\Model\Account\Redirect::class); - $this->object->setAccountRedirect($redirectMock); - $redirectMock->expects($this->once())->method('getRedirectCookie')->willReturn('some_url1'); + $this->withCookieManager(); - $scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->object->setScopeConfig($scopeConfigMock); - $scopeConfigMock->expects($this->once())->method('getValue') - ->with('customer/startup/redirect_dashboard') - ->willReturn(0); + $this->withScopeConfig(); - $this->redirectMock->expects($this->once())->method('success')->willReturn('some_url2'); - $this->resultRaw->expects($this->never())->method('setHttpResponseCode'); + $this->redirect->method('success') + ->willReturn('some_url2'); + $this->resultRaw->expects(self::never()) + ->method('setHttpResponseCode'); $result = [ 'errors' => false, @@ -194,67 +226,103 @@ public function testLogin() 'redirectUrl' => 'some_url2', ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginSuccessResponse); - $this->assertEquals($loginSuccessResponse, $this->object->execute()); + self::assertEquals($loginSuccessResponse, $this->controller->execute()); } + /** + * Checks unsuccessful login. + */ public function testLoginFailure() { $jsonRequest = '{"username":"invalid@example.com", "password":"invalid"}'; $loginFailureResponse = '{"message":"Invalid login or password."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->once()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'invalid@example.com', 'password' => 'invalid']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('invalid@example.com', 'invalid') ->willThrowException(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); - $this->customerSession->expects($this->never()) + $this->customerSession->expects(self::never()) ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); - - $this->customerSession->expects($this->never())->method('regenerateId'); + ->with($customer); + $this->customerSession->expects(self::never()) + ->method('regenerateId'); + $this->customerSession->method('getData') + ->with('user_login_show_captcha') + ->willReturn(false); $result = [ 'errors' => true, - 'message' => __('Invalid login or password.') + 'message' => __('Invalid login or password.'), + 'captcha' => false ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginFailureResponse); - $this->assertEquals($loginFailureResponse, $this->object->execute()); + self::assertEquals($loginFailureResponse, $this->controller->execute()); + } + + /** + * Emulates request behavior. + * + * @param string $jsonRequest + */ + private function withRequest(string $jsonRequest) + { + $this->request->method('getContent') + ->willReturn($jsonRequest); + + $this->request->method('getMethod') + ->willReturn('POST'); + + $this->request->method('isXmlHttpRequest') + ->willReturn(true); + } + + /** + * Emulates cookie manager behavior. + */ + private function withCookieManager() + { + $this->cookieManager->method('getCookie') + ->with('mage-cache-sessid') + ->willReturn(true); + $cookieMetadata = $this->getMockBuilder(CookieMetadata::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadataFactory->method('createCookieMetadata') + ->willReturn($cookieMetadata); + $cookieMetadata->method('setPath') + ->with('/') + ->willReturnSelf(); + $this->cookieManager->method('deleteCookie') + ->with('mage-cache-sessid', $cookieMetadata) + ->willReturnSelf(); + } + + /** + * Emulates config behavior. + */ + private function withScopeConfig() + { + /** @var ScopeConfigInterface|MockObject $scopeConfig */ + $scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->controller->setScopeConfig($scopeConfig); + $scopeConfig->method('getValue') + ->with('customer/startup/redirect_dashboard') + ->willReturn(0); } } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php index 2552beeca463d..04da8e77867d8 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php @@ -81,13 +81,13 @@ protected function setUp() } /** - * @param $sectionNames - * @param $updateSectionID - * @param $sectionNamesAsArray - * @param $updateIds + * @param string $sectionNames + * @param bool $forceNewSectionTimestamp + * @param string[] $sectionNamesAsArray + * @param bool $forceNewTimestamp * @dataProvider executeDataProvider */ - public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArray, $updateIds) + public function testExecute($sectionNames, $forceNewSectionTimestamp, $sectionNamesAsArray, $forceNewTimestamp) { $this->resultJsonFactoryMock->expects($this->once()) ->method('create') @@ -101,12 +101,12 @@ public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArra $this->httpRequestMock->expects($this->exactly(2)) ->method('getParam') - ->withConsecutive(['sections'], ['update_section_id']) - ->willReturnOnConsecutiveCalls($sectionNames, $updateSectionID); + ->withConsecutive(['sections'], ['force_new_section_timestamp']) + ->willReturnOnConsecutiveCalls($sectionNames, $forceNewSectionTimestamp); $this->sectionPoolMock->expects($this->once()) ->method('getSectionsData') - ->with($sectionNamesAsArray, $updateIds) + ->with($sectionNamesAsArray, $forceNewTimestamp) ->willReturn([ 'message' => 'some message', 'someKey' => 'someValue' @@ -123,20 +123,23 @@ public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArra $this->loadAction->execute(); } + /** + * @return array + */ public function executeDataProvider() { return [ [ 'sectionNames' => 'sectionName1,sectionName2,sectionName3', - 'updateSectionID' => 'updateSectionID', + 'forceNewSectionTimestamp' => 'forceNewSectionTimestamp', 'sectionNamesAsArray' => ['sectionName1', 'sectionName2', 'sectionName3'], - 'updateIds' => true + 'forceNewTimestamp' => true ], [ 'sectionNames' => null, - 'updateSectionID' => null, + 'forceNewSectionTimestamp' => null, 'sectionNamesAsArray' => null, - 'updateIds' => false + 'forceNewTimestamp' => false ], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/CustomerData/Plugin/SessionCheckerTest.php b/app/code/Magento/Customer/Test/Unit/CustomerData/Plugin/SessionCheckerTest.php index a4246b6398fd1..5beea22bda3d7 100644 --- a/app/code/Magento/Customer/Test/Unit/CustomerData/Plugin/SessionCheckerTest.php +++ b/app/code/Magento/Customer/Test/Unit/CustomerData/Plugin/SessionCheckerTest.php @@ -86,6 +86,9 @@ public function testBeforeStart($result, $callCount) $this->plugin->beforeStart($this->sessionManager); } + /** + * @return array + */ public function beforeStartDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php index 98fee70e335f7..2b67df1aee292 100644 --- a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php +++ b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php @@ -63,7 +63,7 @@ public function testGetSectionsDataAllSections() $this->identifierMock->expects($this->once()) ->method('markSections') - //check also default value for $updateIds = false + //check also default value for $forceTimestamp = false ->with($allSectionsData, $sectionNames, false) ->willReturn($identifierResult); $modelResult = $this->model->getSectionsData($sectionNames); diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php index 50785247d7965..b2aa3161b36bf 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php @@ -78,6 +78,9 @@ public function testGetStreetLines($numLines, $expectedNumLines) $this->assertEquals($expectedNumLines, $this->helper->getStreetLines()); } + /** + * @return array + */ public function providerGetStreetLines() { return [ @@ -190,6 +193,9 @@ public function testConvertStreetLines($origStreets, $toCount, $result) $this->assertEquals($result, $this->helper->convertStreetLines($origStreets, $toCount)); } + /** + * @return array + */ public function getConvertStreetLinesDataProvider() { return [ @@ -330,6 +336,9 @@ public function testGetFormatTypeRenderer($code, $result) $this->assertEquals($result, $this->helper->getFormatTypeRenderer($code)); } + /** + * @return array + */ public function getFormatTypeRendererDataProvider() { $renderer = $this->getMockBuilder(\Magento\Customer\Block\Address\Renderer\RendererInterface::class) @@ -366,6 +375,9 @@ public function testGetFormat($code, $result) $this->assertEquals($result, $this->helper->getFormat($code)); } + /** + * @return array + */ public function getFormatDataProvider() { return [ @@ -396,6 +408,9 @@ public function testIsAttributeVisible($attributeCode, $isMetadataExists) $this->assertEquals($isMetadataExists, $this->helper->isAttributeVisible($attributeCode)); } + /** + * @return array + */ public function isAttributeVisibleDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php index 16259191b7d28..008fe8fd1849b 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/Session/CurrentCustomerTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Helper\Session; class CurrentCustomerTest extends \PHPUnit\Framework\TestCase @@ -72,7 +70,10 @@ protected function setUp() { $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); $this->layoutMock = $this->createMock(\Magento\Framework\View\Layout::class); - $this->customerInterfaceFactoryMock = $this->createPartialMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create', 'setGroupId']); + $this->customerInterfaceFactoryMock = $this->createPartialMock( + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, + ['create', 'setGroupId'] + ); $this->customerDataMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); @@ -98,38 +99,20 @@ public function testGetCustomerDepersonalizeCustomerData() $this->requestMock->expects($this->once())->method('isAjax')->will($this->returnValue(false)); $this->layoutMock->expects($this->once())->method('isCacheable')->will($this->returnValue(true)); $this->viewMock->expects($this->once())->method('isLayoutLoaded')->will($this->returnValue(true)); - $this->moduleManagerMock->expects( - $this->once() - )->method( - 'isEnabled' - )->with( - $this->equalTo('Magento_PageCache') - )->will( - $this->returnValue(true) - ); - $this->customerSessionMock->expects( - $this->once() - )->method( - 'getCustomerGroupId' - )->will( - $this->returnValue($this->customerGroupId) - ); - $this->customerInterfaceFactoryMock->expects( - $this->once() - )->method( - 'create' - )->will( - $this->returnValue($this->customerDataMock) - ); - $this->customerDataMock->expects( - $this->once() - )->method( - 'setGroupId' - )->with( - $this->equalTo($this->customerGroupId) - )->will( - $this->returnSelf() - ); + $this->moduleManagerMock->expects($this->once()) + ->method('isEnabled') + ->with($this->equalTo('Magento_PageCache')) + ->willReturn(true); + $this->customerSessionMock->expects($this->once()) + ->method('getCustomerGroupId') + ->willReturn($this->customerGroupId); + $this->customerInterfaceFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->customerDataMock); + $this->customerDataMock->expects($this->once()) + ->method('setGroupId') + ->with($this->equalTo($this->customerGroupId)) + ->willReturnSelf(); $this->assertEquals($this->customerDataMock, $this->currentCustomer->getCustomer()); } @@ -138,31 +121,17 @@ public function testGetCustomerDepersonalizeCustomerData() */ public function testGetCustomerLoadCustomerFromService() { - $this->moduleManagerMock->expects( - $this->once() - )->method( - 'isEnabled' - )->with( - $this->equalTo('Magento_PageCache') - )->will( - $this->returnValue(false) - ); - $this->customerSessionMock->expects( - $this->once() - )->method( - 'getId' - )->will( - $this->returnValue($this->customerId) - ); - $this->customerRepositoryMock->expects( - $this->once() - )->method( - 'getById' - )->with( - $this->equalTo($this->customerId) - )->will( - $this->returnValue($this->customerDataMock) - ); + $this->moduleManagerMock->expects($this->once()) + ->method('isEnabled') + ->with($this->equalTo('Magento_PageCache')) + ->willReturn(false); + $this->customerSessionMock->expects($this->once()) + ->method('getId') + ->willReturn($this->customerId); + $this->customerRepositoryMock->expects($this->once()) + ->method('getById') + ->with($this->equalTo($this->customerId)) + ->willReturn($this->customerDataMock); $this->assertEquals($this->customerDataMock, $this->currentCustomer->getCustomer()); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Account/RedirectTest.php b/app/code/Magento/Customer/Test/Unit/Model/Account/RedirectTest.php index 95e6223d91afb..98b08b73e4829 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Account/RedirectTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Account/RedirectTest.php @@ -1,13 +1,9 @@ <?php /** - * Unit test for Magento\Customer\Test\Unit\Model\Account\Redirect - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Model\Account; use Magento\Customer\Model\Account\Redirect; @@ -18,6 +14,8 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** + * Unit test for Magento\Customer\Test\Unit\Model\Account\Redirect. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RedirectTest extends \PHPUnit\Framework\TestCase diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php new file mode 100644 index 0000000000000..ae246665b28ed --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Model; + +use Magento\Customer\Model\AccountConfirmation; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Registry; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AccountConfirmationTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AccountConfirmation|\PHPUnit_Framework_MockObject_MockObject + */ + private $accountConfirmation; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $registry; + + protected function setUp() + { + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->registry = $this->createMock(Registry::class); + + $this->accountConfirmation = new AccountConfirmation( + $this->scopeConfig, + $this->registry + ); + } + + /** + * @param $customerId + * @param $customerEmail + * @param $skipConfirmationIfEmail + * @param $isConfirmationEnabled + * @param $expected + * @dataProvider dataProviderIsConfirmationRequired + */ + public function testIsConfirmationRequired( + $customerId, + $customerEmail, + $skipConfirmationIfEmail, + $isConfirmationEnabled, + $expected + ) { + $websiteId = 1; + + $this->scopeConfig->expects($this->any()) + ->method('getValue') + ->with( + $this->accountConfirmation::XML_PATH_IS_CONFIRM, + ScopeInterface::SCOPE_WEBSITES, + $websiteId + )->willReturn($isConfirmationEnabled); + + $this->registry->expects($this->any()) + ->method('registry') + ->with('skip_confirmation_if_email') + ->willReturn($skipConfirmationIfEmail); + + self::assertEquals( + $expected, + $this->accountConfirmation->isConfirmationRequired($websiteId, $customerId, $customerEmail) + ); + } + + /** + * @return array + */ + public function dataProviderIsConfirmationRequired() + { + return [ + [null, 'customer@example.com', null, true, true], + [null, 'customer@example.com', null, false, false], + [1, 'customer@example.com', 'customer@example.com', true, false], + [1, 'customer@example.com', 'customer1@example.com', false, false], + [1, 'customer@example.com', 'customer1@example.com', true, true], + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 2a6b9fe6fd4ea..f5688c1f87481 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -3,775 +3,298 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Test\Unit\Model; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\EmailNotificationInterface; -use Magento\Framework\App\Area; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Store\Model\ScopeInterface; /** + * Unit test for Magento\Customer\Model\AccountManagement. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountManagementTest extends \PHPUnit\Framework\TestCase { /** @var AccountManagement */ - protected $accountManagement; + private $accountManagement; /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + private $objectManagerHelper; /** @var \Magento\Customer\Model\CustomerFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerFactory; + private $customerFactoryMock; /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $manager; + private $managerMock; /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + protected $storeManagerMock; /** @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject */ - protected $random; + private $randomMock; /** @var \Magento\Customer\Model\Metadata\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validator; + private $validatorMock; /** @var \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $validationResultsInterfaceFactory; + private $validationResultsInterfaceFactoryMock; /** @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $addressRepository; + private $addressRepositoryMock; /** @var \Magento\Customer\Api\CustomerMetadataInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerMetadata; + private $customerMetadataMock; /** @var \Magento\Customer\Model\CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRegistry; + private $customerRegistryMock; /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $logger; + private $loggerMock; /** @var \Magento\Framework\Encryption\EncryptorInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $encryptor; + private $encryptorMock; /** @var \Magento\Customer\Model\Config\Share|\PHPUnit_Framework_MockObject_MockObject */ - protected $share; + private $shareMock; /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ - protected $string; + private $stringMock; /** @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRepository; + private $customerRepositoryMock; /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfig; + private $scopeConfigMock; /** @var \Magento\Framework\Mail\Template\TransportBuilder|\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilder; + private $transportBuilderMock; /** @var \Magento\Framework\Reflection\DataObjectProcessor|\PHPUnit_Framework_MockObject_MockObject */ - protected $dataObjectProcessor; + private $dataObjectProcessorMock; /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registry; + private $registryMock; /** @var \Magento\Customer\Helper\View|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerViewHelper; + private $customerViewHelperMock; /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ - protected $dateTime; + private $dateTimeMock; /** @var \Magento\Customer\Model\Customer|\PHPUnit_Framework_MockObject_MockObject */ - protected $customer; + private $customerMock; /** @var \Magento\Framework\DataObjectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectFactory; + private $objectFactoryMock; /** @var \Magento\Framework\Api\ExtensibleDataObjectConverter|\PHPUnit_Framework_MockObject_MockObject */ - protected $extensibleDataObjectConverter; + private $extensibleDataObjectConverterMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\Store - */ - protected $store; + /** @var \Magento\Customer\Model\Data\CustomerSecure|\PHPUnit_Framework_MockObject_MockObject */ + private $customerSecureMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Model\Data\CustomerSecure - */ - protected $customerSecure; + /** @var AuthenticationInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $authenticationMock; - /** - * @var AuthenticationInterface |\PHPUnit_Framework_MockObject_MockObject - */ - protected $authenticationMock; + /** @var EmailNotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $emailNotificationMock; - /** - * @var EmailNotificationInterface |\PHPUnit_Framework_MockObject_MockObject - */ - protected $emailNotificationMock; + /** @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject */ + private $dateTimeFactoryMock; - /** - * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $dateTimeFactory; + /** @var AccountConfirmation|\PHPUnit_Framework_MockObject_MockObject */ + private $accountConfirmationMock; + + /** @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $sessionManagerMock; + + /** @var \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + private $visitorCollectionFactoryMock; + + /** @var \Magento\Framework\Session\SaveHandlerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $saveHandlerMock; + + /** @var \Magento\Customer\Model\AddressRegistry|\PHPUnit_Framework_MockObject_MockObject */ + private $addressRegistryMock; + + /** @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject */ + private $searchCriteriaBuilderMock; /** + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { - $this->customerFactory = $this->createPartialMock(\Magento\Customer\Model\CustomerFactory::class, ['create']); - $this->manager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->random = $this->createMock(\Magento\Framework\Math\Random::class); - $this->validator = $this->createMock(\Magento\Customer\Model\Metadata\Validator::class); - $this->validationResultsInterfaceFactory = $this->createMock( + $this->customerFactoryMock = $this->createPartialMock( + \Magento\Customer\Model\CustomerFactory::class, + ['create'] + ); + $this->managerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->randomMock = $this->createMock(\Magento\Framework\Math\Random::class); + $this->validatorMock = $this->createMock(\Magento\Customer\Model\Metadata\Validator::class); + $this->validationResultsInterfaceFactoryMock = $this->createMock( \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory::class ); - $this->addressRepository = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->customerMetadata = $this->createMock(\Magento\Customer\Api\CustomerMetadataInterface::class); - $this->customerRegistry = $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); - $this->logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->encryptor = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); - $this->share = $this->createMock(\Magento\Customer\Model\Config\Share::class); - $this->string = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class); - $this->customerRepository = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->transportBuilder = $this->createMock(\Magento\Framework\Mail\Template\TransportBuilder::class); - $this->dataObjectProcessor = $this->createMock(\Magento\Framework\Reflection\DataObjectProcessor::class); - $this->registry = $this->createMock(\Magento\Framework\Registry::class); - $this->customerViewHelper = $this->createMock(\Magento\Customer\Helper\View::class); - $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); - $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); - $this->objectFactory = $this->createMock(\Magento\Framework\DataObjectFactory::class); - $this->extensibleDataObjectConverter = $this->createMock( + $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); + $this->customerMetadataMock = $this->createMock(\Magento\Customer\Api\CustomerMetadataInterface::class); + $this->customerRegistryMock = $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->encryptorMock = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); + $this->shareMock = $this->createMock(\Magento\Customer\Model\Config\Share::class); + $this->stringMock = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class); + $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->transportBuilderMock = $this->createMock(\Magento\Framework\Mail\Template\TransportBuilder::class); + $this->dataObjectProcessorMock = $this->createMock(\Magento\Framework\Reflection\DataObjectProcessor::class); + $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); + $this->customerViewHelperMock = $this->createMock(\Magento\Customer\Helper\View::class); + $this->dateTimeMock = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); + $this->customerMock = $this->createMock(\Magento\Customer\Model\Customer::class); + $this->objectFactoryMock = $this->createMock(\Magento\Framework\DataObjectFactory::class); + $this->addressRegistryMock = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); + $this->extensibleDataObjectConverterMock = $this->createMock( \Magento\Framework\Api\ExtensibleDataObjectConverter::class ); - $this->authenticationMock = $this->getMockBuilder(AuthenticationInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->emailNotificationMock = $this->getMockBuilder(EmailNotificationInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->authenticationMock = $this->createMock(AuthenticationInterface::class); + $this->emailNotificationMock = $this->createMock(EmailNotificationInterface::class); - $this->customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) + $this->customerSecureMock = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) ->setMethods(['setRpToken', 'addData', 'setRpTokenCreatedAt', 'setData']) ->disableOriginalConstructor() ->getMock(); - $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); + $this->accountConfirmationMock = $this->createMock(AccountConfirmation::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + + $this->visitorCollectionFactoryMock = $this->getMockBuilder( + \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory::class + )->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->sessionManagerMock = $this->createMock(\Magento\Framework\Session\SessionManagerInterface::class); + $this->saveHandlerMock = $this->createMock(\Magento\Framework\Session\SaveHandlerInterface::class); + + $this->dateTimeInit(); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->accountManagement = $this->objectManagerHelper->getObject( - \Magento\Customer\Model\AccountManagement::class, + AccountManagement::class, [ - 'customerFactory' => $this->customerFactory, - 'eventManager' => $this->manager, - 'storeManager' => $this->storeManager, - 'mathRandom' => $this->random, - 'validator' => $this->validator, - 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, - 'addressRepository' => $this->addressRepository, - 'customerMetadataService' => $this->customerMetadata, - 'customerRegistry' => $this->customerRegistry, - 'logger' => $this->logger, - 'encryptor' => $this->encryptor, - 'configShare' => $this->share, - 'stringHelper' => $this->string, - 'customerRepository' => $this->customerRepository, - 'scopeConfig' => $this->scopeConfig, - 'transportBuilder' => $this->transportBuilder, - 'dataProcessor' => $this->dataObjectProcessor, - 'registry' => $this->registry, - 'customerViewHelper' => $this->customerViewHelper, - 'dateTime' => $this->dateTime, - 'customerModel' => $this->customer, - 'objectFactory' => $this->objectFactory, - 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, - 'dateTimeFactory' => $this->dateTimeFactory, + 'customerFactory' => $this->customerFactoryMock, + 'eventManager' => $this->managerMock, + 'storeManager' => $this->storeManagerMock, + 'mathRandom' => $this->randomMock, + 'validator' => $this->validatorMock, + 'validationResultsDataFactory' => $this->validationResultsInterfaceFactoryMock, + 'addressRepository' => $this->addressRepositoryMock, + 'customerMetadataService' => $this->customerMetadataMock, + 'customerRegistry' => $this->customerRegistryMock, + 'logger' => $this->loggerMock, + 'encryptor' => $this->encryptorMock, + 'configShare' => $this->shareMock, + 'stringHelper' => $this->stringMock, + 'customerRepository' => $this->customerRepositoryMock, + 'scopeConfig' => $this->scopeConfigMock, + 'transportBuilder' => $this->transportBuilderMock, + 'dataProcessor' => $this->dataObjectProcessorMock, + 'registry' => $this->registryMock, + 'customerViewHelper' => $this->customerViewHelperMock, + 'dateTime' => $this->dateTimeMock, + 'customerModel' => $this->customerMock, + 'objectFactory' => $this->objectFactoryMock, + 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock, + 'dateTimeFactory' => $this->dateTimeFactoryMock, + 'accountConfirmation' => $this->accountConfirmationMock, + 'sessionManager' => $this->sessionManagerMock, + 'saveHandler' => $this->saveHandlerMock, + 'visitorCollectionFactory' => $this->visitorCollectionFactoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'addressRegistry' => $this->addressRegistryMock, ] ); - $reflection = new \ReflectionClass(get_class($this->accountManagement)); - $reflectionProperty = $reflection->getProperty('authentication'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->accountManagement, $this->authenticationMock); - $reflectionProperty = $reflection->getProperty('emailNotification'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->accountManagement, $this->emailNotificationMock); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - */ - public function testCreateAccountWithPasswordHashWithExistingCustomer() - { - $websiteId = 1; - $storeId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer->expects($this->once()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->once()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager - ->expects($this->once()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $this->accountManagement->createAccountWithPasswordHash($customer, $hash); - } - - /** - * @expectedException \Magento\Framework\Exception\State\InputMismatchException - */ - public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() - { - $websiteId = 1; - $storeId = null; - $defaultStoreId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->once()) - ->method('getId') - ->willReturn($defaultStoreId); - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->once()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer->expects($this->once()) - ->method('setStoreId') - ->with($defaultStoreId); - $customer - ->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); - $customer - ->expects($this->once()) - ->method('setAddresses') - ->with(null); - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager - ->expects($this->atLeastOnce()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $exception = new \Magento\Framework\Exception\AlreadyExistsException( - new \Magento\Framework\Phrase('Exception message') - ); - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customer, $hash) - ->willThrowException($exception); - - $this->accountManagement->createAccountWithPasswordHash($customer, $hash); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - */ - public function testCreateAccountWithPasswordHashWithLocalizedException() - { - $websiteId = 1; - $storeId = null; - $defaultStoreId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->once()) - ->method('getId') - ->willReturn($defaultStoreId); - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->once()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer->expects($this->once()) - ->method('setStoreId') - ->with($defaultStoreId); - $customer - ->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); - $customer - ->expects($this->once()) - ->method('setAddresses') - ->with(null); - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager - ->expects($this->atLeastOnce()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $exception = new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase('Exception message') - ); - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customer, $hash) - ->willThrowException($exception); - - $this->accountManagement->createAccountWithPasswordHash($customer, $hash); } /** - * @expectedException \Magento\Framework\Exception\LocalizedException + * Init DateTimeFactory. + * + * @return void */ - public function testCreateAccountWithPasswordHashWithAddressException() + private function dateTimeInit() { - $websiteId = 1; - $storeId = null; - $defaultStoreId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) + $dateTime = '2017-10-25 18:57:08'; + $timestamp = '1508983028'; + $dateTimeMock = $this->getMockBuilder(\DateTime::class) ->disableOriginalConstructor() + ->setMethods(['format', 'getTimestamp', 'setTimestamp']) ->getMock(); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->once()) - ->method('getId') - ->willReturn($defaultStoreId); - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->once()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer->expects($this->once()) - ->method('setStoreId') - ->with($defaultStoreId); - $customer - ->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); - $customer - ->expects($this->once()) - ->method('setAddresses') - ->with(null); - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager - ->expects($this->atLeastOnce()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customer, $hash) - ->willReturn($customer); - $exception = new \Magento\Framework\Exception\InputException( - new \Magento\Framework\Phrase('Exception message') - ); - $this->addressRepository - ->expects($this->atLeastOnce()) - ->method('save') - ->with($address) - ->willThrowException($exception); - $this->customerRepository - ->expects($this->once()) - ->method('delete') - ->with($customer); - - $this->accountManagement->createAccountWithPasswordHash($customer, $hash); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - */ - public function testCreateAccountWithPasswordHashWithNewCustomerAndLocalizedException() - { - $storeId = 1; - $storeName = 'store_name'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - $customerMock->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(null); - $customerMock->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customerMock->expects($this->once()) - ->method('setCreatedIn') - ->with($storeName) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('getAddresses') - ->willReturn([]); - $customerMock->expects($this->once()) - ->method('setAddresses') - ->with(null) + $dateTimeMock->expects($this->once()) + ->method('format') + ->with(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) + ->willReturn($dateTime); + $dateTimeMock->expects($this->once()) + ->method('getTimestamp') + ->willReturn($timestamp); + $dateTimeMock->expects($this->once()) + ->method('setTimestamp') ->willReturnSelf(); - - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $this->dateTimeFactoryMock = $this->getMockBuilder(DateTimeFactory::class) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); - - $storeMock->expects($this->once()) - ->method('getName') - ->willReturn($storeName); - - $this->storeManager->expects($this->exactly(2)) - ->method('getStore') - ->with($storeId) - ->willReturn($storeMock); - $exception = new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase('Exception message') - ); - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customerMock, $hash) - ->willThrowException($exception); - - $this->accountManagement->createAccountWithPasswordHash($customerMock, $hash); + $this->dateTimeFactoryMock->expects($this->once())->method('create')->willReturn($dateTimeMock); } /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * Test for changePassword method. + * + * @return void */ - public function testCreateAccountWithoutPassword() + public function testChangePassword() { - $websiteId = 1; - $storeId = null; - $defaultStoreId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $newLinkToken = '2jh43j5h2345jh23lh452h345hfuzasd96ofu'; + $customerId = 7; + $email = 'test@example.com'; + $currentPassword = '1234567'; + $newPassword = 'abcdefg'; - $datetime = $this->prepareDateTimeFactory(); + $customer = $this->createMock(CustomerInterface::class); - $address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->once()) - ->method('getId') - ->willReturn($defaultStoreId); - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->atLeastOnce()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); - $customer->expects($this->atLeastOnce()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer->expects($this->once()) - ->method('setStoreId') - ->with($defaultStoreId); $customer->expects($this->once()) ->method('getAddresses') - ->willReturn([$address]); - $customer->expects($this->once()) - ->method('setAddresses') - ->with(null); - $this->customerRepository - ->expects($this->once()) + ->willReturn([]); + $this->customerRepositoryMock->expects($this->once()) ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager->expects($this->atLeastOnce()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $this->customerRepository->expects($this->atLeastOnce()) - ->method('save') - ->willReturn($customer); - $this->addressRepository->expects($this->atLeastOnce()) - ->method('save') - ->with($address); - $this->customerRepository->expects($this->once()) - ->method('getById') - ->with($customerId) + ->with($email) ->willReturn($customer); - $this->random->expects($this->once()) - ->method('getUniqueHash') - ->willReturn($newLinkToken); - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->setMethods(['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash']) - ->disableOriginalConstructor() - ->getMock(); - $customerSecure->expects($this->any()) + $this->customerSecureMock->expects($this->once()) ->method('setRpToken') - ->with($newLinkToken); - $customerSecure->expects($this->any()) + ->with(null) + ->willReturnSelf(); + $this->customerSecureMock->expects($this->once()) ->method('setRpTokenCreatedAt') - ->with($datetime) + ->with(null) ->willReturnSelf(); - $customerSecure->expects($this->any()) - ->method('getPasswordHash') - ->willReturn(null); - $this->customerRegistry->expects($this->atLeastOnce()) + $this->customerRegistryMock->expects($this->once()) ->method('retrieveSecureData') - ->willReturn($customerSecure); - $this->emailNotificationMock->expects($this->once()) - ->method('newAccount') - ->willReturnSelf(); - - $this->accountManagement->createAccount($customer); - } - - /** - * Data provider for testCreateAccountWithPasswordInputException test - * - * @return array - */ - public function dataProviderCheckPasswordStrength() - { - return [ - [ - 'testNumber' => 1, - 'password' => 'qwer', - 'minPasswordLength' => 5, - 'minCharacterSetsNum' => 1 - ], - [ - 'testNumber' => 2, - 'password' => 'wrfewqedf1', - 'minPasswordLength' => 5, - 'minCharacterSetsNum' => 3 - ] - ]; - } - - /** - * @param int $testNumber - * @param string $password - * @param int $minPasswordLength - * @param int $minCharacterSetsNum - * @dataProvider dataProviderCheckPasswordStrength - */ - public function testCreateAccountWithPasswordInputException( - $testNumber, - $password, - $minPasswordLength, - $minCharacterSetsNum - ) { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->will( - $this->returnValueMap( - [ - [ - AccountManagement::XML_PATH_MINIMUM_PASSWORD_LENGTH, - 'default', - null, - $minPasswordLength, - ], - [ - AccountManagement::XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER, - 'default', - null, - $minCharacterSetsNum], - ] - ) - ); - - $this->string->expects($this->any()) - ->method('strlen') - ->with($password) - ->willReturn(iconv_strlen($password, 'UTF-8')); - - if ($testNumber == 1) { - $this->expectException( - \Magento\Framework\Exception\InputException::class, - 'Please enter a password with at least ' . $minPasswordLength . ' characters.' - ); - } - - if ($testNumber == 2) { - $this->expectException( - \Magento\Framework\Exception\InputException::class, - 'Minimum of different classes of characters in password is ' . $minCharacterSetsNum . - '. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.' - ); - } - - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $this->accountManagement->createAccount($customer, $password); - } - - public function testCreateAccountInputExceptionExtraLongPassword() - { - $password = '257*chars*************************************************************************************' - . '****************************************************************************************************' - . '***************************************************************'; - - $this->string->expects($this->any()) - ->method('strlen') - ->with($password) - ->willReturn(iconv_strlen($password, 'UTF-8')); - - $this->expectException( - \Magento\Framework\Exception\InputException::class, - 'Please enter a password with at most 256 characters.' - ); - - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $this->accountManagement->createAccount($customer, $password); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testCreateAccountWithPassword() - { - $websiteId = 1; - $storeId = null; - $defaultStoreId = 1; - $customerId = 1; - $customerEmail = 'email@email.com'; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - $newLinkToken = '2jh43j5h2345jh23lh452h345hfuzasd96ofu'; - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - $password = 'wrfewqedf1'; - $minPasswordLength = 5; - $minCharacterSetsNum = 2; - - $datetime = $this->prepareDateTimeFactory(); + ->with($customerId) + ->willReturn($this->customerSecureMock); - $this->scopeConfig->expects($this->any()) + $this->scopeConfigMock->expects($this->atLeastOnce()) ->method('getValue') ->willReturnMap( [ @@ -779,946 +302,46 @@ public function testCreateAccountWithPassword() AccountManagement::XML_PATH_MINIMUM_PASSWORD_LENGTH, 'default', null, - $minPasswordLength, + 7, ], [ AccountManagement::XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER, 'default', null, - $minCharacterSetsNum], - [ - AccountManagement::XML_PATH_REGISTER_EMAIL_TEMPLATE, - ScopeInterface::SCOPE_STORE, - $defaultStoreId, - $templateIdentifier, - ], - [ - AccountManagement::XML_PATH_REGISTER_EMAIL_IDENTITY, - ScopeInterface::SCOPE_STORE, 1, - $sender - ] + ], ] ); - $this->string->expects($this->any()) + $this->stringMock->expects($this->atLeastOnce()) ->method('strlen') - ->with($password) - ->willReturn(iconv_strlen($password, 'UTF-8')); - $this->encryptor->expects($this->once()) - ->method('getHash') - ->with($password, true) - ->willReturn($hash); - $address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->once()) - ->method('getId') - ->willReturn($defaultStoreId); - $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->atLeastOnce()) - ->method('getStoreIds') - ->willReturn([1, 2, 3]); - $website->expects($this->once()) - ->method('getDefaultStore') - ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->atLeastOnce()) - ->method('getEmail') - ->willReturn($customerEmail); - $customer->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer->expects($this->once()) - ->method('setStoreId') - ->with($defaultStoreId); - $customer->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address]); - $customer->expects($this->once()) - ->method('setAddresses') - ->with(null); - $this->customerRepository + ->with($newPassword) + ->willReturn(7); + $this->customerRepositoryMock ->expects($this->once()) - ->method('get') - ->with($customerEmail) - ->willReturn($customer); - $this->share->expects($this->once()) - ->method('isWebsiteScope') - ->willReturn(true); - $this->storeManager->expects($this->atLeastOnce()) - ->method('getWebsite') - ->with($websiteId) - ->willReturn($website); - $this->customerRepository->expects($this->atLeastOnce()) - ->method('save') - ->willReturn($customer); - $this->addressRepository->expects($this->atLeastOnce()) ->method('save') - ->with($address); - $this->customerRepository->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customer); - $this->random->expects($this->once()) - ->method('getUniqueHash') - ->willReturn($newLinkToken); - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->setMethods(['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash']) + ->with($customer); + $this->sessionManagerMock->expects($this->atLeastOnce())->method('getSessionId'); + $visitor = $this->getMockBuilder(\Magento\Customer\Model\Visitor::class) ->disableOriginalConstructor() + ->setMethods(['getSessionId']) ->getMock(); - $customerSecure->expects($this->any()) - ->method('setRpToken') - ->with($newLinkToken); - $customerSecure->expects($this->any()) - ->method('setRpTokenCreatedAt') - ->with($datetime) - ->willReturnSelf(); - $customerSecure->expects($this->any()) - ->method('getPasswordHash') - ->willReturn($hash); - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieveSecureData') - ->willReturn($customerSecure); - $this->emailNotificationMock->expects($this->once()) - ->method('newAccount') - ->willReturnSelf(); - - $this->accountManagement->createAccount($customer, $password); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testSendPasswordReminderEmail() - { - $customerId = 1; - $customerStoreId = 2; - $customerEmail = 'email@email.com'; - $customerData = ['key' => 'value']; - $customerName = 'Customer Name'; - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMock(); - $customer->expects($this->any()) - ->method('getStoreId') - ->willReturn($customerStoreId); - $customer->expects($this->any()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->any()) - ->method('getEmail') - ->willReturn($customerEmail); - - $this->store->expects($this->any()) - ->method('getId') - ->willReturn($customerStoreId); - - $this->storeManager->expects($this->at(0)) - ->method('getStore') - ->willReturn($this->store); - - $this->storeManager->expects($this->at(1)) - ->method('getStore') - ->with($customerStoreId) - ->willReturn($this->store); - - $this->customerRegistry->expects($this->once()) - ->method('retrieveSecureData') - ->with($customerId) - ->willReturn($this->customerSecure); - - $this->dataObjectProcessor->expects($this->once()) - ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) - ->willReturn($customerData); - - $this->customerViewHelper->expects($this->any()) - ->method('getCustomerName') - ->with($customer) - ->willReturn($customerName); - - $this->customerSecure->expects($this->once()) - ->method('addData') - ->with($customerData) - ->willReturnSelf(); - $this->customerSecure->expects($this->once()) - ->method('setData') - ->with('name', $customerName) - ->willReturnSelf(); - - $this->scopeConfig->expects($this->at(0)) - ->method('getValue') - ->with(AccountManagement::XML_PATH_REMIND_EMAIL_TEMPLATE, ScopeInterface::SCOPE_STORE, $customerStoreId) - ->willReturn($templateIdentifier); - $this->scopeConfig->expects($this->at(1)) - ->method('getValue') - ->with(AccountManagement::XML_PATH_FORGOT_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $customerStoreId) - ->willReturn($sender); - - $transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMock(); - - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with(['area' => Area::AREA_FRONTEND, 'store' => $customerStoreId]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with(['customer' => $this->customerSecure, 'store' => $this->store]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($sender) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($customerEmail, $customerName) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('getTransport') - ->willReturn($transport); - - $transport->expects($this->once()) - ->method('sendMessage'); - - $this->assertEquals($this->accountManagement, $this->accountManagement->sendPasswordReminderEmail($customer)); - } - - /** - * @param string $email - * @param string $templateIdentifier - * @param string $sender - * @param int $storeId - * @param int $customerId - * @param string $hash - */ - protected function prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash) - { - $websiteId = 1; - - $datetime = $this->prepareDateTimeFactory(); - - $customerData = ['key' => 'value']; - $customerName = 'Customer Name'; - - $this->store->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $this->store->expects($this->any()) - ->method('getId') - ->willReturn($storeId); - - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMock(); - $customer->expects($this->any()) - ->method('getEmail') - ->willReturn($email); - $customer->expects($this->any()) - ->method('getId') - ->willReturn($customerId); - $customer->expects($this->any()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->customerRepository->expects($this->once()) - ->method('get') - ->with($email, $websiteId) - ->willReturn($customer); - $this->customerRepository->expects($this->once()) - ->method('save') - ->with($customer) - ->willReturnSelf(); - - $this->random->expects($this->once()) - ->method('getUniqueHash') - ->willReturn($hash); - - $this->customerViewHelper->expects($this->any()) - ->method('getCustomerName') - ->with($customer) - ->willReturn($customerName); - - $this->customerSecure->expects($this->any()) - ->method('setRpToken') - ->with($hash) - ->willReturnSelf(); - $this->customerSecure->expects($this->any()) - ->method('setRpTokenCreatedAt') - ->with($datetime) - ->willReturnSelf(); - $this->customerSecure->expects($this->any()) - ->method('addData') - ->with($customerData) - ->willReturnSelf(); - $this->customerSecure->expects($this->any()) - ->method('setData') - ->with('name', $customerName) - ->willReturnSelf(); - - $this->customerRegistry->expects($this->any()) - ->method('retrieveSecureData') - ->with($customerId) - ->willReturn($this->customerSecure); - - $this->dataObjectProcessor->expects($this->any()) - ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) - ->willReturn($customerData); - - $this->prepareEmailSend($email, $templateIdentifier, $sender, $storeId, $customerName); - } - - /** - * @param $email - * @param $templateIdentifier - * @param $sender - * @param $storeId - * @param $customerName - */ - protected function prepareEmailSend($email, $templateIdentifier, $sender, $storeId, $customerName) - { - $transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMock(); - - $this->transportBuilder->expects($this->any()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->any()) - ->method('setTemplateOptions') - ->with(['area' => Area::AREA_FRONTEND, 'store' => $storeId]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->any()) - ->method('setTemplateVars') - ->with(['customer' => $this->customerSecure, 'store' => $this->store]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->any()) - ->method('setFrom') - ->with($sender) - ->willReturnSelf(); - $this->transportBuilder->expects($this->any()) - ->method('addTo') - ->with($email, $customerName) - ->willReturnSelf(); - $this->transportBuilder->expects($this->any()) - ->method('getTransport') - ->willReturn($transport); - - $transport->expects($this->any()) - ->method('sendMessage'); - } - - public function testInitiatePasswordResetEmailReminder() - { - $customerId = 1; - - $email = 'test@example.com'; - $template = AccountManagement::EMAIL_REMINDER; - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - - $storeId = 1; - - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); - - $this->emailNotificationMock->expects($this->once()) - ->method('passwordReminder') - ->willReturnSelf(); - - $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); - - $this->assertTrue($this->accountManagement->initiatePasswordReset($email, $template)); - } - - public function testInitiatePasswordResetEmailReset() - { - $storeId = 1; - $customerId = 1; - - $email = 'test@example.com'; - $template = AccountManagement::EMAIL_RESET; - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); - - $this->emailNotificationMock->expects($this->once()) - ->method('passwordResetConfirmation') - ->willReturnSelf(); - - $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); - - $this->assertTrue($this->accountManagement->initiatePasswordReset($email, $template)); - } - - public function testInitiatePasswordResetNoTemplate() - { - $storeId = 1; - $customerId = 1; - - $email = 'test@example.com'; - $template = null; - $templateIdentifier = 'Template Identifier'; - $sender = 'Sender'; - - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); - - $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); - - $this->expectException(\Magento\Framework\Exception\InputException::class); - $this->expectExceptionMessage( - 'Invalid value of "" provided for the template field. Possible values: email_reminder or email_reset.' - ); - $this->accountManagement->initiatePasswordReset($email, $template); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage Invalid value of "" provided for the customerId field - */ - public function testValidateResetPasswordTokenBadCustomerId() - { - $this->accountManagement->validateResetPasswordLinkToken(null, ''); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage resetPasswordLinkToken is a required field - */ - public function testValidateResetPasswordTokenBadResetPasswordLinkToken() - { - $this->accountManagement->validateResetPasswordLinkToken(22, null); - } - - /** - * @expectedException \Magento\Framework\Exception\State\InputMismatchException - * @expectedExceptionMessage Reset password token mismatch - */ - public function testValidateResetPasswordTokenTokenMismatch() - { - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieveSecureData') - ->willReturn($this->customerSecure); - - $this->accountManagement->validateResetPasswordLinkToken(22, 'newStringToken'); - } - - /** - * @expectedException \Magento\Framework\Exception\State\ExpiredException - * @expectedExceptionMessage Reset password token expired - */ - public function testValidateResetPasswordTokenTokenExpired() - { - $this->reInitModel(); - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieveSecureData') - ->willReturn($this->customerSecure); - - $this->accountManagement->validateResetPasswordLinkToken(22, 'newStringToken'); - } - - /** - * return bool - */ - public function testValidateResetPasswordToken() - { - $this->reInitModel(); - - $this->customer - ->expects($this->once()) - ->method('getResetPasswordLinkExpirationPeriod') - ->willReturn(100000); - - $this->customerRegistry->expects($this->atLeastOnce()) - ->method('retrieveSecureData') - ->willReturn($this->customerSecure); - - $this->assertTrue($this->accountManagement->validateResetPasswordLinkToken(22, 'newStringToken')); - } - - /** - * reInit $this->accountManagement object - */ - private function reInitModel() - { - $this->customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->disableOriginalConstructor() - ->setMethods(['getRpToken', 'getRpTokenCreatedAt']) - ->getMock(); - - $this->customerSecure - ->expects($this->any()) - ->method('getRpToken') - ->willReturn('newStringToken'); - - $pastDateTime = '2016-10-25 00:00:00'; - - $this->customerSecure - ->expects($this->any()) - ->method('getRpTokenCreatedAt') - ->willReturn($pastDateTime); - - $this->customer = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) - ->disableOriginalConstructor() - ->setMethods(['getResetPasswordLinkExpirationPeriod']) - ->getMock(); - - $this->prepareDateTimeFactory(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->accountManagement = $this->objectManagerHelper->getObject( - \Magento\Customer\Model\AccountManagement::class, - [ - 'customerFactory' => $this->customerFactory, - 'customerRegistry' => $this->customerRegistry, - 'customerRepository' => $this->customerRepository, - 'customerModel' => $this->customer, - 'dateTimeFactory' => $this->dateTimeFactory, - ] - ); - $reflection = new \ReflectionClass(get_class($this->accountManagement)); - $reflectionProperty = $reflection->getProperty('authentication'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($this->accountManagement, $this->authenticationMock); - } - - /** - * @return void - */ - public function testChangePassword() - { - $customerId = 7; - $email = 'test@example.com'; - $currentPassword = '1234567'; - $newPassword = 'abcdefg'; - $passwordHash = '1a2b3f4c'; - - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMock(); - $customer->expects($this->any()) - ->method('getId') - ->willReturn($customerId); - - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($email) - ->willReturn($customer); - - $this->authenticationMock->expects($this->once()) - ->method('authenticate'); - - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->setMethods(['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash']) - ->disableOriginalConstructor() - ->getMock(); - $customerSecure->expects($this->once()) - ->method('setRpToken') - ->with(null); - $customerSecure->expects($this->once()) - ->method('setRpTokenCreatedAt') - ->willReturnSelf(); - $customerSecure->expects($this->any()) - ->method('getPasswordHash') - ->willReturn($passwordHash); - - $this->customerRegistry->expects($this->any()) - ->method('retrieveSecureData') - ->with($customerId) - ->willReturn($customerSecure); - - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->willReturnMap( - [ - [ - AccountManagement::XML_PATH_MINIMUM_PASSWORD_LENGTH, - 'default', - null, - 7, - ], - [ - AccountManagement::XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER, - 'default', - null, - 1 - ], - ] - ); - $this->string->expects($this->any()) - ->method('strlen') - ->with($newPassword) - ->willReturn(7); - - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customer); + $visitor->expects($this->atLeastOnce())->method('getSessionId') + ->willReturnOnConsecutiveCalls('session_id_1', 'session_id_2'); + $visitorCollection = $this->getMockBuilder( + \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory::class + )->disableOriginalConstructor() + ->setMethods(['addFieldToFilter', 'getItems']) + ->getMock(); + $visitorCollection->expects($this->atLeastOnce())->method('addFieldToFilter')->willReturnSelf(); + $visitorCollection->expects($this->once())->method('getItems')->willReturn([$visitor, $visitor]); + $this->visitorCollectionFactoryMock->expects($this->once())->method('create') + ->willReturn($visitorCollection); + $this->saveHandlerMock->expects($this->atLeastOnce())->method('destroy') + ->withConsecutive( + ['session_id_1'], + ['session_id_2'] + ); $this->assertTrue($this->accountManagement->changePassword($email, $currentPassword, $newPassword)); } - - /** - * @return void - */ - public function testChangePasswordException() - { - $email = 'test@example.com'; - $currentPassword = '1234567'; - $newPassword = 'abcdefg'; - - $exception = new NoSuchEntityException( - new \Magento\Framework\Phrase('Exception message') - ); - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($email) - ->willThrowException($exception); - - $this->expectException( - \Magento\Framework\Exception\InvalidEmailOrPasswordException::class, - 'Invalid login or password.' - ); - - $this->accountManagement->changePassword($email, $currentPassword, $newPassword); - } - - /** - * @return void - */ - public function testAuthenticate() - { - $username = 'login'; - $password = '1234567'; - $passwordHash = '1a2b3f4c'; - - $customerData = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMock(); - - $customerModel = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - $customerModel->expects($this->once()) - ->method('updateData') - ->willReturn($customerModel); - - $this->customerRepository - ->expects($this->once()) - ->method('get') - ->with($username) - ->willReturn($customerData); - - $this->authenticationMock->expects($this->once()) - ->method('authenticate'); - - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->setMethods(['getPasswordHash']) - ->disableOriginalConstructor() - ->getMock(); - $customerSecure->expects($this->any()) - ->method('getPasswordHash') - ->willReturn($passwordHash); - - $this->customerRegistry->expects($this->any()) - ->method('retrieveSecureData') - ->willReturn($customerSecure); - - $this->customerFactory->expects($this->once()) - ->method('create') - ->willReturn($customerModel); - - $this->manager->expects($this->exactly(2)) - ->method('dispatch') - ->withConsecutive( - [ - 'customer_customer_authenticated', - ['model' => $customerModel, 'password' => $password] - ], - [ - 'customer_data_object_login', ['customer' => $customerData] - ] - ); - - $this->assertEquals($customerData, $this->accountManagement->authenticate($username, $password)); - } - - /** - * @param string|null $skipConfirmationIfEmail - * @param int $isConfirmationRequired - * @param string|null $confirmation - * @param string $expected - * @dataProvider dataProviderGetConfirmationStatus - */ - public function testGetConfirmationStatus( - $skipConfirmationIfEmail, - $isConfirmationRequired, - $confirmation, - $expected - ) { - $websiteId = 1; - $customerId = 1; - $customerEmail = 'test1@example.com'; - - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $customerMock->expects($this->once()) - ->method('getId') - ->willReturn($customerId); - $customerMock->expects($this->any()) - ->method('getConfirmation') - ->willReturn($confirmation); - $customerMock->expects($this->any()) - ->method('getEmail') - ->willReturn($customerEmail); - $customerMock->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($websiteId); - - $this->registry->expects($this->once()) - ->method('registry') - ->with('skip_confirmation_if_email') - ->willReturn($skipConfirmationIfEmail); - - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->with(AccountManagement::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, $websiteId) - ->willReturn($isConfirmationRequired); - - $this->customerRepository->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $this->assertEquals($expected, $this->accountManagement->getConfirmationStatus($customerId)); - } - - /** - * @return array - */ - public function dataProviderGetConfirmationStatus() - { - return [ - [null, 0, null, AccountManagement::ACCOUNT_CONFIRMATION_NOT_REQUIRED], - ['test1@example.com', 0, null, AccountManagement::ACCOUNT_CONFIRMATION_NOT_REQUIRED], - ['test2@example.com', 0, null, AccountManagement::ACCOUNT_CONFIRMATION_NOT_REQUIRED], - ['test2@example.com', 1, null, AccountManagement::ACCOUNT_CONFIRMED], - ['test2@example.com', 1, 'test', AccountManagement::ACCOUNT_CONFIRMATION_REQUIRED], - ]; - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - */ - public function testCreateAccountWithPasswordHashForGuest() - { - $storeId = 1; - $storeName = 'store_name'; - $websiteId = 1; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $storeMock->expects($this->once()) - ->method('getId') - ->willReturn($storeId); - $storeMock->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $storeMock->expects($this->once()) - ->method('getName') - ->willReturn($storeName); - - $this->storeManager->expects($this->exactly(3)) - ->method('getStore') - ->willReturn($storeMock); - - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - $customerMock->expects($this->exactly(2)) - ->method('getId') - ->willReturn(null); - $customerMock->expects($this->exactly(3)) - ->method('getStoreId') - ->willReturn(null); - $customerMock->expects($this->exactly(2)) - ->method('getWebsiteId') - ->willReturn(null); - $customerMock->expects($this->once()) - ->method('setStoreId') - ->with($storeId) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('setWebsiteId') - ->with($websiteId) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('setCreatedIn') - ->with($storeName) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('getAddresses') - ->willReturn(null); - $customerMock->expects($this->once()) - ->method('setAddresses') - ->with(null) - ->willReturnSelf(); - - $this->customerRepository - ->expects($this->once()) - ->method('save') - ->with($customerMock, $hash) - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Exception message'))); - - $this->accountManagement->createAccountWithPasswordHash($customerMock, $hash); - } - - public function testCreateAccountWithPasswordHashWithCustomerAddresses() - { - $websiteId = 1; - $addressId = 2; - $customerId = null; - $storeId = 1; - $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - - $this->prepareDateTimeFactory(); - - //Handle store - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)->disableOriginalConstructor()->getMock(); - $store->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($websiteId); - //Handle address - existing and non-existing. Non-Existing should return null when call getId method - $existingAddress = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $nonExistingAddress = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - //Ensure that existing address is not in use - $this->addressRepository - ->expects($this->atLeastOnce()) - ->method("save") - ->withConsecutive( - [$this->logicalNot($this->identicalTo($existingAddress))], - [$this->identicalTo($nonExistingAddress)] - ); - - $existingAddress - ->expects($this->any()) - ->method("getId") - ->willReturn($addressId); - //Expects that id for existing address should be unset - $existingAddress - ->expects($this->once()) - ->method("setId") - ->with(null); - //Handle Customer calls - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); - $customer - ->expects($this->atLeastOnce()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $customer - ->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); - $customer - ->expects($this->any()) - ->method("getId") - ->willReturn($customerId); - //Return Customer from customer repository - $this->customerRepository - ->expects($this->atLeastOnce()) - ->method('save') - ->willReturn($customer); - $this->customerRepository - ->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customer); - $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) - ->setMethods(['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash']) - ->disableOriginalConstructor() - ->getMock(); - $customerSecure->expects($this->once()) - ->method('setRpToken') - ->with($hash); - - $customerSecure->expects($this->any()) - ->method('getPasswordHash') - ->willReturn($hash); - - $this->customerRegistry->expects($this->any()) - ->method('retrieveSecureData') - ->with($customerId) - ->willReturn($customerSecure); - - $this->random->expects($this->once()) - ->method('getUniqueHash') - ->willReturn($hash); - - $customer - ->expects($this->atLeastOnce()) - ->method('getAddresses') - ->willReturn([$existingAddress, $nonExistingAddress]); - - $this->storeManager - ->expects($this->atLeastOnce()) - ->method('getStore') - ->willReturn($store); - - $this->assertSame($customer, $this->accountManagement->createAccountWithPasswordHash($customer, $hash)); - } - - /** - * @return string - */ - private function prepareDateTimeFactory() - { - $dateTime = '2017-10-25 18:57:08'; - $timestamp = '1508983028'; - $dateTimeMock = $this->createMock(\DateTime::class); - $dateTimeMock->expects($this->any()) - ->method('format') - ->with(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) - ->willReturn($dateTime); - - $dateTimeMock - ->expects($this->any()) - ->method('getTimestamp') - ->willReturn($timestamp); - - $this->dateTimeFactory - ->expects($this->any()) - ->method('create') - ->willReturn($dateTimeMock); - - return $dateTime; - } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php index 2eef9a44cab74..0a94c94b95b88 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php @@ -6,6 +6,8 @@ namespace Magento\Customer\Test\Unit\Model\Address; +use Magento\Store\Model\ScopeInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -41,6 +43,9 @@ class AbstractAddressTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Customer\Model\Address\AbstractAddress */ protected $model; + /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ + private $objectManager; + protected function setUp() { $this->contextMock = $this->createMock(\Magento\Framework\Model\Context::class); @@ -69,8 +74,17 @@ protected function setUp() $this->resourceCollectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection\AbstractDb::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $objectManager->getObject( + $this->allowedCountriesReaderMock = $this->createPartialMock( + \Magento\Directory\Model\AllowedCountries::class, + ['getAllowedCountries'] + ); + $this->shareConfigMock = $this->createPartialMock( + \Magento\Customer\Model\Config\Share::class, + ['isGlobalScope'] + ); + + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $this->objectManager->getObject( \Magento\Customer\Model\Address\AbstractAddress::class, [ 'context' => $this->contextMock, @@ -81,7 +95,8 @@ protected function setUp() 'regionFactory' => $this->regionFactoryMock, 'countryFactory' => $this->countryFactoryMock, 'resource' => $this->resourceMock, - 'resourceCollection' => $this->resourceCollectionMock + 'resourceCollection' => $this->resourceCollectionMock, + 'escaper' => $this->objectManager->getObject(\Magento\Framework\Escaper::class) ] ); } @@ -275,13 +290,14 @@ public function testSetDataWithObject() } /** - * @param $data - * @param $expected + * @param array $data + * @param array|bool $expected * * @dataProvider validateDataProvider */ - public function testValidate($data, $expected) + public function testValidate(array $data, $expected) { + $countryId = isset($data['country_id']) ? $data['country_id'] : null; $attributeMock = $this->createMock(\Magento\Eav\Model\Entity\Attribute::class); $attributeMock->expects($this->any()) ->method('getIsRequired') @@ -295,8 +311,54 @@ public function testValidate($data, $expected) ->method('getCountriesWithOptionalZip') ->will($this->returnValue([])); - $this->directoryDataMock->expects($this->never()) - ->method('isRegionRequired'); + $this->directoryDataMock->expects($this->any()) + ->method('isRegionRequired') + ->willReturn($data['region_required']); + + $countryCollectionMock = $this->getMockBuilder(\Magento\Directory\Model\ResourceModel\Country\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); + + $this->directoryDataMock->method('getCountryCollection') + ->willReturn($countryCollectionMock); + + $countryCollectionMock->method('getAllIds') + ->willReturn([$countryId]); + + $regionModelMock = $this->getMockBuilder(\Magento\Directory\Model\Region::class) + ->disableOriginalConstructor() + ->setMethods(['getCountryId', 'getName', 'load', 'loadByCode']) + ->getMock(); + + $this->regionFactoryMock->expects($this->any())->method('create')->willReturn($regionModelMock); + + $regionModelMock->expects($this->any())->method('load')->with($data['region_id'])->willReturnSelf(); + $regionModelMock->expects($this->any())->method('getCountryId')->willReturn($countryId); + $regionModelMock->expects($this->any())->method('getName')->willReturn($data['region']); + $regionModelMock->expects($this->any()) + ->method('loadByCode') + ->with($data['region'], $countryId) + ->willReturnSelf(); + + $countryModelMock = $this->getMockBuilder(\Magento\Directory\Model\Country::class) + ->disableOriginalConstructor() + ->setMethods(['getRegionCollection', 'load']) + ->getMock(); + + $this->objectManager->setBackwardCompatibleProperty( + $this->model, + '_countryModels', + [$countryId => $countryModelMock] + ); + + $countryModelMock->expects($this->any())->method('load')->with($countryId, null)->willReturnSelf(); + $regionCollectionMock = $this->getMockBuilder(\Magento\Directory\Model\ResourceModel\Region\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); + $countryModelMock->expects($this->any())->method('getRegionCollection')->willReturn($regionCollectionMock); + $regionCollectionMock->expects($this->any())->method('getAllIds')->willReturn($data['allowed_regions']); foreach ($data as $key => $value) { $this->model->setData($key, $value); @@ -321,8 +383,11 @@ public function validateDataProvider() 'country_id' => $countryId, 'postcode' => 07201, 'region_id' => 1, + 'region' => 'RegionName', + 'region_required' => false, 'company' => 'Magento', - 'fax' => '222-22-22' + 'fax' => '222-22-22', + 'allowed_regions' => ['1'], ]; return [ 'firstname' => [ @@ -349,6 +414,30 @@ public function validateDataProvider() array_merge(array_diff_key($data, ['postcode' => '']), ['country_id' => $countryId++]), ['postcode is a required field.'], ], + 'region' => [ + array_merge( + $data, + [ + 'region_required' => true, + 'country_id' => $countryId++, + 'allowed_regions' => [], + 'region' => '', + ] + ), + ['region is a required field.'], + ], + 'region_id1' => [ + array_merge($data, ['country_id' => $countryId, 'region_required' => true, 'region_id' => '']), + ['regionId is a required field.'], + ], + 'region_id2' => [ + array_merge($data, ['country_id' => $countryId, 'region_id' => 2, 'allowed_regions' => []]), + true, + ], + 'region_id3' => [ + array_merge($data, ['country_id' => $countryId, 'region_id' => 2, 'allowed_regions' => [1, 3]]), + ['Invalid value of "2" provided for the regionId field.'], + ], 'country_id' => [ array_diff_key($data, ['country_id' => '']), ['countryId is a required field.'], @@ -388,4 +477,13 @@ public function getStreetFullDataProvider() ['single line', 'single line'], ]; } + + protected function tearDown() + { + $this->objectManager->setBackwardCompatibleProperty( + $this->model, + '_countryModels', + [] + ); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php index 1b013a913b9f8..c64f7aca96fe6 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php @@ -39,6 +39,9 @@ public function testExemplarXml($fixtureXml, array $expectedErrors) $this->assertEquals($expectedErrors, $actualErrors); } + /** + * @return array + */ public function exemplarXmlDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php b/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php index 480f5be96e318..8454b793cf5ff 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AttributeCheckerTest.php @@ -75,6 +75,9 @@ public function testIsAttributeAllowedOnForm( $this->assertEquals($isAllowed, $this->model->isAttributeAllowedOnForm($attributeCode, $formName)); } + /** + * @return array + */ public function attributeOnFormDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/AttributeTest.php b/app/code/Magento/Customer/Test/Unit/Model/AttributeTest.php index 12421eef519ed..dc8edb031bcb1 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AttributeTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AttributeTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Model; use Magento\Customer\Model\Attribute; @@ -143,8 +141,8 @@ protected function setUp() ->getMock(); $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) ->getMock(); - $this->extensionAttributesFactory = $this->getMockBuilder( - \Magento\Framework\Api\ExtensionAttributesFactory::class) + $this->extensionAttributesFactory = + $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesFactory::class) ->disableOriginalConstructor() ->getMock(); $this->attributeValueFactoryMock = $this->getMockBuilder(\Magento\Framework\Api\AttributeValueFactory::class) @@ -176,13 +174,15 @@ protected function setUp() ->getMock(); $this->timezoneMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) ->getMock(); - $this->reservedAttributeListMock = $this->getMockBuilder( - \Magento\Catalog\Model\Product\ReservedAttributeList::class) + $this->reservedAttributeListMock = + $this->getMockBuilder(\Magento\Catalog\Model\Product\ReservedAttributeList::class) ->disableOriginalConstructor() ->getMock(); $this->resolverMock = $this->getMockBuilder(\Magento\Framework\Locale\ResolverInterface::class) ->getMock(); - $this->dateTimeFormatter = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface::class); + $this->dateTimeFormatter = $this->createMock( + \Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface::class + ); $this->resourceMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\AbstractResource::class) ->setMethods(['_construct', 'getConnection', 'getIdFieldName', 'saveInSetIncluding']) diff --git a/app/code/Magento/Customer/Test/Unit/Model/AuthenticationTest.php b/app/code/Magento/Customer/Test/Unit/Model/AuthenticationTest.php index ee788913373e5..14adc7bcf8795 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AuthenticationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AuthenticationTest.php @@ -196,6 +196,9 @@ public function testProcessAuthenticationFailureFirstAttempt( $this->authentication->processAuthenticationFailure($customerId); } + /** + * @return array + */ public function processAuthenticationFailureDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Checkout/ConfigProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Checkout/ConfigProviderTest.php index 011ba9091eaf2..58b099a1d387d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Checkout/ConfigProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Checkout/ConfigProviderTest.php @@ -41,6 +41,11 @@ class ConfigProviderTest extends \PHPUnit\Framework\TestCase */ protected $store; + /** + * @var Url|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerUrl; + protected function setUp() { $this->storeManager = $this->getMockForAbstractClass( @@ -49,12 +54,14 @@ protected function setUp() '', false ); + $this->urlBuilder = $this->getMockForAbstractClass( \Magento\Framework\UrlInterface::class, [], '', false ); + $this->scopeConfig = $this->getMockForAbstractClass( \Magento\Framework\App\Config\ScopeConfigInterface::class, [], @@ -71,10 +78,13 @@ protected function setUp() ['getBaseUrl'] ); + $this->customerUrl = $this->createMock(\Magento\Customer\Model\Url::class); + $this->provider = new ConfigProvider( $this->urlBuilder, $this->storeManager, - $this->scopeConfig + $this->scopeConfig, + $this->customerUrl ); } @@ -83,9 +93,8 @@ public function testGetConfigWithoutRedirect() $loginUrl = 'http://url.test/customer/login'; $baseUrl = 'http://base-url.test'; - $this->urlBuilder->expects($this->exactly(2)) - ->method('getUrl') - ->with(Url::ROUTE_ACCOUNT_LOGIN) + $this->customerUrl->expects($this->exactly(2)) + ->method('getLoginUrl') ->willReturn($loginUrl); $this->storeManager->expects($this->once()) ->method('getStore') @@ -112,9 +121,8 @@ public function testGetConfig() $loginUrl = 'http://base-url.test/customer/login'; $baseUrl = 'http://base-url.test'; - $this->urlBuilder->expects($this->exactly(2)) - ->method('getUrl') - ->with(Url::ROUTE_ACCOUNT_LOGIN) + $this->customerUrl->expects($this->exactly(2)) + ->method('getLoginUrl') ->willReturn($loginUrl); $this->storeManager->expects($this->once()) ->method('getStore') diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/PasswordTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/PasswordTest.php index 9a9449a64ecbd..368e7cfd47f2f 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/PasswordTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/PasswordTest.php @@ -39,6 +39,9 @@ public function testValidatePositive() $this->assertTrue($this->testable->validate($object)); } + /** + * @return array + */ public function passwordNegativeDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Source/WebsiteTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Source/WebsiteTest.php index 11e3d602ddf90..2f35ab9be4804 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Source/WebsiteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Source/WebsiteTest.php @@ -7,6 +7,8 @@ use Magento\Customer\Model\Customer\Attribute\Source\Website; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManagerInterface; class WebsiteTest extends \PHPUnit\Framework\TestCase { @@ -22,6 +24,9 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Store\Model\System\Store|\PHPUnit_Framework_MockObject_MockObject */ protected $storeMock; + /** @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $objectManagerMock; + protected function setUp() { $this->collectionFactoryMock = @@ -36,6 +41,20 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->setMethods(['get']) + ->getMockForAbstractClass(); + + $escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + + ObjectManager::setInstance($this->objectManagerMock); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(\Magento\Framework\Escaper::class) + ->willReturn($escaper); + $this->model = new Website( $this->collectionFactoryMock, $this->optionFactoryMock, @@ -91,4 +110,12 @@ public function testGetOptionTextWithoutOption() $this->assertEquals(false, $this->model->getOptionText('value')); } + + protected function tearDown() + { + $property = (new \ReflectionClass(ObjectManager::class))->getProperty('_instance'); + $property->setAccessible(true); + $property->setValue(null, null); + parent::tearDown(); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 029949c5f35b0..ede4ea3fa96de 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -6,27 +6,46 @@ namespace Magento\Customer\Test\Unit\Model\Customer; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Attribute; use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Customer\DataProvider as CustomerDataProvider; +use Magento\Customer\Model\FileProcessor; +use Magento\Customer\Model\FileProcessorFactory; use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; +use Magento\Customer\Model\ResourceModel\Customer\Collection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; use Magento\Eav\Model\Entity\Type; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; /** - * Class DataProviderTest - * - * Test for class \Magento\Customer\Model\Customer\DataProvider + * Unit tests for \Magento\Customer\Model\Customer\DataProvider class. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ class DataProviderTest extends \PHPUnit\Framework\TestCase { const ATTRIBUTE_CODE = 'test-code'; - const OPTIONS_RESULT = 'test-options'; + const OPTIONS_RESULT = [ + [ + 'label' => 'label-1', + 'value' => 'value-1' + ], + [ + 'label' => 'label-2', + 'value' => 'value-2' + ], + ]; /** * @var Config|\PHPUnit_Framework_MockObject_MockObject @@ -44,17 +63,17 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase protected $eavValidationRulesMock; /** - * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $sessionMock; /** - * @var \Magento\Customer\Model\FileProcessorFactory|\PHPUnit_Framework_MockObject_MockObject + * @var FileProcessorFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $fileProcessorFactory; /** - * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject + * @var FileProcessor|\PHPUnit_Framework_MockObject_MockObject */ protected $fileProcessor; @@ -65,34 +84,34 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + $this->eavConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); $this->customerCollectionFactoryMock = $this->createPartialMock( - \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class, + CollectionFactory::class, ['create'] ); $this->eavValidationRulesMock = $this - ->getMockBuilder(\Magento\Ui\DataProvider\EavValidationRules::class) + ->getMockBuilder(EavValidationRules::class) ->disableOriginalConstructor() ->getMock(); $this->sessionMock = $this - ->getMockBuilder(\Magento\Framework\Session\SessionManagerInterface::class) + ->getMockBuilder(SessionManagerInterface::class) ->setMethods(['getCustomerFormData', 'unsCustomerFormData']) ->getMockForAbstractClass(); - $this->fileProcessor = $this->getMockBuilder(\Magento\Customer\Model\FileProcessor::class) + $this->fileProcessor = $this->getMockBuilder(FileProcessor::class) ->disableOriginalConstructor() ->getMock(); - $this->fileProcessorFactory = $this->getMockBuilder(\Magento\Customer\Model\FileProcessorFactory::class) + $this->fileProcessorFactory = $this->getMockBuilder(FileProcessorFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); } /** - * Run test getAttributesMeta method + * Run test getAttributesMeta method. * * @param array $expected * @return void @@ -102,16 +121,16 @@ protected function setUp() public function testGetAttributesMetaWithOptions(array $expected) { $helper = new ObjectManager($this); - /** @var \Magento\Customer\Model\Customer\DataProvider $dataProvider */ + /** @var CustomerDataProvider $dataProvider */ $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), ] ); @@ -127,7 +146,7 @@ public function testGetAttributesMetaWithOptions(array $expected) } /** - * Data provider for testGetAttributesMeta + * Data provider for testGetAttributesMeta. * * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -145,8 +164,19 @@ public function getAttributesMetaDataProvider() 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => null, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + '__disableTmpl' => ['label' => true], + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + '__disableTmpl' => ['label' => true], + ], + ], + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -154,6 +184,7 @@ public function getAttributesMetaDataProvider() 'default' => 'default_value', 'size' => 'multiline_count', 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], @@ -164,7 +195,7 @@ public function getAttributesMetaDataProvider() 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => null, + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -177,6 +208,7 @@ public function getAttributesMetaDataProvider() 'true' => 1, 'false' => 0, ], + '__disableTmpl' => ['label' => true], ], ], ], @@ -191,8 +223,19 @@ public function getAttributesMetaDataProvider() 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => null, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + '__disableTmpl' => ['label' => true], + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + '__disableTmpl' => ['label' => true], + ], + ], + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -200,6 +243,7 @@ public function getAttributesMetaDataProvider() 'default' => 'default_value', 'size' => 'multiline_count', 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], @@ -210,7 +254,7 @@ public function getAttributesMetaDataProvider() 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => null, + 'visible' => false, 'required' => 'is_required', 'label' => 'frontend_label', 'sortOrder' => 'sort_order', @@ -223,6 +267,7 @@ public function getAttributesMetaDataProvider() 'true' => 1, 'false' => 0, ], + '__disableTmpl' => ['label' => true], ], ], ], @@ -233,8 +278,17 @@ public function getAttributesMetaDataProvider() 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => null, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + ], + ], + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -245,15 +299,35 @@ public function getAttributesMetaDataProvider() 'filterBy' => [ 'target' => '${ $.provider }:data.customer.website_id', 'field' => 'website_ids' - ] + ], + '__disableTmpl' => ['label' => true], + ], + ], + ], + ], + 'street' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'multiline', + 'formElement' => 'multiline', + 'visible' => true, + 'required' => '1', + 'label' => __('Multiline address'), + 'sortOrder' => '70', + 'notice' => 'note', + 'default' => 'Default', + 'size' => 2, + 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], - ] + ], ], ], - ] - ] + ], + ], ]; } @@ -262,7 +336,7 @@ public function getAttributesMetaDataProvider() */ protected function getCustomerCollectionFactoryMock() { - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -299,7 +373,7 @@ protected function getEavConfigMock($customerAttributes = []) */ protected function getTypeCustomerMock($customerAttributes = []) { - $typeCustomerMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $typeCustomerMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); $attributesCollection = !empty($customerAttributes) ? $customerAttributes : $this->getAttributeMock(); @@ -324,7 +398,7 @@ protected function getTypeCustomerMock($customerAttributes = []) */ protected function getTypeAddressMock() { - $typeAddressMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $typeAddressMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); @@ -387,7 +461,7 @@ private function injectVisibilityProps( */ protected function getAttributeMock($type = 'customer', $options = []) { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods( [ 'getAttributeCode', @@ -403,7 +477,7 @@ protected function getAttributeMock($type = 'customer', $options = []) ) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $sourceMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class) + $sourceMock = $this->getMockBuilder(AbstractSource::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -431,7 +505,7 @@ protected function getAttributeMock($type = 'customer', $options = []) ->method('getSource') ->willReturn($sourceMock); - $attributeBooleanMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + $attributeBooleanMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods( [ 'getAttributeCode', @@ -477,6 +551,7 @@ protected function getAttributeMock($type = 'customer', $options = []) $this->injectVisibilityProps($attributeMock, $attributeBooleanMock, $options); if ($type == "address") { $mocks[] = $this->getCountryAttrMock(); + $mocks[] = $this->getStreetAttrMock(); } return $mocks; } @@ -493,6 +568,9 @@ private function attributeGetUsingMethodCallback() }; } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getCountryAttrMock() { $countryByWebsiteMock = $this->getMockBuilder(CountryWithWebsites::class) @@ -500,11 +578,11 @@ private function getCountryAttrMock() ->getMock(); $countryByWebsiteMock->expects($this->any()) ->method('getAllOptions') - ->willReturn('test-options'); + ->willReturn(self::OPTIONS_RESULT); $shareMock = $this->getMockBuilder(Share::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $objectManagerMock->expects($this->any()) ->method('get') ->willReturnMap([ @@ -512,7 +590,7 @@ private function getCountryAttrMock() [Share::class, $shareMock], ]); \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); - $countryAttrMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + $countryAttrMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode', 'getDataUsingMethod', 'usesSource', 'getSource', 'getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -541,6 +619,54 @@ function ($origName) { return $countryAttrMock; } + /** + * @return AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + */ + private function getStreetAttrMock() + { + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getAttributeCode', + 'getDataUsingMethod', + 'usesSource', + 'getFrontendInput', + 'getIsVisible', + 'getSource', + 'getIsUserDefined', + 'getUsedInForms', + 'getEntityType', + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $map = [ + ['frontend_input', null, 'multiline'], + ['is_required', null, '1'], + ['frontend_label', null, __('Multiline address')], + ['note', null, 'note'], + ['sort_order', null, '70'], + ['note', null, null], + ['default_value', null, 'Default'], + ['multiline_count', null, 2], + ]; + + $attributeMock->method('getDataUsingMethod') + ->will($this->returnValueMap($map)); + + $attributeMock->method('getAttributeCode') + ->willReturn('street'); + + $attributeMock->method('usesSource') + ->willReturn(false); + + $attributeMock->method('getIsVisible') + ->willReturn(true); + + return $attributeMock; + } + /** * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -561,13 +687,13 @@ public function testGetData() 'street' => "street\nstreet", ]; - $customer = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) + $customer = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); - $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + $address = $this->getMockBuilder(Address::class) ->disableOriginalConstructor() ->getMock(); - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -608,14 +734,14 @@ public function testGetData() $helper = new ObjectManager($this); $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), ] ); @@ -652,9 +778,9 @@ public function testGetData() ], 'default_billing' => 2, 'default_shipping' => 2, - ] - ] - ] + ], + ], + ], ], $dataProvider->getData() ); @@ -688,13 +814,13 @@ public function testGetDataWithCustomerFormData() ], ]; - $customer = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) + $customer = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); - $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + $address = $this->getMockBuilder(Address::class) ->disableOriginalConstructor() ->getMock(); - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -746,14 +872,14 @@ public function testGetDataWithCustomerFormData() $helper = new ObjectManager($this); $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), ] ); @@ -807,7 +933,7 @@ public function testGetDataWithCustomAttributeImage() ], ]; - $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) + $attributeMock = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->getMock(); $attributeMock->expects($this->exactly(2)) @@ -817,14 +943,14 @@ public function testGetDataWithCustomAttributeImage() ->method('getAttributeCode') ->willReturn('img1'); - $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $entityTypeMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); $entityTypeMock->expects($this->once()) ->method('getEntityTypeCode') ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) + $customerMock = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); $customerMock->expects($this->once()) @@ -846,7 +972,7 @@ public function testGetDataWithCustomAttributeImage() ->method('getEntityType') ->willReturn($entityTypeMock); - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $collectionMock->expects($this->once()) @@ -887,14 +1013,14 @@ public function testGetDataWithCustomAttributeImage() $objectManager = new ObjectManager($this); $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), ] ); @@ -927,7 +1053,7 @@ public function testGetDataWithCustomAttributeImageNoData() ], ]; - $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) + $attributeMock = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->getMock(); $attributeMock->expects($this->once()) @@ -937,14 +1063,14 @@ public function testGetDataWithCustomAttributeImageNoData() ->method('getAttributeCode') ->willReturn('img1'); - $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $entityTypeMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); $entityTypeMock->expects($this->once()) ->method('getEntityTypeCode') ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) + $customerMock = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); $customerMock->expects($this->once()) @@ -965,7 +1091,7 @@ public function testGetDataWithCustomAttributeImageNoData() ->method('getEntityType') ->willReturn($entityTypeMock); - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $collectionMock->expects($this->once()) @@ -982,14 +1108,14 @@ public function testGetDataWithCustomAttributeImageNoData() $objectManager = new ObjectManager($this); $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), ] ); @@ -1019,7 +1145,7 @@ public function testGetAttributesMetaWithCustomAttributeImage() $attributeCode = 'img1'; - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) + $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $collectionMock->expects($this->once()) @@ -1030,7 +1156,7 @@ public function testGetAttributesMetaWithCustomAttributeImage() ->method('create') ->willReturn($collectionMock); - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods([ 'getAttributeCode', 'getFrontendInput', @@ -1052,7 +1178,7 @@ function ($origName) { } ); - $typeCustomerMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $typeCustomerMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); $typeCustomerMock->expects($this->once()) @@ -1062,7 +1188,7 @@ function ($origName) { ->method('getEntityTypeCode') ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - $typeAddressMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + $typeAddressMock = $this->getMockBuilder(Type::class) ->disableOriginalConstructor() ->getMock(); $typeAddressMock->expects($this->once()) @@ -1090,6 +1216,7 @@ function ($origName) { 'default' => 'default_value', 'size' => 'multiline_count', 'label' => __('frontend_label'), + '__disableTmpl' => ['label' => true], ]) ->willReturn([ 'max_file_size' => $maxFileSize, @@ -1105,7 +1232,7 @@ function ($origName) { $objectManager = new ObjectManager($this); $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', @@ -1128,6 +1255,7 @@ function ($origName) { 'arguments' => [ 'data' => [ 'config' => [ + 'dataType' => 'frontend_input', 'formElement' => 'fileUploader', 'componentType' => 'fileUploader', 'maxFileSize' => $maxFileSize, @@ -1137,7 +1265,7 @@ function ($origName) { ], 'sortOrder' => 'sort_order', 'required' => 'is_required', - 'visible' => null, + 'visible' => false, 'validation' => [ 'max_file_size' => $maxFileSize, 'file_extensions' => 'ext1, eXt2 ', @@ -1170,14 +1298,14 @@ public function testGetDataWithVisibleAttributes() 'visible' => true, 'is_used_in_forms' => ['customer_account_edit'], 'user_defined' => true, - 'specific_code_prefix' => "_1" + 'specific_code_prefix' => "_1", ], 'test-code-boolean' => [ 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => true, - 'specific_code_prefix' => "_1" - ] + 'specific_code_prefix' => "_1", + ], ] ); $secondAttributesBundle = $this->getAttributeMock( @@ -1187,28 +1315,28 @@ public function testGetDataWithVisibleAttributes() 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => false, - 'specific_code_prefix' => "_2" + 'specific_code_prefix' => "_2", ], 'test-code-boolean' => [ 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => true, - 'specific_code_prefix' => "_2" - ] + 'specific_code_prefix' => "_2", + ], ] ); $helper = new ObjectManager($this); - /** @var \Magento\Customer\Model\Customer\DataProvider $dataProvider */ + /** @var CustomerDataProvider $dataProvider */ $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) + 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), ] ); @@ -1235,14 +1363,14 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() 'visible' => true, 'is_used_in_forms' => ['customer_account_edit'], 'user_defined' => true, - 'specific_code_prefix' => "_1" + 'specific_code_prefix' => "_1", ], 'test-code-boolean' => [ 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => true, - 'specific_code_prefix' => "_1" - ] + 'specific_code_prefix' => "_1", + ], ] ); $secondAttributesBundle = $this->getAttributeMock( @@ -1252,28 +1380,28 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => false, - 'specific_code_prefix' => "_2" + 'specific_code_prefix' => "_2", ], 'test-code-boolean' => [ 'visible' => true, 'is_used_in_forms' => ['customer_account_create'], 'user_defined' => true, - 'specific_code_prefix' => "_2" - ] + 'specific_code_prefix' => "_2", + ], ] ); $helper = new ObjectManager($this); - $context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + $context = $this->getMockBuilder(ContextInterface::class) ->setMethods(['getRequestParam']) - ->getMockforAbstractClass(); + ->getMockForAbstractClass(); $context->expects($this->any()) ->method('getRequestParam') ->with('request-field-name') ->willReturn(1); - /** @var \Magento\Customer\Model\Customer\DataProvider $dataProvider */ + /** @var CustomerDataProvider $dataProvider */ $dataProvider = $helper->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, + CustomerDataProvider::class, [ 'name' => 'test-name', 'primaryFieldName' => 'primary-field-name', @@ -1281,7 +1409,7 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), 'context' => $context, - 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) + 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), ] ); $helper->setBackwardCompatibleProperty( @@ -1292,16 +1420,15 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); - $this->assertEquals($this->getExpectationForVisibleAttributes(false), $meta); + $this->assertEquals($this->getExpectationForVisibleAttributes(), $meta); } /** - * Retrieve all customer variations of attributes with all variations of visibility + * Retrieve all customer variations of attributes with all variations of visibility. * - * @param bool $isRegistration * @return array */ - private function getCustomerAttributeExpectations($isRegistration) + private function getCustomerAttributeExpectations() { return [ self::ATTRIBUTE_CODE . "_1" => [ @@ -1310,8 +1437,19 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => !$isRegistration, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + '__disableTmpl' => ['label' => true], + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + '__disableTmpl' => ['label' => true], + ], + ], + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1319,6 +1457,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'default' => 'default_value', 'size' => 'multiline_count', 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], @@ -1329,7 +1468,18 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + '__disableTmpl' => ['label' => true], + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + '__disableTmpl' => ['label' => true], + ], + ], 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), @@ -1338,6 +1488,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'default' => 'default_value', 'size' => 'multiline_count', 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], @@ -1348,7 +1499,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => $isRegistration, + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1361,6 +1512,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'true' => 1, 'false' => 0, ], + '__disableTmpl' => ['label' => true], ], ], ], @@ -1371,7 +1523,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => $isRegistration, + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1384,6 +1536,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'true' => 1, 'false' => 0, ], + '__disableTmpl' => ['label' => true], ], ], ], @@ -1392,16 +1545,15 @@ private function getCustomerAttributeExpectations($isRegistration) } /** - * Retrieve all variations of attributes with all variations of visibility + * Retrieve all variations of attributes with all variations of visibility. * - * @param bool $isRegistration * @return array */ - private function getExpectationForVisibleAttributes($isRegistration = true) + private function getExpectationForVisibleAttributes() { return [ 'customer' => [ - 'children' => $this->getCustomerAttributeExpectations($isRegistration), + 'children' => $this->getCustomerAttributeExpectations(), ], 'address' => [ 'children' => [ @@ -1411,8 +1563,19 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => null, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + '__disableTmpl' => ['label' => true], + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + '__disableTmpl' => ['label' => true], + ], + ], + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1420,6 +1583,7 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'default' => 'default_value', 'size' => 'multiline_count', 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], ], ], ], @@ -1430,7 +1594,7 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => null, + 'visible' => false, 'required' => 'is_required', 'label' => 'frontend_label', 'sortOrder' => 'sort_order', @@ -1443,6 +1607,7 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'true' => 1, 'false' => 0, ], + '__disableTmpl' => ['label' => true], ], ], ], @@ -1453,8 +1618,17 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'options' => 'test-options', - 'visible' => null, + 'options' => [ + [ + 'label' => 'label-1', + 'value' => 'value-1', + ], + [ + 'label' => 'label-2', + 'value' => 'value-2', + ], + ], + 'visible' => false, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1465,11 +1639,31 @@ private function getExpectationForVisibleAttributes($isRegistration = true) 'filterBy' => [ 'target' => '${ $.provider }:data.customer.website_id', 'field' => 'website_ids' - ] + ], + '__disableTmpl' => ['label' => true], ], ], ], - ] + ], + 'street' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'multiline', + 'formElement' => 'multiline', + 'visible' => true, + 'required' => '1', + 'label' => __('Multiline address'), + 'sortOrder' => '70', + 'notice' => 'note', + 'default' => 'Default', + 'size' => 2, + 'componentType' => Field::NAME, + '__disableTmpl' => ['label' => true], + ], + ], + ], + ], ], ], ]; diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php index a1a243066bb7d..81a612c519f52 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerAuthUpdateTest.php @@ -5,7 +5,14 @@ */ namespace Magento\Customer\Test\Unit\Model; +use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Data\CustomerSecure; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** * Class CustomerAuthUpdateTest @@ -18,17 +25,22 @@ class CustomerAuthUpdateTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Customer\Model\CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject + * @var CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject */ protected $customerRegistry; /** - * @var \Magento\Customer\Model\ResourceModel\Customer|\PHPUnit_Framework_MockObject_MockObject + * @var CustomerResourceModel|\PHPUnit_Framework_MockObject_MockObject */ protected $customerResourceModel; /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var CustomerModel|\PHPUnit_Framework_MockObject_MockObject + */ + protected $customerModel; + + /** + * @var ObjectManager */ protected $objectManager; @@ -37,32 +49,36 @@ class CustomerAuthUpdateTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->objectManager = new ObjectManager($this); $this->customerRegistry = - $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); + $this->createMock(CustomerRegistry::class); $this->customerResourceModel = - $this->createMock(\Magento\Customer\Model\ResourceModel\Customer::class); + $this->createMock(CustomerResourceModel::class); + $this->customerModel = + $this->createMock(CustomerModel::class); $this->model = $this->objectManager->getObject( - \Magento\Customer\Model\CustomerAuthUpdate::class, + CustomerAuthUpdate::class, [ 'customerRegistry' => $this->customerRegistry, 'customerResourceModel' => $this->customerResourceModel, + 'customerModel' => $this->customerModel ] ); } /** * test SaveAuth + * @throws NoSuchEntityException */ public function testSaveAuth() { $customerId = 1; - $customerSecureMock = $this->createMock(\Magento\Customer\Model\Data\CustomerSecure::class); + $customerSecureMock = $this->createMock(CustomerSecure::class); - $dbAdapter = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $dbAdapter = $this->createMock(AdapterInterface::class); $this->customerRegistry->expects($this->once()) ->method('retrieveSecureData') @@ -98,6 +114,9 @@ public function testSaveAuth() $customerId ); + $this->customerModel->expects($this->once()) + ->method('reindex'); + $this->model->saveAuth($customerId); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php index 8b3f7875e3c97..e63e4371e628c 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php @@ -12,10 +12,12 @@ namespace Magento\Customer\Test\Unit\Model; use Magento\Customer\Model\Customer; -use Magento\Store\Model\ScopeInterface; +use Magento\Customer\Model\AccountConfirmation; +use Magento\Framework\Math\Random; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class CustomerTest extends \PHPUnit\Framework\TestCase { @@ -63,6 +65,19 @@ class CustomerTest extends \PHPUnit\Framework\TestCase */ private $dataObjectProcessor; + /** + * @var AccountConfirmation|\PHPUnit_Framework_MockObject_MockObject + */ + private $accountConfirmation; + + /** + * @var Random|\PHPUnit_Framework_MockObject_MockObject + */ + private $mathRandom; + + /** + * @inheritdoc + */ protected function setUp() { $this->_website = $this->createMock(\Magento\Store\Model\Website::class); @@ -94,6 +109,8 @@ protected function setUp() $this->registryMock = $this->createPartialMock(\Magento\Framework\Registry::class, ['registry']); $this->_encryptor = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->mathRandom = $this->createMock(Random::class); $this->_model = $helper->getObject( \Magento\Customer\Model\Customer::class, [ @@ -105,7 +122,9 @@ protected function setUp() 'attributeFactory' => $this->attributeFactoryMock, 'registry' => $this->registryMock, 'resource' => $this->resourceMock, - 'dataObjectProcessor' => $this->dataObjectProcessor + 'dataObjectProcessor' => $this->dataObjectProcessor, + 'accountConfirmation' => $this->accountConfirmation, + 'mathRandom' => $this->mathRandom, ] ); } @@ -215,32 +234,27 @@ public function isCustomerLockedDataProvider() /** * @param int $customerId * @param int $websiteId - * @param string|null $skipConfirmationIfEmail + * @param bool $isConfirmationRequired * @param bool $expected * @dataProvider dataProviderIsConfirmationRequired */ public function testIsConfirmationRequired( $customerId, $websiteId, - $skipConfirmationIfEmail, + $isConfirmationRequired, $expected ) { $customerEmail = 'test1@example.com'; - $this->registryMock->expects($this->any()) - ->method('registry') - ->with('skip_confirmation_if_email') - ->willReturn($skipConfirmationIfEmail); - - $this->_scopeConfigMock->expects($this->any()) - ->method('getValue') - ->with(Customer::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, $websiteId) - ->willReturn($expected); - $this->_model->setData('id', $customerId); $this->_model->setData('website_id', $websiteId); $this->_model->setData('email', $customerEmail); + $this->accountConfirmation->expects($this->once()) + ->method('isConfirmationRequired') + ->with($websiteId, $customerId, $customerEmail) + ->willReturn($isConfirmationRequired); + $this->assertEquals($expected, $this->_model->isConfirmationRequired()); } @@ -250,12 +264,9 @@ public function testIsConfirmationRequired( public function dataProviderIsConfirmationRequired() { return [ - [null, null, null, false], - [1, 1, null, false], - [1, 1, 'test1@example.com', false], - [1, 1, 'test2@example.com', true], - [1, 0, 'test2@example.com', true], - [1, null, 'test2@example.com', true], + [null, null, false, false], + [1, 1, true, true], + [1, null, true, true], ]; } @@ -308,9 +319,23 @@ public function testUpdateData() } $expectedResult[$attribute->getAttributeCode()] = $attribute->getValue(); - $expectedResult['attribute_set_id'] = - \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; $this->assertEquals($this->_model->getData(), $expectedResult); } + + /** + * Check getRandomConfirmationKey use cryptographically secure function + * + * @return void + */ + public function testGetRandomConfirmationKey() + { + $this->mathRandom + ->expects($this->once()) + ->method('getRandomString') + ->with(32) + ->willReturn('random_string'); + + $this->_model->getRandomConfirmationKey(); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php index 0240b7ab29ab7..33757b82db891 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Test\Unit\Model; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\EmailNotification; use Magento\Framework\App\Area; +use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Store\Model\ScopeInterface; /** @@ -47,7 +50,7 @@ class EmailNotificationTest extends \PHPUnit\Framework\TestCase private $customerSecureMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var \Magento\Framework\App\Config\ScopeConfigInterface | \PHPUnit_Framework_MockObject_MockObject */ private $scopeConfigMock; @@ -61,6 +64,11 @@ class EmailNotificationTest extends \PHPUnit\Framework\TestCase */ private $model; + /** + * @var SenderResolverInterface | \PHPUnit_Framework_MockObject_MockObject + */ + private $senderResolverMock; + public function setUp() { $this->customerRegistryMock = $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); @@ -88,17 +96,23 @@ public function setUp() $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $this->senderResolverMock = $this->getMockBuilder(SenderResolverInterface::class) + ->setMethods(['resolve']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManager->getObject( EmailNotification::class, [ - 'customerRegistry' => $this->customerRegistryMock, + 'customerRegistry' => $this->customerRegistryMock, 'storeManager' => $this->storeManagerMock, 'transportBuilder' => $this->transportBuilderMock, 'customerViewHelper' => $this->customerViewHelperMock, 'dataProcessor' => $this->dataProcessorMock, - 'scopeConfig' => $this->scopeConfigMock + 'scopeConfig' => $this->scopeConfigMock, + 'senderResolver' => $this->senderResolverMock ] ); } @@ -121,7 +135,10 @@ public function testCredentialsChanged($testNumber, $oldEmail, $newEmail, $isPas $customerName = 'Customer Name'; $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; + $senderValues = ['name' => $sender, 'email' => $sender]; + $expects = $this->once(); + $xmlPathTemplate = EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE; switch ($testNumber) { case 1: $xmlPathTemplate = EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE; @@ -137,7 +154,14 @@ public function testCredentialsChanged($testNumber, $oldEmail, $newEmail, $isPas break; } - $origCustomer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->senderResolverMock + ->expects($expects) + ->method('resolve') + ->with($sender, $customerStoreId) + ->willReturn($senderValues); + + /** @var \PHPUnit_Framework_MockObject_MockObject $origCustomer */ + $origCustomer = $this->createMock(CustomerInterface::class); $origCustomer->expects($this->any()) ->method('getStoreId') ->willReturn(0); @@ -175,7 +199,7 @@ public function testCredentialsChanged($testNumber, $oldEmail, $newEmail, $isPas $this->dataProcessorMock->expects(clone $expects) ->method('buildOutputDataArray') - ->with($origCustomer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($origCustomer, CustomerInterface::class) ->willReturn($customerData); $this->customerViewHelperMock->expects($this->any()) @@ -192,6 +216,7 @@ public function testCredentialsChanged($testNumber, $oldEmail, $newEmail, $isPas ->with('name', $customerName) ->willReturnSelf(); + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $savedCustomer */ $savedCustomer = clone $origCustomer; $origCustomer->expects($this->any()) @@ -234,7 +259,7 @@ public function testCredentialsChanged($testNumber, $oldEmail, $newEmail, $isPas ->willReturnSelf(); $this->transportBuilderMock->expects(clone $expects) ->method('setFrom') - ->with($sender) + ->with($senderValues) ->willReturnSelf(); $this->transportBuilderMock->expects(clone $expects) @@ -287,14 +312,27 @@ public function sendNotificationEmailsDataProvider() public function testPasswordReminder() { $customerId = 1; + $customerWebsiteId = 1; $customerStoreId = 2; $customerEmail = 'email@email.com'; $customerData = ['key' => 'value']; $customerName = 'Customer Name'; $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $senderValues = ['name' => $sender, 'email' => $sender]; + $storeIds = [1, 2]; + + $this->senderResolverMock + ->expects($this->once()) + ->method('resolve') + ->with($sender, $customerStoreId) + ->willReturn($senderValues); + + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->createMock(CustomerInterface::class); + $customer->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($customerWebsiteId); $customer->expects($this->any()) ->method('getStoreId') ->willReturn($customerStoreId); @@ -313,11 +351,16 @@ public function testPasswordReminder() ->method('getStore') ->willReturn($this->storeMock); - $this->storeManagerMock->expects($this->at(1)) - ->method('getStore') - ->with($customerStoreId) - ->willReturn($this->storeMock); + $websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getStoreIds']); + $websiteMock->expects($this->any()) + ->method('getStoreIds') + ->willReturn($storeIds); + $this->storeManagerMock->expects($this->any()) + ->method('getWebsite') + ->with($customerWebsiteId) + ->willReturn($websiteMock); + $this->customerRegistryMock->expects($this->once()) ->method('retrieveSecureData') ->with($customerId) @@ -325,7 +368,7 @@ public function testPasswordReminder() $this->dataProcessorMock->expects($this->once()) ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($customer, CustomerInterface::class) ->willReturn($customerData); $this->customerViewHelperMock->expects($this->any()) @@ -351,34 +394,120 @@ public function testPasswordReminder() ->with(EmailNotification::XML_PATH_FORGOT_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($sender); - $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $this->mockDefaultTransportBuilder( + $templateIdentifier, + $customerStoreId, + $senderValues, + $customerEmail, + $customerName, + ['customer' => $this->customerSecureMock, 'store' => $this->storeMock] + ); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateOptions') - ->with(['area' => Area::AREA_FRONTEND, 'store' => $customerStoreId]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateVars') - ->with(['customer' => $this->customerSecureMock, 'store' => $this->storeMock]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setFrom') - ->with($sender) + $this->model->passwordReminder($customer); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPasswordReminderCustomerWithoutStoreId() + { + $customerId = 1; + $customerWebsiteId = 1; + $customerStoreId = null; + $customerEmail = 'email@email.com'; + $customerData = ['key' => 'value']; + $customerName = 'Customer Name'; + $templateIdentifier = 'Template Identifier'; + $sender = 'Sender'; + $senderValues = ['name' => $sender, 'email' => $sender]; + $storeIds = [1, 2]; + $defaultStoreId = reset($storeIds); + + $this->senderResolverMock + ->expects($this->once()) + ->method('resolve') + ->with($sender, $defaultStoreId) + ->willReturn($senderValues); + + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->createMock(CustomerInterface::class); + $customer->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($customerWebsiteId); + $customer->expects($this->any()) + ->method('getStoreId') + ->willReturn($customerStoreId); + $customer->expects($this->any()) + ->method('getId') + ->willReturn($customerId); + $customer->expects($this->any()) + ->method('getEmail') + ->willReturn($customerEmail); + + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn($defaultStoreId); + + $this->storeManagerMock->expects($this->at(0)) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeManagerMock->expects($this->at(1)) + ->method('getStore') + ->with($defaultStoreId) + ->willReturn($this->storeMock); + + $websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getStoreIds']); + $websiteMock->expects($this->any()) + ->method('getStoreIds') + ->willReturn($storeIds); + + $this->storeManagerMock->expects($this->any()) + ->method('getWebsite') + ->with($customerWebsiteId) + ->willReturn($websiteMock); + + $this->customerRegistryMock->expects($this->once()) + ->method('retrieveSecureData') + ->with($customerId) + ->willReturn($this->customerSecureMock); + + $this->dataProcessorMock->expects($this->once()) + ->method('buildOutputDataArray') + ->with($customer, CustomerInterface::class) + ->willReturn($customerData); + + $this->customerViewHelperMock->expects($this->any()) + ->method('getCustomerName') + ->with($customer) + ->willReturn($customerName); + + $this->customerSecureMock->expects($this->once()) + ->method('addData') + ->with($customerData) ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('addTo') - ->with($customerEmail, $customerName) + $this->customerSecureMock->expects($this->once()) + ->method('setData') + ->with('name', $customerName) ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('getTransport') - ->willReturn($transport); - $transport->expects($this->once()) - ->method('sendMessage'); + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with(EmailNotification::XML_PATH_REMIND_EMAIL_TEMPLATE, ScopeInterface::SCOPE_STORE, $defaultStoreId) + ->willReturn($templateIdentifier); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with(EmailNotification::XML_PATH_FORGOT_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $defaultStoreId) + ->willReturn($sender); + + $this->mockDefaultTransportBuilder( + $templateIdentifier, + $defaultStoreId, + $senderValues, + $customerEmail, + $customerName, + ['customer' => $this->customerSecureMock, 'store' => $this->storeMock] + ); $this->model->passwordReminder($customer); } @@ -395,8 +524,16 @@ public function testPasswordResetConfirmation() $customerName = 'Customer Name'; $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; + $senderValues = ['name' => $sender, 'email' => $sender]; + + $this->senderResolverMock + ->expects($this->once()) + ->method('resolve') + ->with($sender, $customerStoreId) + ->willReturn($senderValues); - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->createMock(CustomerInterface::class); $customer->expects($this->any()) ->method('getStoreId') ->willReturn($customerStoreId); @@ -427,7 +564,7 @@ public function testPasswordResetConfirmation() $this->dataProcessorMock->expects($this->once()) ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($customer, CustomerInterface::class) ->willReturn($customerData); $this->customerViewHelperMock->expects($this->any()) @@ -453,52 +590,52 @@ public function testPasswordResetConfirmation() ->with(EmailNotification::XML_PATH_FORGOT_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($sender); - $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateOptions') - ->with(['area' => Area::AREA_FRONTEND, 'store' => $customerStoreId]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateVars') - ->with(['customer' => $this->customerSecureMock, 'store' => $this->storeMock]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setFrom') - ->with($sender) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('addTo') - ->with($customerEmail, $customerName) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('getTransport') - ->willReturn($transport); - - $transport->expects($this->once()) - ->method('sendMessage'); + $this->mockDefaultTransportBuilder( + $templateIdentifier, + $customerStoreId, + $senderValues, + $customerEmail, + $customerName, + ['customer' => $this->customerSecureMock, 'store' => $this->storeMock] + ); $this->model->passwordResetConfirmation($customer); } /** + * @dataProvider emailDataProvider + * @param string $emailType + * @param string $template + * @param string $templateIdentifier + * @param string|null $sendemailStoreId + * @param array $extensions + * + * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testNewAccount() - { + public function testNewAccount( + string $emailType, + string $template, + string $templateIdentifier, + $sendemailStoreId, + array $extensions + ) { $customerId = 1; $customerStoreId = 2; + $customerName = 'Customer Name'; $customerEmail = 'email@email.com'; $customerData = ['key' => 'value']; - $customerName = 'Customer Name'; - $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; + $senderEmail = 'sender@sender.com'; + $senderValues = ['name' => $sender, 'email' => $senderEmail]; + + $this->senderResolverMock + ->expects($this->once()) + ->method('resolve') + ->willReturn($senderValues); - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->createMock(CustomerInterface::class); $customer->expects($this->any()) ->method('getStoreId') ->willReturn($customerStoreId); @@ -525,7 +662,7 @@ public function testNewAccount() $this->dataProcessorMock->expects($this->once()) ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($customer, CustomerInterface::class) ->willReturn($customerData); $this->customerViewHelperMock->expects($this->any()) @@ -544,13 +681,96 @@ public function testNewAccount() $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') - ->with(EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE, ScopeInterface::SCOPE_STORE, $customerStoreId) + ->with($template, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($templateIdentifier); + $this->scopeConfigMock->expects($this->at(1)) ->method('getValue') ->with(EmailNotification::XML_PATH_REGISTER_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $customerStoreId) ->willReturn($sender); + $templateVars = [ + 'customer' => $this->customerSecureMock, + 'back_url' => '', + 'store' => $this->storeMock, + ]; + + if ($template === EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE) { + if (!empty($extensions)) { + $templateVars['url'] = $extensions['url']; + $templateVars['extensions'] = $extensions['extension_info']; + } else { + $templateVars['url'] = EmailNotification::CUSTOMER_CONFIRM_URL; + $templateVars['extensions'] = $extensions; + } + } + + $this->mockDefaultTransportBuilder( + $templateIdentifier, + $customerStoreId, + $senderValues, + $customerEmail, + $customerName, + $templateVars + ); + + $this->model->newAccount($customer, $emailType, '', $customerStoreId, $sendemailStoreId, $extensions); + } + + /** + * @return array + */ + public function emailDataProvider(): array + { + return + [ + [ + EmailNotification::NEW_ACCOUNT_EMAIL_REGISTERED, + EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE, + 'Register', + null, + [], + ], + [ + EmailNotification::NEW_ACCOUNT_EMAIL_CONFIRMATION, + EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE, + 'Confirm', + null, + [], + ], + [ + EmailNotification::NEW_ACCOUNT_EMAIL_CONFIRMATION, + EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE, + 'Confirm', + null, + [ + 'url' => "customer/account/confirm", + 'extension_info' => [ + 'test_extension' => "NTowU0Q5amZneEtSZnRDajRFZFVDVmZCbnhRWnZ0cFFkOA,,", + ], + ], + ], + ]; + } + + /** + * Create defaul mock for $this->transportBuilderMock + * + * @param string $templateIdentifier + * @param int $customerStoreId + * @param array $senderValues + * @param string $customerEmail + * @param string $customerName + * @param array $templateVars + */ + protected function mockDefaultTransportBuilder( + $templateIdentifier, + $customerStoreId, + array $senderValues, + $customerEmail, + $customerName, + array $templateVars = [] + ) { $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); $this->transportBuilderMock->expects($this->once()) @@ -563,11 +783,11 @@ public function testNewAccount() ->willReturnSelf(); $this->transportBuilderMock->expects($this->once()) ->method('setTemplateVars') - ->with(['customer' => $this->customerSecureMock, 'back_url' => '', 'store' => $this->storeMock]) + ->with($templateVars) ->willReturnSelf(); $this->transportBuilderMock->expects($this->once()) ->method('setFrom') - ->with($sender) + ->with($senderValues) ->willReturnSelf(); $this->transportBuilderMock->expects($this->once()) ->method('addTo') @@ -579,7 +799,5 @@ public function testNewAccount() $transport->expects($this->once()) ->method('sendMessage'); - - $this->model->newAccount($customer, EmailNotification::NEW_ACCOUNT_EMAIL_REGISTERED, '', $customerStoreId); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index f2db8c6cab6be..dc7cfbef3fa03 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -71,6 +71,12 @@ protected function setUp() ->getMock(); } + /** + * @param $entityTypeCode + * @param array $allowedExtensions + * + * @return FileProcessor + */ private function getModel($entityTypeCode, array $allowedExtensions = []) { $model = new FileProcessor( diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php index f2489c8626a4f..82e1c31b54b92 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileUploaderTest.php @@ -140,7 +140,7 @@ public function testUpload() 'name' => $resultFileName, 'file' => $resultFileName, 'path' => $resultFilePath, - 'tmp_name' => $resultFilePath . $resultFileName, + 'tmp_name' => ltrim($resultFileName, '/'), 'url' => $resultFileUrl, ]; diff --git a/app/code/Magento/Customer/Test/Unit/Model/LoggerTest.php b/app/code/Magento/Customer/Test/Unit/Model/LoggerTest.php index 4cea7ee22837d..408389182ae49 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/LoggerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/LoggerTest.php @@ -71,7 +71,8 @@ public function testLog($customerId, $data) $data = array_filter($data); if (!$data) { - $this->expectException('\InvalidArgumentException', 'Log data is empty'); + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('Log data is empty'); $this->logger->log($customerId, $data); return; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php index 658472d13ab93..d93ed3c7b351a 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php @@ -15,7 +15,14 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +/** + * Class AttributeMetadataCache Test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase { /** @@ -43,6 +50,16 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase */ private $attributeMetadataCache; + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -50,13 +67,18 @@ protected function setUp() $this->stateMock = $this->createMock(StateInterface::class); $this->serializerMock = $this->createMock(SerializerInterface::class); $this->attributeMetadataHydratorMock = $this->createMock(AttributeMetadataHydrator::class); + $this->storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + $this->storeMock->method('getId')->willReturn(1); $this->attributeMetadataCache = $objectManager->getObject( AttributeMetadataCache::class, [ 'cache' => $this->cacheMock, 'state' => $this->stateMock, 'serializer' => $this->serializerMock, - 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock + 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock, + 'storeManager' => $this->storeManagerMock ] ); } @@ -80,7 +102,8 @@ public function testLoadNoCache() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $this->stateMock->expects($this->once()) ->method('isEnabled') ->with(Type::TYPE_IDENTIFIER) @@ -96,7 +119,8 @@ public function testLoad() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', @@ -156,7 +180,8 @@ public function testSave() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataHydratorTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataHydratorTest.php index ec9831dde081e..248cae999065c 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataHydratorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataHydratorTest.php @@ -205,7 +205,7 @@ public function testExtract() ->method('buildOutputDataArray') ->with( $this->attributeMetadataMock, - AttributeMetadataInterface::class + AttributeMetadata::class ) ->willReturn($data); $this->assertSame( diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php index b9f8564d3616a..667fc87b6a82b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php @@ -94,6 +94,9 @@ public function testSetRequestScopeOnly($bool) $this->assertSame($bool, $this->_model->isRequestScopeOnly()); } + /** + * @return array + */ public function trueFalseDataProvider() { return [[true], [false]]; @@ -122,6 +125,9 @@ public function testApplyInputFilter($input, $output, $filter) $this->assertEquals($output, $this->_model->applyInputFilter($input)); } + /** + * @return array + */ public function applyInputFilterProvider() { return [ @@ -160,6 +166,9 @@ public function testDateFilterFormat($format, $output) $this->assertEquals($output, $actual); } + /** + * @return array + */ public function dateFilterFormatProvider() { return [[null, 'Whatever I put'], [false, self::MODEL], ['something else', self::MODEL]]; @@ -196,6 +205,8 @@ public function applyOutputFilterDataProvider() } /** + * Tests input validation rules. + * * @param null|string $value * @param null|string $label * @param null|string $inputValidation @@ -208,29 +219,25 @@ public function testValidateInputRule($value, $label, $inputValidation, $expecte ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule->expects($this->any()) - ->method('getName') - ->will($this->returnValue('input_validation')); - $validationRule->expects($this->any()) - ->method('getValue') - ->will($this->returnValue($inputValidation)); - - $this->_attributeMock->expects($this->any())->method('getStoreLabel')->will($this->returnValue($label)); - $this->_attributeMock->expects( - $this->any() - )->method( - 'getValidationRules' - )->will( - $this->returnValue( - [ - $validationRule, - ] - ) - ); + + $validationRule->method('getName') + ->willReturn('input_validation'); + + $validationRule->method('getValue') + ->willReturn($inputValidation); + + $this->_attributeMock->method('getStoreLabel') + ->willReturn($label); + + $this->_attributeMock->method('getValidationRules') + ->willReturn([$validationRule]); $this->assertEquals($expectedOutput, $this->_model->validateInputRule($value)); } + /** + * @return array + */ public function validateInputRuleDataProvider() { return [ @@ -244,6 +251,16 @@ public function validateInputRuleDataProvider() \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' ] ], + [ + 'abc qaz', + 'mylabel', + 'alphanumeric', + [ + \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.', + ], + ], + ['abcqaz', 'mylabel', 'alphanumeric', true], + ['abc qaz', 'mylabel', 'alphanum-with-spaces', true], [ '!@#$', 'mylabel', @@ -319,6 +336,9 @@ public function testGetRequestValue($request, $attributeCode, $requestScope, $re $this->assertEquals($expectedValue, $this->_model->getRequestValue($request)); } + /** + * @return array + */ public function getRequestValueDataProvider() { $expectedValue = 'EXPECTED_VALUE'; diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/BooleanTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/BooleanTest.php index 4315340d65bff..d9f101b922cc8 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/BooleanTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/BooleanTest.php @@ -28,6 +28,9 @@ public function testGetOptionText($value, $expected) $this->assertSame($expected, (string)$boolean->outputValue()); } + /** + * @return array + */ public function getOptionTextDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php index 2c09555374aef..553efea38a82b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/DateTest.php @@ -12,6 +12,9 @@ class DateTest extends AbstractFormTestCase /** @var \Magento\Customer\Model\Metadata\Form\Date */ protected $date; + /** + * @inheritdoc + */ protected function setUp() { parent::setUp(); @@ -46,6 +49,9 @@ protected function setUp() ); } + /** + * Test extractValue + */ public function testExtractValue() { $requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) @@ -112,6 +118,9 @@ public function testValidateValue($value, $validation, $required, $expected) $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function validateValueDataProvider() { return [ @@ -163,12 +172,15 @@ public function testCompactValue($value, $expected) $this->assertSame($expected, $this->date->compactValue($value)); } + /** + * @return array + */ public function compactAndRestoreValueDataProvider() { return [ [1, 1], [false, false], - ['', null], + [null, null], ['test', 'test'], [['element1', 'element2'], ['element1', 'element2']] ]; @@ -185,6 +197,9 @@ public function testRestoreValue($value, $expected) $this->assertSame($expected, $this->date->restoreValue($value)); } + /** + * Test outputValue + */ public function testOutputValue() { $this->assertEquals(null, $this->date->outputValue()); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php index 97452b995ba0b..1cffaa6fe0379 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php @@ -118,6 +118,9 @@ public function testExtractValueNoRequestScope($expected, $attributeCode = '', $ } } + /** + * @return array + */ public function extractValueNoRequestScopeDataProvider() { return [ @@ -178,6 +181,9 @@ public function testExtractValueWithRequestScope($expected, $requestScope, $main } } + /** + * @return array + */ public function extractValueWithRequestScopeDataProvider() { return [ @@ -228,6 +234,9 @@ public function testValidateValueNotToUpload($expected, $value, $isAjax = false, $this->assertEquals($expected, $model->validateValue($value)); } + /** + * @return array + */ public function validateValueNotToUploadDataProvider() { return [ @@ -285,6 +294,9 @@ public function testValidateValueToUpload($expected, $value, $parameters = []) $this->assertEquals($expected, $model->validateValue($value)); } + /** + * @return array + */ public function validateValueToUploadDataProvider() { return [ @@ -429,6 +441,9 @@ public function testOutputValueNonJson($format) $this->assertSame('', $model->outputValue($format)); } + /** + * @return array + */ public function outputValueDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/MultilineTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/MultilineTest.php index 25f10d7bb93c6..e74ddebdb597b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/MultilineTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/MultilineTest.php @@ -42,6 +42,9 @@ public function testValidateValueRequired($value, $expected) parent::testValidateValueRequired($value, $expected); } + /** + * @return array + */ public function validateValueRequiredDataProvider() { return array_merge( @@ -66,6 +69,9 @@ public function testValidateValueLength($value, $expected) parent::testValidateValueLength($value, $expected); } + /** + * @return array + */ public function validateValueLengthDataProvider() { return array_merge( diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/SelectTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/SelectTest.php index c8564df6b086f..5861ef1f93784 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/SelectTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/SelectTest.php @@ -42,6 +42,9 @@ public function testValidateValue($value, $expected) $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function validateValueDataProvider() { return [ @@ -74,6 +77,9 @@ public function testValidateValueRequired($value, $expected) } } + /** + * @return array + */ public function validateValueRequiredDataProvider() { return [ @@ -145,6 +151,9 @@ public function testOutputValue($value, $expected) $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function outputValueDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/TextTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/TextTest.php index 9a3e1c1d8a7cb..292d46a936092 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/TextTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/TextTest.php @@ -5,8 +5,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Test\Unit\Model\Metadata\Form; +use Magento\Customer\Api\Data\ValidationRuleInterface; use Magento\Customer\Model\Metadata\Form\Text; class TextTest extends AbstractFormTestCase @@ -52,6 +54,9 @@ public function testValidateValue($value, $expected) $this->assertEquals($expected, $actual); } + /** + * @return array + */ public function validateValueDataProvider() { return [ @@ -84,6 +89,9 @@ public function testValidateValueRequired($value, $expected) } } + /** + * @return array + */ public function validateValueRequiredDataProvider() { return [ @@ -105,7 +113,7 @@ public function validateValueRequiredDataProvider() */ public function testValidateValueLength($value, $expected) { - $minTextLengthRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $minTextLengthRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); @@ -116,7 +124,7 @@ public function testValidateValueLength($value, $expected) ->method('getValue') ->will($this->returnValue(4)); - $maxTextLengthRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) + $maxTextLengthRule = $this->getMockBuilder(ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); @@ -127,7 +135,19 @@ public function testValidateValueLength($value, $expected) ->method('getValue') ->will($this->returnValue(8)); + $inputValidationRule = $this->getMockBuilder(ValidationRuleInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'getValue']) + ->getMockForAbstractClass(); + $inputValidationRule->expects($this->any()) + ->method('getName') + ->will($this->returnValue('input_validation')); + $inputValidationRule->expects($this->any()) + ->method('getValue') + ->will($this->returnValue('other')); + $validationRules = [ + 'input_validation' => $inputValidationRule, 'min_text_length' => $minTextLengthRule, 'max_text_length' => $maxTextLengthRule, ]; @@ -150,6 +170,9 @@ public function testValidateValueLength($value, $expected) } } + /** + * @return array + */ public function validateValueLengthDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/ValidatorTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/ValidatorTest.php index bef2db9bf2694..354932b0ede0b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/ValidatorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/ValidatorTest.php @@ -79,6 +79,9 @@ public function testIsValid($isValid) $this->assertEquals($isValid, $this->validator->isValid(new \Magento\Framework\DataObject($data))); } + /** + * @return array + */ public function trueFalseDataProvider() { return [[true], [false]]; diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerFlushFormKeyTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerFlushFormKeyTest.php new file mode 100644 index 0000000000000..1b30fb5c60e9c --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerFlushFormKeyTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Model\Plugin; + +use Magento\Customer\Model\Plugin\CustomerFlushFormKey; +use Magento\Customer\Model\Session; +use Magento\Framework\App\PageCache\FormKey as CookieFormKey; +use Magento\Framework\Data\Form\FormKey as DataFormKey; +use Magento\Framework\Event\Observer; +use Magento\PageCache\Observer\FlushFormKey; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class CustomerFlushFormKeyTest extends TestCase +{ + /** + * @var CookieFormKey | MockObject + */ + private $cookieFormKey; + + /** + * @var Session | MockObject + */ + private $customerSession; + + /** + * @var DataFormKey | MockObject + */ + private $dataFormKey; + + protected function setUp() + { + + /** @var CookieFormKey | MockObject */ + $this->cookieFormKey = $this->getMockBuilder(CookieFormKey::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var DataFormKey | MockObject */ + $this->dataFormKey = $this->getMockBuilder(DataFormKey::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Session | MockObject */ + $this->customerSession = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getBeforeRequestParams', 'setBeforeRequestParams']) + ->getMock(); + } + + /** + * @dataProvider aroundFlushFormKeyProvider + * @param $beforeFormKey + * @param $currentFormKey + * @param $getFormKeyTimes + * @param $setBeforeParamsTimes + */ + public function testAroundFlushFormKey( + $beforeFormKey, + $currentFormKey, + $getFormKeyTimes, + $setBeforeParamsTimes + ) { + $observerDto = new Observer(); + $observer = new FlushFormKey($this->cookieFormKey, $this->dataFormKey); + $plugin = new CustomerFlushFormKey($this->customerSession, $this->dataFormKey); + + $beforeParams['form_key'] = $beforeFormKey; + + $this->dataFormKey->expects($this->exactly($getFormKeyTimes)) + ->method('getFormKey') + ->willReturn($currentFormKey); + + $this->customerSession->expects($this->once()) + ->method('getBeforeRequestParams') + ->willReturn($beforeParams); + + $this->customerSession->expects($this->exactly($setBeforeParamsTimes)) + ->method('setBeforeRequestParams') + ->with($beforeParams); + + $proceed = function ($observerDto) use ($observer) { + return $observer->execute($observerDto); + }; + + $plugin->aroundExecute($observer, $proceed, $observerDto); + } + + /** + * Data provider for testAroundFlushFormKey + * + * @return array + */ + public function aroundFlushFormKeyProvider() + { + return [ + ['form_key_value', 'form_key_value', 2, 1], + ['form_old_key_value', 'form_key_value', 1, 0], + [null, 'form_key_value', 1, 0] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index 759c823eec7f9..0f0fb9f29eb2c 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Test\Unit\Model\Renderer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + class RegionTest extends \PHPUnit\Framework\TestCase { /** @@ -23,7 +25,7 @@ public function testRender($regionCollection) $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); $elementMock = $this->createPartialMock( \Magento\Framework\Data\Form\Element\AbstractElement::class, - ['getForm', 'getHtmlAttributes'] + ['getForm', 'getHtmlAttributes', 'getHtmlId', 'getName'] ); $countryMock = $this->createPartialMock( \Magento\Framework\Data\Form\Element\AbstractElement::class, @@ -58,6 +60,15 @@ public function testRender($regionCollection) ] ) ); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $objectManager->setBackwardCompatibleProperty( + $elementMock, + '_escaper', + $escaper + ); + $formMock->expects( $this->any() )->method( @@ -90,6 +101,9 @@ public function testRender($regionCollection) $this->assertContains('required-entry', $html); } + /** + * @return array + */ public function renderDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php index b083bea54cb82..86ee21aef994b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsitesTest.php @@ -12,7 +12,13 @@ use Magento\Framework\Data\Collection\AbstractDb; use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManagerInterface; +/** + * Tests for \Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CountryWithWebsitesTest extends \PHPUnit\Framework\TestCase { /** @@ -40,6 +46,9 @@ class CountryWithWebsitesTest extends \PHPUnit\Framework\TestCase */ private $shareConfigMock; + /** @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $objectManagerMock; + public function setUp() { $this->countriesFactoryMock = @@ -62,6 +71,21 @@ public function setUp() $this->shareConfigMock = $this->getMockBuilder(Share::class) ->disableOriginalConstructor() ->getMock(); + + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->setMethods(['get']) + ->getMockForAbstractClass(); + + $escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->disableOriginalConstructor() + ->getMock(); + + ObjectManager::setInstance($this->objectManagerMock); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(\Magento\Framework\Escaper::class) + ->willReturn($escaper); + $this->countryByWebsite = new CountryWithWebsites( $eavCollectionFactoryMock, $optionsFactoryMock, @@ -117,4 +141,12 @@ public function testGetAllOptions() ['value' => 'AM', 'label' => 'UZ', 'website_ids' => [1, 2]] ], $this->countryByWebsite->getAllOptions()); } + + protected function tearDown() + { + $property = (new \ReflectionClass(ObjectManager::class))->getProperty('_instance'); + $property->setAccessible(true); + $property->setValue(null, null); + parent::tearDown(); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/AddressTest.php index 723ce6fa1826a..29ec1f0588e4f 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/AddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/AddressTest.php @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile namespace Magento\Customer\Test\Unit\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite; @@ -33,9 +32,13 @@ class AddressTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->entitySnapshotMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class); + $this->entitySnapshotMock = $this->createMock( + \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class + ); - $this->entityRelationCompositeMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite::class); + $this->entityRelationCompositeMock = $this->createMock( + \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite::class + ); $this->addressResource = (new ObjectManagerHelper($this))->getObject( \Magento\Customer\Test\Unit\Model\ResourceModel\SubResourceModelAddress::class, @@ -157,7 +160,10 @@ protected function prepareResource() */ protected function prepareEavConfig() { - $attributeMock = $this->createPartialMock(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, ['getAttributeCode', 'getBackend', '__wakeup']); + $attributeMock = $this->createPartialMock( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + ['getAttributeCode', 'getBackend', '__wakeup'] + ); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn('entity_id'); @@ -167,12 +173,18 @@ protected function prepareEavConfig() $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) ); - $this->eavConfigType = $this->createPartialMock(\Magento\Eav\Model\Entity\Type::class, ['getEntityIdField', 'getId', 'getEntityTable', '__wakeup']); + $this->eavConfigType = $this->createPartialMock( + \Magento\Eav\Model\Entity\Type::class, + ['getEntityIdField', 'getId', 'getEntityTable', '__wakeup'] + ); $this->eavConfigType->expects($this->any())->method('getEntityIdField')->willReturn(false); $this->eavConfigType->expects($this->any())->method('getId')->willReturn(false); $this->eavConfigType->expects($this->any())->method('getEntityTable')->willReturn('customer_address_entity'); - $eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getEntityType', 'getEntityAttributeCodes', 'getAttribute']); + $eavConfig = $this->createPartialMock( + \Magento\Eav\Model\Config::class, + ['getEntityType', 'getEntityAttributeCodes', 'getAttribute'] + ); $eavConfig->expects($this->any()) ->method('getEntityType') ->with('customer_address') @@ -227,6 +239,9 @@ protected function prepareValidatorFactory() return $validatorFactory; } + /** + * @return \Magento\Customer\Model\CustomerFactory|\PHPUnit_Framework_MockObject_MockObject + */ protected function prepareCustomerFactory() { $this->customerFactory = $this->createPartialMock(\Magento\Customer\Model\CustomerFactory::class, ['create']); @@ -243,23 +258,36 @@ public function testGetType() * Class SubResourceModelAddress * Mock method getAttributeLoader * @package Magento\Customer\Test\Unit\Model\ResourceModel + * @codingStandardsIgnoreStart */ class SubResourceModelAddress extends \Magento\Customer\Model\ResourceModel\Address { protected $attributeLoader; + /** + * @param null $object + * + * @return \Magento\Customer\Model\ResourceModel\Address|\Magento\Eav\Model\Entity\AbstractEntity + */ public function loadAllAttributes($object = null) { return $this->getAttributeLoader()->loadAllAttributes($this, $object); } + /** + * @param $attributeLoader + */ public function setAttributeLoader($attributeLoader) { $this->attributeLoader = $attributeLoader; } + /** + * @return \Magento\Eav\Model\Entity\AttributeLoaderInterface + */ protected function getAttributeLoader() { return $this->attributeLoader; } } +// @codingStandardsIgnoreEnd diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 61c25641df6cd..16dfdc4eb9158 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -19,72 +19,72 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Customer\Model\CustomerFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerFactory; + private $customerFactory; /** * @var \Magento\Customer\Model\Data\CustomerSecureFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSecureFactory; + private $customerSecureFactory; /** * @var \Magento\Customer\Model\CustomerRegistry|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRegistry; + private $customerRegistry; /** * @var \Magento\Customer\Model\ResourceModel\AddressRepository|\PHPUnit_Framework_MockObject_MockObject */ - protected $addressRepository; + private $addressRepository; /** * @var \Magento\Customer\Model\ResourceModel\Customer|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerResourceModel; + private $customerResourceModel; /** * @var \Magento\Customer\Api\CustomerMetadataInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerMetadata; + private $customerMetadata; /** * @var \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $searchResultsFactory; + private $searchResultsFactory; /** * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; + private $eventManager; /** * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; + private $storeManager; /** * @var \Magento\Framework\Api\ExtensibleDataObjectConverter|\PHPUnit_Framework_MockObject_MockObject */ - protected $extensibleDataObjectConverter; + private $extensibleDataObjectConverter; /** * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject */ - protected $dataObjectHelper; + private $dataObjectHelper; /** * @var \Magento\Framework\Api\ImageProcessorInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $imageProcessor; + private $imageProcessor; /** * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $extensionAttributesJoinProcessor; + private $extensionAttributesJoinProcessor; /** * @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customer; + private $customer; /** * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject @@ -92,14 +92,14 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase private $collectionProcessorMock; /** - * @var \Magento\Customer\Model\ResourceModel\CustomerRepository + * @var NotificationStorage|\PHPUnit_Framework_MockObject_MockObject */ - protected $model; + private $notificationStorage; /** - * @var NotificationStorage + * @var \Magento\Customer\Model\ResourceModel\CustomerRepository */ - private $notificationStorage; + private $model; protected function setUp() { @@ -107,7 +107,7 @@ protected function setUp() $this->createMock(\Magento\Customer\Model\ResourceModel\Customer::class); $this->customerRegistry = $this->createMock(\Magento\Customer\Model\CustomerRegistry::class); $this->dataObjectHelper = $this->createMock(\Magento\Framework\Api\DataObjectHelper::class); - $this->customerFactory = + $this->customerFactory = $this->createPartialMock(\Magento\Customer\Model\CustomerFactory::class, ['create']); $this->customerSecureFactory = $this->createPartialMock( \Magento\Customer\Model\Data\CustomerSecureFactory::class, @@ -193,38 +193,10 @@ protected function setUp() public function testSave() { $customerId = 1; - $storeId = 2; - $region = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\RegionInterface::class, [], '', false); - $address = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $address2 = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, + $customerModel = $this->createPartialMock( + \Magento\Customer\Model\Customer::class, [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ 'getId', 'setId', 'setStoreId', @@ -239,14 +211,11 @@ public function testSave() 'setFirstFailure', 'setLockExpires', 'save', - ]); + ] + ); $origCustomer = $this->customer; - $this->customer->expects($this->atLeastOnce()) - ->method('__toArray') - ->willReturn(['default_billing', 'default_shipping']); - $customerAttributesMetaData = $this->getMockForAbstractClass( \Magento\Framework\Api\CustomAttributesDataInterface::class, [], @@ -262,17 +231,23 @@ public function testSave() 'setAddresses' ] ); - $customerSecureData = $this->createPartialMock(\Magento\Customer\Model\Data\CustomerSecure::class, [ + $customerSecureData = $this->createPartialMock( + \Magento\Customer\Model\Data\CustomerSecure::class, + [ 'getRpToken', 'getRpTokenCreatedAt', 'getPasswordHash', 'getFailuresNum', 'getFirstFailure', 'getLockExpires', - ]); + ] + ); $this->customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn([]); $this->customerRegistry->expects($this->atLeastOnce()) ->method('retrieve') ->with($customerId) @@ -287,28 +262,6 @@ public function testSave() $this->customerRegistry->expects($this->atLeastOnce()) ->method("remove") ->with($customerId); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $address->expects($this->once()) - ->method('getRegion') - ->willReturn($region); - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(7); - $address->expects($this->once()) - ->method('setRegion') - ->with($region); - $customerAttributesMetaData->expects($this->atLeastOnce()) - ->method('getAddresses') - ->willReturn([$address]); - $customerAttributesMetaData->expects($this->at(1)) - ->method('setAddresses') - ->with([]); - $customerAttributesMetaData->expects($this->at(2)) - ->method('setAddresses') - ->with([$address]); $this->extensibleDataObjectConverter->expects($this->once()) ->method('toNestedArray') ->with($customerAttributesMetaData, [], \Magento\Customer\Api\Data\CustomerInterface::class) @@ -320,26 +273,9 @@ public function testSave() $customerModel->expects($this->once()) ->method('getStoreId') ->willReturn(null); - $store = $this->createMock(\Magento\Store\Model\Store::class); - $store->expects($this->once()) - ->method('getId') - ->willReturn($storeId); - $this->storeManager - ->expects($this->once()) - ->method('getStore') - ->willReturn($store); - $customerModel->expects($this->once()) - ->method('setStoreId') - ->with($storeId); $customerModel->expects($this->once()) ->method('setId') ->with($customerId); - $customerModel->expects($this->once()) - ->method('getAttributeSetId') - ->willReturn(null); - $customerModel->expects($this->once()) - ->method('setAttributeSetId') - ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); $customerAttributesMetaData->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -368,16 +304,20 @@ public function testSave() $customerModel->expects($this->once()) ->method('setRpToken') - ->willReturnMap([ - ['rpToken', $customerModel], - [null, $customerModel], - ]); + ->willReturnMap( + [ + ['rpToken', $customerModel], + [null, $customerModel], + ] + ); $customerModel->expects($this->once()) ->method('setRpTokenCreatedAt') - ->willReturnMap([ - ['rpTokenCreatedAt', $customerModel], - [null, $customerModel], - ]); + ->willReturnMap( + [ + ['rpTokenCreatedAt', $customerModel], + [null, $customerModel], + ] + ); $customerModel->expects($this->once()) ->method('setPasswordHash') @@ -399,12 +339,6 @@ public function testSave() $this->customerRegistry->expects($this->once()) ->method('push') ->with($customerModel); - $this->customer->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address, $address2]); - $this->addressRepository->expects($this->once()) - ->method('save') - ->with($address); $customerAttributesMetaData->expects($this->once()) ->method('getEmail') ->willReturn('example@example.com'); @@ -419,7 +353,11 @@ public function testSave() ->method('dispatch') ->with( 'customer_save_after_data_object', - ['customer_data_object' => $this->customer, 'orig_customer_data_object' => $origCustomer] + [ + 'customer_data_object' => $this->customer, + 'orig_customer_data_object' => $origCustomer, + 'delegate_data' => [], + ] ); $this->model->save($this->customer); @@ -431,59 +369,24 @@ public function testSave() public function testSaveWithPasswordHash() { $customerId = 1; - $storeId = 2; $passwordHash = 'ukfa4sdfa56s5df02asdf4rt'; - $customerSecureData = $this->createPartialMock(\Magento\Customer\Model\Data\CustomerSecure::class, [ + $customerSecureData = $this->createPartialMock( + \Magento\Customer\Model\Data\CustomerSecure::class, + [ 'getRpToken', 'getRpTokenCreatedAt', 'getPasswordHash', 'getFailuresNum', 'getFirstFailure', 'getLockExpires', - ]); - $region = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\RegionInterface::class, - [], - '', - false - ); - $address = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' ] ); - $address2 = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $origCustomer = $this->customer; - $this->customer->expects($this->atLeastOnce()) - ->method('__toArray') - ->willReturn(['default_billing', 'default_shipping']); - - $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ + $customerModel = $this->createPartialMock( + \Magento\Customer\Model\Customer::class, + [ 'getId', 'setId', 'setStoreId', @@ -495,7 +398,8 @@ public function testSaveWithPasswordHash() 'getDataModel', 'setPasswordHash', 'save', - ]); + ] + ); $customerAttributesMetaData = $this->getMockForAbstractClass( \Magento\Framework\Api\CustomAttributesDataInterface::class, [], @@ -546,10 +450,12 @@ public function testSaveWithPasswordHash() $customerSecureData->expects($this->once()) ->method('getLockExpires') ->willReturn('lockExpires'); - $this->customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn([]); $this->customerRegistry->expects($this->atLeastOnce()) ->method('retrieve') ->with($customerId) @@ -561,28 +467,6 @@ public function testSaveWithPasswordHash() ->method('save') ->with($this->customer, CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $this->customer) ->willReturn($customerAttributesMetaData); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $address->expects($this->once()) - ->method('getRegion') - ->willReturn($region); - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(7); - $address->expects($this->once()) - ->method('setRegion') - ->with($region); - $customerAttributesMetaData->expects($this->any()) - ->method('getAddresses') - ->willReturn([$address]); - $customerAttributesMetaData->expects($this->at(1)) - ->method('setAddresses') - ->with([]); - $customerAttributesMetaData->expects($this->at(2)) - ->method('setAddresses') - ->with([$address]); $customerAttributesMetaData ->expects($this->atLeastOnce()) ->method('getId') @@ -595,29 +479,9 @@ public function testSaveWithPasswordHash() ->method('create') ->with(['data' => ['customerData']]) ->willReturn($customerModel); - $customerModel->expects($this->once()) - ->method('getStoreId') - ->willReturn(null); - $store = $this->createMock(\Magento\Store\Model\Store::class); - $store->expects($this->once()) - ->method('getId') - ->willReturn($storeId); - $this->storeManager - ->expects($this->once()) - ->method('getStore') - ->willReturn($store); - $customerModel->expects($this->once()) - ->method('setStoreId') - ->with($storeId); $customerModel->expects($this->once()) ->method('setId') ->with($customerId); - $customerModel->expects($this->once()) - ->method('getAttributeSetId') - ->willReturn(null); - $customerModel->expects($this->once()) - ->method('setAttributeSetId') - ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); $customerModel->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -626,12 +490,6 @@ public function testSaveWithPasswordHash() $this->customerRegistry->expects($this->once()) ->method('push') ->with($customerModel); - $this->customer->expects($this->any()) - ->method('getAddresses') - ->willReturn([$address, $address2]); - $this->addressRepository->expects($this->once()) - ->method('save') - ->with($address); $customerAttributesMetaData->expects($this->once()) ->method('getEmail') ->willReturn('example@example.com'); @@ -646,7 +504,11 @@ public function testSaveWithPasswordHash() ->method('dispatch') ->with( 'customer_save_after_data_object', - ['customer_data_object' => $this->customer, 'orig_customer_data_object' => $origCustomer] + [ + 'customer_data_object' => $this->customer, + 'orig_customer_data_object' => $origCustomer, + 'delegate_data' => [], + ] ); $this->model->save($this->customer, $passwordHash); @@ -744,7 +606,7 @@ public function testGetList() ->willReturnSelf(); $collection->expects($this->at(7)) ->method('joinAttribute') - ->with('company', 'customer_address/company', 'default_billing', null, 'left') + ->with('billing_company', 'customer_address/company', 'default_billing', null, 'left') ->willReturnSelf(); $this->collectionProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Group/Grid/ServiceCollectionTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Group/Grid/ServiceCollectionTest.php index 61081e1aaf224..76651f9f07589 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Group/Grid/ServiceCollectionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Group/Grid/ServiceCollectionTest.php @@ -227,6 +227,9 @@ public function testAddFieldToFilterInconsistentArrays($fields, $conditions) $this->serviceCollection->addFieldToFilter($fields, $conditions); } + /** + * @return array + */ public function addFieldToFilterInconsistentArraysDataProvider() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupRepositoryTest.php index 98cf8c212a784..9e8440b500989 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupRepositoryTest.php @@ -38,6 +38,11 @@ class GroupRepositoryTest extends \PHPUnit\Framework\TestCase */ protected $group; + /** + * @var \Magento\Customer\Api\Data\GroupInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $factoryCreatedGroup; + /** * @var \Magento\Customer\Model\ResourceModel\Group|\PHPUnit_Framework_MockObject_MockObject */ @@ -153,6 +158,12 @@ private function setupGroupObjects() 'group', false ); + $this->factoryCreatedGroup = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupInterface::class, + [], + 'group', + false + ); $this->groupResourceModel = $this->createMock(\Magento\Customer\Model\ResourceModel\Group::class); } @@ -162,16 +173,22 @@ public function testSave() $groupId = 0; $taxClass = $this->getMockForAbstractClass(\Magento\Tax\Api\Data\TaxClassInterface::class, [], '', false); + $extensionAttributes = $this->getMockForAbstractClass( + \Magento\Customer\Api\Data\GroupExtensionInterface::class + ); - $this->group->expects($this->once()) + $this->group->expects($this->atLeastOnce()) ->method('getCode') ->willReturn('Code'); $this->group->expects($this->atLeastOnce()) ->method('getId') ->willReturn($groupId); - $this->group->expects($this->once()) + $this->group->expects($this->atLeastOnce()) ->method('getTaxClassId') ->willReturn(17); + $this->group->expects($this->atLeastOnce()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); $this->groupModel->expects($this->atLeastOnce()) ->method('getId') @@ -185,22 +202,33 @@ public function testSave() $this->groupModel->expects($this->atLeastOnce()) ->method('getTaxClassName') ->willReturn('Tax class name'); - $this->group->expects($this->once()) + + $this->factoryCreatedGroup->expects($this->once()) ->method('setId') ->with($groupId) ->willReturnSelf(); - $this->group->expects($this->once()) + $this->factoryCreatedGroup->expects($this->once()) ->method('setCode') ->with('Code') ->willReturnSelf(); - $this->group->expects($this->once()) + $this->factoryCreatedGroup->expects($this->once()) ->method('setTaxClassId') ->with(234) ->willReturnSelf(); - $this->group->expects($this->once()) + $this->factoryCreatedGroup->expects($this->once()) ->method('setTaxClassName') ->with('Tax class name') ->willReturnSelf(); + $this->factoryCreatedGroup->expects($this->once()) + ->method('setExtensionAttributes') + ->with($extensionAttributes) + ->willReturnSelf(); + $this->factoryCreatedGroup->expects($this->atLeastOnce()) + ->method('getCode') + ->willReturn('Code'); + $this->factoryCreatedGroup->expects($this->atLeastOnce()) + ->method('getTaxClassId') + ->willReturn(17); $this->taxClassRepository->expects($this->once()) ->method('get') @@ -229,9 +257,12 @@ public function testSave() ->with($groupId); $this->groupDataFactory->expects($this->once()) ->method('create') - ->willReturn($this->group); + ->willReturn($this->factoryCreatedGroup); + + $updatedGroup = $this->model->save($this->group); - $this->assertSame($this->group, $this->model->save($this->group)); + $this->assertSame($this->group->getCode(), $updatedGroup->getCode()); + $this->assertSame($this->group->getTaxClassId(), $updatedGroup->getTaxClassId()); } /** diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php index 7855b1e6f823e..f4a4de0b29f4e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Customer\Test\Unit\Model\ResourceModel; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -51,17 +49,27 @@ protected function setUp() { $this->resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->customerVat = $this->createMock(\Magento\Customer\Model\Vat::class); - $this->customersFactory = $this->createPartialMock(\Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class, ['create']); - $this->groupManagement = $this->createPartialMock(\Magento\Customer\Api\GroupManagementInterface::class, ['getDefaultGroup', 'getNotLoggedInGroup', 'isReadOnly', 'getLoggedInGroups', 'getAllCustomersGroup']); + $this->customersFactory = $this->createPartialMock( + \Magento\Customer\Model\ResourceModel\Customer\CollectionFactory::class, + ['create'] + ); + $this->groupManagement = $this->createPartialMock( + \Magento\Customer\Api\GroupManagementInterface::class, + ['getDefaultGroup', 'getNotLoggedInGroup', 'isReadOnly', 'getLoggedInGroups', 'getAllCustomersGroup'] + ); $this->groupModel = $this->createMock(\Magento\Customer\Model\Group::class); $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); $contextMock->expects($this->once())->method('getResources')->willReturn($this->resource); - $this->relationProcessorMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor::class); + $this->relationProcessorMock = $this->createMock( + \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor::class + ); - $this->snapshotMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class); + $this->snapshotMock = $this->createMock( + \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class + ); $transactionManagerMock = $this->createMock( \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface::class @@ -89,7 +97,7 @@ protected function setUp() /** * Test for save() method when we try to save entity with system's reserved ID. - * + * * @return void */ public function testSaveWithReservedId() @@ -143,7 +151,10 @@ public function testDelete() $dbAdapter = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); $this->resource->expects($this->any())->method('getConnection')->will($this->returnValue($dbAdapter)); - $customer = $this->createPartialMock(\Magento\Customer\Model\Customer::class, ['__wakeup', 'load', 'getId', 'getStoreId', 'setGroupId', 'save']); + $customer = $this->createPartialMock( + \Magento\Customer\Model\Customer::class, + ['__wakeup', 'load', 'getId', 'getStoreId', 'setGroupId', 'save'] + ); $customerId = 1; $customer->expects($this->once())->method('getId')->will($this->returnValue($customerId)); $customer->expects($this->once())->method('load')->with($customerId)->will($this->returnSelf()); diff --git a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php index 7a6807562f906..858b7ec24324a 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php @@ -131,18 +131,21 @@ public function testAuthenticate() $urlMock = $this->createMock(\Magento\Framework\Url::class); $urlMock->expects($this->exactly(2)) ->method('getUrl') - ->will($this->returnValue('')); + ->willReturn(''); $urlMock->expects($this->once()) ->method('getRebuiltUrl') - ->will($this->returnValue('')); - $this->urlFactoryMock->expects($this->exactly(3)) + ->willReturn(''); + $this->urlFactoryMock->expects($this->exactly(4)) ->method('create') - ->will($this->returnValue($urlMock)); + ->willReturn($urlMock); + $urlMock->expects($this->once()) + ->method('getUseSession') + ->willReturn(false); $this->responseMock->expects($this->once()) ->method('setRedirect') ->with('') - ->will($this->returnValue('')); + ->willReturn(''); $this->assertFalse($this->_model->authenticate()); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/VisitorTest.php b/app/code/Magento/Customer/Test/Unit/Model/VisitorTest.php index ca6b8708f695c..e1f3113be8ca1 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/VisitorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/VisitorTest.php @@ -77,14 +77,14 @@ protected function setUp() public function testInitByRequest() { - $this->session->expects($this->once())->method('getSessionId') - ->will($this->returnValue('asdfhasdfjhkj2198sadf8sdf897')); + $oldSessionId = 'asdfhasdfjhkj2198sadf8sdf897'; + $newSessionId = 'bsdfhasdfjhkj2198sadf8sdf897'; + $this->session->expects($this->any())->method('getSessionId') + ->will($this->returnValue($newSessionId)); + $this->session->expects($this->atLeastOnce())->method('getVisitorData') + ->willReturn(['session_id' => $oldSessionId]); $this->visitor->initByRequest(null); - $this->assertEquals('asdfhasdfjhkj2198sadf8sdf897', $this->visitor->getSessionId()); - - $this->visitor->setData(['visitor_id' => 1]); - $this->visitor->initByRequest(null); - $this->assertNull($this->visitor->getSessionId()); + $this->assertEquals($newSessionId, $this->visitor->getSessionId()); } public function testSaveByRequest() diff --git a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php index 939e2856f5eaa..8592d1bda66c1 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php @@ -411,6 +411,9 @@ public function testAfterAddressSaveDefaultGroup( $this->model->execute($observer); } + /** + * @return array + */ public function dataProviderAfterAddressSaveDefaultGroup() { return [ @@ -600,6 +603,9 @@ public function testAfterAddressSaveNewGroup( $this->model->execute($observer); } + /** + * @return array + */ public function dataProviderAfterAddressSaveNewGroup() { return [ diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php index 8971f155f782e..313121604e567 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Observer\UpgradeCustomerPasswordObserver; +/** + * Unit test for Magento\Customer\Observer\UpgradeCustomerPasswordObserver. + */ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -29,9 +32,13 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase */ protected $customerRegistry; + /** + * @inheritdoc + */ protected function setUp() { - $this->customerRepository = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + $this->customerRepository = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->customerRegistry = $this->getMockBuilder(\Magento\Customer\Model\CustomerRegistry::class) ->disableOriginalConstructor() @@ -47,6 +54,9 @@ protected function setUp() ); } + /** + * Unit test for verifying customers password upgrade observer + */ public function testUpgradeCustomerPassword() { $customerId = '1'; @@ -57,6 +67,8 @@ public function testUpgradeCustomerPassword() ->setMethods(['getId']) ->getMock(); $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() ->getMockForAbstractClass(); $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php index 131b1ee94cc14..9b9146e894afb 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -90,6 +90,7 @@ public function testCreate() ] ], 'component' => 'Magento_Ui/js/grid/columns/column', + '__disableTmpl' => 'true', ], ], 'context' => $this->context, diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php index 7fbf9d2a2a10a..a0681ce6e94a5 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php @@ -69,6 +69,7 @@ public function testCreate() 'config' => [ 'dataScope' => $filterName, 'label' => __('Label'), + '__disableTmpl' => 'true', 'options' => [['value' => 'Value', 'label' => 'Label']], 'caption' => __('Select...'), ], diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php index 0662235c0d5ac..75297b1e5b485 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php @@ -144,8 +144,9 @@ public function testGetList() 'options' => [ [ 'label' => 'Label', - 'value' => 'Value' - ] + 'value' => 'Value', + '__disableTmpl' => true, + ], ], 'is_used_in_grid' => true, 'is_visible_in_grid' => true, diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php index 056c7e71e1827..4a16acd98d827 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ActionsTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Ui\Component\Listing\Column\Actions; +/** + * Class ActionsTest + */ class ActionsTest extends \PHPUnit\Framework\TestCase { /** @var Actions */ @@ -64,7 +67,8 @@ public function testPrepareDataSource() 'edit' => [ 'href' => 'http://magento.com/customer/index/edit', 'label' => new \Magento\Framework\Phrase('Edit'), - 'hidden' => false + 'hidden' => false, + '__disableTmpl' => true, ] ] ], diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ConfirmationTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ConfirmationTest.php index e55cee49b5c94..b712c0f30b430 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ConfirmationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ConfirmationTest.php @@ -5,13 +5,12 @@ */ namespace Magento\Customer\Test\Unit\Ui\Component\Listing\Column; -use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Ui\Component\Listing\Column\Confirmation; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Store\Model\ScopeInterface; class ConfirmationTest extends \PHPUnit\Framework\TestCase { @@ -40,6 +39,11 @@ class ConfirmationTest extends \PHPUnit\Framework\TestCase */ protected $processor; + /** + * @var AccountConfirmation|\PHPUnit_Framework_MockObject_MockObject + */ + protected $accountConfirmation; + public function setup() { $this->processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) @@ -60,12 +64,15 @@ public function setup() $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) ->getMockForAbstractClass(); + $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->confirmation = new Confirmation( $this->context, $this->uiComponentFactory, $this->scopeConfig, [], - [] + [], + $this->accountConfirmation ); } @@ -81,12 +88,17 @@ public function testPrepareDataSource( $expected ) { $websiteId = 1; + $customerId = 1; + $customerEmail = 'customer@example.com'; $dataSource = [ 'data' => [ 'items' => [ [ + 'id_field_name' => 'entity_id', + 'entity_id' => $customerId, 'confirmation' => $confirmation, + 'email' => $customerEmail, 'website_id' => [ $websiteId, ], @@ -100,9 +112,9 @@ public function testPrepareDataSource( ->with($this->confirmation) ->willReturnSelf(); - $this->scopeConfig->expects($this->once()) - ->method('getValue') - ->with(AccountManagement::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, $websiteId) + $this->accountConfirmation->expects($this->once()) + ->method('isConfirmationRequired') + ->with($websiteId, $customerId, $customerEmail) ->willReturn($isConfirmationRequired); $this->confirmation->setData('name', 'confirmation'); diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php index 130b3acd11e76..b57bc53ef09f9 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php @@ -18,12 +18,6 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->validationRules = $this->getMockBuilder( - \Magento\Customer\Ui\Component\Listing\Column\ValidationRules::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -31,18 +25,25 @@ protected function setUp() $this->validationRules = new ValidationRules(); } - public function testGetValidationRules() + /** + * Tests input validation rules. + * + * @param string $validationRule + * @param string $validationClass + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testGetValidationRules(string $validationRule, string $validationClass) { $expectsRules = [ 'required-entry' => true, - 'validate-number' => true, + $validationClass => true, ]; - $this->validationRule->expects($this->atLeastOnce()) - ->method('getName') + $this->validationRule->method('getName') ->willReturn('input_validation'); - $this->validationRule->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn('numeric'); + + $this->validationRule->method('getValue') + ->willReturn($validationRule); $this->assertEquals( $expectsRules, @@ -66,4 +67,21 @@ public function testGetValidationRulesWithOnlyRequiredRule() $this->validationRules->getValidationRules(true, []) ); } + + /** + * Provides possible validation rules. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'validate-email'], + ]; + } } diff --git a/app/code/Magento/Customer/Ui/Component/ColumnFactory.php b/app/code/Magento/Customer/Ui/Component/ColumnFactory.php index 60bf3ea26b78c..a1f09c35acc98 100644 --- a/app/code/Magento/Customer/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Customer/Ui/Component/ColumnFactory.php @@ -9,6 +9,9 @@ use Magento\Customer\Ui\Component\Listing\Column\InlineEditUpdater; use Magento\Customer\Api\CustomerMetadataInterface; +/** + * Class ColumnFactory. Responsible for the column object generation. + */ class ColumnFactory { /** @@ -55,6 +58,8 @@ public function __construct( } /** + * Creates column object for grid ui component. + * * @param array $attributeData * @param string $columnName * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context @@ -69,6 +74,7 @@ public function create(array $attributeData, $columnName, $context, array $confi 'align' => 'left', 'visible' => (bool)$attributeData[AttributeMetadata::IS_VISIBLE_IN_GRID], 'component' => $this->getJsComponent($this->getDataType($attributeData[AttributeMetadata::FRONTEND_INPUT])), + '__disableTmpl' => 'true', ], $config); if ($attributeData[AttributeMetadata::FRONTEND_INPUT] == 'date') { $config['dateFormat'] = 'MMM d, y'; @@ -101,6 +107,8 @@ public function create(array $attributeData, $columnName, $context, array $confi } /** + * Returns component map. + * * @param string $dataType * @return string */ @@ -110,6 +118,8 @@ protected function getJsComponent($dataType) } /** + * Returns component map depends on data type. + * * @param string $frontendType * @return string */ diff --git a/app/code/Magento/Customer/Ui/Component/FilterFactory.php b/app/code/Magento/Customer/Ui/Component/FilterFactory.php index 9d8fcdb9715ca..d8a85e1ec077c 100644 --- a/app/code/Magento/Customer/Ui/Component/FilterFactory.php +++ b/app/code/Magento/Customer/Ui/Component/FilterFactory.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface as AttributeMetadata; +/** + * Class FilterFactory. Responsible for generation filter object. + */ class FilterFactory { /** @@ -34,6 +37,8 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ } /** + * Creates filter object. + * * @param array $attributeData * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context * @return \Magento\Ui\Component\Listing\Columns\ColumnInterface @@ -43,6 +48,7 @@ public function create(array $attributeData, $context) $config = [ 'dataScope' => $attributeData[AttributeMetadata::ATTRIBUTE_CODE], 'label' => __($attributeData[AttributeMetadata::FRONTEND_LABEL]), + '__disableTmpl' => 'true', ]; if ($attributeData[AttributeMetadata::OPTIONS]) { $config['options'] = $attributeData[AttributeMetadata::OPTIONS]; @@ -63,6 +69,8 @@ public function create(array $attributeData, $context) } /** + * Returns filter type. + * * @param string $frontendInput * @return string */ diff --git a/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php b/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php index d0af1ec21467f..bfad7cf74aae0 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php @@ -13,6 +13,9 @@ use Magento\Customer\Api\MetadataManagementInterface; use Magento\Customer\Model\Indexer\Attribute\Filter; +/** + * Class AttributeRepository + */ class AttributeRepository { const BILLING_ADDRESS_PREFIX = 'billing_'; @@ -69,6 +72,8 @@ public function __construct( } /** + * Returns attribute list for current customer. + * * @return array */ public function getList() @@ -93,6 +98,8 @@ public function getList() } /** + * Returns attribute list for given entity type code. + * * @param AttributeMetadataInterface[] $metadata * @param string $entityTypeCode * @param MetadataManagementInterface $management @@ -136,12 +143,19 @@ protected function getOptionArray(array $options) { /** @var \Magento\Customer\Api\Data\OptionInterface $option */ foreach ($options as &$option) { - $option = ['label' => (string)$option->getLabel(), 'value' => $option->getValue()]; + $option = [ + 'label' => (string)$option->getLabel(), + 'value' => $option->getValue(), + '__disableTmpl' => true, + ]; } + return $options; } /** + * Return customer group's metadata by given group code. + * * @param string $code * @return [] */ diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php index d6a4067ef3db6..9441beeb7dc61 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/Actions.php @@ -60,6 +60,7 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Edit'), 'hidden' => false, + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/Confirmation.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/Confirmation.php index dcaaa665ad392..1786c52844a75 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/Confirmation.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/Confirmation.php @@ -5,35 +5,42 @@ */ namespace Magento\Customer\Ui\Component\Listing\Column; -use Magento\Customer\Model\AccountManagement; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Store\Model\ScopeInterface; use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\App\ObjectManager; +use Magento\Customer\Model\AccountConfirmation; +/** + * Class Confirmation column. + */ class Confirmation extends Column { /** - * @var ScopeConfigInterface + * @var AccountConfirmation */ - private $scopeConfig; + private $accountConfirmation; /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory - * @param ScopeConfigInterface $scopeConfig + * @param ScopeConfigInterface $scopeConfig @deprecated * @param array $components * @param array $data + * @param AccountConfirmation $accountConfirmation + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, ScopeConfigInterface $scopeConfig, array $components, - array $data + array $data, + AccountConfirmation $accountConfirmation = null ) { - $this->scopeConfig = $scopeConfig; + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); parent::__construct($context, $uiComponentFactory, $components, $data); } @@ -58,7 +65,13 @@ public function prepareDataSource(array $dataSource) */ private function getFieldLabel(array $item) { - if ($this->isConfirmationRequired($item)) { + $isConfirmationRequired = $this->accountConfirmation->isConfirmationRequired( + $item['website_id'][0], + $item[$item['id_field_name']], + $item['email'] + ); + + if ($isConfirmationRequired) { if ($item[$this->getData('name')] === null) { return __('Confirmed'); } @@ -66,19 +79,4 @@ private function getFieldLabel(array $item) } return __('Confirmation Not Required'); } - - /** - * Check if confirmation is required - * - * @param array $item - * @return bool - */ - private function isConfirmationRequired(array $item) - { - return (bool)$this->scopeConfig->getValue( - AccountManagement::XML_PATH_IS_CONFIRM, - ScopeInterface::SCOPE_WEBSITES, - $item['website_id'][0] - ); - } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php index f521a95e1e616..615ad2243a467 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php @@ -43,6 +43,11 @@ public function toOptionArray() if ($this->options === null) { $this->options = $this->collectionFactory->create()->toOptionArray(); } + + array_walk($this->options, function (&$item) { + $item['__disableTmpl'] = true; + }); + return $this->options; } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php index b8f83421a6d62..6befec8e942a1 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; +/** + * Provides validation classes according to corresponding rules. + */ class ValidationRules { /** @@ -16,6 +19,7 @@ class ValidationRules 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'validate-email', ]; diff --git a/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php b/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php index ad2af528a89c5..4b2ab8c4563b9 100644 --- a/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php +++ b/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php @@ -89,6 +89,7 @@ public function jsonSerialize() $this->options[$optionCode['value']] = [ 'type' => 'customer_group_' . $optionCode['value'], 'label' => __($optionCode['label']), + '__disableTmpl' => true, ]; if ($this->urlPath && $this->paramName) { diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index bb64ccc076cc6..d7521115643dd 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-customer", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-directory": "100.2.*", @@ -29,7 +29,7 @@ "magento/module-customer-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.2", + "version": "101.0.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Customer/etc/acl.xml b/app/code/Magento/Customer/etc/acl.xml index a500608e1cdf5..e8e6219bef4fe 100644 --- a/app/code/Magento/Customer/etc/acl.xml +++ b/app/code/Magento/Customer/etc/acl.xml @@ -12,6 +12,7 @@ <resource id="Magento_Customer::customer" title="Customers" translate="title" sortOrder="40"> <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10" /> <resource id="Magento_Customer::online" title="Now Online" translate="title" sortOrder="20" /> + <resource id="Magento_Customer::group" title="Customer Groups" translate="title" sortOrder="30" /> </resource> <resource id="Magento_Backend::stores"> <resource id="Magento_Backend::stores_settings"> @@ -19,10 +20,7 @@ <resource id="Magento_Customer::config_customer" title="Customers Section" translate="title" sortOrder="50" /> </resource> </resource> - <resource id="Magento_Backend::stores_other_settings"> - <resource id="Magento_Customer::group" title="Customer Groups" translate="title" sortOrder="10" /> - </resource> - </resource> + </resource> </resource> </resources> </acl> diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index fa1566af0c943..8a4d07d2bc4f3 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -26,7 +26,7 @@ </group> <group id="create_account" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Create New Account Options</label> - <field id="auto_group_assign" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="auto_group_assign" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Enable Automatic Assignment to Customer Group</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> @@ -170,7 +170,7 @@ <comment>Use 0 to disable account locking.</comment> <frontend_class>required-entry validate-digits</frontend_class> </field> - <field id="lockout_threshold" translate="label" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="lockout_threshold" translate="label comment" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Lockout Time (minutes)</label> <comment>Account will be unlocked after provided time.</comment> <frontend_class>required-entry validate-digits</frontend_class> @@ -210,7 +210,7 @@ <field id="prefix_options" translate="label comment" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Prefix Dropdown Options</label> <comment> - <![CDATA[Semicolon (;) separated values.<br/>Put semicolon in the beginning for empty first option.<br/>Leave empty for open text field.]]> + <![CDATA[Semicolon (;) separated values.<br/>Leave empty for open text field.]]> </comment> </field> <field id="middlename_show" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> @@ -228,7 +228,7 @@ <field id="suffix_options" translate="label comment" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Suffix Dropdown Options</label> <comment> - <![CDATA[Semicolon (;) separated values.<br/>Put semicolon in the beginning for empty first option.<br/>Leave empty for open text field.]]> + <![CDATA[Semicolon (;) separated values.<br/>Leave empty for open text field.]]> </comment> </field> <field id="dob_show" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> @@ -272,16 +272,17 @@ </group> <group id="address_templates" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Address Templates</label> - <field id="text" type="textarea" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="text" translate="label" type="textarea" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Text</label> </field> - <field id="oneline" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="oneline" translate="label" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Text One Line</label> </field> - <field id="html" type="textarea" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="html" translate="label" type="textarea" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>HTML</label> + <comment>Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed</comment> </field> - <field id="pdf" type="textarea" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="pdf" translate="label" type="textarea" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>PDF</label> </field> </group> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index 6eea4e1582a97..c6e383fb73bde 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -75,6 +75,13 @@ <argument name="addressConfig" xsi:type="object">Magento\Customer\Model\Address\Config\Proxy</argument> </arguments> </type> + <type name="Magento\Framework\App\Http\Context"> + <arguments> + <argument name="default" xsi:type="array"> + <item name="customer_group" xsi:type="const">Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID</item> + </argument> + </arguments> + </type> <type name="Magento\Customer\Model\Config\Share"> <arguments> <argument name="customerResource" xsi:type="object">Magento\Customer\Model\ResourceModel\Customer\Proxy</argument> @@ -120,6 +127,13 @@ <argument name="groupManagement" xsi:type="object">Magento\Customer\Api\GroupManagementInterface\Proxy</argument> </arguments> </type> + <type name="Magento\Customer\Model\Metadata\CustomerMetadata"> + <arguments> + <argument name="systemAttributes" xsi:type="array"> + <item name="disable_auto_group_change" xsi:type="string">disable_auto_group_change</item> + </argument> + </arguments> + </type> <virtualType name="SectionInvalidationConfigReader" type="Magento\Framework\Config\Reader\Filesystem"> <arguments> <argument name="idAttributes" xsi:type="array"> @@ -323,6 +337,9 @@ <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="customerNotification" type="Magento\Customer\Model\Plugin\CustomerNotification"/> </type> + <type name="Magento\PageCache\Observer\FlushFormKey"> + <plugin name="customerFlushFormKey" type="Magento\Customer\Model\Plugin\CustomerFlushFormKey"/> + </type> <type name="Magento\Customer\Model\Customer\NotificationStorage"> <arguments> <argument name="cache" xsi:type="object">Magento\Customer\Model\Cache\Type\Notification</argument> @@ -337,9 +354,9 @@ <virtualType name="Magento\Customer\Model\Api\SearchCriteria\CollectionProcessor\GroupFilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> <arguments> <argument name="fieldMapping" xsi:type="array"> - <item name="code" xsi:type="string">customer_group_code</item> - <item name="id" xsi:type="string">customer_group_id</item> - <item name="tax_class_name" xsi:type="string">class_name</item> + <item name="code" xsi:type="string">main_table.customer_group_code</item> + <item name="id" xsi:type="string">main_table.customer_group_id</item> + <item name="tax_class_name" xsi:type="string">tax_class_table.class_name</item> </argument> </arguments> </virtualType> @@ -347,9 +364,9 @@ <virtualType name="Magento\Customer\Model\Api\SearchCriteria\CollectionProcessor\GroupSortingProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor"> <arguments> <argument name="fieldMapping" xsi:type="array"> - <item name="code" xsi:type="string">customer_group_code</item> - <item name="id" xsi:type="string">customer_group_id</item> - <item name="tax_class_name" xsi:type="string">class_name</item> + <item name="code" xsi:type="string">main_table.customer_group_code</item> + <item name="id" xsi:type="string">main_table.customer_group_id</item> + <item name="tax_class_name" xsi:type="string">tax_class_table.class_name</item> </argument> <argument name="defaultOrders" xsi:type="array"> <item name="id" xsi:type="string">ASC</item> @@ -404,4 +421,12 @@ </argument> </arguments> </type> + <type name="Magento\Customer\Model\AccountManagement"> + <arguments> + <argument name="sessionManager" xsi:type="object">Magento\Framework\Session\SessionManagerInterface\Proxy</argument> + </arguments> + </type> + <preference + for="Magento\Customer\Api\AccountDelegationInterface" + type="Magento\Customer\Model\Delegation\AccountDelegation" /> </config> diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml index 66c9a3813892c..d841d8faa9c38 100644 --- a/app/code/Magento/Customer/etc/events.xml +++ b/app/code/Magento/Customer/etc/events.xml @@ -10,7 +10,7 @@ <observer name="customer_address_before_save_viv_observer" instance="Magento\Customer\Observer\BeforeAddressSaveObserver" /> </event> <event name="customer_address_save_after"> - <observer name="customer_addres_after_save_viv_observer" instance="Magento\Customer\Observer\AfterAddressSaveObserver" /> + <observer name="customer_address_after_save_viv_observer" instance="Magento\Customer\Observer\AfterAddressSaveObserver" /> </event> <event name="sales_quote_save_after"> <observer name="customer_visitor" instance="Magento\Customer\Observer\Visitor\BindQuoteCreateObserver" /> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 4a45c4ad48d19..c31742519e581 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -57,7 +57,7 @@ <type name="Magento\Checkout\Block\Cart\Sidebar"> <plugin name="customer_cart" type="Magento\Customer\Model\Cart\ConfigPlugin" /> </type> - <type name="Magento\Framework\Session\SessionManager"> + <type name="Magento\Framework\Session\SessionManagerInterface"> <plugin name="session_checker" type="Magento\Customer\CustomerData\Plugin\SessionChecker" /> </type> <type name="Magento\Authorization\Model\CompositeUserContext"> @@ -77,4 +77,4 @@ </argument> </arguments> </type> -</config> +</config> \ No newline at end of file diff --git a/app/code/Magento/Customer/etc/frontend/page_types.xml b/app/code/Magento/Customer/etc/frontend/page_types.xml index 2c0feeac532a1..a49d735b4467e 100644 --- a/app/code/Magento/Customer/etc/frontend/page_types.xml +++ b/app/code/Magento/Customer/etc/frontend/page_types.xml @@ -11,7 +11,7 @@ <type id="customer_account_createpassword" label="Reset a Password"/> <type id="customer_account_edit" label="Customer Account Edit Form"/> <type id="customer_account_forgotpassword" label="Customer Forgot Password Form"/> - <type id="customer_account_index" label="Customer My Account Dashboard"/> + <type id="customer_account_index" label="Customer My Account"/> <type id="customer_account_login" label="Customer Account Login Form"/> <type id="customer_account_logoutsuccess" label="Customer Account Logout Success"/> <type id="customer_address_form" label="Customer My Account Address Edit Form"/> diff --git a/app/code/Magento/Customer/etc/module.xml b/app/code/Magento/Customer/etc/module.xml index 3f0d42b12649a..2dfe561d0da8f 100644 --- a/app/code/Magento/Customer/etc/module.xml +++ b/app/code/Magento/Customer/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Customer" setup_version="2.0.12"> + <module name="Magento_Customer" setup_version="2.0.13"> <sequence> <module name="Magento_Eav"/> <module name="Magento_Directory"/> diff --git a/app/code/Magento/Customer/etc/webapi.xml b/app/code/Magento/Customer/etc/webapi.xml index c536e26bcc82a..38717619406aa 100644 --- a/app/code/Magento/Customer/etc/webapi.xml +++ b/app/code/Magento/Customer/etc/webapi.xml @@ -134,7 +134,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me" method="PUT"> + <route url="/V1/customers/me" method="PUT" soapOperation="saveSelf"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="save"/> <resources> <resource ref="self"/> @@ -143,7 +143,7 @@ <parameter name="customer.id" force="true">%customer_id%</parameter> </data> </route> - <route url="/V1/customers/me" method="GET"> + <route url="/V1/customers/me" method="GET" soapOperation="getSelf"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="getById"/> <resources> <resource ref="self"/> @@ -244,7 +244,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me/billingAddress" method="GET"> + <route url="/V1/customers/me/billingAddress" method="GET" soapOperation="getMyDefaultBillingAddress"> <service class="Magento\Customer\Api\AccountManagementInterface" method="getDefaultBillingAddress"/> <resources> <resource ref="self"/> @@ -259,7 +259,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me/shippingAddress" method="GET"> + <route url="/V1/customers/me/shippingAddress" method="GET" soapOperation="getMyDefaultShippingAddress"> <service class="Magento\Customer\Api\AccountManagementInterface" method="getDefaultShippingAddress"/> <resources> <resource ref="self"/> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index f2457963a5f3d..5f3ca2fdb7453 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -13,7 +13,7 @@ <arguments> <argument name="userContexts" xsi:type="array"> <item name="customerSessionUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext</item> + <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext\Proxy</item> <item name="sortOrder" xsi:type="string">20</item> </item> </argument> diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index aececbf6deeb4..a606788b48f25 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -479,9 +479,9 @@ Strong,Strong "The title that goes before name (Mr., Mrs., etc.)","The title that goes before name (Mr., Mrs., etc.)" "Prefix Dropdown Options","Prefix Dropdown Options" " - Semicolon (;) separated values.<br/>Put semicolon in the beginning for empty first option.<br/>Leave empty for open text field. + Semicolon (;) separated values.<br/>Leave empty for open text field. "," - Semicolon (;) separated values.<br/>Put semicolon in the beginning for empty first option.<br/>Leave empty for open text field. + Semicolon (;) separated values.<br/>Leave empty for open text field. " "Show Middle Name (initial)","Show Middle Name (initial)" "Always optional.","Always optional." @@ -500,6 +500,7 @@ Strong,Strong "Address Templates","Address Templates" "Online Customers Options","Online Customers Options" "Online Minutes Interval","Online Minutes Interval" +"Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed","Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed" "Leave empty for default (15 minutes).","Leave empty for default (15 minutes)." "Customer Notification","Customer Notification" "Customer Grid","Customer Grid" @@ -531,8 +532,6 @@ Type,Type "Send Welcome Email From","Send Welcome Email From" "Are you sure you want to delete this item?","Are you sure you want to delete this item?" Addresses,Addresses -"Account Dashboard","Account Dashboard" "Edit Account Information","Edit Account Information" "Password forgotten","Password forgotten" -"My Dashboard","My Dashboard" "You are signed out","You are signed out" diff --git a/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml b/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml index 143b4be507af9..bf97e92cb2922 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/edit/js.phtml @@ -4,6 +4,5 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml index f55562682d9be..b792bc27f5b64 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/sales/order/create/address/form/renderer/vat.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Customer\Block\Adminhtml\Sales\Order\Address\Form\Renderer\Vat $block */ $_element = $block->getElement(); @@ -13,16 +11,16 @@ $_note = $_element->getNote(); $_class = $_element->getFieldsetHtmlClass(); $_validateButton = $block->getValidateButton(); ?> -<?php if (!$_element->getNoDisplay()): ?> +<?php if (!$_element->getNoDisplay()) : ?> <div class="admin__field field-vat-number"> - <?php if ($_element->getType() == 'hidden'): ?> + <?php if ($_element->getType() == 'hidden') : ?> <div class="hidden"><?= $_element->getElementHtml() ?></div> - <?php else: ?> + <?php else : ?> <?= $_element->getLabelHtml() ?> <div class="admin__field-control <?= /* @noEscape */ $_element->hasValueClass() ? $block->escapeHtmlAttr($_element->getValueClass()) : 'value' ?><?= $_class ? $block->escapeHtmlAttr($_class) . '-value' : '' ?>"> <?= $_element->getElementHtml() ?> - <?php if ($_note): ?> - <div class="admin__field-note<?= $_class ? " {$_class}-note" : '' ?>" id="note_<?= $block->escapeHtmlAttr($_element->getId()) ?>"> + <?php if ($_note) : ?> + <div class="admin__field-note<?= /* @noEscape */ $_class ? " {$block->escapeHtmlAttr($_class)}-note" : '' ?>" id="note_<?= $block->escapeHtmlAttr($_element->getId()) ?>"> <span><?= $block->escapeHtml($_note) ?></span> </div> <?php endif; ?> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml index 8eb3057d0a390..ab1671ede6e8a 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Customer\Block\Adminhtml\System\Config\Validatevat $block */ ?> <script> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml index 76fa53c12548d..434e5606cd032 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart $block */ ?> -<?php if ($block->getCartHeader()): ?> +<?php if ($block->getCartHeader()) : ?> <div class="content-header skip-header"> <table> <tr> @@ -19,76 +17,74 @@ <?php endif ?> <?= $block->getGridParentHtml() ?> <?php if ($block->canDisplayContainer()) : ?> -<?php - $listType = $block->getJsObjectName(); -?> -<script> -require([ - "Magento_Ui/js/modal/alert", - "Magento_Ui/js/modal/confirm", - "Magento_Catalog/catalog/product/composite/configure" -], function(alert, confirm){ + <?php $listType = $block->getJsObjectName(); ?> + <script> + require([ + "Magento_Ui/js/modal/alert", + "Magento_Ui/js/modal/confirm", + "Magento_Catalog/catalog/product/composite/configure" + ], function(alert, confirm){ -<?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { - reload: function (params) { - if (!params) { - params = {}; - } - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; - <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; - }, + <?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { + reload: function (params) { + if (!params) { + params = {}; + } + <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; + <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); + <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; + }, - configureItem: function (itemId) { - productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); - productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); - return false; - }, + configureItem: function (itemId) { + productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); + productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); + return false; + }, - cbOnLoadIframe: function (response) { - if (!response.ok) { - return; - } - this.reload(); - }, + cbOnLoadIframe: function (response) { + if (!response.ok) { + return; + } + this.reload(); + }, - removeItem: function (itemId) { - var self = this; + removeItem: function (itemId) { + var self = this; - if (!itemId) { - alert({ - content: '<?= $block->escapeJs(__('No item specified.')) ?>' - }); + if (!itemId) { + alert({ + content: '<?= $block->escapeJs(__('No item specified.')) ?>' + }); - return false; - } + return false; + } - confirm({ - content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', - actions: { - confirm: function(){ - self.reload({'delete':itemId}); + confirm({ + content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', + actions: { + confirm: function(){ + self.reload({'delete':itemId}); + } } - } - }); - } -}; + }); + } + }; -<?php -$params = [ - 'customer_id' => $block->getCustomerId(), - 'website_id' => $block->getWebsiteId(), -]; -?> -productConfigure.addListType( - '<?= $block->escapeJs($listType) ?>', - { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' - } -); + <?php + $params = [ + 'customer_id' => $block->getCustomerId(), + 'website_id' => $block->getWebsiteId(), + ]; + ?> + productConfigure.addListType( + '<?= $block->escapeJs($listType) ?>', + { + urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', + urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' + } + ); -}); -</script> + }); + </script> <?php endif ?> <br /> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml index 30acb16c158d2..12d4902fb1892 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/newsletter.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <div class="entry-edit"> <?= $block->getForm()->getHtml() ?> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml index f43cb8ab5f0de..a0e7a2faefbab 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Customer\Block\Adminhtml\Edit\Tab\View\PersonalInfo $block */ $lastLoginDateAdmin = $block->getLastLoginDate(); @@ -13,6 +11,7 @@ $lastLoginDateStore = $block->getStoreLastLoginDate(); $createDateAdmin = $block->getCreateDate(); $createDateStore = $block->getStoreCreateDate(); +$allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> <div class="fieldset-wrapper customer-information"> <div class="fieldset-wrapper-title"> @@ -25,7 +24,7 @@ $createDateStore = $block->getStoreCreateDate(); <th><?= $block->escapeHtml(__('Last Logged In:')) ?></th> <td><?= $block->escapeHtml($lastLoginDateAdmin) ?> (<?= $block->escapeHtml($block->getCurrentStatus()) ?>)</td> </tr> - <?php if ($lastLoginDateAdmin != $lastLoginDateStore): ?> + <?php if ($lastLoginDateAdmin != $lastLoginDateStore) : ?> <tr> <th><?= $block->escapeHtml(__('Last Logged In (%1):', $block->getStoreLastLoginDateTimezone())) ?></th> <td><?= $block->escapeHtml($lastLoginDateStore) ?> (<?= $block->escapeHtml($block->getCurrentStatus()) ?>)</td> @@ -43,7 +42,7 @@ $createDateStore = $block->getStoreCreateDate(); <th><?= $block->escapeHtml(__('Account Created:')) ?></th> <td><?= $block->escapeHtml($createDateAdmin) ?></td> </tr> - <?php if ($createDateAdmin != $createDateStore): ?> + <?php if ($createDateAdmin != $createDateStore) : ?> <tr> <th><?= $block->escapeHtml(__('Account Created on (%1):', $block->getStoreCreateDateTimezone())) ?></th> <td><?= $block->escapeHtml($createDateStore) ?></td> @@ -61,7 +60,7 @@ $createDateStore = $block->getStoreCreateDate(); </table> <address> <strong><?= $block->escapeHtml(__('Default Billing Address')) ?></strong><br/> - <?= $block->getBillingAddressHtml() ?> + <?= $block->escapeHtml($block->getBillingAddressHtml(), $allowedAddressHtmlTags) ?> </address> </div> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml index 12eae5cac9b1a..f14396b0f95a2 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/sales.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Sales $block */ $singleStoreMode = $block->isSingleStoreMode(); @@ -19,7 +17,7 @@ $singleStoreMode = $block->isSingleStoreMode(); <table class="data-table"> <thead> <tr> - <?php if (!$singleStoreMode): ?> + <?php if (!$singleStoreMode) : ?> <th><?= $block->escapeHtml(__('Web Site')) ?></th> <th><?= $block->escapeHtml(__('Store')) ?></th> <th><?= $block->escapeHtml(__('Store View')) ?></th> @@ -28,7 +26,7 @@ $singleStoreMode = $block->isSingleStoreMode(); <th class="last"><?= $block->escapeHtml(__('Average Sale')) ?></th> </tr> </thead> - <?php if (!$singleStoreMode): ?> + <?php if (!$singleStoreMode) : ?> <tfoot> <tr> <td colspan="3"><strong><?= $block->escapeHtml(__('All Store Views')) ?></strong></td> @@ -37,30 +35,30 @@ $singleStoreMode = $block->isSingleStoreMode(); </tr> </tfoot> <?php endif; ?> - <?php if ($block->getRows()): ?> + <?php if ($block->getRows()) : ?> <tbody> <?php $_i = 0; ?> - <?php foreach ($block->getRows() as $_websiteId => $_groups): ?> - <?php $_websiteRow = false; ?> - <?php foreach ($_groups as $_groupId => $_stores): ?> - <?php $_groupRow = false; ?> - <?php foreach ($_stores as $_row): ?> - <?php if (!$singleStoreMode): ?> - <?php if ($_row->getStoreId() == 0): ?> - <td colspan="3"><?= $block->escapeHtml($_row->getStoreName()) ?></td> - <?php else: ?> + <?php foreach ($block->getRows() as $_websiteId => $_groups) : ?> + <?php $_websiteRow = false; ?> + <?php foreach ($_groups as $_groupId => $_stores) : ?> + <?php $_groupRow = false; ?> + <?php foreach ($_stores as $_row) : ?> + <?php if (!$singleStoreMode) : ?> + <?php if ($_row->getStoreId() == 0) : ?> + <td colspan="3"><?= $block->escapeHtml($_row->getStoreName()) ?></td> + <?php else : ?> <tr<?= ($_i++ % 2 ? ' class="even"' : '') ?>> - <?php if (!$_websiteRow): ?> + <?php if (!$_websiteRow) : ?> <td rowspan="<?= $block->escapeHtmlAttr($block->getWebsiteCount($_websiteId)) ?>"><?= $block->escapeHtml($_row->getWebsiteName()) ?></td> <?php $_websiteRow = true; ?> <?php endif; ?> - <?php if (!$_groupRow): ?> + <?php if (!$_groupRow) : ?> <td rowspan="<?= count($_stores) ?>"><?= $block->escapeHtml($_row->getGroupName()) ?></td> <?php $_groupRow = true; ?> <?php endif; ?> <td><?= $block->escapeHtml($_row->getStoreName()) ?></td> <?php endif; ?> - <?php else: ?> + <?php else : ?> <tr> <?php endif; ?> <td><?= $block->escapeHtml($block->formatCurrency($_row->getLifetime(), $_row->getWebsiteId())) ?></td> @@ -70,7 +68,7 @@ $singleStoreMode = $block->isSingleStoreMode(); <?php endforeach; ?> <?php endforeach; ?> </tbody> - <?php else: ?> + <?php else : ?> <tbody> <tr class="hidden"><td colspan="<?= /* @noEscape */ $singleStoreMode ? 2 : 5 ?>"></td></tr> </tbody> diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml index f8aa078f45e4d..29f68966777ee 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml @@ -157,23 +157,18 @@ <column name="billing_telephone" sortOrder="60"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">Phone</label> </settings> </column> <column name="billing_postcode" sortOrder="70"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">ZIP</label> </settings> </column> <column name="billing_country_id" component="Magento_Ui/js/grid/columns/select" sortOrder="80"> <settings> + <options class="Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites"/> <filter>select</filter> <dataType>select</dataType> <label translate="true">Country</label> @@ -268,9 +263,6 @@ <column name="billing_city" sortOrder="210"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">City</label> <visible>false</visible> </settings> @@ -278,9 +270,6 @@ <column name="billing_fax" sortOrder="220"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">Fax</label> <visible>false</visible> </settings> @@ -288,9 +277,6 @@ <column name="billing_vat_id" sortOrder="230"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">VAT Number</label> <visible>false</visible> </settings> @@ -298,9 +284,6 @@ <column name="billing_company" sortOrder="240"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">Company</label> <visible>false</visible> </settings> @@ -308,9 +291,6 @@ <column name="billing_firstname" sortOrder="250"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">Billing Firstname</label> <visible>false</visible> </settings> @@ -318,9 +298,6 @@ <column name="billing_lastname" sortOrder="260"> <settings> <filter>text</filter> - <editor> - <editorType>text</editorType> - </editor> <label translate="true">Billing Lastname</label> <visible>false</visible> </settings> diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 4de6644b948fb..d2a9b3f44624d 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -486,9 +486,6 @@ </item> </argument> <settings> - <validation> - <rule name="required-entry" xsi:type="boolean">true</rule> - </validation> <dataType>text</dataType> </settings> </field> diff --git a/app/code/Magento/Customer/view/frontend/email/account_new.html b/app/code/Magento/Customer/view/frontend/email/account_new.html index 6a60aee863eb4..7b77883e41f71 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new.html @@ -4,9 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> @@ -14,7 +16,7 @@ {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html index 010087ace2d42..cdd71830e1dad 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Please confirm your %store_name account" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Please confirm your %store_name account" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/confirm/', [_query:[id:$customer.id, key:$customer.confirmation, back_url:$back_url]])":"Account Confirmation URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,$url,[_query:[id:$customer.id,key:$customer.confirmation,extensions:$extensions,back_url:$back_url],_nosid:1])":"Account Confirmation URL", "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var customer.email":"Customer Email", -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var extensions":"Extensions", +"var url":"Url" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +26,7 @@ <table class="inner-wrapper" border="0" cellspacing="0" cellpadding="0" align="center"> <tr> <td align="center"> - <a href="{{var this.getUrl($store,'customer/account/confirm/',[_query:[id:$customer.id,key:$customer.confirmation,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> + <a href="{{var this.getUrl($store,$url,[_query:[id:$customer.id,key:$customer.confirmation,extensions:$extensions,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> </td> </tr> </table> diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html index 931851b28ac21..34e1103fb2f9d 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmed.html @@ -4,16 +4,18 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Thank you for confirming your %store_name account." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Thank you for confirming your %store_name account." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site, use these credentials during checkout or on the <a href="%customer_url">My Account</a> page:' diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html b/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html index 26e417d7da5a7..6d7d89067d8a2 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_no_password.html @@ -4,16 +4,18 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Welcome to %store_name" store_name=$store.getFrontendName()}} @--> -<!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +<!--@subject {{trans "Welcome to %store_name" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Create Password URL", "var customer.email":"Customer Email", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "%name," name=$customer.name}}</p> -<p>{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}</p> +<p>{{trans "Welcome to %store_name." store_name=$store.frontend_name}}</p> <p> {{trans 'To sign in to our site and set a password, click on the <a href="%create_password_url">link</a>:' diff --git a/app/code/Magento/Customer/view/frontend/email/change_email.html b/app/code/Magento/Customer/view/frontend/email/change_email.html index f343433fe35e2..4853adf638066 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email.html @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name email has been changed" store_name=$store.getFrontendName()}} @--> -<!--@vars {} @--> +<!--@subject {{trans "Your %store_name email has been changed" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" +} @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Hello,"}}</p> <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html index 0876e75beacad..49867bdedc9e0 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name email and password has been changed" store_name=$store.getFrontendName()}} @--> -<!--@vars {} @--> +<!--@subject {{trans "Your %store_name email and password has been changed" store_name=$store.frontend_name}} @--> +<!--@vars { +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" +} @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Hello,"}}</p> <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_new.html b/app/code/Magento/Customer/view/frontend/email/password_new.html index 1d2468374c6f3..975c8f7254976 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_new.html +++ b/app/code/Magento/Customer/view/frontend/email/password_new.html @@ -4,9 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Reset your %store_name password" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Reset your %store_name password" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl(store, 'customer/account/')":"Customer Account URL", +"var store.frontend_name":"Store Name", +"var this.getUrl($store, 'customer/account/')":"Customer Account URL", +"var this.getUrl($store,'customer/account/createPassword',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])":"Password Reset URL", "var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html index bfa5330cbf5b0..79015117c2280 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html @@ -4,9 +4,12 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name password has been changed" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name password has been changed" store_name=$store.frontend_name}} @--> <!--@vars { -"var customer.name":"Customer Name" +"var customer.name":"Customer Name", +"var store.frontend_name":"Store Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone" } @--> {{template config_path="design/email/header_template"}} @@ -14,11 +17,11 @@ <br> <p> - {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.getFrontendName()}} + {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> <br> -<p>{{trans "Thanks,<br>%store_name" store_name=$store.getFrontendName() |raw}}</p> +<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html index 6c17762a88227..5dc0e2dfafee9 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html @@ -4,10 +4,11 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Reset your %store_name password" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Reset your %store_name password" store_name=$store.frontend_name}} @--> <!--@vars { +"var store.frontend_name":"Store Name", "var customer.name":"Customer Name", -"var this.getUrl($store, 'customer/account/createPassword/', [_query:[id:$customer.id, token:$customer.rp_token]])":"Reset Password URL" +"var this.getUrl($store,'customer/account/createPassword/',[_query:[token:$customer.rp_token],_nosid:1])":"Reset Password URL" } @--> {{template config_path="design/email/header_template"}} @@ -21,7 +22,7 @@ <table class="inner-wrapper" border="0" cellspacing="0" cellpadding="0" align="center"> <tr> <td align="center"> - <a href="{{var this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])}}" target="_blank">{{trans "Set a New Password"}}</a> + <a href="{{var this.getUrl($store,'customer/account/createPassword/',[_query:[token:$customer.rp_token],_nosid:1])}}" target="_blank">{{trans "Set a New Password"}}</a> </td> </tr> </table> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account.xml index ac03fa7d293a4..a2a15a4166b73 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd" label="Customer My Account (All Pages)" design_abstraction="custom"> + <head> + <title>My Account + @@ -19,7 +22,7 @@ - Account Dashboard + My Account customer/account 250 diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml index 8f65d54f458bc..fd5ecbfa7f277 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml @@ -12,6 +12,9 @@ + + Magento\Customer\Block\DataProviders\AddressAttributeData + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml index 9a701c14a0307..24cede5f0232a 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml @@ -7,7 +7,7 @@ --> - Forgot Your Password + Forgot Your Password? diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_index.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_index.xml index 6384ee9c6562c..4494a5dd67103 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_index.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_index.xml @@ -10,7 +10,7 @@ - My Dashboard + My Account diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index d49dae6dee58f..3518df736c4ac 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> + + Customer Login + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml index 194dfd1bc7d2b..f5ee2b347a5b2 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml @@ -17,7 +17,12 @@ - + + + Magento\Customer\Block\DataProviders\AddressAttributeData + Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData + + diff --git a/app/code/Magento/Customer/view/frontend/requirejs-config.js b/app/code/Magento/Customer/view/frontend/requirejs-config.js index 20dd53ded11c9..f1bf5c1d1b67f 100644 --- a/app/code/Magento/Customer/view/frontend/requirejs-config.js +++ b/app/code/Magento/Customer/view/frontend/requirejs-config.js @@ -7,11 +7,13 @@ var config = { map: { '*': { checkoutBalance: 'Magento_Customer/js/checkout-balance', - address: 'Magento_Customer/address', - changeEmailPassword: 'Magento_Customer/change-email-password', + address: 'Magento_Customer/js/address', + changeEmailPassword: 'Magento_Customer/js/change-email-password', passwordStrengthIndicator: 'Magento_Customer/js/password-strength-indicator', zxcvbn: 'Magento_Customer/js/zxcvbn', - addressValidation: 'Magento_Customer/js/addressValidation' + addressValidation: 'Magento_Customer/js/addressValidation', + 'Magento_Customer/address': 'Magento_Customer/js/address', + 'Magento_Customer/change-email-password': 'Magento_Customer/js/change-email-password' } } }; diff --git a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml index ca7393f2129e0..0d4cf3c721d14 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Customer\Block\Account\AuthenticationPopup $block */ ?>
    - + - - + + - getVirtualQuoteItems() as $_item): ?> + getVirtualQuoteItems() as $_item) : ?> - + diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml index 1de8357db8986..a696a693fa002 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml @@ -4,27 +4,26 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Files.LineLength ?> -escapeHtml($block->getProductName()) ?> -getOptionList()): ?> +escapeHtml($block->getProductName()) ?> +getOptionList()) : ?>
    getFormatedOptionValue($_option) ?>
    escapeHtml($_option['label']) ?>
    - class="tooltip wrapper"> - - + > + escapeHtml($_formatedOptionValue['value'], ['span']) ?> +
    escapeHtml($_option['label']) ?>
    -
    +
    escapeHtml($_formatedOptionValue['full_view'], ['span']) ?>
    -getProductAdditionalInformationBlock()): ?> +getProductAdditionalInformationBlock()) : ?> setItem($block->getItem())->toHtml() ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/link.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/link.phtml index ae65caf6983d0..15fe496ddd802 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/link.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/link.phtml @@ -4,7 +4,5 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> - +escapeHtml(__('Check Out with Multiple Addresses')) ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml index 2549eff3aca7d..5fff0d72e8000 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml @@ -4,105 +4,148 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var \Magento\Multishipping\Block\Checkout\Overview $block */ ?> -
    - getBlockHtml('formkey') ?> +getCheckoutData()->getAddressErrors(); ?> + $error) : ?> +
    + escapeHtml($error); ?> + escapeHtml(__('Please see')); ?> + + escapeHtml(__('details below')); ?>. +
    + + + getBlockHtml('formkey'); ?>
    -
    +
    escapeHtml(__('Billing Information')); ?>
    - getBillingAddress() ?> + getBillingAddress() ?> - - + escapeHtml(__('Billing Address')); ?> + escapeHtml(__('Change')); ?>
    - format('html') ?> + format('html') ?>
    - - + escapeHtml(__('Payment Method')); ?> + escapeHtml(__('Change')); ?>
    - - - getPaymentHtml() ?> + + + getPaymentHtml() ?>
    -
    - helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> - getShippingAddresses() as $_index => $_address): ?> +
    escapeHtml(__('Shipping Information')); ?>
    + helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> + getShippingAddresses() as $index => $address) : ?>
    +
    - of %2', ($_index+1), $block->getShippingAddressCount()) ?> + escapeHtml(__('Address')); ?> escapeHtml($index + 1); ?> + + escapeHtml(__('of')); ?> + escapeHtml($block->getShippingAddressCount())?> + +
    + getCheckoutData()->getAddressError($address)) : ?> +
    escapeHtml($error); ?>
    +
    - - + escapeHtml(__('Shipping To')); ?> + escapeHtml(__('Change')); ?>
    - format('html') ?> + format('html') ?>
    - - + escapeHtml(__('Shipping Method')); ?> + escapeHtml(__('Change')); ?> - getShippingAddressRate($_address)): ?> + getShippingAddressRate($address)) : ?>
    - escapeHtml($_rate->getCarrierTitle()) ?> (escapeHtml($_rate->getMethodTitle()) ?>) - getShippingPriceExclTax($_address); ?> - getShippingPriceInclTax($_address); ?> - - helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - - - - helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - - - helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - - + escapeHtml($_rate->getCarrierTitle()) ?> + (escapeHtml($_rate->getMethodTitle()) ?>) + getShippingPriceExclTax($address); + $inclTax = $block->getShippingPriceInclTax($address); + $displayBothPrices = $this->helper(Magento\Tax\Helper\Data::class) + ->displayShippingBothPrices() && $inclTax !== $exclTax; + ?> + + + + + + + + + +
    -
    escapeHtml(__('Other items in your order')) ?>
    escapeHtml(__('Product Name')) ?>escapeHtml(__('Qty')) ?>
    getItemHtml($_item) ?>getQty() ?>escapeHtml($_item->getQty()) ?>
    - +
    + - - - - + + + - getShippingAddressItems($_address) as $_item): ?> - getRowItemHtml($_item) ?> + getShippingAddressItems($address) as $item) : ?> + getRowItemHtml($item) ?> - renderTotals($block->getShippingAddressTotals($_address)) ?> + renderTotals( + $block->getShippingAddressTotals($address) + ); ?>
    escapeHtml(__('Order Review')); ?>
    - + escapeHtml(__('Item')); ?> + + escapeHtml(__('Edit')); ?> + escapeHtml(__('Price')); ?>escapeHtml(__('Qty')); ?>escapeHtml(__('Subtotal')); ?>
    @@ -112,33 +155,40 @@ - getQuote()->hasVirtualItems()): ?> + getQuote()->hasVirtualItems()) : ?>
    -
    + getQuote()->getBillingAddress(); ?> + +
    escapeHtml(__('Other items in your order')); ?>
    + getCheckoutData()->getAddressError($billingAddress)) :?> +
    escapeHtml($error); ?>
    +
    - - + escapeHtml(__('Items')); ?> + escapeHtml(__('Edit Items')); ?> - helper('Magento\Tax\Helper\Data')->displayCartBothPrices() ? 2 : 1); ?> + helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?>
    - + - - - - + + + + - getVirtualItems() as $_item): ?> - getRowItemHtml($_item) ?> + getVirtualItems() as $_item) : ?> + getRowItemHtml($_item) ?> - renderTotals($block->getBillinAddressTotals()) ?> + renderTotals($block->getBillingAddressTotals()); ?>
    escapeHtml(__('Items')); ?>
    escapeHtml(__('Product Name')); ?>escapeHtml(__('Price')); ?>escapeHtml(__('Qty')); ?>escapeHtml(__('Subtotal')); ?>
    @@ -146,23 +196,34 @@
    - getChildHtml('items_after') ?> + getChildHtml('items_after') ?>
    - getChildHtml('agreements') ?> + getChildHtml('agreements') ?>
    - - helper('Magento\Checkout\Helper\Data')->formatPrice($block->getTotal()) ?> + escapeHtml(__('Grand Total:')); ?> + + helper(Magento\Checkout\Helper\Data::class) + ->formatPrice($block->getTotal()); ?> +
    - +
    -
    diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview/item.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview/item.phtml index 5424f37efac07..774973be07765 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview/item.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview/item.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Files.LineLength, Magento2.Templates.ThisInTemplate ?> - helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + helper(Magento\Tax\Helper\Data::class)->displayCartPriceInclTax() || $this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices()) : ?> getUnitPriceInclTaxHtml($_item) ?> - helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + helper(Magento\Tax\Helper\Data::class)->displayCartPriceExclTax() || $this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices()) : ?> getUnitPriceExclTaxHtml($_item) ?> @@ -37,17 +37,17 @@ - getQty()*1 ?> + getQty() * 1 ?> - helper('Magento\Tax\Helper\Data')->displayCartPriceInclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + helper(Magento\Tax\Helper\Data::class)->displayCartPriceInclTax() || $this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices()) : ?> getRowTotalInclTaxHtml($_item) ?> - helper('Magento\Tax\Helper\Data')->displayCartPriceExclTax() || $this->helper('Magento\Tax\Helper\Data')->displayCartBothPrices()): ?> + helper(Magento\Tax\Helper\Data::class)->displayCartPriceExclTax() || $this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices()) : ?> getRowTotalExclTaxHtml($_item) ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml new file mode 100644 index 0000000000000..dacf96f9c0baf --- /dev/null +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml @@ -0,0 +1,90 @@ +getOrderIds(); +?> +
    +

    + + escapeHtml(__('Not all items were included.')); ?> + + escapeHtml(__('For details, see')); ?> + escapeHtml(__('Failed to Order')); ?> + escapeHtml(__('section below')); ?> +

    + +

    + escapeHtml(__('For successfully ordered items, you\'ll receive a confirmation email '. + 'including order numbers, tracking information, and more details.')); ?> +

    +
    +

    escapeHtml(__('Successfully Ordered')); ?>

    +
      + $incrementId) : ?> +
    • + + getOrderShippingAddress($orderId); ?> +
      + + escapeHtml(__('Ship to:')); ?> + + escapeHtml($block->formatOrderShippingAddress($shippingAddress)); ?> + + + + escapeHtml(__('No shipping required.')); ?> + + +
      +
    • + +
    +
    + +
    +

    escapeHtml(__('Failed to Order')); ?>

    +
    +
    + escapeHtml(__('To purchase these items: Return to the')); ?> + + escapeHtml(__('Review page in Checkout')); ?>, + escapeHtml(__('resolve any errors, and place a new order.'))?> +
    +
    + getFailedAddresses() ?> + +
      + +
    1. +
      +
      + isShippingAddress($address)) : ?> + escapeHtml(__('Ship to:')); ?> + + escapeHtml($block->formatQuoteShippingAddress($address)); ?> + + + + escapeHtml(__('No shipping required.')); ?> + + +
      +
      + escapeHtml(__('Error:')); ?> + + getAddressError($address); ?> + +
      +
      +
    2. + +
    + +
    +
    diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/shipping.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/shipping.phtml index f66a2093b0aff..af67c087ed24d 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/shipping.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/shipping.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Files.LineLength, Magento2.Templates.ThisInTemplate ?> - - getAddresses() as $_index => $_address): ?> + + getAddresses() as $_index => $_address) : ?>
    -
    of %2', ($_index+1), $block->getAddressCount()) ?>
    +
    escapeHtml(__('Address %1 of %2', ($_index+1), $block->getAddressCount()), ['span']) ?>
    - - + escapeHtml(__('Shipping To')) ?> + + escapeHtml(__('Change')) ?> +
    -
    format('html') ?>
    +
    format('html') ?>
    - + escapeHtml(__('Shipping Method')) ?>
    - getShippingRates($_address))): ?> -

    - + getShippingRates($_address))) : ?> +

    escapeHtml(__('Sorry, no quotes are available for this order right now.')) ?>

    +
    - $_rates): ?> + $_rates) : ?>
    escapeHtml($block->getCarrierName($code)) ?>
    escapeHtml($block->getCarrierName($code)) ?>
    - +
    - getErrorMessage()): ?> + getErrorMessage()) : ?> escapeHtml($_rate->getCarrierTitle()) ?>: escapeHtml($_rate->getErrorMessage()) ?> - +
    - - - getCode()===$block->getAddressShippingMethod($_address)) echo ' checked="checked"' ?> class="radio" /> + + + getCode()===$block->getAddressShippingMethod($_address)) ? ' checked="checked"' : '' ?> class="radio" />
    -
    @@ -78,29 +83,29 @@
    - getItemsBoxTextAfter($_address) ?> + getItemsBoxTextAfter($_address) ?>
    - - + escapeHtml(__('Items')) ?> + escapeHtml(__('Edit Items')) ?>
    - - +
    + - - + + - getAddressItems($_address) as $_item): ?> + getAddressItems($_address) as $_item) : ?> - - + + @@ -114,10 +119,10 @@ getChildHtml('checkout_billing_items') ?>
    - +
    diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/state.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/state.phtml index bf520d639a58d..4d8b63e605732 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/state.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/state.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Files.LineLength ?>
      - getSteps() as $_step): ?> -
    1. getLabel() ?>
    2. + getSteps() as $_step) : ?> +
    3. + escapeHtml($_step->getLabel()) ?> +
    diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml index 3403c745e6495..57c4afaee6541 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml @@ -4,27 +4,48 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Multishipping\Block\Checkout\Success $block */ ?> -
    -

    escapeHtml(__('Thank you for your purchase!')) ?>

    -

    escapeHtml(__('Thanks for your order. We\'ll email you order details and tracking information.')) ?>

    - getOrderIds()): ?> -

    - - - 1): ?> - escapeHtml(__('Your order numbers are: ')) ?> - - escapeHtml(__('Your order number is: ')) ?> - - - $incrementId): ?> -

    - - getChildHtml() ?> -
    - escapeHtml(__('Continue Shopping')) ?> +
    +
    +

    escapeHtml(__('For successfully order items, you\'ll receive a confirmation email including '. + 'order numbers, tracking information and more details.')) ?>

    + getOrderIds()) : ?> +

    escapeHtml(__('Successfully ordered'))?>

    +
    +
      + $incrementId) : ?> +
    • + + getCheckoutData()->getOrderShippingAddress($orderId); ?> +
      + + escapeHtml(__('Ship to:')); ?> + + escapeHtml( + $block->getCheckoutData()->formatOrderShippingAddress($shippingAddress) + ); ?> + + + + escapeHtml(__('No shipping required.')); ?> + + +
      +
    • + +
    +
    + + getChildHtml() ?> +
    +
    + +
    +
    -
    + diff --git a/app/code/Magento/Multishipping/view/frontend/templates/js/components.phtml b/app/code/Magento/Multishipping/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> getChildHtml() ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/multishipping/item/default.phtml b/app/code/Magento/Multishipping/view/frontend/templates/multishipping/item/default.phtml index 79e6d02465847..9245aec92ace5 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/multishipping/item/default.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/multishipping/item/default.phtml @@ -4,29 +4,28 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Files.LineLength ?>
    - escapeHtml($block->getProductName()) ?> - getOptionList()): ?> + escapeHtml($block->getProductName()) ?> + getOptionList()) : ?>
    getFormatedOptionValue($_option) ?>
    escapeHtml($_option['label']) ?>
    - class="tooltip wrapper"> - - + > + escapeHtml($_formatedOptionValue['value']) ?> +
    escapeHtml($_option['label']) ?>
    -
    +
    escapeHtml($_formatedOptionValue['full_view']) ?>
    - getProductAdditionalInformationBlock()): ?> + getProductAdditionalInformationBlock()) : ?> setItem($block->getItem())->toHtml() ?>
    diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 9b867cd7217b1..3a6d73e304974 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -15,7 +15,7 @@ define([ opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled. pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader. placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button. - agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions + agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions }, /** diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/payment.js b/app/code/Magento/Multishipping/view/frontend/web/js/payment.js index 94987328bb278..da24b99597d42 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/payment.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/payment.js @@ -63,9 +63,12 @@ define([ parentsDl = element.closest('dl'); parentsDl.find('dt input:radio').prop('checked', false); - parentsDl.find('.items').hide().find('[name^="payment["]').prop('disabled', true); + parentsDl.find('dd').addClass('no-display').end() + .find('.items').hide() + .find('[name^="payment["]').prop('disabled', true); element.prop('checked', true).parent() - .nextUntil('dt').find('.items').show().find('[name^="payment["]').prop('disabled', false); + .next('dd').removeClass('no-display') + .find('.items').show().find('[name^="payment["]').prop('disabled', false); }, /** @@ -122,16 +125,35 @@ define([ this.element.find(this.options.methodsContainer).show(); }, + /** + * Returns checked payment method. + * + * @private + */ + _getSelectedPaymentMethod: function () { + return this.element.find('input[name=\'payment[method]\']:checked'); + }, + /** * Validate before form submit * @private * @param {EventObject} e */ _submitHandler: function (e) { + var currentMethod, + submitButton; + e.preventDefault(); if (this._validatePaymentMethod()) { - this.element.submit(); + currentMethod = this._getSelectedPaymentMethod(); + submitButton = currentMethod.parent().next('dd').find('button[type=submit]'); + + if (submitButton.length) { + submitButton.first().trigger('click'); + } else { + this.element.submit(); + } } } }); diff --git a/app/code/Magento/NewRelicReporting/Console/Command/DeployMarker.php b/app/code/Magento/NewRelicReporting/Console/Command/DeployMarker.php new file mode 100644 index 0000000000000..92231dae69fbe --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Console/Command/DeployMarker.php @@ -0,0 +1,81 @@ +deploymentsFactory = $deploymentsFactory; + $this->serviceShellUser = $serviceShellUser; + parent::__construct($name); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName("newrelic:create:deploy-marker"); + $this->setDescription("Check the deploy queue for entries and create an appropriate deploy marker.") + ->addArgument( + 'message', + InputArgument::REQUIRED, + 'Deploy Message?' + ) + ->addArgument( + 'changelog', + InputArgument::REQUIRED, + 'Change Log?' + ) + ->addArgument( + 'user', + InputArgument::OPTIONAL, + 'Deployment User' + ); + parent::configure(); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->deploymentsFactory->create()->setDeployment( + $input->getArgument('message'), + $input->getArgument('changelog'), + $this->serviceShellUser->get($input->getArgument('user')) + ); + $output->writeln('NewRelic deployment information sent'); + } +} diff --git a/app/code/Magento/NewRelicReporting/Model/Config.php b/app/code/Magento/NewRelicReporting/Model/Config.php index 32e1078c01c9d..4bb381eb2f12d 100644 --- a/app/code/Magento/NewRelicReporting/Model/Config.php +++ b/app/code/Magento/NewRelicReporting/Model/Config.php @@ -5,6 +5,9 @@ */ namespace Magento\NewRelicReporting\Model; +/** + * NewRelic configuration model + */ class Config { /**#@+ @@ -161,6 +164,16 @@ public function getNewRelicAppName() return (string)$this->scopeConfig->getValue('newrelicreporting/general/app_name'); } + /** + * Returns configured separate apps value + * + * @return bool + */ + public function isSeparateApps() + { + return (bool)$this->scopeConfig->getValue('newrelicreporting/general/separate_apps'); + } + /** * Returns config setting for overall cron to be enabled * diff --git a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php index 9cdc90bc46b2a..78c485c5bb6f5 100644 --- a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php +++ b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php @@ -64,6 +64,7 @@ public function report() $moduleData = $this->collect->getModuleData(); if (count($moduleData['changes']) > 0) { foreach ($moduleData['changes'] as $change) { + $modelData = []; switch ($change['type']) { case Config::ENABLED: $modelData = [ diff --git a/app/code/Magento/NewRelicReporting/Model/Cron/ReportNewRelicCron.php b/app/code/Magento/NewRelicReporting/Model/Cron/ReportNewRelicCron.php index a4a7d30b44f5b..6b2bd50dc456b 100644 --- a/app/code/Magento/NewRelicReporting/Model/Cron/ReportNewRelicCron.php +++ b/app/code/Magento/NewRelicReporting/Model/Cron/ReportNewRelicCron.php @@ -175,7 +175,6 @@ protected function reportCounts() public function report() { if ($this->config->isNewRelicEnabled()) { - $this->reportModules(); $this->reportCounts(); } diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php index 845ed0429d2c3..9882a1ce9b0b8 100644 --- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php +++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php @@ -31,7 +31,7 @@ public function addCustomParameter($param, $value) /** * Wrapper for 'newrelic_notice_error' function * - * @param Exception $exception + * @param \Exception $exception * @return void */ public function reportError($exception) @@ -41,6 +41,19 @@ public function reportError($exception) } } + /** + * Wrapper for 'newrelic_set_appname' + * + * @param string $appName + * @return void + */ + public function setAppName(string $appName) + { + if (extension_loaded('newrelic')) { + newrelic_set_appname($appName); + } + } + /** * Checks whether newrelic-php5 agent is installed * diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdmins.php b/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdmins.php index 615c80633cb0f..9dfd0e1e3319a 100644 --- a/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdmins.php +++ b/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdmins.php @@ -66,8 +66,8 @@ public function execute(Observer $observer) $user = $this->backendAuthSession->getUser(); $jsonData = [ 'id' => $user->getId(), - 'username' => $user->getUsername(), - 'name' => $user->getFirstname() . ' ' . $user->getLastname(), + 'username' => $user->getUserName(), + 'name' => $user->getFirstName() . ' ' . $user->getLastName(), ]; $modelData = [ diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdminsToNewRelic.php b/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdminsToNewRelic.php index cff1b159d481d..2f142f6ac8124 100644 --- a/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdminsToNewRelic.php +++ b/app/code/Magento/NewRelicReporting/Model/Observer/ReportConcurrentAdminsToNewRelic.php @@ -58,10 +58,10 @@ public function execute(Observer $observer) if ($this->backendAuthSession->isLoggedIn()) { $user = $this->backendAuthSession->getUser(); $this->newRelicWrapper->addCustomParameter(Config::ADMIN_USER_ID, $user->getId()); - $this->newRelicWrapper->addCustomParameter(Config::ADMIN_USER, $user->getUsername()); + $this->newRelicWrapper->addCustomParameter(Config::ADMIN_USER, $user->getUserName()); $this->newRelicWrapper->addCustomParameter( Config::ADMIN_NAME, - $user->getFirstname() . ' ' . $user->getLastname() + $user->getFirstName() . ' ' . $user->getLastName() ); } } diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/ReportSystemCacheFlushToNewRelic.php b/app/code/Magento/NewRelicReporting/Model/Observer/ReportSystemCacheFlushToNewRelic.php index 0e3ad1605f948..5500aba195936 100644 --- a/app/code/Magento/NewRelicReporting/Model/Observer/ReportSystemCacheFlushToNewRelic.php +++ b/app/code/Magento/NewRelicReporting/Model/Observer/ReportSystemCacheFlushToNewRelic.php @@ -58,8 +58,8 @@ public function execute(Observer $observer) if ($user->getId()) { $this->deploymentsFactory->create()->setDeployment( 'Cache Flush', - $user->getUsername() . ' flushed the cache.', - $user->getUsername() + $user->getUserName() . ' flushed the cache.', + $user->getUserName() ); } } diff --git a/app/code/Magento/NewRelicReporting/Model/ServiceShellUser.php b/app/code/Magento/NewRelicReporting/Model/ServiceShellUser.php new file mode 100644 index 0000000000000..c038be4fb2a76 --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Model/ServiceShellUser.php @@ -0,0 +1,34 @@ +config = $config; + $this->newRelicWrapper = $newRelicWrapper; + $this->logger = $logger; + } + + /** + * Set separate appname + * + * @param State $subject + * @param mixed $result + * @return mixed + */ + public function afterSetAreaCode(State $subject, $result) + { + if (!$this->shouldSetAppName()) { + return $result; + } + + try { + $this->newRelicWrapper->setAppName($this->appName($subject)); + } catch (LocalizedException $e) { + $this->logger->critical($e); + return $result; + } + + return $result; + } + + /** + * Format appName. + * + * @param State $state + * @return string + * @throws LocalizedException + */ + private function appName(State $state): string + { + $code = $state->getAreaCode(); + $current = $this->config->getNewRelicAppName(); + + return $current . ';' . $current . '_' . $code; + } + + /** + * Check if app name should be set. + * + * @return bool + */ + private function shouldSetAppName(): bool + { + return ( + $this->config->isSeparateApps() && + $this->config->getNewRelicAppName() && + $this->config->isNewRelicEnabled() + ); + } +} diff --git a/app/code/Magento/NewRelicReporting/Test/Mftf/LICENSE.txt b/app/code/Magento/NewRelicReporting/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/NewRelicReporting/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/app/code/Magento/NewRelicReporting/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/NewRelicReporting/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/NewRelicReporting/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/app/code/Magento/NewRelicReporting/Test/Mftf/README.md b/app/code/Magento/NewRelicReporting/Test/Mftf/README.md new file mode 100644 index 0000000000000..911d92102a572 --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# New Relic Reporting Functional Tests + +The Functional Test Module for **Magento New Relic Reporting** module. diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php index eb665773d5f4b..1193ac088633f 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php @@ -252,6 +252,9 @@ public function testSetDeploymentRequestFail() ); } + /** + * @return array + */ private function getDataVariables() { $description = 'Event description'; diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportCountsTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportCountsTest.php index d0159ebbea368..f2d50e913aedc 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportCountsTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportCountsTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\NewRelicReporting\Test\Unit\Model\Cron; use Magento\NewRelicReporting\Model\Cron\ReportCounts; @@ -11,9 +12,7 @@ use Magento\Catalog\Api\CategoryManagementInterface; /** - * Class ReportCountsTest - * - * @codingStandardsIgnoreFile + * Test for Magento\NewRelicReporting\Model\Cron\ReportCounts */ class ReportCountsTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php index 70fdcd0b6191c..400bcefa9828b 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php @@ -144,24 +144,10 @@ public function testReportNewRelicCronModuleDisabledFromConfig() */ public function testReportNewRelicCron() { - $testModuleData = [ - 'changes' => [ - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'enabled'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'disabled'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'installed'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'uninstalled'], - ], - 'enabled' => 1, - 'disabled' => 1, - 'installed' => 1, - ]; $this->config->expects($this->once()) ->method('isNewRelicEnabled') ->willReturn(true); - $this->collect->expects($this->once()) - ->method('getModuleData') - ->willReturn($testModuleData); $this->counter->expects($this->once()) ->method('getAllProductsCount'); $this->counter->expects($this->once()) @@ -198,24 +184,10 @@ public function testReportNewRelicCron() */ public function testReportNewRelicCronRequestFailed() { - $testModuleData = [ - 'changes' => [ - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'enabled'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'disabled'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'installed'], - ['name' => 'name', 'setup_version' => '2.0.0', 'type' => 'uninstalled'], - ], - 'enabled' => 1, - 'disabled' => 1, - 'installed' => 1, - ]; $this->config->expects($this->once()) ->method('isNewRelicEnabled') ->willReturn(true); - $this->collect->expects($this->once()) - ->method('getModuleData') - ->willReturn($testModuleData); $this->counter->expects($this->once()) ->method('getAllProductsCount'); $this->counter->expects($this->once()) diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php index a9dbacdfb0405..32aae7b456cdf 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\NewRelicReporting\Test\Unit\Model\Module; use Magento\NewRelicReporting\Model\Module\Collect; @@ -10,11 +11,10 @@ use Magento\Framework\Module\ModuleListInterface; use Magento\Framework\Module\Manager; use Magento\NewRelicReporting\Model\Module; +use Magento\NewRelicReporting\Model\ResourceModel\Module\CollectionFactory; /** - * Class CollectTest - * - * @codingStandardsIgnoreFile + * Test for \Magento\NewRelicReporting\Model\Module\Collect */ class CollectTest extends \PHPUnit\Framework\TestCase { @@ -44,7 +44,7 @@ class CollectTest extends \PHPUnit\Framework\TestCase protected $moduleFactoryMock; /** - * @var \Magento\NewRelicReporting\Model\ResourceModel\Module\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $moduleCollectionFactoryMock; @@ -65,9 +65,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->moduleFactoryMock = $this->createPartialMock(\Magento\NewRelicReporting\Model\ModuleFactory::class, ['create']); + $this->moduleFactoryMock = $this->createPartialMock( + \Magento\NewRelicReporting\Model\ModuleFactory::class, + ['create'] + ); - $this->moduleCollectionFactoryMock = $this->createPartialMock(\Magento\NewRelicReporting\Model\ResourceModel\Module\CollectionFactory::class, ['create']); + $this->moduleCollectionFactoryMock = $this->createPartialMock(CollectionFactory::class, ['create']); $this->model = new Collect( $this->moduleListMock, @@ -86,7 +89,8 @@ protected function setUp() public function testGetModuleDataWithoutRefresh() { $moduleCollectionMock = $this->getMockBuilder( - \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class) + \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class + ) ->disableOriginalConstructor() ->getMock(); $itemMock = $this->createMock(\Magento\NewRelicReporting\Model\Module::class); @@ -175,11 +179,15 @@ public function testGetModuleDataWithoutRefresh() public function testGetModuleDataRefresh($data) { $moduleCollectionMock = $this->getMockBuilder( - \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class) + \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class + ) ->disableOriginalConstructor() ->getMock(); /** @var \Magento\NewRelicReporting\Model\Module|\PHPUnit_Framework_MockObject_MockObject $itemMock */ - $itemMock = $this->createPartialMock(\Magento\NewRelicReporting\Model\Module::class, ['getName', 'getData', 'setData', 'getState', 'save']); + $itemMock = $this->createPartialMock( + \Magento\NewRelicReporting\Model\Module::class, + ['getName', 'getData', 'setData', 'getState', 'save'] + ); $modulesMockArray = [ 'Module_Name1' => [ 'name' => 'Module_Name1', @@ -265,11 +273,15 @@ public function testGetModuleDataRefresh($data) public function testGetModuleDataRefreshOrStatement($data) { $moduleCollectionMock = $this->getMockBuilder( - \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class) + \Magento\NewRelicReporting\Model\ResourceModel\Module\Collection::class + ) ->disableOriginalConstructor() ->getMock(); /** @var \Magento\NewRelicReporting\Model\Module|\PHPUnit_Framework_MockObject_MockObject $itemMock */ - $itemMock = $this->createPartialMock(\Magento\NewRelicReporting\Model\Module::class, ['getName', 'getData', 'setData', 'getState', 'save']); + $itemMock = $this->createPartialMock( + \Magento\NewRelicReporting\Model\Module::class, + ['getName', 'getData', 'setData', 'getState', 'save'] + ); $modulesMockArray = [ 'Module_Name1' => [ 'name' => 'Module_Name1', @@ -346,6 +358,9 @@ public function testGetModuleDataRefreshOrStatement($data) ); } + /** + * @return array + */ public function itemDataProvider() { return [ diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/ReportProductSavedToNewRelicTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/ReportProductSavedToNewRelicTest.php index 20791511324bf..ba5f13d247fe0 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/ReportProductSavedToNewRelicTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/ReportProductSavedToNewRelicTest.php @@ -140,6 +140,9 @@ public function testReportProductUpdatedToNewRelic($isNewObject) $this->model->execute($eventObserver); } + /** + * @return array + */ public function actionDataProvider() { return [[true], [false]]; diff --git a/app/code/Magento/NewRelicReporting/composer.json b/app/code/Magento/NewRelicReporting/composer.json index 4a02d673a54f6..49e3bb027061c 100644 --- a/app/code/Magento/NewRelicReporting/composer.json +++ b/app/code/Magento/NewRelicReporting/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-new-relic-reporting", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/module-customer": "101.0.*", @@ -13,7 +13,7 @@ "magento/magento-composer-installer": "*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml b/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml index 582b7c752386a..98f9c55adbdf0 100644 --- a/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml +++ b/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml @@ -46,6 +46,11 @@ This is located by navigating to Settings from the New Relic APM website + + + Magento\Config\Model\Config\Source\Yesno + In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set. + diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml index cba92f91cd4bb..bab7d6611f14b 100644 --- a/app/code/Magento/NewRelicReporting/etc/di.xml +++ b/app/code/Magento/NewRelicReporting/etc/di.xml @@ -30,4 +30,14 @@ + + + + + + + Magento\NewRelicReporting\Console\Command\DeployMarker + + + diff --git a/app/code/Magento/NewRelicReporting/i18n/en_US.csv b/app/code/Magento/NewRelicReporting/i18n/en_US.csv index 433b1b22fcddd..5ea64d3d43439 100644 --- a/app/code/Magento/NewRelicReporting/i18n/en_US.csv +++ b/app/code/Magento/NewRelicReporting/i18n/en_US.csv @@ -21,3 +21,5 @@ General,General "This is located by navigating to Settings from the New Relic APM website","This is located by navigating to Settings from the New Relic APM website" Cron,Cron "Enable Cron","Enable Cron" +"Send Adminhtml and Frontend as Separate Apps","Send Adminhtml and Frontend as Separate Apps" +"In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set.","In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set." diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php b/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php index c5f4dc68d4dd2..61a17d7ad5e51 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php @@ -19,7 +19,7 @@ class Problem extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'problem/list.phtml'; + protected $_template = 'Magento_Newsletter::problem/list.phtml'; /** * @var \Magento\Newsletter\Model\ResourceModel\Problem\Collection diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php index f085b0f6c9c8b..ca90b5d84a10f 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php @@ -19,7 +19,7 @@ class Edit extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'queue/edit.phtml'; + protected $_template = 'Magento_Newsletter::queue/edit.phtml'; /** * Core registry diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php index 6a76ed51f41ce..60746864766c4 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Newsletter\Block\Adminhtml\Queue\Edit; /** diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php b/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php index 86e7e7ee4756d..4d5165db68736 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php @@ -29,7 +29,7 @@ class Subscriber extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'subscriber/list.phtml'; + protected $_template = 'Magento_Newsletter::subscriber/list.phtml'; /** * @var \Magento\Newsletter\Model\ResourceModel\Queue\CollectionFactory diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template.php index 02b60e049f254..92ae6e6c3db04 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template.php @@ -16,7 +16,7 @@ class Template extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'template/list.phtml'; + protected $_template = 'Magento_Newsletter::template/list.phtml'; /** * @return $this diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Edit.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Edit.php index d7ce2654f03e3..2ac3f07bfdae6 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Edit.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Edit.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Newsletter Template Edit Block * @@ -69,12 +67,6 @@ public function getModel() */ protected function _prepareLayout() { - // Load Wysiwyg on demand and Prepare layout -// $block = $this->getLayout()->getBlock('head'); -// if ($this->_wysiwygConfig->isEnabled() && $block) { -// $block->setCanLoadTinyMce(true); -// } - $this->getToolbar()->addChild( 'back_button', \Magento\Backend\Block\Widget\Button::class, @@ -213,12 +205,12 @@ public function getHeaderText() public function getForm() { return $this->getLayout()->createBlock( - \Magento\Newsletter\Block\Adminhtml\Template\Edit\Form::class + \Magento\Newsletter\Block\Adminhtml\Template\Edit\Form::class )->toHtml(); } /** - * Return return template name for JS + * Return template name for JS * * @return string */ @@ -240,6 +232,8 @@ public function getSaveUrl() /** * Return preview action url for form * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + * * @return string */ public function getPreviewUrl() @@ -273,6 +267,8 @@ public function getTemplateType() /** * Return delete url for customer group * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + * * @return string */ public function getDeleteUrl() @@ -283,6 +279,8 @@ public function getDeleteUrl() /** * Retrieve Save As Flag * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + * * @return int */ public function getSaveAsFlag() diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Grid/Renderer/Sender.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Grid/Renderer/Sender.php index fcad9a10a0526..c9e9e969c7c82 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Grid/Renderer/Sender.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Grid/Renderer/Sender.php @@ -11,6 +11,9 @@ */ namespace Magento\Newsletter\Block\Adminhtml\Template\Grid\Renderer; +/** + * Class Sender + */ class Sender extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -23,10 +26,10 @@ public function render(\Magento\Framework\DataObject $row) { $str = ''; if ($row->getTemplateSenderName()) { - $str .= htmlspecialchars($row->getTemplateSenderName()) . ' '; + $str .= $this->escapeHtml($row->getTemplateSenderName()) . ' '; } if ($row->getTemplateSenderEmail()) { - $str .= '[' . $row->getTemplateSenderEmail() . ']'; + $str .= '[' . $this->escapeHtml($row->getTemplateSenderEmail()) . ']'; } if ($str == '') { $str .= '---'; diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php index f9474da452cac..a755556456bdd 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php @@ -10,6 +10,7 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ class Preview extends \Magento\Backend\Block\Widget { @@ -65,6 +66,8 @@ protected function _toHtml() $template->setTemplateType($previewData['type']); $template->setTemplateText($previewData['text']); $template->setTemplateStyles($previewData['styles']); + // Emulate DB-loaded template to invoke strict mode + $template->setTemplateId(123); } \Magento\Framework\Profiler::start($this->profilerName); @@ -84,7 +87,7 @@ protected function _toHtml() $template->revertDesign(); if ($template->isPlain()) { - $templateProcessed = "
    " . htmlspecialchars($templateProcessed) . "
    "; + $templateProcessed = "
    " . $this->escapeHtml($templateProcessed) . "
    "; } \Magento\Framework\Profiler::stop($this->profilerName); @@ -142,6 +145,8 @@ protected function getStoreId() } /** + * Return template + * * @param \Magento\Newsletter\Model\Template $template * @param string $id * @return $this diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php index 7293b350fcd01..9d42ec0bf594f 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php @@ -1,26 +1,30 @@ getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /* @var $queue \Magento\Newsletter\Model\Queue */ $queue = $this->_objectManager->create(\Magento\Newsletter\Model\Queue::class); @@ -30,7 +34,9 @@ public function execute() $template = $this->_objectManager->create(\Magento\Newsletter\Model\Template::class)->load($templateId); if (!$template->getId() || $template->getIsSystem()) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please correct the newsletter template and try again.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Please correct the newsletter template and try again.') + ); } $queue->setTemplateId( diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php index 0dd2e5c4b387f..4794d86faa17a 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php @@ -6,31 +6,61 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; -class MassDelete extends \Magento\Newsletter\Controller\Adminhtml\Subscriber +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\Exception\NotFoundException; +use Magento\Newsletter\Controller\Adminhtml\Subscriber; +use Magento\Newsletter\Model\SubscriberFactory; + +class MassDelete extends Subscriber { + /** + * @var SubscriberFactory + */ + private $subscriberFactory; + + /** + * @param Context $context + * @param FileFactory $fileFactory + */ + public function __construct( + Context $context, + FileFactory $fileFactory, + SubscriberFactory $subscriberFactory = null + ) { + $this->subscriberFactory = $subscriberFactory ?: ObjectManager::getInstance()->get(SubscriberFactory::class); + parent::__construct($context, $fileFactory); + } + /** * Delete one or more subscribers action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { - $this->messageManager->addError(__('Please select one or more subscribers.')); + $this->messageManager->addErrorMessage(__('Please select one or more subscribers.')); } else { try { foreach ($subscribersIds as $subscriberId) { - $subscriber = $this->_objectManager->create( - \Magento\Newsletter\Model\Subscriber::class - )->load( + $subscriber = $this->subscriberFactory->create()->load( $subscriberId ); $subscriber->delete(); } - $this->messageManager->addSuccess(__('Total of %1 record(s) were deleted.', count($subscribersIds))); + $this->messageManager->addSuccessMessage( + __('Total of %1 record(s) were deleted.', count($subscribersIds)) + ); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php index f294d23f46ece..3b3ea0d4c67a0 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php @@ -6,24 +6,53 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; -class MassUnsubscribe extends \Magento\Newsletter\Controller\Adminhtml\Subscriber +use Magento\Framework\Exception\NotFoundException; +use Magento\Newsletter\Controller\Adminhtml\Subscriber; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Newsletter\Model\SubscriberFactory; +use Magento\Framework\App\ObjectManager; + +class MassUnsubscribe extends Subscriber { + /** + * @var SubscriberFactory + */ + private $subscriberFactory; + + /** + * @param Context $context + * @param FileFactory $fileFactory + * @param SubscriberFactory $subscriberFactory + */ + public function __construct( + Context $context, + FileFactory $fileFactory, + SubscriberFactory $subscriberFactory = null + ) { + $this->subscriberFactory = $subscriberFactory ?: ObjectManager::getInstance()->get(SubscriberFactory::class); + parent::__construct($context, $fileFactory); + } + /** * Unsubscribe one or more subscribers action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { $this->messageManager->addError(__('Please select one or more subscribers.')); } else { try { foreach ($subscribersIds as $subscriberId) { - $subscriber = $this->_objectManager->create( - \Magento\Newsletter\Model\Subscriber::class - )->load( + $subscriber = $this->subscriberFactory->create()->load( $subscriberId ); $subscriber->unsubscribe(); diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php index d327d44feceb8..ac47f7e217b36 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php @@ -1,6 +1,5 @@ getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $template = $this->_objectManager->create( \Magento\Newsletter\Model\Template::class )->load( diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php index 52d46065ad05b..54a5eb651d99b 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php @@ -6,15 +6,22 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; +use Magento\Framework\Exception\NotFoundException; + class Drop extends \Magento\Newsletter\Controller\Adminhtml\Template { /** * Drop Newsletter Template * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $this->_view->loadLayout('newsletter_template_preview_popup'); $this->_view->renderLayout(); } diff --git a/app/code/Magento/Newsletter/Controller/Manage/Save.php b/app/code/Magento/Newsletter/Controller/Manage/Save.php index 75ef8b26f50a9..1aa2a4505d518 100644 --- a/app/code/Magento/Newsletter/Controller/Manage/Save.php +++ b/app/code/Magento/Newsletter/Controller/Manage/Save.php @@ -4,10 +4,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Manage; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\Data\Customer; +use Magento\Newsletter\Model\Subscriber; +/** + * Controller for customer newsletter subscription save. + */ class Save extends \Magento\Newsletter\Controller\Manage { /** @@ -56,7 +62,7 @@ public function __construct( } /** - * Save newsletter subscription preference action + * Save newsletter subscription preference action. * * @return void|null */ @@ -74,13 +80,30 @@ public function execute() $customer = $this->customerRepository->getById($customerId); $storeId = $this->storeManager->getStore()->getId(); $customer->setStoreId($storeId); - $this->customerRepository->save($customer); - if ((boolean)$this->getRequest()->getParam('is_subscribed', false)) { - $this->subscriberFactory->create()->subscribeCustomerById($customerId); - $this->messageManager->addSuccess(__('We saved the subscription.')); + $isSubscribedState = $customer->getExtensionAttributes() + ->getIsSubscribed(); + $isSubscribedParam = (boolean)$this->getRequest() + ->getParam('is_subscribed', false); + if ($isSubscribedParam !== $isSubscribedState) { + // No need to validate customer and customer address while saving subscription preferences + $this->setIgnoreValidationFlag($customer); + $this->customerRepository->save($customer); + if ($isSubscribedParam) { + $subscribeModel = $this->subscriberFactory->create() + ->subscribeCustomerById($customerId); + $subscribeStatus = $subscribeModel->getStatus(); + if ($subscribeStatus == Subscriber::STATUS_SUBSCRIBED) { + $this->messageManager->addSuccess(__('We have saved your subscription.')); + } else { + $this->messageManager->addSuccess(__('A confirmation request has been sent.')); + } + } else { + $this->subscriberFactory->create() + ->unsubscribeCustomerById($customerId); + $this->messageManager->addSuccess(__('We have removed your newsletter subscription.')); + } } else { - $this->subscriberFactory->create()->unsubscribeCustomerById($customerId); - $this->messageManager->addSuccess(__('We removed the subscription.')); + $this->messageManager->addSuccess(__('We have updated your subscription.')); } } catch (\Exception $e) { $this->messageManager->addError(__('Something went wrong while saving your subscription.')); @@ -88,4 +111,15 @@ public function execute() } $this->_redirect('customer/account/'); } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation. + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag(Customer $customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php b/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php index 004677b899cd0..4e338c2d1df34 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php @@ -32,6 +32,8 @@ public function execute() } } - $this->getResponse()->setRedirect($this->_storeManager->getStore()->getBaseUrl()); + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setUrl($this->_storeManager->getStore()->getBaseUrl()); + return $resultRedirect; } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php index 13ab40665e591..ab4950b8a3452 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php @@ -10,19 +10,32 @@ use Magento\Customer\Model\Session; use Magento\Customer\Model\Url as CustomerUrl; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; +use Magento\Framework\Validator\EmailAddress as EmailValidator; +use Magento\Newsletter\Controller\Subscriber as SubscriberController; +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Newsletter\Model\SubscriberFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class NewAction extends \Magento\Newsletter\Controller\Subscriber +class NewAction extends SubscriberController { /** * @var CustomerAccountManagement */ protected $customerAccountManagement; + /** + * @var EmailValidator + */ + private $emailValidator; + /** * Initialize dependencies. * @@ -32,6 +45,7 @@ class NewAction extends \Magento\Newsletter\Controller\Subscriber * @param StoreManagerInterface $storeManager * @param CustomerUrl $customerUrl * @param CustomerAccountManagement $customerAccountManagement + * @param EmailValidator $emailValidator */ public function __construct( Context $context, @@ -39,9 +53,11 @@ public function __construct( Session $customerSession, StoreManagerInterface $storeManager, CustomerUrl $customerUrl, - CustomerAccountManagement $customerAccountManagement + CustomerAccountManagement $customerAccountManagement, + EmailValidator $emailValidator = null ) { $this->customerAccountManagement = $customerAccountManagement; + $this->emailValidator = $emailValidator ?: ObjectManager::getInstance()->get(EmailValidator::class); parent::__construct( $context, $subscriberFactory, @@ -55,16 +71,17 @@ public function __construct( * Validates that the email address isn't being used by a different account. * * @param string $email - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return void */ protected function validateEmailAvailable($email) { $websiteId = $this->_storeManager->getStore()->getWebsiteId(); - if ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email + if ($this->_customerSession->isLoggedIn() + && ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email && !$this->customerAccountManagement->isEmailAvailable($email, $websiteId) - ) { - throw new \Magento\Framework\Exception\LocalizedException( + )) { + throw new LocalizedException( __('This email address is already assigned to another user.') ); } @@ -73,19 +90,19 @@ protected function validateEmailAvailable($email) /** * Validates that if the current user is a guest, that they can subscribe to a newsletter. * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return void */ protected function validateGuestSubscription() { - if ($this->_objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class) + if ($this->_objectManager->get(ScopeConfigInterface::class) ->getValue( - \Magento\Newsletter\Model\Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG, + ScopeInterface::SCOPE_STORE ) != 1 && !$this->_customerSession->isLoggedIn() ) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __( 'Sorry, but the administrator denied subscription for guests. Please register.', $this->_customerUrl->getRegisterUrl() @@ -98,20 +115,19 @@ protected function validateGuestSubscription() * Validates the format of the email address * * @param string $email - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return void */ protected function validateEmailFormat($email) { - if (!\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a valid email address.')); + if (!$this->emailValidator->isValid($email)) { + throw new LocalizedException(__('Please enter a valid email address.')); } } /** * New subscription action * - * @throws \Magento\Framework\Exception\LocalizedException * @return void */ public function execute() @@ -126,28 +142,37 @@ public function execute() $subscriber = $this->_subscriberFactory->create()->loadByEmail($email); if ($subscriber->getId() - && $subscriber->getSubscriberStatus() == \Magento\Newsletter\Model\Subscriber::STATUS_SUBSCRIBED + && (int) $subscriber->getSubscriberStatus() === Subscriber::STATUS_SUBSCRIBED ) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('This email address is already subscribed.') ); } - $status = $this->_subscriberFactory->create()->subscribe($email); - if ($status == \Magento\Newsletter\Model\Subscriber::STATUS_NOT_ACTIVE) { - $this->messageManager->addSuccess(__('The confirmation request has been sent.')); - } else { - $this->messageManager->addSuccess(__('Thank you for your subscription.')); - } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addException( + $status = (int) $this->_subscriberFactory->create()->subscribe($email); + $this->messageManager->addSuccessMessage($this->getSuccessMessage($status)); + } catch (LocalizedException $e) { + $this->messageManager->addExceptionMessage( $e, __('There was a problem with the subscription: %1', $e->getMessage()) ); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong with the subscription.')); + $this->messageManager->addExceptionMessage($e, __('Something went wrong with the subscription.')); } } $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * @param int $status + * @return Phrase + */ + private function getSuccessMessage(int $status): Phrase + { + if ($status === Subscriber::STATUS_NOT_ACTIVE) { + return __('The confirmation request has been sent.'); + } + + return __('Thank you for your subscription.'); + } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php index efc469e15deaa..ed415d04450a6 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php @@ -4,13 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; +/** + * Controller for unsubscribing customers. + */ class Unsubscribe extends \Magento\Newsletter\Controller\Subscriber { /** * Unsubscribe newsletter - * @return void + * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() { @@ -27,6 +31,10 @@ public function execute() $this->messageManager->addException($e, __('Something went wrong while unsubscribing you.')); } } - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + /** @var \Magento\Backend\Model\View\Result\Redirect $redirect */ + $redirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + $redirectUrl = $this->_redirect->getRedirectUrl(); + + return $redirect->setUrl($redirectUrl); } } diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index b82d6fe06918f..58b51009c205a 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -8,6 +8,10 @@ use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Newsletter\Model\SubscriberFactory; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Newsletter\Model\ResourceModel\Subscriber; +use Magento\Customer\Api\Data\CustomerExtensionInterface; +use Magento\Framework\App\ObjectManager; class CustomerPlugin { @@ -18,14 +22,37 @@ class CustomerPlugin */ private $subscriberFactory; + /** + * @var ExtensionAttributesFactory + */ + private $extensionFactory; + + /** + * @var Subscriber + */ + private $subscriberResource; + + /** + * @var array + */ + private $customerSubscriptionStatus = []; + /** * Initialize dependencies. * * @param SubscriberFactory $subscriberFactory + * @param ExtensionAttributesFactory|null $extensionFactory + * @param Subscriber|null $subscriberResource */ - public function __construct(SubscriberFactory $subscriberFactory) - { + public function __construct( + SubscriberFactory $subscriberFactory, + ExtensionAttributesFactory $extensionFactory = null, + Subscriber $subscriberResource = null + ) { $this->subscriberFactory = $subscriberFactory; + $this->extensionFactory = $extensionFactory + ?: ObjectManager::getInstance()->get(ExtensionAttributesFactory::class); + $this->subscriberResource = $subscriberResource ?: ObjectManager::getInstance()->get(Subscriber::class); } /** @@ -41,14 +68,30 @@ public function __construct(SubscriberFactory $subscriberFactory) */ public function afterSave(CustomerRepository $subject, CustomerInterface $result, CustomerInterface $customer) { - $this->subscriberFactory->create()->updateSubscription($result->getId()); - if ($result->getId() && $customer->getExtensionAttributes()) { - if ($customer->getExtensionAttributes()->getIsSubscribed() === true) { - $this->subscriberFactory->create()->subscribeCustomerById($result->getId()); - } elseif ($customer->getExtensionAttributes()->getIsSubscribed() === false) { - $this->subscriberFactory->create()->unsubscribeCustomerById($result->getId()); + $resultId = $result->getId(); + /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ + $subscriber = $this->subscriberFactory->create(); + $subscriber->updateSubscription($resultId); + // update the result only if the original customer instance had different value. + $initialExtensionAttributes = $result->getExtensionAttributes(); + if ($initialExtensionAttributes === null) { + /** @var CustomerExtensionInterface $initialExtensionAttributes */ + $initialExtensionAttributes = $this->extensionFactory->create(CustomerInterface::class); + $result->setExtensionAttributes($initialExtensionAttributes); + } + $newExtensionAttributes = $customer->getExtensionAttributes(); + if ($newExtensionAttributes + && $initialExtensionAttributes->getIsSubscribed() !== $newExtensionAttributes->getIsSubscribed() + ) { + if ($newExtensionAttributes->getIsSubscribed() === true) { + $subscriber->subscribeCustomerById($resultId); + } elseif ($newExtensionAttributes->getIsSubscribed() === false) { + $subscriber->unsubscribeCustomerById($resultId); } } + $isSubscribed = $subscriber->isSubscribed(); + $this->customerSubscriptionStatus[$resultId] = $isSubscribed; + $initialExtensionAttributes->setIsSubscribed($isSubscribed); return $result; } @@ -94,4 +137,44 @@ public function afterDelete(CustomerRepository $subject, $result, CustomerInterf } return $result; } + + /** + * Plugin after getById customer that obtains newsletter subscription status for given customer. + * + * @param CustomerRepository $subject + * @param CustomerInterface $customer + * @return CustomerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetById(CustomerRepository $subject, CustomerInterface $customer) + { + $extensionAttributes = $customer->getExtensionAttributes(); + if ($extensionAttributes === null) { + /** @var CustomerExtensionInterface $extensionAttributes */ + $extensionAttributes = $this->extensionFactory->create(CustomerInterface::class); + $customer->setExtensionAttributes($extensionAttributes); + } + if ($extensionAttributes->getIsSubscribed() === null) { + $isSubscribed = $this->isSubscribed($customer); + $extensionAttributes->setIsSubscribed($isSubscribed); + } + return $customer; + } + + /** + * This method returns newsletters subscription status for given customer. + * + * @param CustomerInterface $customer + * @return mixed + */ + private function isSubscribed(CustomerInterface $customer) + { + $customerId = $customer->getId(); + if (!isset($this->customerSubscriptionStatus[$customerId])) { + $subscriber = $this->subscriberResource->loadByCustomerData($customer); + $this->customerSubscriptionStatus[$customerId] = isset($subscriber['subscriber_status']) + && $subscriber['subscriber_status'] == 1; + } + return $this->customerSubscriptionStatus[$customerId]; + } } diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Grid/Collection.php index b5b837aa5cfc1..ac1b1cc9e824d 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Grid/Collection.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Newsletter problems collection * diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php index 6854b90a8888c..ce0e1446d216c 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php @@ -165,8 +165,8 @@ protected function _addCustomersData() $customerName = $this->_customerView->getCustomerName($customer); foreach ($problems as $problem) { $problem->setCustomerName($customerName) - ->setCustomerFirstName($customer->getFirstName()) - ->setCustomerLastName($customer->getLastName()); + ->setCustomerFirstName($customer->getFirstname()) + ->setCustomerLastName($customer->getLastname()); } } catch (NoSuchEntityException $e) { // do nothing if customer is not found by id diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index 3e221047ec40a..0f0a97fd9e983 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -221,7 +221,7 @@ public function addOnlyForSendingFilter() [\Magento\Newsletter\Model\Queue::STATUS_SENDING, \Magento\Newsletter\Model\Queue::STATUS_NEVER] )->where( 'main_table.queue_start_at < ?', - $this->_date->gmtdate() + $this->_date->gmtDate() )->where( 'main_table.queue_start_at IS NOT NULL' ); diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index c7ce4b2f2f11b..33e3826e9180b 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -5,6 +5,9 @@ */ namespace Magento\Newsletter\Model\ResourceModel; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; + /** * Newsletter subscriber resource model * @@ -48,6 +51,13 @@ class Subscriber extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $mathRandom; + /** + * Store manager + * + * @var StoreManagerInterface + */ + private $storeManager; + /** * Construct * @@ -55,15 +65,19 @@ class Subscriber extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Framework\Stdlib\DateTime\DateTime $date * @param \Magento\Framework\Math\Random $mathRandom * @param string $connectionName + * @param StoreManagerInterface $storeManager */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Stdlib\DateTime\DateTime $date, \Magento\Framework\Math\Random $mathRandom, - $connectionName = null + $connectionName = null, + StoreManagerInterface $storeManager = null ) { $this->_date = $date; $this->mathRandom = $mathRandom; + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManagerInterface::class); parent::__construct($context, $connectionName); } @@ -118,40 +132,34 @@ public function loadByEmail($subscriberEmail) */ public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer) { - $select = $this->connection - ->select() - ->from($this->getMainTable()) - ->where('customer_id=:customer_id and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'customer_id' => $customer->getId(), - 'store_id' => $customer->getStoreId() - ] - ); + $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds(); + + if ($customer->getId()) { + $select = $this->connection + ->select() + ->from($this->getMainTable()) + ->where('customer_id = ?', $customer->getId()) + ->where('store_id IN (?)', $storeIds); + + $result = $this->connection->fetchRow($select); - if ($result) { - return $result; + if ($result) { + return $result; + } } - $select = $this->connection - ->select() - ->from($this->getMainTable()) - ->where('subscriber_email=:subscriber_email and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'subscriber_email' => $customer->getEmail(), - 'store_id' => $customer->getStoreId() - ] - ); + if ($customer->getEmail()) { + $select = $this->connection + ->select() + ->from($this->getMainTable()) + ->where('subscriber_email = ?', $customer->getEmail()) + ->where('store_id IN (?)', $storeIds); + + $result = $this->connection->fetchRow($select); - if ($result) { - return $result; + if ($result) { + return $result; + } } return []; diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Template.php b/app/code/Magento/Newsletter/Model/ResourceModel/Template.php index 0ba7e7c61509c..9d53587e954f7 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Template.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Template.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Newsletter\Model\ResourceModel; /** @@ -83,7 +81,7 @@ public function checkUsageInQueue(\Magento\Newsletter\Model\Template $template) */ public function checkCodeUsage(\Magento\Newsletter\Model\Template $template) { - if ($template->getTemplateActual() != 0 || is_null($template->getTemplateActual())) { + if ($template->getTemplateActual() != 0 || $template->getTemplateActual() === null) { $bind = [ 'template_id' => $template->getId(), 'template_code' => $template->getTemplateCode(), diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index cc143fdc52e3b..cfed56a7675a2 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -11,6 +11,8 @@ use Magento\Framework\Exception\MailException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\Api\DataObjectHelper; /** * Subscriber model @@ -129,6 +131,16 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel */ protected $inlineTranslation; + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var DataObjectHelper + */ + private $dataObjectHelper; + /** * Initialize dependencies. * @@ -146,6 +158,8 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param DateTime|null $dateTime + * @param CustomerInterfaceFactory|null $customerFactory + * @param DataObjectHelper|null $dataObjectHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -162,7 +176,9 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - DateTime $dateTime = null + DateTime $dateTime = null, + CustomerInterfaceFactory $customerFactory = null, + DataObjectHelper $dataObjectHelper = null ) { $this->_newsletterData = $newsletterData; $this->_scopeConfig = $scopeConfig; @@ -173,6 +189,8 @@ public function __construct( $this->customerAccountManagement = $customerAccountManagement; $this->inlineTranslation = $inlineTranslation; $this->dateTime = $dateTime ?: ObjectManager::getInstance()->get(DateTime::class); + $this->customerFactory = $customerFactory ?: ObjectManager::getInstance()->get(CustomerInterfaceFactory::class); + $this->dataObjectHelper = $dataObjectHelper ?: ObjectManager::getInstance()->get(DataObjectHelper::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -346,7 +364,17 @@ public function isSubscribed() */ public function loadByEmail($subscriberEmail) { - $this->addData($this->getResource()->loadByEmail($subscriberEmail)); + $storeId = $this->_storeManager->getStore()->getId(); + $customerData = ['store_id' => $storeId, 'email'=> $subscriberEmail]; + + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $this->customerFactory->create(); + $this->dataObjectHelper->populateWithArray( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->addData($this->getResource()->loadByCustomerData($customer)); return $this; } @@ -361,6 +389,9 @@ public function loadByCustomerId($customerId) try { $customerData = $this->customerRepository->getById($customerId); $customerData->setStoreId($this->_storeManager->getStore()->getId()); + if ($customerData->getWebsiteId() === null) { + $customerData->setWebsiteId($this->_storeManager->getStore()->getWebsiteId()); + } $data = $this->getResource()->loadByCustomerData($customerData); $this->addData($data); if (!empty($data) && $customerData->getId() && !$this->getCustomerId()) { @@ -419,7 +450,6 @@ public function subscribe($email) self::XML_PATH_CONFIRMATION_FLAG, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) == 1 ? true : false; - $isOwnSubscribes = false; $isSubscribeOwnEmail = $this->_customerSession->isLoggedIn() && $this->_customerSession->getCustomerDataObject()->getEmail() == $email; @@ -428,13 +458,7 @@ public function subscribe($email) || $this->getStatus() == self::STATUS_NOT_ACTIVE ) { if ($isConfirmNeed === true) { - // if user subscribes own login email - confirmation is not needed - $isOwnSubscribes = $isSubscribeOwnEmail; - if ($isOwnSubscribes == true) { - $this->setStatus(self::STATUS_SUBSCRIBED); - } else { - $this->setStatus(self::STATUS_NOT_ACTIVE); - } + $this->setStatus(self::STATUS_NOT_ACTIVE); } else { $this->setStatus(self::STATUS_SUBSCRIBED); } @@ -460,9 +484,7 @@ public function subscribe($email) try { /* Save model before sending out email */ $this->save(); - if ($isConfirmNeed === true - && $isOwnSubscribes === false - ) { + if ($isConfirmNeed === true) { $this->sendConfirmationRequestEmail(); } else { $this->sendConfirmationSuccessEmail(); @@ -530,7 +552,7 @@ public function updateSubscription($customerId) } /** - * Saving customer subscription status + * Saving customer subscription status. * * @param int $customerId * @param bool $subscribe indicates whether the customer should be subscribed or unsubscribed @@ -566,13 +588,22 @@ protected function _updateCustomerSubscription($customerId, $subscribe) if (AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED == $this->customerAccountManagement->getConfirmationStatus($customerId) ) { - $status = self::STATUS_UNCONFIRMED; + if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) { + // if a customer was already subscribed then keep the subscribed + $status = self::STATUS_SUBSCRIBED; + } else { + $status = self::STATUS_UNCONFIRMED; + } } elseif ($isConfirmNeed) { - $status = self::STATUS_NOT_ACTIVE; + if ($this->getStatus() != self::STATUS_SUBSCRIBED) { + $status = self::STATUS_NOT_ACTIVE; + } } } elseif (($this->getStatus() == self::STATUS_UNCONFIRMED) && ($customerData->getConfirmation() === null)) { $status = self::STATUS_SUBSCRIBED; $sendInformationEmail = true; + } elseif (($this->getStatus() == self::STATUS_NOT_ACTIVE) && ($customerData->getConfirmation() === null)) { + $status = self::STATUS_NOT_ACTIVE; } else { $status = self::STATUS_UNSUBSCRIBED; } @@ -589,29 +620,36 @@ protected function _updateCustomerSubscription($customerId, $subscribe) $this->setStatus($status); + $storeId = $customerData->getStoreId(); + if ((int)$customerData->getStoreId() === 0) { + $storeId = $this->_storeManager->getWebsite($customerData->getWebsiteId())->getDefaultStore()->getId(); + } + if (!$this->getId()) { - $storeId = $customerData->getStoreId(); - if ($customerData->getStoreId() == 0) { - $storeId = $this->_storeManager->getWebsite($customerData->getWebsiteId())->getDefaultStore()->getId(); - } $this->setStoreId($storeId) ->setCustomerId($customerData->getId()) ->setEmail($customerData->getEmail()); } else { - $this->setStoreId($customerData->getStoreId()) + $this->setStoreId($storeId) ->setEmail($customerData->getEmail()); } $this->save(); $sendSubscription = $sendInformationEmail; - if ($sendSubscription === null xor $sendSubscription) { + if ($sendSubscription === null xor $sendSubscription && $this->isStatusChanged()) { try { - if ($isConfirmNeed) { - $this->sendConfirmationRequestEmail(); - } elseif ($this->isStatusChanged() && $status == self::STATUS_UNSUBSCRIBED) { - $this->sendUnsubscriptionEmail(); - } elseif ($this->isStatusChanged() && $status == self::STATUS_SUBSCRIBED) { - $this->sendConfirmationSuccessEmail(); + switch ($status) { + case self::STATUS_UNSUBSCRIBED: + $this->sendUnsubscriptionEmail(); + break; + case self::STATUS_SUBSCRIBED: + $this->sendConfirmationSuccessEmail(); + break; + case self::STATUS_NOT_ACTIVE: + if ($isConfirmNeed) { + $this->sendConfirmationRequestEmail(); + } + break; } } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored @@ -633,6 +671,8 @@ public function confirm($code) $this->setStatus(self::STATUS_SUBSCRIBED) ->setStatusChanged(true) ->save(); + + $this->sendConfirmationSuccessEmail(); return true; } @@ -686,7 +726,13 @@ public function sendConfirmationRequestEmail() 'store' => $this->_storeManager->getStore()->getId(), ] )->setTemplateVars( - ['subscriber' => $this, 'store' => $this->_storeManager->getStore()] + [ + 'subscriber' => $this, + 'store' => $this->_storeManager->getStore(), + 'subscriber_data' => [ + 'confirmation_link' => $this->getConfirmationLink(), + ], + ] )->setFrom( $this->_scopeConfig->getValue( self::XML_PATH_CONFIRM_EMAIL_IDENTITY, diff --git a/app/code/Magento/Newsletter/Model/Template.php b/app/code/Magento/Newsletter/Model/Template.php index 115e65c9d59e9..3e40aa73ec7b6 100644 --- a/app/code/Magento/Newsletter/Model/Template.php +++ b/app/code/Magento/Newsletter/Model/Template.php @@ -199,9 +199,16 @@ public function getProcessedTemplateSubject(array $variables) { $variables['this'] = $this; - return $this->getTemplateFilter() - ->setVariables($variables) - ->filter($this->getTemplateSubject()); + $filter = $this->getTemplateFilter(); + $filter->setVariables($variables); + + $previousStrictMode = $filter->setStrictMode( + !$this->getData('is_legacy') && is_numeric($this->getTemplateId()) + ); + $result = $filter->filter($this->getTemplateSubject()); + $filter->setStrictMode($previousStrictMode); + + return $result; } /** @@ -226,6 +233,8 @@ public function getTemplateText() } /** + * Return the filter factory + * * @return \Magento\Newsletter\Model\Template\FilterFactory */ protected function getFilterFactory() diff --git a/app/code/Magento/Newsletter/Setup/UpgradeSchema.php b/app/code/Magento/Newsletter/Setup/UpgradeSchema.php new file mode 100644 index 0000000000000..9b412bae01ce8 --- /dev/null +++ b/app/code/Magento/Newsletter/Setup/UpgradeSchema.php @@ -0,0 +1,51 @@ +startSetup(); + + if (version_compare($context->getVersion(), '2.0.1', '<')) { + $connection = $setup->getConnection(); + + $connection->addIndex( + $setup->getTable('newsletter_subscriber'), + $setup->getIdxName('newsletter_subscriber', ['subscriber_email']), + ['subscriber_email'] + ); + } + + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $connection = $setup->getConnection(); + $connection->addColumn( + $setup->getTable('newsletter_template'), + 'is_legacy', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_BOOLEAN, + 'nullable' => false, + 'default' => 0, + 'comment' => 'Should the template render in legacy mode', + ] + ); + $connection->update($setup->getTable('newsletter_template'), ['is_legacy' => '1']); + } + + $setup->endSetup(); + } +} diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml new file mode 100644 index 0000000000000..8a6bf27c16632 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml new file mode 100644 index 0000000000000..81b444fb5c1dc --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Newsletter/Test/Mftf/Data/NewsletterTemplateData.xml b/app/code/Magento/Newsletter/Test/Mftf/Data/NewsletterTemplateData.xml new file mode 100644 index 0000000000000..0b34bff8a9baa --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Data/NewsletterTemplateData.xml @@ -0,0 +1,19 @@ + + + + + + Test Newsletter Template + Test Newsletter Subject + Admin + admin@magento.com + Template {{var this.template_id}}{{var this.getData(template_id)}} Text + Template 123 Text + + diff --git a/app/code/Magento/Newsletter/Test/Mftf/LICENSE.txt b/app/code/Magento/Newsletter/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Newsletter/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/app/code/Magento/Newsletter/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Newsletter/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Newsletter/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/app/code/Magento/Newsletter/Test/Mftf/Page/NewsletterTemplatePage.xml b/app/code/Magento/Newsletter/Test/Mftf/Page/NewsletterTemplatePage.xml new file mode 100644 index 0000000000000..400b1b8354e54 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Page/NewsletterTemplatePage.xml @@ -0,0 +1,14 @@ + + + + +
    +
    + + diff --git a/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml new file mode 100644 index 0000000000000..81fd3eb7c391c --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml @@ -0,0 +1,13 @@ + + + + +
    + + diff --git a/app/code/Magento/Newsletter/Test/Mftf/README.md b/app/code/Magento/Newsletter/Test/Mftf/README.md new file mode 100644 index 0000000000000..266c5c5723f63 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Newsletter Functional Tests + +The Functional Test Module for **Magento Newsletter** module. diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml new file mode 100644 index 0000000000000..8c8026e09146b --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection.xml @@ -0,0 +1,19 @@ + + + +
    + + + + + + + +
    +
    diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml new file mode 100644 index 0000000000000..36870fbfb0182 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml new file mode 100644 index 0000000000000..15d6debd7ef25 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml new file mode 100644 index 0000000000000..96a944a4952ac --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNewsletterPreviewTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNewsletterPreviewTest.xml new file mode 100644 index 0000000000000..1c335dec1a104 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNewsletterPreviewTest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + <description value="Admin should be able to preview newsletter content in draft mode"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-23058"/> + <stories value="Newsletter Preview"/> + <skip> + <issueId value="MC-29326"/> + </skip> + </annotations> + + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminNavigateToNewsletterGrid" stepKey="navigateToGrid"/> + + <actionGroup ref="AdminFillNewsletterForm" stepKey="fillForm"> + <argument name="Newsletter" value="NewsletterWithDirectives"/> + </actionGroup> + + <actionGroup ref="AdminOpenNewsletterPreviewTab" stepKey="openPreviewTab"/> + + <actionGroup ref="AdminAssertNewsletterContent" stepKey="assertTemplateContent"> + <argument name="Newsletter" value="NewsletterWithDirectives"/> + </actionGroup> + + <closeTab stepKey="closeTab"/> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml new file mode 100644 index 0000000000000..ce28797881cb3 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectNewsletter"> + <annotations> + <features value="Newsletter"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Newsletter Pages"/> + <description value="Verify that the Secure URL configuration applies to the Newsletter pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15706"/> + <group value="newsletter"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/newsletter/manage" stepKey="goToUnsecureNewsletterManageURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/newsletter/manage" stepKey="seeSecureNewsletterManageURL"/> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php index 219f9127d4ad8..b0989569d9d4a 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php @@ -81,13 +81,17 @@ protected function setUp() $queueFactory->expects($this->any())->method('create')->will($this->returnValue($this->queue)); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $escaper = $this->objectManager->getObject(\Magento\Framework\Escaper::class); + $context->expects($this->once())->method('getEscaper')->willReturn($escaper); + $this->preview = $this->objectManager->getObject( \Magento\Newsletter\Block\Adminhtml\Queue\Preview::class, [ 'context' => $context, 'templateFactory' => $templateFactory, 'subscriberFactory' => $subscriberFactory, - 'queueFactory' => $queueFactory, + 'queueFactory' => $queueFactory ] ); } @@ -103,12 +107,12 @@ public function testToHtmlEmpty() public function testToHtmlWithId() { - $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap( + $this->request->expects($this->any())->method('getParam')->willReturnMap( [ ['id', null, 1], ['store_id', null, 0] ] - )); + ); $this->queue->expects($this->once())->method('load')->will($this->returnSelf()); $this->template->expects($this->any())->method('isPlain')->will($this->returnValue(true)); /** @var \Magento\Store\Model\Store $store */ diff --git a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/Grid/Renderer/SenderTest.php b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/Grid/Renderer/SenderTest.php new file mode 100644 index 0000000000000..4de2d5d17c550 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/Grid/Renderer/SenderTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Newsletter\Test\Unit\Block\Adminhtml\Template\Grid\Renderer; + +/** + * Test for \Magento\Newsletter\Block\Adminhtml\Template\Grid\Renderer\Sender. + */ +class SenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Newsletter\Block\Adminhtml\Template\Grid\Renderer\Sender + */ + private $sender; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManagerHelper; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $escaper = $this->objectManagerHelper->getObject( + \Magento\Framework\Escaper::class + ); + $this->sender = $this->objectManagerHelper->getObject( + \Magento\Newsletter\Block\Adminhtml\Template\Grid\Renderer\Sender::class, + [ + 'escaper' => $escaper + ] + ); + } + + /** + * @dataProvider rendererDataProvider + * @param array $expectedSender + * @param array $passedSender + */ + public function testRender(array $passedSender, array $expectedSender) + { + $row = $this->getMockBuilder(\Magento\Framework\DataObject::class) + ->setMethods(['getTemplateSenderName', 'getTemplateSenderEmail']) + ->getMock(); + $row->expects($this->atLeastOnce())->method('getTemplateSenderName') + ->willReturn($passedSender['sender']); + $row->expects($this->atLeastOnce())->method('getTemplateSenderEmail') + ->willReturn($passedSender['sender_email']); + $this->assertEquals( + $expectedSender['sender'] . ' [' . $expectedSender['sender_email'] . ']', + $this->sender->render($row) + ); + } + + /** + * @return array + */ + public function rendererDataProvider() + { + return [ + [ + [ + 'sender' => 'Sender', + 'sender_email' => 'sender@example.com', + ], + [ + 'sender' => 'Sender', + 'sender_email' => 'sender@example.com', + ], + ], + [ + [ + 'sender' => "<br>'Sender'</br>", + 'sender_email' => "<br>'email@example.com'</br>", + ], + [ + 'sender' => "<br>'Sender'</br>", + 'sender_email' => "<br>'email@example.com'</br>", + ], + ], + [ + [ + 'sender' => '"<script>alert(document.domain)</script>"@example.com', + 'sender_email' => '"<script>alert(document.domain)</script>"@example.com', + ], + [ + 'sender' => '"<script>alert(document.domain)</script>"@example.com', + 'sender_email' => '"<script>alert(document.domain)</script>"@example.com', + ], + ], + ]; + } +} diff --git a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/PreviewTest.php b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/PreviewTest.php index 70407b50cacc0..f4e120b6f0e4d 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/PreviewTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Template/PreviewTest.php @@ -7,7 +7,11 @@ use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Escaper; +/** + * Test for \Magento\Newsletter\Block\Adminhtml\Template\Preview + */ class PreviewTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Newsletter\Block\Adminhtml\Template\Preview */ @@ -36,7 +40,9 @@ protected function setUp() $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->appState = $this->createMock(\Magento\Framework\App\State::class); $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->template = $this->createPartialMock(\Magento\Newsletter\Model\Template::class, [ + $this->template = $this->createPartialMock( + \Magento\Newsletter\Model\Template::class, + [ 'setTemplateType', 'setTemplateText', 'setTemplateStyles', @@ -45,7 +51,8 @@ protected function setUp() 'revertDesign', 'getProcessedTemplate', 'load' - ]); + ] + ); $templateFactory = $this->createPartialMock(\Magento\Newsletter\Model\TemplateFactory::class, ['create']); $templateFactory->expects($this->once())->method('create')->willReturn($this->template); $this->subscriberFactory = $this->createPartialMock( @@ -54,6 +61,7 @@ protected function setUp() ); $this->objectManagerHelper = new ObjectManagerHelper($this); + $escaper = new Escaper(); $this->preview = $this->objectManagerHelper->getObject( \Magento\Newsletter\Block\Adminhtml\Template\Preview::class, [ @@ -61,7 +69,8 @@ protected function setUp() 'storeManager' => $this->storeManager, 'request' => $this->request, 'templateFactory' => $templateFactory, - 'subscriberFactory' => $this->subscriberFactory + 'subscriberFactory' => $this->subscriberFactory, + 'escaper' => $escaper ] ); } diff --git a/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php b/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php index e34c0a7e4a623..10743df422574 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Newsletter\Test\Unit\Controller\Manage; use Magento\Framework\Exception\NoSuchEntityException; @@ -85,7 +83,8 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->action = $objectManager->getObject( - \Magento\Newsletter\Controller\Manage\Save::class, [ + \Magento\Newsletter\Controller\Manage\Save::class, + [ 'request' => $this->requestMock, 'response' => $this->responseMock, 'messageManager' => $this->messageManagerMock, @@ -93,7 +92,8 @@ protected function setUp() 'customerSession' => $this->customerSessionMock, 'formKeyValidator' => $this->formKeyValidatorMock, 'customerRepository' => $this->customerRepositoryMock - ]); + ] + ); } public function testSaveActionInvalidFormKey() @@ -140,12 +140,11 @@ public function testSaveActionWithException() ->will($this->returnValue(1)); $this->customerRepositoryMock->expects($this->any()) ->method('getById') - ->will($this->throwException( - new NoSuchEntityException( - __( - 'No such entity with %fieldName = %fieldValue', - ['fieldName' => 'customerId', 'value' => 'value'] - ) + ->willThrowException( + new NoSuchEntityException( + __( + 'No such entity with %fieldName = %fieldValue', + ['fieldName' => 'customerId', 'value' => 'value'] ) ) ); diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php index 47d4584857bde..39a9c2a0d95d2 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php @@ -7,13 +7,16 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Customer\Api\Data\CustomerExtensionInterface; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Newsletter\Model\ResourceModel\Subscriber; class CustomerPluginTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Newsletter\Model\Plugin\CustomerPlugin */ - protected $plugin; + private $plugin; /** * @var \Magento\Newsletter\Model\SubscriberFactory|\PHPUnit_Framework_MockObject_MockObject @@ -28,7 +31,27 @@ class CustomerPluginTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ - protected $objectManager; + private $objectManager; + + /** + * @var ExtensionAttributesFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $extensionFactoryMock; + + /** + * @var CustomerExtensionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerExtensionMock; + + /** + * @var Subscriber|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriberResourceMock; + + /** + * @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerMock; protected function setUp() { @@ -44,92 +67,97 @@ protected function setUp() 'delete', 'updateSubscription', 'subscribeCustomerById', - 'unsubscribeCustomerById' + 'unsubscribeCustomerById', + 'isSubscribed' ] )->disableOriginalConstructor() ->getMock(); + $this->extensionFactoryMock = $this->getMockBuilder(ExtensionAttributesFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->customerExtensionMock = $this->getMockBuilder(CustomerExtensionInterface::class) + ->setMethods(["getIsSubscribed", "setIsSubscribed"]) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->subscriberResourceMock = $this->getMockBuilder(Subscriber::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerMock = $this->getMockBuilder(CustomerInterface::class) + ->setMethods(["getExtensionAttributes"]) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->subscriberFactory->expects($this->any())->method('create')->willReturn($this->subscriber); - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->plugin = $this->objectManager->getObject( \Magento\Newsletter\Model\Plugin\CustomerPlugin::class, [ - 'subscriberFactory' => $this->subscriberFactory + 'subscriberFactory' => $this->subscriberFactory, + 'extensionFactory' => $this->extensionFactoryMock, + 'subscriberResource' => $this->subscriberResourceMock ] ); } - public function testAfterSaveWithoutIsSubscribed() + /** + * @param bool $subscriptionOriginalValue + * @param bool $subscriptionNewValue + * @dataProvider afterSaveDataProvider + */ + public function testAfterSave($subscriptionOriginalValue, $subscriptionNewValue) { $customerId = 1; - /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - /** @var CustomerRepository | \PHPUnit_Framework_MockObject_MockObject $subject */ + /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $result */ + $result = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var CustomerRepository |\PHPUnit_Framework_MockObject_MockObject $subject */ $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - - $customer->expects($this->atLeastOnce()) - ->method("getId") - ->willReturn($customerId); - - $this->assertEquals($customer, $this->plugin->afterSave($subject, $customer, $customer)); + /** @var CustomerExtensionInterface|\PHPUnit_Framework_MockObject_MockObject $resultExtensionAttributes */ + $resultExtensionAttributes = $this->getMockBuilder(CustomerExtensionInterface::class) + ->setMethods(['getIsSubscribed', 'setIsSubscribed']) + ->getMockForAbstractClass(); + $result->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + $result->expects($this->any())->method('getExtensionAttributes')->willReturn(null); + $this->extensionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($resultExtensionAttributes); + $result->expects($this->once()) + ->method('setExtensionAttributes') + ->with($resultExtensionAttributes) + ->willReturnSelf(); + $this->customerMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->customerExtensionMock); + $resultExtensionAttributes->expects($this->any()) + ->method('getIsSubscribed') + ->willReturn($subscriptionOriginalValue); + $this->customerExtensionMock->expects($this->any()) + ->method('getIsSubscribed') + ->willReturn($subscriptionNewValue); + if ($subscriptionOriginalValue !== $subscriptionNewValue) { + if ($subscriptionNewValue === true) { + $this->subscriber->expects($this->once())->method('subscribeCustomerById')->with($customerId); + } elseif ($subscriptionNewValue === false) { + $this->subscriber->expects($this->once())->method('unsubscribeCustomerById')->with($customerId); + } + $this->subscriber->expects($this->once())->method('isSubscribed')->willReturn($subscriptionNewValue); + $resultExtensionAttributes->expects($this->once())->method('setIsSubscribed')->with($subscriptionNewValue); + } + $this->assertEquals($result, $this->plugin->afterSave($subject, $result, $this->customerMock)); } /** * @return array */ - public function afterSaveExtensionAttributeDataProvider() + public function afterSaveDataProvider() { return [ [true, true], - [false, false] + [false, false], + [true, false], + [false, true], ]; } - /** - * @param boolean $isSubscribed - * @param boolean $subscribeIsCreated - * @dataProvider afterSaveExtensionAttributeDataProvider - */ - public function testAfterSaveWithIsSubscribed($isSubscribed, $subscribeIsCreated) - { - $customerId = 1; - /** @var CustomerInterface | \PHPUnit_Framework_MockObject_MockObject $customer */ - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $extensionAttributes = $this - ->getMockBuilder(\Magento\Customer\Api\Data\CustomerExtensionInterface::class) - ->setMethods(["getIsSubscribed", "setIsSubscribed"]) - ->getMockForAbstractClass(); - - $extensionAttributes - ->expects($this->atLeastOnce()) - ->method("getIsSubscribed") - ->willReturn($isSubscribed); - - $customer->expects($this->atLeastOnce()) - ->method("getExtensionAttributes") - ->willReturn($extensionAttributes); - - if ($subscribeIsCreated) { - $this->subscriber->expects($this->once()) - ->method("subscribeCustomerById") - ->with($customerId); - } else { - $this->subscriber->expects($this->once()) - ->method("unsubscribeCustomerById") - ->with($customerId); - } - - /** @var CustomerRepository | \PHPUnit_Framework_MockObject_MockObject $subject */ - $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - - $customer->expects($this->atLeastOnce()) - ->method("getId") - ->willReturn($customerId); - - $this->assertEquals($customer, $this->plugin->afterSave($subject, $customer, $customer)); - } - public function testAfterDelete() { $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); @@ -138,7 +166,6 @@ public function testAfterDelete() $this->subscriber->expects($this->once())->method('loadByEmail')->with('test@test.com')->willReturnSelf(); $this->subscriber->expects($this->once())->method('getId')->willReturn(1); $this->subscriber->expects($this->once())->method('delete')->willReturnSelf(); - $this->assertEquals(true, $this->plugin->afterDelete($subject, true, $customer)); } @@ -155,7 +182,77 @@ public function testAroundDeleteById() $this->subscriber->expects($this->once())->method('loadByEmail')->with('test@test.com')->willReturnSelf(); $this->subscriber->expects($this->once())->method('getId')->willReturn(1); $this->subscriber->expects($this->once())->method('delete')->willReturnSelf(); - $this->assertEquals(true, $this->plugin->aroundDeleteById($subject, $deleteCustomerById, $customerId)); } + + /** + * @param int|null $subscriberStatusKey + * @param int|null $subscriberStatusValue + * @param bool $isSubscribed + * @dataProvider afterGetByIdDataProvider + */ + public function testAfterGetByIdCreatesExtensionAttributesIfItIsNotSet( + $subscriberStatusKey, + $subscriberStatusValue, + $isSubscribed + ) { + $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $subscriber = [$subscriberStatusKey => $subscriberStatusValue]; + $this->extensionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->customerExtensionMock); + $this->customerMock->expects($this->once()) + ->method('setExtensionAttributes') + ->with($this->customerExtensionMock) + ->willReturnSelf(); + $this->customerMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->subscriberResourceMock->expects($this->once()) + ->method('loadByCustomerData') + ->with($this->customerMock) + ->willReturn($subscriber); + $this->customerExtensionMock->expects($this->once())->method('setIsSubscribed')->with($isSubscribed); + $this->assertEquals( + $this->customerMock, + $this->plugin->afterGetById($subject, $this->customerMock) + ); + } + + public function testAfterGetByIdSetsIsSubscribedFlagIfItIsNotSet() + { + $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $subscriber = ['subscriber_id' => 1, 'subscriber_status' => 1]; + $this->customerMock->expects($this->any()) + ->method('getExtensionAttributes') + ->willReturn($this->customerExtensionMock); + $this->customerExtensionMock->expects($this->any()) + ->method('getIsSubscribed') + ->willReturn(null); + $this->subscriberResourceMock->expects($this->once()) + ->method('loadByCustomerData') + ->with($this->customerMock) + ->willReturn($subscriber); + $this->customerExtensionMock->expects($this->once()) + ->method('setIsSubscribed') + ->willReturnSelf(); + $this->assertEquals( + $this->customerMock, + $this->plugin->afterGetById($subject, $this->customerMock) + ); + } + + /** + * @return array + */ + public function afterGetByIdDataProvider() + { + return [ + ['subscriber_status', 1, true], + ['subscriber_status', 2, false], + ['subscriber_status', 3, false], + ['subscriber_status', 4, false], + [null, null, false] + ]; + } } diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php new file mode 100644 index 0000000000000..1de3e6096cd96 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php @@ -0,0 +1,200 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Test\Unit\Model; + +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Newsletter\Model\Problem as ProblemModel; +use Magento\Newsletter\Model\Queue; +use Magento\Newsletter\Model\ResourceModel\Problem as ProblemResource; +use Magento\Newsletter\Model\Subscriber; +use Magento\Newsletter\Model\SubscriberFactory; + +class ProblemTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $registryMock; + + /** + * @var SubscriberFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriberFactoryMock; + + /** + * @var Subscriber|\PHPUnit_Framework_MockObject_MockObject + */ + private $subscriberMock; + + /** + * @var ProblemResource|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceModelMock; + + /** + * @var AbstractDb|\PHPUnit_Framework_MockObject_MockObject + */ + private $abstractDbMock; + + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var ProblemModel + */ + private $problemModel; + + protected function setUp() + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->registryMock = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->subscriberFactoryMock = $this->getMockBuilder(SubscriberFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->subscriberMock = $this->getMockBuilder(Subscriber::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resourceModelMock = $this->getMockBuilder(ProblemResource::class) + ->disableOriginalConstructor() + ->getMock(); + $this->abstractDbMock = $this->getMockBuilder(AbstractDb::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resourceModelMock->expects($this->any()) + ->method('getIdFieldName') + ->willReturn('id'); + + $this->objectManager = new ObjectManager($this); + + $this->problemModel = $this->objectManager->getObject( + ProblemModel::class, + [ + 'context' => $this->contextMock, + 'registry' => $this->registryMock, + 'subscriberFactory' => $this->subscriberFactoryMock, + 'resource' => $this->resourceModelMock, + 'resourceCollection' => $this->abstractDbMock, + 'data' => [], + ] + ); + } + + public function testAddSubscriberData() + { + $subscriberId = 1; + $this->subscriberMock->expects($this->once()) + ->method('getId') + ->willReturn($subscriberId); + + $result = $this->problemModel->addSubscriberData($this->subscriberMock); + + self::assertEquals($result, $this->problemModel); + self::assertEquals($subscriberId, $this->problemModel->getSubscriberId()); + } + + public function testAddQueueData() + { + $queueId = 1; + $queueMock = $this->getMockBuilder(Queue::class) + ->disableOriginalConstructor() + ->getMock(); + $queueMock->expects($this->once()) + ->method('getId') + ->willReturn($queueId); + + $result = $this->problemModel->addQueueData($queueMock); + + self::assertEquals($result, $this->problemModel); + self::assertEquals($queueId, $this->problemModel->getQueueId()); + } + + public function testAddErrorData() + { + $exceptionMessage = 'Some message'; + $exceptionCode = 111; + $exception = new \Exception($exceptionMessage, $exceptionCode); + + $result = $this->problemModel->addErrorData($exception); + + self::assertEquals($result, $this->problemModel); + self::assertEquals($exceptionMessage, $this->problemModel->getProblemErrorText()); + self::assertEquals($exceptionCode, $this->problemModel->getProblemErrorCode()); + } + + public function testGetSubscriberWithNoSubscriberId() + { + self::assertNull($this->problemModel->getSubscriber()); + } + + public function testGetSubscriber() + { + $this->setSubscriber(); + self::assertEquals($this->subscriberMock, $this->problemModel->getSubscriber()); + } + + public function testUnsubscribeWithNoSubscriber() + { + $this->subscriberMock->expects($this->never()) + ->method('__call') + ->with($this->equalTo('setSubscriberStatus')); + + $result = $this->problemModel->unsubscribe(); + + self::assertEquals($this->problemModel, $result); + } + + public function testUnsubscribe() + { + $this->setSubscriber(); + $this->subscriberMock->expects($this->at(1)) + ->method('__call') + ->with($this->equalTo('setSubscriberStatus'), $this->equalTo([Subscriber::STATUS_UNSUBSCRIBED])) + ->willReturnSelf(); + $this->subscriberMock->expects($this->at(2)) + ->method('__call') + ->with($this->equalTo('setIsStatusChanged')) + ->willReturnSelf(); + $this->subscriberMock->expects($this->once()) + ->method('save'); + + $result = $this->problemModel->unsubscribe(); + + self::assertEquals($this->problemModel, $result); + } + + /** + * Sets subscriber to the Problem model + */ + private function setSubscriber() + { + $subscriberId = 1; + $this->problemModel->setSubscriberId($subscriberId); + $this->subscriberFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->subscriberMock); + $this->subscriberMock->expects($this->once()) + ->method('load') + ->with($subscriberId) + ->willReturnSelf(); + } +} diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php index 3452d9821ef5a..427c32e7c9e6c 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/Queue/TransportBuilderTest.php @@ -8,6 +8,11 @@ use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Mail\MessageInterface; +/** + * Tests \Magento\Newsletter\Model\Queue\TransportBuilder. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class TransportBuilderTest extends \PHPUnit\Framework\TestCase { /** @@ -45,6 +50,11 @@ class TransportBuilderTest extends \PHPUnit\Framework\TestCase */ protected $mailTransportFactoryMock; + /** + * @var \Magento\Framework\Mail\MessageInterfaceFactory | \PHPUnit_Framework_MockObject_MockObject + */ + private $messageFactoryMock; + /** * @return void */ @@ -52,7 +62,10 @@ public function setUp() { $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->templateFactoryMock = $this->createMock(\Magento\Framework\Mail\Template\FactoryInterface::class); - $this->messageMock = $this->createMock(\Magento\Framework\Mail\Message::class); + $this->messageMock = $this->getMockBuilder(\Magento\Framework\Mail\MessageInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setMessageType', 'setSubject', 'setBody']) + ->getMockForAbstractClass(); $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); $this->senderResolverMock = $this->createMock(\Magento\Framework\Mail\Template\SenderResolverInterface::class); $this->mailTransportFactoryMock = $this->getMockBuilder( @@ -60,6 +73,11 @@ public function setUp() )->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); + $this->messageFactoryMock = $this->getMockBuilder(\Magento\Framework\Mail\MessageInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); + $this->messageFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($this->messageMock); $this->builder = $objectManagerHelper->getObject( $this->builderClassName, [ @@ -67,7 +85,8 @@ public function setUp() 'message' => $this->messageMock, 'objectManager' => $this->objectManagerMock, 'senderResolver' => $this->senderResolverMock, - 'mailTransportFactory' => $this->mailTransportFactoryMock + 'mailTransportFactory' => $this->mailTransportFactoryMock, + 'messageFactory' => $this->messageFactoryMock, ] ); } @@ -95,95 +114,33 @@ public function testGetTransport( $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; $template = $this->createMock(\Magento\Email\Model\Template::class); - $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->will($this->returnSelf()); - $template->expects( - $this->once() - )->method( - 'setOptions' - )->with( - $this->equalTo($options) - )->will( - $this->returnSelf() - ); - $template->expects($this->once())->method('getSubject')->will($this->returnValue('Email Subject')); - $template->expects($this->once())->method('setData')->with($this->equalTo($data))->will($this->returnSelf()); + $template->expects($this->once())->method('setVars')->with($vars)->willReturnSelf(); + $template->expects($this->once())->method('setOptions')->with($options)->willReturnSelf(); + $template->expects($this->once())->method('getSubject')->willReturn('Email Subject'); + $template->expects($this->once())->method('setData')->with($data)->willReturnSelf(); $template->expects($this->once()) ->method('getProcessedTemplate') ->with($vars) - ->will($this->returnValue($bodyText)); + ->willReturn($bodyText); $template->expects($this->once()) ->method('setTemplateFilter') ->with($filter); - $this->templateFactoryMock->expects( - $this->once() - )->method( - 'get' - )->with( - $this->equalTo('identifier') - )->will( - $this->returnValue($template) - ); + $this->templateFactoryMock->expects($this->once())->method('get')->with('identifier')->willReturn($template); - $this->messageMock->expects( - $this->once() - )->method( - 'setSubject' - )->with( - $this->equalTo('Email Subject') - )->will( - $this->returnSelf() - ); - $this->messageMock->expects( - $this->once() - )->method( - 'setMessageType' - )->with( - $this->equalTo($messageType) - )->will( - $this->returnSelf() - ); - $this->messageMock->expects( - $this->once() - )->method( - 'setBody' - )->with( - $this->equalTo($bodyText) - )->will( - $this->returnSelf() - ); + $this->messageMock->expects($this->once())->method('setSubject')->with('Email Subject')->willReturnSelf(); + $this->messageMock->expects($this->once())->method('setMessageType')->with($messageType)->willReturnSelf(); + $this->messageMock->expects($this->once())->method('setBody')->with($bodyText)->willReturnSelf(); $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); - $this->mailTransportFactoryMock->expects( - $this->at(0) - )->method( - 'create' - )->with( - $this->equalTo(['message' => $this->messageMock]) - )->will( - $this->returnValue($transport) - ); + $this->mailTransportFactoryMock->expects($this->at(0))->method('create') + ->with(['message' => $this->messageMock])->willReturn($transport); - $this->objectManagerMock->expects( - $this->at(0) - )->method( - 'create' - )->with( - $this->equalTo(\Magento\Framework\Mail\Message::class) - )->will( - $this->returnValue($transport) - ); - - $this->builder->setTemplateIdentifier( - 'identifier' - )->setTemplateVars( - $vars - )->setTemplateOptions( - $options - )->setTemplateData( - $data - ); + $this->builder->setTemplateIdentifier('identifier') + ->setTemplateVars($vars) + ->setTemplateOptions($options) + ->setTemplateData($data); $result = $this->builder->getTransport(); diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php index 5a4032dc4dffd..0bf6f24a6b989 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Newsletter\Test\Unit\Model; +use Magento\Newsletter\Model\Subscriber; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -60,6 +62,16 @@ class SubscriberTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** + * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataObjectHelper; + + /** + * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerFactory; + /** * @var \Magento\Newsletter\Model\Subscriber */ @@ -94,7 +106,13 @@ protected function setUp() 'received' ]); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - + $this->customerFactory = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) + ->disableOriginalConstructor() + ->getMock(); $this->subscriber = $this->objectManager->getObject( \Magento\Newsletter\Model\Subscriber::class, [ @@ -106,7 +124,9 @@ protected function setUp() 'customerRepository' => $this->customerRepository, 'customerAccountManagement' => $this->customerAccountManagement, 'inlineTranslation' => $this->inlineTranslation, - 'resource' => $this->resource + 'resource' => $this->resource, + 'customerFactory' => $this->customerFactory, + 'dataObjectHelper' => $this->dataObjectHelper ] ); } @@ -114,9 +134,23 @@ protected function setUp() public function testSubscribe() { $email = 'subscriber_email@magento.com'; - $this->resource->expects($this->any())->method('loadByEmail')->willReturn( + $storeId = 1; + $customerData = ['store_id' => $storeId, 'email' => $email]; + $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->any())->method('getId')->willReturn($storeId); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerFactory->expects($this->once())->method('create')->willReturn($customer); + $this->dataObjectHelper->expects($this->once())->method('populateWithArray')->with( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->resource->expects($this->any())->method('loadByCustomerData')->with($customer)->willReturn( [ - 'subscriber_status' => 3, + 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED, 'subscriber_email' => $email, 'name' => 'subscriber_name' ] @@ -128,20 +162,34 @@ public function testSubscribe() $this->customerSession->expects($this->any())->method('getCustomerId')->willReturn(1); $customerDataModel->expects($this->any())->method('getEmail')->willReturn($email); $this->customerRepository->expects($this->any())->method('getById')->willReturn($customerDataModel); - $customerDataModel->expects($this->any())->method('getStoreId')->willReturn(1); + $customerDataModel->expects($this->any())->method('getStoreId')->willReturn($storeId); $customerDataModel->expects($this->any())->method('getId')->willReturn(1); $this->sendEmailCheck(); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $this->assertEquals(1, $this->subscriber->subscribe($email)); + $this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->subscribe($email)); } public function testSubscribeNotLoggedIn() { $email = 'subscriber_email@magento.com'; - $this->resource->expects($this->any())->method('loadByEmail')->willReturn( + $storeId = 1; + $customerData = ['store_id' => $storeId, 'email' => $email]; + $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->any())->method('getId')->willReturn($storeId); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerFactory->expects($this->once())->method('create')->willReturn($customer); + $this->dataObjectHelper->expects($this->once())->method('populateWithArray')->with( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->resource->expects($this->any())->method('loadByCustomerData')->with($customer)->willReturn( [ - 'subscriber_status' => 3, + 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED, 'subscriber_email' => $email, 'name' => 'subscriber_name' ] @@ -153,16 +201,23 @@ public function testSubscribeNotLoggedIn() $this->customerSession->expects($this->any())->method('getCustomerId')->willReturn(1); $customerDataModel->expects($this->any())->method('getEmail')->willReturn($email); $this->customerRepository->expects($this->any())->method('getById')->willReturn($customerDataModel); - $customerDataModel->expects($this->any())->method('getStoreId')->willReturn(1); + $customerDataModel->expects($this->any())->method('getStoreId')->willReturn($storeId); $customerDataModel->expects($this->any())->method('getId')->willReturn(1); $this->sendEmailCheck(); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $this->assertEquals(2, $this->subscriber->subscribe($email)); + $this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->subscribe($email)); } + /** + * Update status with Confirmation Status - required. + * + * @return void + */ public function testUpdateSubscription() { + $websiteId = 1; + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -175,7 +230,7 @@ public function testUpdateSubscription() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => 1 + 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED, ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); @@ -184,20 +239,25 @@ public function testUpdateSubscription() ->method('getConfirmationStatus') ->with($customerId) ->willReturn('account_confirmation_required'); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); + $customerDataMock->expects($this->exactly(2))->method('getWebsiteId')->willReturn(null); + $customerDataMock->expects($this->exactly(2))->method('setWebsiteId')->with($websiteId)->willReturnSelf(); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() - ->setMethods(['getId']) + ->setMethods(['getId', 'getWebsiteId']) ->getMock(); $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->exactly(2))->method('getWebsiteId')->willReturn($websiteId); + $data = $this->subscriber->updateSubscription($customerId); - $this->assertEquals($this->subscriber, $this->subscriber->updateSubscription($customerId)); + $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $data->getSubscriberStatus()); } public function testUnsubscribeCustomerById() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -210,12 +270,12 @@ public function testUnsubscribeCustomerById() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => 1 + 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); @@ -224,6 +284,7 @@ public function testUnsubscribeCustomerById() public function testSubscribeCustomerById() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -236,12 +297,12 @@ public function testSubscribeCustomerById() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => 3 + 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); @@ -250,6 +311,7 @@ public function testSubscribeCustomerById() public function testSubscribeCustomerById1() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -262,12 +324,12 @@ public function testSubscribeCustomerById1() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => 3 + 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); $this->customerAccountManagement->expects($this->once()) @@ -276,11 +338,12 @@ public function testSubscribeCustomerById1() $this->scopeConfig->expects($this->atLeastOnce())->method('getValue')->with()->willReturn(true); $this->subscriber->subscribeCustomerById($customerId); - $this->assertEquals(\Magento\Newsletter\Model\Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->getStatus()); + $this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->getStatus()); } public function testSubscribeCustomerByIdAfterConfirmation() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -293,19 +356,19 @@ public function testSubscribeCustomerByIdAfterConfirmation() ->willReturn( [ 'subscriber_id' => 1, - 'subscriber_status' => 4 + 'subscriber_status' => Subscriber::STATUS_UNCONFIRMED ] ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); $this->customerAccountManagement->expects($this->never())->method('getConfirmationStatus'); $this->scopeConfig->expects($this->atLeastOnce())->method('getValue')->with()->willReturn(true); $this->subscriber->updateSubscription($customerId); - $this->assertEquals(\Magento\Newsletter\Model\Subscriber::STATUS_SUBSCRIBED, $this->subscriber->getStatus()); + $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $this->subscriber->getStatus()); } public function testUnsubscribe() @@ -341,6 +404,21 @@ public function testConfirm() $code = 111; $this->subscriber->setCode($code); $this->resource->expects($this->once())->method('save')->willReturnSelf(); + $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMock(); + $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $this->scopeConfig->expects($this->any())->method('getValue')->willReturn(true); + $this->transportBuilder->expects($this->once())->method('setTemplateIdentifier')->willReturnSelf(); + $this->transportBuilder->expects($this->once())->method('setTemplateOptions')->willReturnSelf(); + $this->transportBuilder->expects($this->once())->method('setTemplateVars')->willReturnSelf(); + $this->transportBuilder->expects($this->once())->method('setFrom')->willReturnSelf(); + $this->transportBuilder->expects($this->once())->method('addTo')->willReturnSelf(); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->any())->method('getId')->willReturn(1); + $this->transportBuilder->expects($this->once())->method('getTransport')->willReturn($transport); + $transport->expects($this->once())->method('sendMessage')->willReturnSelf(); $this->assertTrue($this->subscriber->confirm($code)); } @@ -363,6 +441,9 @@ public function testReceived() $this->assertEquals($this->subscriber, $this->subscriber->received($queue)); } + /** + * @return $this + */ protected function sendEmailCheck() { $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php index 5773c77c5f7f9..b53ac39f0e4e2 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/TemplateTest.php @@ -197,20 +197,20 @@ protected function getModelMock(array $mockedMethods = []) public function testGetProcessedTemplateSubject() { - $model = $this->getModelMock([ - 'getTemplateFilter', - 'getDesignConfig', - 'applyDesignConfig', - 'setVariables', - ]); + $model = $this->getModelMock( + [ + 'getTemplateFilter', + 'getDesignConfig', + 'applyDesignConfig', + 'setVariables', + ] + ); $templateSubject = 'templateSubject'; $model->setTemplateSubject($templateSubject); + $model->setTemplateId('foobar'); - $filterTemplate = $this->getMockBuilder(\Magento\Framework\Filter\Template::class) - ->setMethods(['setVariables', 'setStoreId', 'filter']) - ->disableOriginalConstructor() - ->getMock(); + $filterTemplate = $this->createMock(\Magento\Framework\Filter\Template::class); $model->expects($this->once()) ->method('getTemplateFilter') ->will($this->returnValue($filterTemplate)); @@ -221,6 +221,11 @@ public function testGetProcessedTemplateSubject() ->with($templateSubject) ->will($this->returnValue($expectedResult)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(false)], [$this->equalTo(true)]) + ->willReturnOnConsecutiveCalls(true, false); + $variables = ['key' => 'value']; $filterTemplate->expects($this->once()) ->method('setVariables') @@ -245,21 +250,24 @@ public function testGetProcessedTemplateSubject() */ public function testGetProcessedTemplate($variables, $templateType, $storeId, $expectedVariables, $expectedResult) { + class_exists(\Magento\Newsletter\Model\Template\Filter::class, true); $filterTemplate = $this->getMockBuilder(\Magento\Newsletter\Model\Template\Filter::class) - ->setMethods([ - 'setUseSessionInUrl', - 'setPlainTemplateMode', - 'setIsChildTemplate', - 'setDesignParams', - 'setVariables', - 'setStoreId', - 'filter', - 'getStoreId', - 'getInlineCssFiles', - ]) + ->setMethods( + [ + 'setUseSessionInUrl', + 'setPlainTemplateMode', + 'setIsChildTemplate', + 'setDesignParams', + 'setVariables', + 'setStoreId', + 'filter', + 'getStoreId', + 'getInlineCssFiles', + 'setStrictMode', + ] + ) ->disableOriginalConstructor() ->getMock(); - $filterTemplate->expects($this->once()) ->method('setUseSessionInUrl') ->with(false) @@ -281,12 +289,15 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e ->method('getStoreId') ->will($this->returnValue($storeId)); + $filterTemplate->expects($this->exactly(2)) + ->method('setStrictMode') + ->withConsecutive([$this->equalTo(true)], [$this->equalTo(false)]) + ->willReturnOnConsecutiveCalls(false, true); + // The following block of code tests to ensure that the store id of the subscriber will be used, if the // 'subscriber' variable is set. $subscriber = $this->getMockBuilder(\Magento\Newsletter\Model\Subscriber::class) - ->setMethods([ - 'getStoreId', - ]) + ->setMethods(['getStoreId']) ->disableOriginalConstructor() ->getMock(); $subscriber->expects($this->once()) @@ -296,18 +307,20 @@ public function testGetProcessedTemplate($variables, $templateType, $storeId, $e $variables['subscriber'] = $subscriber; $expectedVariables['store'] = $this->store; - - $model = $this->getModelMock([ - 'getDesignParams', - 'applyDesignConfig', - 'getTemplateText', - 'isPlain', - ]); + $model = $this->getModelMock( + [ + 'getDesignParams', + 'applyDesignConfig', + 'getTemplateText', + 'isPlain', + ] + ); $filterTemplate->expects($this->any()) ->method('setVariables') ->with(array_merge(['this' => $model], $expectedVariables)); $model->setTemplateFilter($filterTemplate); $model->setTemplateType($templateType); + $model->setTemplateId('123'); $designParams = [ 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, @@ -401,6 +414,9 @@ public function testIsValidForSend($senderName, $senderEmail, $templateSubject, $this->assertEquals($expectedValue, $model->isValidForSend()); } + /** + * @return array + */ public function isValidForSendDataProvider() { return [ diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index 3cd5e15d46398..6b7209a062ceb 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-newsletter", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-customer": "101.0.*", "magento/module-widget": "101.0.*", @@ -14,7 +14,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Newsletter/etc/adminhtml/system.xml b/app/code/Magento/Newsletter/etc/adminhtml/system.xml index 1173f64310304..277005240eabc 100644 --- a/app/code/Magento/Newsletter/etc/adminhtml/system.xml +++ b/app/code/Magento/Newsletter/etc/adminhtml/system.xml @@ -11,39 +11,39 @@ <label>Newsletter</label> <tab>customer</tab> <resource>Magento_Newsletter::newsletter</resource> - <group id="subscription" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="subscription" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Subscription Options</label> - <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Allow Guest Subscription</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Need to Confirm</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_identity" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="confirm_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_template" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="success_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_identity" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="success_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_template" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="un_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_identity" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="un_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_template" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> diff --git a/app/code/Magento/Newsletter/etc/module.xml b/app/code/Magento/Newsletter/etc/module.xml index f338445225222..3c2976a31b69b 100644 --- a/app/code/Magento/Newsletter/etc/module.xml +++ b/app/code/Magento/Newsletter/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Newsletter" setup_version="2.0.0"> + <module name="Magento_Newsletter" setup_version="2.0.2"> <sequence> <module name="Magento_Store"/> <module name="Magento_Customer"/> diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv index c49fdc80da810..388b583f990b1 100644 --- a/app/code/Magento/Newsletter/i18n/en_US.csv +++ b/app/code/Magento/Newsletter/i18n/en_US.csv @@ -67,8 +67,8 @@ Subscribers,Subscribers "Something went wrong while saving this template.","Something went wrong while saving this template." "Newsletter Subscription","Newsletter Subscription" "Something went wrong while saving your subscription.","Something went wrong while saving your subscription." -"We saved the subscription.","We saved the subscription." -"We removed the subscription.","We removed the subscription." +"We have saved your subscription.","We have saved your subscription." +"We have removed your newsletter subscription.","We have removed your newsletter subscription." "Your subscription has been confirmed.","Your subscription has been confirmed." "This is an invalid subscription confirmation code.","This is an invalid subscription confirmation code." "This is an invalid subscription ID.","This is an invalid subscription ID." @@ -76,7 +76,7 @@ Subscribers,Subscribers "Sorry, but the administrator denied subscription for guests. Please <a href=""%1"">register</a>.","Sorry, but the administrator denied subscription for guests. Please <a href=""%1"">register</a>." "Please enter a valid email address.","Please enter a valid email address." "This email address is already subscribed.","This email address is already subscribed." -"The confirmation request has been sent.","The confirmation request has been sent." +"A confirmation request has been sent.","A confirmation request has been sent." "Thank you for your subscription.","Thank you for your subscription." "There was a problem with the subscription: %1","There was a problem with the subscription: %1" "Something went wrong with the subscription.","Something went wrong with the subscription." @@ -151,3 +151,4 @@ Unconfirmed,Unconfirmed Store,Store "Store View","Store View" "Newsletter Subscriptions","Newsletter Subscriptions" +"We have updated your subscription.","We have updated your subscription." diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index a64185ce67958..a4f84043aea74 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Backend\Block\Page $block */ ?> <div id="preview" class="cms-revision-preview"> @@ -16,7 +14,14 @@ </div> <?php endif;?> </div> - <iframe name="preview_iframe" id="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%"></iframe> + <iframe + name="preview_iframe" + id="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock"> + </iframe> <?= $block->getChildHtml('preview_form') ?> </div> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml index 2583ccea6fca4..d5a24fad2ac91 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml @@ -3,12 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($websites = $block->getWebsites()): ?> +<?php if ($websites = $block->getWebsites()) : ?> <div class="field field-store-switcher"> <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Choose Store View:')) ?></label> <div class="control"> @@ -16,22 +13,22 @@ id="store_switcher" class="admin__control-select" name="store_switcher"> - <?php foreach ($websites as $website): ?> + <?php foreach ($websites as $website) : ?> <?php $showWebsite = false; ?> - <?php foreach ($website->getGroups() as $group): ?> + <?php foreach ($website->getGroups() as $group) : ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStores($group) as $store): ?> - <?php if ($showWebsite == false): ?> + <?php foreach ($block->getStores($group) as $store) : ?> + <?php if ($showWebsite == false) : ?> <?php $showWebsite = true; ?> <optgroup label="<?= $block->escapeHtmlAttr($website->getName()) ?>"></optgroup> <?php endif; ?> - <?php if ($showGroup == false): ?> + <?php if ($showGroup == false) : ?> <?php $showGroup = true; ?> <optgroup label="   <?= $block->escapeHtmlAttr($group->getName()) ?>"> <?php endif; ?> - <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>"<?php if ($block->getStoreId() == $store->getId()): ?> selected="selected"<?php endif; ?>>    <?= $block->escapeHtml($store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>"<?php if ($block->getStoreId() == $store->getId()) : ?> selected="selected"<?php endif; ?>>    <?= $block->escapeHtml($store->getName()) ?></option> <?php endforeach; ?> - <?php if ($showGroup): ?> + <?php if ($showGroup) : ?> </optgroup> <?php endif; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml index 51808cab7f7a0..a3d88de9d35b2 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml @@ -3,13 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?= $block->getChildHtml('grid') ?> -<?php if ($block->getShowButtons()): ?> +<?php if ($block->getShowButtons()) : ?> <div class="form-buttons"> <?= $block->getUnsubscribeButtonHtml() ?> <?= $block->getDeleteButtonHtml() ?> @@ -18,7 +15,6 @@ <script> require(["prototype", "mage/adminhtml/events"], function(){ - <!-- problemController = { checkCheckboxes:function (controlCheckbox) { var elements = $('problemGrid').getElementsByClassName('problemCheckbox'); diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml index eea865eac1f22..b3723d97b70df 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml @@ -4,19 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /* @var $block \Magento\Newsletter\Block\Adminhtml\Queue\Edit */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> <?= $block->getPreviewButtonHtml() ?> - <?php if (!$block->getIsPreview()): ?> + <?php if (!$block->getIsPreview()) : ?> <?= $block->getResetButtonHtml() ?> <?= $block->getSaveButtonHtml() ?> <?php endif ?> - <?php if ($block->getCanResume()): ?> + <?php if ($block->getCanResume()) : ?> <?= $block->getResumeButtonHtml() ?> <?php endif ?> </div> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/list.phtml index 902c6932f0ae1..754cb0fe576fd 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/list.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml('grid') ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml index a405a6895b1d3..13bd5d5118be0 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml @@ -4,16 +4,14 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Newsletter\Block\Adminhtml\Subscriber $block */ ?> <?= $block->getChildHtml('grid') ?> -<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()): ?> +<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()) : ?> <div class="form-buttons"> <select id="queueList" name="queue"> - <?php foreach ($block->getQueueAsOptions() as $_queue): ?> + <?php foreach ($block->getQueueAsOptions() as $_queue) : ?> <option value="<?= $block->escapeHtmlAttr($_queue['value']) ?>"><?= $block->escapeHtml($_queue['label']) ?> #<?= $block->escapeHtml($_queue['value']) ?></option> <?php endforeach; ?> </select> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index eeca4fabd348d..99d7700bafa1e 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -6,8 +6,6 @@ use Magento\Framework\App\TemplateTypesInterface; -// @codingStandardsIgnoreFile - /* @var $block \Magento\Newsletter\Block\Adminhtml\Template\Edit */ ?> @@ -34,9 +32,10 @@ require([ 'tinymce', 'Magento_Ui/js/modal/prompt', 'Magento_Ui/js/modal/confirm', + 'mage/dataPost', 'mage/mage', 'prototype' -], function(jQuery, tinyMCE, prompt, confirm){ +], function(jQuery, tinyMCE, prompt, confirm, dataPost){ //<![CDATA[ jQuery('#newsletter_template_edit_form').mage('form').mage('validation'); @@ -203,7 +202,10 @@ require([ content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", actions: { confirm: function() { - window.location.href = '<?= $block->escapeUrl($block->getDeleteUrl()) ?>'; + dataPost().postData({ + action: '<?= $block->escapeUrl($block->getDeleteUrl()) ?>', + data: {} + }); } } }); diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/list.phtml index 902c6932f0ae1..754cb0fe576fd 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/list.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml('grid') ?> diff --git a/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html b/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html index eaf760c080370..beeda47d9d738 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html +++ b/app/code/Magento/Newsletter/view/frontend/email/subscr_confirm.html @@ -6,14 +6,13 @@ --> <!--@subject {{trans "Newsletter subscription confirmation"}} @--> <!--@vars { -"var customer.name":"Customer Name", -"var subscriber.getConfirmationLink()":"Subscriber Confirmation URL" +"var subscriber_data.confirmation_link":"Subscriber Confirmation URL" } @--> {{template config_path="design/email/header_template"}} <p class="greeting">{{trans "Thank you for subscribing to our newsletter."}}</p> <p>{{trans "To begin receiving the newsletter, you must first confirm your subscription by clicking on the following link:"}}</p> -<p><a href="{{var subscriber.getConfirmationLink()}}">{{var subscriber.getConfirmationLink()}}</a></p> +<p><a href="{{var subscriber_data.confirmation_link}}">{{var subscriber_data.confirmation_link}}</a></p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Newsletter/view/frontend/templates/js/components.phtml b/app/code/Magento/Newsletter/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index b633c61d9dc35..1769a9aba84fc 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Newsletter\Block\Subscribe $block */ ?> @@ -22,7 +20,7 @@ <label class="label" for="newsletter"><span><?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?></span></label> <div class="control"> <input name="email" type="email" id="newsletter" - placeholder="<?= $block->escapeHtmlAttr(__('Enter your email address')) ?>" + placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" data-validate="{required:true, 'validate-email':true}"/> </div> </div> diff --git a/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php b/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php index c4fe10c386645..d60348d9dc1c7 100644 --- a/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php +++ b/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php @@ -15,5 +15,5 @@ class Banktransfer extends \Magento\OfflinePayments\Block\Form\AbstractInstructi * * @var string */ - protected $_template = 'form/banktransfer.phtml'; + protected $_template = 'Magento_OfflinePayments::form/banktransfer.phtml'; } diff --git a/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php b/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php index 4e0f7d48ce09b..de0f7a57bae62 100644 --- a/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php +++ b/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php @@ -15,5 +15,5 @@ class Cashondelivery extends \Magento\OfflinePayments\Block\Form\AbstractInstruc * * @var string */ - protected $_template = 'form/cashondelivery.phtml'; + protected $_template = 'Magento_OfflinePayments::form/cashondelivery.phtml'; } diff --git a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php index 2e0e58060c757..464142df5b996 100644 --- a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php +++ b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php @@ -5,6 +5,8 @@ */ namespace Magento\OfflinePayments\Model; +use Magento\Framework\Exception\LocalizedException; + /** * Class Purchaseorder * @@ -46,11 +48,29 @@ class Purchaseorder extends \Magento\Payment\Model\Method\AbstractMethod * * @param \Magento\Framework\DataObject|mixed $data * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function assignData(\Magento\Framework\DataObject $data) { $this->getInfoInstance()->setPoNumber($data->getPoNumber()); return $this; } + + /** + * Validate payment method information object + * + * @return $this + * @throws LocalizedException + * @api + */ + public function validate() + { + parent::validate(); + + if (empty($this->getInfoInstance()->getPoNumber())) { + throw new LocalizedException(__('Purchase order number is a required field.')); + } + + return $this; + } } diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/LICENSE.txt b/app/code/Magento/OfflinePayments/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/OfflinePayments/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 <insert your license name here>" 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/app/code/Magento/OfflinePayments/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/OfflinePayments/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/OfflinePayments/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 <insert your license name here>" 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/app/code/Magento/OfflinePayments/Test/Mftf/README.md b/app/code/Magento/OfflinePayments/Test/Mftf/README.md new file mode 100644 index 0000000000000..f12b3518c5672 --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Offline Payments Functional Tests + +The Functional Test Module for **Magento Offline Payments** module. diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Model/CheckmoConfigProviderTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Model/CheckmoConfigProviderTest.php index 7509ff03c3780..8d65146ec102b 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Model/CheckmoConfigProviderTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Model/CheckmoConfigProviderTest.php @@ -63,6 +63,9 @@ public function testGetConfig($isAvailable, $mailingAddress, $payableTo, $result $this->assertEquals($result, $this->model->getConfig()); } + /** + * @return array + */ public function dataProviderGetConfig() { $checkmoCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Model/InstructionsConfigProviderTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Model/InstructionsConfigProviderTest.php index 120a7eb6ed88f..97a64d8ab59b9 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Model/InstructionsConfigProviderTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Model/InstructionsConfigProviderTest.php @@ -82,6 +82,9 @@ public function testGetConfig($isOneAvailable, $instructionsOne, $isTwoAvailable $this->assertEquals($result, $this->model->getConfig()); } + /** + * @return array + */ public function dataProviderGetConfig() { $oneCode = Banktransfer::PAYMENT_METHOD_BANKTRANSFER_CODE; diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php index 548e1d5fb1874..2eb204651fcf4 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php @@ -5,10 +5,21 @@ */ namespace Magento\OfflinePayments\Test\Unit\Model; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\Payment\Helper\Data as PaymentHelper; +use Magento\Payment\Model\Info as PaymentInfo; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order\Payment; + class PurchaseorderTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\OfflinePayments\Model\Purchaseorder + * @var Purchaseorder */ protected $_object; @@ -19,15 +30,15 @@ class PurchaseorderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $objectManagerHelper = new ObjectManager($this); + $eventManager = $this->createMock(EventManagerInterface::class); + $paymentDataMock = $this->createMock(PaymentHelper::class); $this->_scopeConfig = $this->createPartialMock( - \Magento\Framework\App\Config\ScopeConfigInterface::class, + ScopeConfigInterface::class, ['getValue', 'isSetFlag'] ); $this->_object = $objectManagerHelper->getObject( - \Magento\OfflinePayments\Model\Purchaseorder::class, + Purchaseorder::class, [ 'eventManager' => $eventManager, 'paymentData' => $paymentDataMock, @@ -38,13 +49,37 @@ protected function setUp() public function testAssignData() { - $data = new \Magento\Framework\DataObject([ + $data = new DataObject([ 'po_number' => '12345' ]); - $instance = $this->createMock(\Magento\Payment\Model\Info::class); + $instance = $this->createMock(PaymentInfo::class); $this->_object->setData('info_instance', $instance); $result = $this->_object->assignData($data); $this->assertEquals($result, $this->_object); } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Purchase order number is a required field. + */ + public function testValidate() + { + $data = new DataObject([]); + + $addressMock = $this->createMock(OrderAddressInterface::class); + $addressMock->expects($this->once())->method('getCountryId')->willReturn('UY'); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->expects($this->once())->method('getBillingAddress')->willReturn($addressMock); + + $instance = $this->createMock(Payment::class); + + $instance->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $this->_object->setData('info_instance', $instance); + $this->_object->assignData($data); + + $this->_object->validate(); + } } diff --git a/app/code/Magento/OfflinePayments/composer.json b/app/code/Magento/OfflinePayments/composer.json index f03333a976f17..aa42508a34363 100644 --- a/app/code/Magento/OfflinePayments/composer.json +++ b/app/code/Magento/OfflinePayments/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-offline-payments", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-checkout": "100.2.*", "magento/module-payment": "100.2.*", "magento/framework": "101.0.*" @@ -11,7 +11,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml b/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml index b47bd8f749040..89cc4d0986a00 100644 --- a/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml +++ b/app/code/Magento/OfflinePayments/etc/adminhtml/system.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> <system> - <section id="payment" translate="label" type="text" sortOrder="400" showInDefault="1" showInWebsite="1" showInStore="1"> + <section id="payment" type="text" sortOrder="400" showInDefault="1" showInWebsite="1" showInStore="1"> <group id="checkmo" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Check / Money Order</label> <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> diff --git a/app/code/Magento/OfflinePayments/i18n/en_US.csv b/app/code/Magento/OfflinePayments/i18n/en_US.csv index 43a743b29e3dd..5a180f7af4944 100644 --- a/app/code/Magento/OfflinePayments/i18n/en_US.csv +++ b/app/code/Magento/OfflinePayments/i18n/en_US.csv @@ -11,6 +11,7 @@ Enabled,Enabled "New Order Status","New Order Status" "Sort Order","Sort Order" Title,Title +"Purchase order number is a required field.","Purchase order number is a required field." "Payment from Applicable Countries","Payment from Applicable Countries" "Payment from Specific Countries","Payment from Specific Countries" "Make Check Payable to","Make Check Payable to" diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml index e1ffd7bdaf7a8..a251c609ea324 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml @@ -4,13 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions): ?> +<?php if ($instructions) : ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> <li> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml index ac72b5f5707ea..8e8730640a8a7 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions): ?> +<?php if ($instructions) : ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> <li> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml index 2a1de9df69ecd..db1d7c87ada0e 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo */ ?> <fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none"> - <?php if ($block->getMethod()->getPayableTo()): ?> + <?php if ($block->getMethod()->getPayableTo()) : ?> <label class="label"><span><?= $block->escapeHtml(__('Make Check payable to:')) ?></span></label> <?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()): ?> + <?php if ($block->getMethod()->getMailingAddress()) : ?> <div class="admin__field"> <label class="admin__field-label"><span><?= $block->escapeHtml(__('Send Check to:')) ?></span></label> <div class="admin__field-control checkmo-mailing-address"> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml index d6ee0d53eaaae..c115765697fc5 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder */ diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml index 0882619db8fe3..36f9d35327fce 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml @@ -4,17 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ ?> <?= $block->escapeHtml($block->getMethod()->getTitle()) ?> -<?php if ($block->getInfo()->getAdditionalInformation()): ?> - <?php if ($block->getPayableTo()): ?> +<?php if ($block->getInfo()->getAdditionalInformation()) : ?> + <?php if ($block->getPayableTo()) : ?> <br /><?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> <?php endif; ?> - <?php if ($block->getMailingAddress()): ?> + <?php if ($block->getMailingAddress()) : ?> <label><?= $block->escapeHtml(__('Send Check to:')) ?></label> <div class="checkmo-mailing-address"> <?= /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml index 4d63577319d5b..d8d952526e67b 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml @@ -4,20 +4,19 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ ?> <?= $block->escapeHtml($block->getMethod()->getTitle()) ?> {{pdf_row_separator}} -<?php if ($block->getInfo()->getAdditionalInformation()): ?> +<?php if ($block->getInfo()->getAdditionalInformation()) : ?> {{pdf_row_separator}} - <?php if ($block->getPayableTo()): ?> + <?php if ($block->getPayableTo()) : ?> <?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> {{pdf_row_separator}} <?php endif; ?> - <?php if ($block->getMailingAddress()): ?> + <?php if ($block->getMailingAddress()) : ?> <?= $block->escapeHtml(__('Send Check to:')) ?> {{pdf_row_separator}} <?= /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?> diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml new file mode 100644 index 0000000000000..d8d952526e67b --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @var $block \Magento\OfflinePayments\Block\Info\Checkmo + */ +?> +<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> + {{pdf_row_separator}} +<?php if ($block->getInfo()->getAdditionalInformation()) : ?> + {{pdf_row_separator}} + <?php if ($block->getPayableTo()) : ?> + <?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> + {{pdf_row_separator}} + <?php endif; ?> + <?php if ($block->getMailingAddress()) : ?> + <?= $block->escapeHtml(__('Send Check to:')) ?> + {{pdf_row_separator}} + <?= /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?> + {{pdf_row_separator}} + <?php endif; ?> +<?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml new file mode 100644 index 0000000000000..4a6ea1c00b21c --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** + * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder + */ +?> +<?= $block->escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?> + {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 0000000000000..32810ecef20da --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="checkmo" xsi:type="string">Magento_OfflinePayments::multishipping/checkmo_form.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml index 7c21047fbb90d..568ef7c3f69f2 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml @@ -4,13 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions): ?> +<?php if ($instructions) : ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement checkout-agreement-item-content" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml index f6f019bc84ce4..2943f59be4ab3 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions): ?> +<?php if ($instructions) : ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml index de4b6ac66cf29..36f58fc155a18 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml @@ -4,18 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo */ ?> -<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()): ?> +<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()) : ?> <dl class="items check payable" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none;"> - <?php if ($block->getMethod()->getPayableTo()): ?> + <?php if ($block->getMethod()->getPayableTo()) : ?> <dt class="title"><?= $block->escapeHtml(__('Make Check payable to:')) ?></dt> <dd class="content"><?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?></dd> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()): ?> + <?php if ($block->getMethod()->getMailingAddress()) : ?> <dt class="title"><?= $block->escapeHtml(__('Send Check to:')) ?></dt> <dd class="content"> <address class="checkmo mailing address"> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml index 5d04977ab3455..52b7df9fb9187 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder */ diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/info/checkmo.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/info/checkmo.phtml index cf7496b7c566f..2dc226db4264c 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/info/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/info/checkmo.phtml @@ -4,21 +4,20 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ ?> <dl class="payment-method checkmemo"> <dt class="title"><?= $block->escapeHtml($block->getMethod()->getTitle()) ?></dt> - <?php if ($block->getInfo()->getAdditionalInformation()): ?> - <?php if ($block->getPayableTo()): ?> + <?php if ($block->getInfo()->getAdditionalInformation()) : ?> + <?php if ($block->getPayableTo()) : ?> <dd class="content"> <strong><?= $block->escapeHtml(__('Make Check payable to')) ?></strong> <?= $block->escapeHtml($block->getPayableTo()) ?> </dd> <?php endif; ?> - <?php if ($block->getMailingAddress()): ?> + <?php if ($block->getMailingAddress()) : ?> <dd class="content"> <strong><?= $block->escapeHtml(__('Send Check to')) ?></strong> <address class="checkmo mailing address"> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml new file mode 100644 index 0000000000000..b96918243a7a7 --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'checkmo' + }; + layout([ + { + component: 'Magento_Checkout/js/view/payment/default', + name: 'payment_method_checkmo', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> diff --git a/app/code/Magento/OfflineShipping/Block/Adminhtml/Form/Field/Export.php b/app/code/Magento/OfflineShipping/Block/Adminhtml/Form/Field/Export.php index a258223e06777..1bd55cf5f1720 100644 --- a/app/code/Magento/OfflineShipping/Block/Adminhtml/Form/Field/Export.php +++ b/app/code/Magento/OfflineShipping/Block/Adminhtml/Form/Field/Export.php @@ -21,7 +21,7 @@ class Export extends \Magento\Framework\Data\Form\Element\AbstractElement * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Backend\Helper\Data $helper + * @param \Magento\Backend\Model\UrlInterface $backendUrl * @param array $data */ public function __construct( diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php index b546237b82565..0fc56c7136327 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php @@ -80,9 +80,8 @@ public function collectRates(RateRequest $request) $this->_updateFreeMethodQuote($request); - if ($request->getFreeShipping() || $request->getBaseSubtotalInclTax() >= $this->getConfigData( - 'free_shipping_subtotal' - ) + if ($request->getFreeShipping() + || ($request->getPackageValueWithDiscount() >= $this->getConfigData('free_shipping_subtotal')) ) { /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */ $method = $this->_rateMethodFactory->create(); @@ -97,8 +96,18 @@ public function collectRates(RateRequest $request) $method->setCost('0.00'); $result->append($method); + } elseif ($this->getConfigData('showmethod')) { + $error = $this->_rateErrorFactory->create(); + $error->setCarrier($this->_code); + $error->setCarrierTitle($this->getConfigData('title')); + $errorMsg = $this->getConfigData('specificerrmsg'); + $error->setErrorMessage( + $errorMsg ? $errorMsg : __( + 'Sorry, but we can\'t deliver to the destination country with this shipping module.' + ) + ); + return $error; } - return $result; } diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index 87d4b984cf933..51d42fbc607c8 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -60,6 +60,7 @@ class Tablerate extends \Magento\Shipping\Model\Carrier\AbstractCarrier implemen * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $resultMethodFactory * @param \Magento\OfflineShipping\Model\ResourceModel\Carrier\TablerateFactory $tablerateFactory * @param array $data + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function __construct( @@ -128,8 +129,10 @@ public function collectRates(RateRequest $request) $freeQty += $item->getQty() * ($child->getQty() - $freeShipping); } } - } elseif ($item->getFreeShipping()) { - $freeShipping = is_numeric($item->getFreeShipping()) ? $item->getFreeShipping() : 0; + } elseif ($item->getFreeShipping() || $item->getAddress()->getFreeShipping()) { + $freeShipping = $item->getFreeShipping() ? + $item->getFreeShipping() : $item->getAddress()->getFreeShipping(); + $freeShipping = is_numeric($freeShipping) ? $freeShipping : 0; $freeQty += $item->getQty() - $freeShipping; $freePackageValue += $item->getBaseRowTotal(); } @@ -264,7 +267,7 @@ private function createShippingMethod($shippingPrice, $cost) /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */ $method = $this->_resultMethodFactory->create(); - $method->setCarrier('tablerate'); + $method->setCarrier($this->getCarrierCode()); $method->setCarrierTitle($this->getConfigData('title')); $method->setMethod('bestway'); diff --git a/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php b/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php index e1397dc6e097b..c6234a1247a53 100644 --- a/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php +++ b/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php @@ -3,27 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\OfflineShipping\Model\Quote\Address; -class FreeShipping implements \Magento\Quote\Model\Quote\Address\FreeShippingInterface +use Magento\OfflineShipping\Model\SalesRule\Calculator; +use Magento\OfflineShipping\Model\SalesRule\ExtendedCalculator; +use Magento\Quote\Model\Quote\Address\FreeShippingInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Store\Model\StoreManagerInterface; + +class FreeShipping implements FreeShippingInterface { /** - * @var \Magento\OfflineShipping\Model\SalesRule\Calculator + * @var ExtendedCalculator */ protected $calculator; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\OfflineShipping\Model\SalesRule\Calculator $calculator + * @param StoreManagerInterface $storeManager + * @param Calculator $calculator */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\OfflineShipping\Model\SalesRule\Calculator $calculator + StoreManagerInterface $storeManager, + Calculator $calculator ) { $this->storeManager = $storeManager; $this->calculator = $calculator; @@ -39,6 +47,7 @@ public function isFreeShipping(\Magento\Quote\Model\Quote $quote, $items) return false; } + $result = false; $addressFreeShipping = true; $store = $this->storeManager->getStore($quote->getStoreId()); $this->calculator->init( @@ -62,21 +71,24 @@ public function isFreeShipping(\Magento\Quote\Model\Quote $quote, $items) } $this->calculator->processFreeShipping($item); - $itemFreeShipping = (bool)$item->getFreeShipping(); - $addressFreeShipping = $addressFreeShipping && $itemFreeShipping; - - if ($addressFreeShipping && !$item->getAddress()->getFreeShipping()) { - $item->getAddress()->setFreeShipping(true); + // at least one item matches to the rule and the rule mode is not a strict + if ((bool)$item->getAddress()->getFreeShipping()) { + $result = true; + break; } - /** Parent free shipping we apply to all children*/ - $this->applyToChildren($item, $itemFreeShipping); + $itemFreeShipping = (bool)$item->getFreeShipping(); + $addressFreeShipping = $addressFreeShipping && $itemFreeShipping; + $result = $addressFreeShipping; } - return (bool)$shippingAddress->getFreeShipping(); + + $shippingAddress->setFreeShipping((int)$result); + $this->applyToItems($items, $result); + return $result; } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param AbstractItem $item * @param bool $isFreeShipping * @return void */ @@ -91,4 +103,20 @@ protected function applyToChildren(\Magento\Quote\Model\Quote\Item\AbstractItem } } } + + /** + * Sets free shipping availability to the quote items. + * + * @param array $items + * @param bool $freeShipping + */ + private function applyToItems(array $items, bool $freeShipping) + { + /** @var AbstractItem $item */ + foreach ($items as $item) { + $item->getAddress() + ->setFreeShipping((int)$freeShipping); + $this->applyToChildren($item, $freeShipping); + } + } } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php index 500ab253f2a18..961958a54ac1b 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php @@ -232,10 +232,10 @@ private function importData(array $fields, array $values) $this->_importedRows += count($values); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $connection->rollback(); + $connection->rollBack(); throw new \Magento\Framework\Exception\LocalizedException(__('Unable to import data'), $e); } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); $this->logger->critical($e); throw new \Magento\Framework\Exception\LocalizedException( __('Something went wrong while importing table rates.') diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php index 4d2e11ebb8a1e..9b6f37f1a04e4 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php @@ -9,6 +9,9 @@ use Magento\Framework\Phrase; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\LocationDirectory; +/** + * Row parser. + */ class RowParser { /** @@ -26,6 +29,8 @@ public function __construct(LocationDirectory $locationDirectory) } /** + * Retrieve columns. + * * @return array */ public function getColumns() @@ -42,6 +47,8 @@ public function getColumns() } /** + * Parse provided row data. + * * @param array $rowData * @param int $rowNumber * @param int $websiteId @@ -62,27 +69,39 @@ public function parse( ) { // validate row if (count($rowData) < 5) { - throw new RowException(__('Please correct Table Rates format in the Row #%1.', $rowNumber)); + throw new RowException( + __( + 'The Table Rates File Format is incorrect in row number "%1". Verify the format and try again.', + $rowNumber + ) + ); } $countryId = $this->getCountryId($rowData, $rowNumber, $columnResolver); - $regionId = $this->getRegionId($rowData, $rowNumber, $columnResolver, $countryId); + $regionIds = $this->getRegionIds($rowData, $rowNumber, $columnResolver, $countryId); $zipCode = $this->getZipCode($rowData, $columnResolver); $conditionValue = $this->getConditionValue($rowData, $rowNumber, $conditionFullName, $columnResolver); $price = $this->getPrice($rowData, $rowNumber, $columnResolver); - return [ - 'website_id' => $websiteId, - 'dest_country_id' => $countryId, - 'dest_region_id' => $regionId, - 'dest_zip' => $zipCode, - 'condition_name' => $conditionShortName, - 'condition_value' => $conditionValue, - 'price' => $price, - ]; + $rates = []; + foreach ($regionIds as $regionId) { + $rates[] = [ + 'website_id' => $websiteId, + 'dest_country_id' => $countryId, + 'dest_region_id' => $regionId, + 'dest_zip' => $zipCode, + 'condition_name' => $conditionShortName, + 'condition_value' => $conditionValue, + 'price' => $price, + ]; + } + + return $rates; } /** + * Get country id from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver @@ -99,34 +118,53 @@ private function getCountryId(array $rowData, $rowNumber, ColumnResolver $column } elseif ($countryCode === '*' || $countryCode === '') { $countryId = '0'; } else { - throw new RowException(__('Please correct Country "%1" in the Row #%2.', $countryCode, $rowNumber)); + throw new RowException( + __( + 'The "%1" country in row number "%2" is incorrect. Verify the country and try again.', + $countryCode, + $rowNumber + ) + ); } + return $countryId; } /** + * Retrieve region id from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver * @param int $countryId - * @return int|string + * @return array * @throws ColumnNotFoundException * @throws RowException */ - private function getRegionId(array $rowData, $rowNumber, ColumnResolver $columnResolver, $countryId) + private function getRegionIds(array $rowData, $rowNumber, ColumnResolver $columnResolver, $countryId): array { $regionCode = $columnResolver->getColumnValue(ColumnResolver::COLUMN_REGION, $rowData); if ($countryId !== '0' && $this->locationDirectory->hasRegionId($countryId, $regionCode)) { - $regionId = $this->locationDirectory->getRegionId($countryId, $regionCode); + $regionIds = $this->locationDirectory->getRegionIds($countryId, $regionCode); } elseif ($regionCode === '*' || $regionCode === '') { - $regionId = 0; + $regionIds = [0]; } else { - throw new RowException(__('Please correct Region/State "%1" in the Row #%2.', $regionCode, $rowNumber)); + throw new RowException( + __( + 'The "%1" region or state in row number "%2" is incorrect. ' + . 'Verify the region or state and try again.', + $regionCode, + $rowNumber + ) + ); } - return $regionId; + + return $regionIds; } /** + * Retrieve zip code from provided row data. + * * @param array $rowData * @param ColumnResolver $columnResolver * @return float|int|null|string @@ -138,10 +176,13 @@ private function getZipCode(array $rowData, ColumnResolver $columnResolver) if ($zipCode === '') { $zipCode = '*'; } + return $zipCode; } /** + * Get condition value form provided row data. + * * @param array $rowData * @param int $rowNumber * @param string $conditionFullName @@ -165,10 +206,13 @@ private function getConditionValue(array $rowData, $rowNumber, $conditionFullNam ) ); } + return $value; } /** + * Retrieve price from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver @@ -181,13 +225,21 @@ private function getPrice(array $rowData, $rowNumber, ColumnResolver $columnReso $priceValue = $columnResolver->getColumnValue(ColumnResolver::COLUMN_PRICE, $rowData); $price = $this->_parseDecimalValue($priceValue); if ($price === false) { - throw new RowException(__('Please correct Shipping Price "%1" in the Row #%2.', $priceValue, $rowNumber)); + throw new RowException( + __( + 'The "%1" shipping price in row number "%2" is incorrect. Verify the shipping price and try again.', + $priceValue, + $rowNumber + ) + ); } + return $price; } /** * Parse and validate positive decimal value + * * Return false if value is not decimal or is not positive * * @param string $value @@ -202,6 +254,7 @@ private function _parseDecimalValue($value) $result = $value; } } + return $result; } } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php index 1012394f22fcb..1669364b03f1d 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php @@ -16,6 +16,11 @@ use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\CSV\RowParser; use Magento\Store\Model\StoreManagerInterface; +/** + * Import offline shipping. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Import { /** @@ -84,6 +89,8 @@ public function __construct( } /** + * Check if there are errors. + * * @return bool */ public function hasErrors() @@ -92,6 +99,8 @@ public function hasErrors() } /** + * Get errors. + * * @return array */ public function getErrors() @@ -100,6 +109,8 @@ public function getErrors() } /** + * Retrieve columns. + * * @return array */ public function getColumns() @@ -108,6 +119,8 @@ public function getColumns() } /** + * Get data from file. + * * @param ReadInterface $file * @param int $websiteId * @param string $conditionShortName @@ -132,7 +145,7 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c if (empty($csvLine)) { continue; } - $rowData = $this->rowParser->parse( + $rowsData = $this->rowParser->parse( $csvLine, $rowNumber, $websiteId, @@ -141,20 +154,25 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c $columnResolver ); - // protect from duplicate - $hash = $this->dataHashGenerator->getHash($rowData); - if (array_key_exists($hash, $this->uniqueHash)) { - throw new RowException( - __( - 'Duplicate Row #%1 (duplicates row #%2)', - $rowNumber, - $this->uniqueHash[$hash] - ) - ); + foreach ($rowsData as $rowData) { + // protect from duplicate + $hash = $this->dataHashGenerator->getHash($rowData); + if (array_key_exists($hash, $this->uniqueHash)) { + throw new RowException( + __( + 'Duplicate Row #%1 (duplicates row #%2)', + $rowNumber, + $this->uniqueHash[$hash] + ) + ); + } + $this->uniqueHash[$hash] = $rowNumber; + + $items[] = $rowData; + } + if (count($rowsData) > 1) { + $bunchSize += count($rowsData) - 1; } - $this->uniqueHash[$hash] = $rowNumber; - - $items[] = $rowData; if (count($items) === $bunchSize) { yield $items; $items = []; @@ -169,6 +187,8 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c } /** + * Retrieve column headers. + * * @param ReadInterface $file * @return array|bool * @throws LocalizedException @@ -178,8 +198,11 @@ private function getHeaders(ReadInterface $file) // check and skip headers $headers = $file->readCsv(); if ($headers === false || count($headers) < 5) { - throw new LocalizedException(__('Please correct Table Rates File Format.')); + throw new LocalizedException( + __('The Table Rates File Format is incorrect. Verify the format and try again.') + ); } + return $headers; } } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php index 1a311f3658a0a..3bccaaab481b3 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php @@ -6,6 +6,9 @@ namespace Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; +/** + * Location directory. + */ class LocationDirectory { /** @@ -13,6 +16,11 @@ class LocationDirectory */ protected $regions; + /** + * @var array + */ + private $regionsByCode; + /** * @var array */ @@ -47,6 +55,8 @@ public function __construct( } /** + * Retrieve country id. + * * @param string $countryCode * @return null|string */ @@ -64,7 +74,7 @@ public function getCountryId($countryCode) } /** - * Load directory countries + * Load directory countries. * * @return \Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate */ @@ -88,16 +98,21 @@ protected function loadCountries() } /** + * Check if there is country id with provided country code. + * * @param string $countryCode * @return bool */ public function hasCountryId($countryCode) { $this->loadCountries(); + return isset($this->iso2Countries[$countryCode]) || isset($this->iso3Countries[$countryCode]); } /** + * Check if there is region id with provided region code and country id. + * * @param string $countryId * @param string $regionCode * @return bool @@ -105,6 +120,7 @@ public function hasCountryId($countryCode) public function hasRegionId($countryId, $regionCode) { $this->loadRegions(); + return isset($this->regions[$countryId][$regionCode]); } @@ -115,29 +131,52 @@ public function hasRegionId($countryId, $regionCode) */ protected function loadRegions() { - if ($this->regions !== null) { + if ($this->regions !== null && $this->regionsByCode !== null) { return $this; } $this->regions = []; + $this->regionsByCode = []; /** @var $collection \Magento\Directory\Model\ResourceModel\Region\Collection */ $collection = $this->_regionCollectionFactory->create(); foreach ($collection->getData() as $row) { $this->regions[$row['country_id']][$row['code']] = (int)$row['region_id']; + if (empty($this->regionsByCode[$row['country_id']][$row['code']])) { + $this->regionsByCode[$row['country_id']][$row['code']] = []; + } + $this->regionsByCode[$row['country_id']][$row['code']][] = (int)$row['region_id']; } return $this; } /** + * Retrieve region id. + * * @param int $countryId * @param string $regionCode * @return string + * @deprecated */ public function getRegionId($countryId, $regionCode) { $this->loadRegions(); + return $this->regions[$countryId][$regionCode]; } + + /** + * Return region ids for country and region. + * + * @param string $countryId + * @param string $regionCode + * @return array + */ + public function getRegionIds(string $countryId, string $regionCode): array + { + $this->loadRegions(); + + return $this->regionsByCode[$countryId][$regionCode]; + } } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php index 5a3ad76f0410f..aa561bf4499c1 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php @@ -42,6 +42,7 @@ public function prepareSelect(\Magento\Framework\DB\Select $select) ') OR (', [ "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = :postcode", + "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = :postcode_prefix", "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = ''", // Handle asterisk in dest_zip field @@ -51,7 +52,7 @@ public function prepareSelect(\Magento\Framework\DB\Select $select) "dest_country_id = '0' AND dest_region_id = 0 AND dest_zip = '*'", "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = ''", "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = :postcode", - "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = '*'" + "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = :postcode_prefix" ] ) . ')'; $select->where($orWhere); @@ -85,6 +86,7 @@ public function getBindings() ':country_id' => $this->request->getDestCountryId(), ':region_id' => (int)$this->request->getDestRegionId(), ':postcode' => $this->request->getDestPostcode(), + ':postcode_prefix' => $this->getDestPostcodePrefix() ]; // Render condition by condition name @@ -99,7 +101,7 @@ public function getBindings() } } else { $bind[':condition_name'] = $this->request->getConditionName(); - $bind[':condition_value'] = $this->request->getData($this->request->getConditionName()); + $bind[':condition_value'] = round($this->request->getData($this->request->getConditionName()), 4); } return $bind; @@ -112,4 +114,18 @@ public function getRequest() { return $this->request; } + + /** + * Returns the entire postcode if it contains no dash + * or the part of it prior to the dash in the other case + * @return string + */ + private function getDestPostcodePrefix() + { + if (!preg_match("/^(.+)-(.+)$/", $this->request->getDestPostcode(), $zipParts)) { + return $this->request->getDestPostcode(); + } + + return $zipParts[1]; + } } diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/LICENSE.txt b/app/code/Magento/OfflineShipping/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/OfflineShipping/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 <insert your license name here>" 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/app/code/Magento/OfflineShipping/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/OfflineShipping/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/OfflineShipping/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 <insert your license name here>" 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/app/code/Magento/OfflineShipping/Test/Mftf/README.md b/app/code/Magento/OfflineShipping/Test/Mftf/README.md new file mode 100644 index 0000000000000..3928af293d336 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Offline Shipping Functional Tests + +The Functional Test Module for **Magento Offline Shipping** module. diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ExportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ExportTest.php index cc164e504b665..3e2c7df9087da 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ExportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ExportTest.php @@ -37,7 +37,7 @@ public function testGetElementHtml() $requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); $requestMock->expects($this->once())->method('getParam')->with('website')->will($this->returnValue(1)); - $mockData = $this->createPartialMock(\StdClass::class, ['toHtml']); + $mockData = $this->createPartialMock(\stdClass::class, ['toHtml']); $mockData->expects($this->once())->method('toHtml')->will($this->returnValue($expected)); $blockMock->expects($this->once())->method('getRequest')->will($this->returnValue($requestMock)); diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index 8d75cc32914b4..635d0c636ca36 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -13,6 +13,11 @@ class ImportTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import */ @@ -29,11 +34,16 @@ protected function setUp() \Magento\Framework\Data\Form::class, ['getFieldNameSuffix', 'addSuffixToName', 'getHtmlIdPrefix', 'getHtmlIdSuffix'] ); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $testData = ['name' => 'test_name', 'html_id' => 'test_html_id']; $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class, - ['data' => $testData] + [ + 'escaper' => $this->escaperMock, + 'data' => $testData, + ] ); $this->_object->setForm($this->_formMock); } diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php index 185f393ad4d0b..5f0894874ca3c 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php @@ -52,6 +52,9 @@ public function testAfterGetStateActive($scopeConfigMockReturnValue, $result, $a $this->assertEquals($assertResult, $this->model->afterIsStateActive($subjectMock, $result)); } + /** + * @return array + */ public function afterGetStateActiveDataProvider() { return [ diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php index 5a8b1afa786bb..8266a39bbbb6d 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php @@ -3,94 +3,193 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\OfflineShipping\Test\Unit\Model\Quote\Address; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\OfflineShipping\Model\Quote\Address\FreeShipping; +use Magento\OfflineShipping\Model\SalesRule\Calculator; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class FreeShippingTest extends \PHPUnit\Framework\TestCase { + private static $websiteId = 1; + + private static $customerGroupId = 2; + + private static $couponCode = 3; + + private static $storeId = 1; + /** - * @var \Magento\OfflineShipping\Model\Quote\Address\FreeShipping + * @var FreeShipping */ private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface + * @var MockObject|StoreManagerInterface */ - private $storeManagerMock; + private $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\OfflineShipping\Model\SalesRule\Calculator + * @var MockObject|Calculator */ - private $calculatorMock; + private $calculator; protected function setUp() { - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->calculatorMock = $this->createMock(\Magento\OfflineShipping\Model\SalesRule\Calculator::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->calculator = $this->createMock(Calculator::class); - $this->model = new \Magento\OfflineShipping\Model\Quote\Address\FreeShipping( - $this->storeManagerMock, - $this->calculatorMock + $this->model = new FreeShipping( + $this->storeManager, + $this->calculator ); } - public function testIsFreeShippingIfNoItems() + /** + * Checks free shipping availability based on quote items and cart rule calculations. + * + * @param int $addressFree + * @param int $fItemFree + * @param int $sItemFree + * @param bool $expected + * @dataProvider itemsDataProvider + */ + public function testIsFreeShipping(int $addressFree, int $fItemFree, int $sItemFree, bool $expected) { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->assertFalse($this->model->isFreeShipping($quoteMock, [])); + $address = $this->getShippingAddress(); + $this->withStore(); + $quote = $this->getQuote($address); + $fItem = $this->getItem($quote); + $sItem = $this->getItem($quote); + $items = [$fItem, $sItem]; + + $this->calculator->method('init') + ->with(self::$websiteId, self::$customerGroupId, self::$couponCode); + $this->calculator->method('processFreeShipping') + ->withConsecutive( + [$fItem], + [$sItem] + ) + ->willReturnCallback(function () use ($fItem, $sItem, $addressFree, $fItemFree, $sItemFree) { + // emulate behavior of cart rule calculator + $fItem->getAddress()->setFreeShipping($addressFree); + $fItem->setFreeShipping($fItemFree); + $sItem->setFreeShipping($sItemFree); + }); + + $actual = $this->model->isFreeShipping($quote, $items); + self::assertEquals($expected, $actual); + self::assertEquals($expected, $address->getFreeShipping()); } - public function testIsFreeShipping() + /** + * Gets list of variations with free shipping availability. + * + * @return array + */ + public function itemsDataProvider(): array { - $storeId = 100; - $websiteId = 200; - $customerGroupId = 300; - $objectManagerMock = new ObjectManagerHelper($this); - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getShippingAddress', 'getStoreId', 'getCustomerGroupId', 'getCouponCode'] - ); - $itemMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, [ - 'getNoDiscount', - 'getParentItemId', - 'getFreeShipping', - 'getAddress', - 'isChildrenCalculated', - 'getHasChildren', - 'getChildren' - ]); - - $quoteMock->expects($this->once())->method('getStoreId')->willReturn($storeId); - $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $this->storeManagerMock->expects($this->once())->method('getStore')->with($storeId)->willReturn($storeMock); - - $quoteMock->expects($this->once())->method('getCustomerGroupId')->willReturn($customerGroupId); - $quoteMock->expects($this->once())->method('getCouponCode')->willReturn(null); - - $this->calculatorMock->expects($this->once()) - ->method('init') - ->with($websiteId, $customerGroupId, null) - ->willReturnSelf(); - - $itemMock->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemMock->expects($this->once())->method('getParentItemId')->willReturn(false); - $this->calculatorMock->expects($this->exactly(2))->method('processFreeShipping')->willReturnSelf(); - $itemMock->expects($this->once())->method('getFreeShipping')->willReturn(true); - - $addressMock = $objectManagerMock->getObject(\Magento\Quote\Model\Quote\Address::class); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($addressMock); - $itemMock->expects($this->exactly(2))->method('getAddress')->willReturn($addressMock); - - $itemMock->expects($this->once())->method('getHasChildren')->willReturn(true); - $itemMock->expects($this->once())->method('isChildrenCalculated')->willReturn(true); - - $childMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['setFreeShipping']); - $childMock->expects($this->once())->method('setFreeShipping')->with(true)->willReturnSelf(); - $itemMock->expects($this->once())->method('getChildren')->willReturn([$childMock]); - - $this->assertTrue($this->model->isFreeShipping($quoteMock, [$itemMock])); + return [ + ['addressFree' => 1, 'fItemFree' => 0, 'sItemFree' => 0, 'expected' => true], + ['addressFree' => 0, 'fItemFree' => 1, 'sItemFree' => 0, 'expected' => false], + ['addressFree' => 0, 'fItemFree' => 0, 'sItemFree' => 1, 'expected' => false], + ['addressFree' => 0, 'fItemFree' => 1, 'sItemFree' => 1, 'expected' => true], + ]; + } + + /** + * Creates mock object for store entity. + */ + private function withStore() + { + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->method('getStore') + ->with(self::$storeId) + ->willReturn($store); + + $store->method('getWebsiteId') + ->willReturn(self::$websiteId); + } + + /** + * Get mock object for quote entity. + * + * @param Address $address + * @return Quote + */ + private function getQuote(Address $address): Quote + { + /** @var Quote|MockObject $quote */ + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getCouponCode', 'getCustomerGroupId', 'getShippingAddress', 'getStoreId', 'getItemsQty', + 'getVirtualItemsQty' + ] + ) + ->getMock(); + + $quote->method('getStoreId') + ->willReturn(self::$storeId); + $quote->method('getCustomerGroupId') + ->willReturn(self::$customerGroupId); + $quote->method('getCouponCode') + ->willReturn(self::$couponCode); + $quote->method('getShippingAddress') + ->willReturn($address); + $quote->method('getItemsQty') + ->willReturn(2); + $quote->method('getVirtualItemsQty') + ->willReturn(0); + + return $quote; + } + + /** + * Gets stub object for shipping address. + * + * @return Address|MockObject + */ + private function getShippingAddress(): Address + { + /** @var Address|MockObject $address */ + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->setMethods(['beforeSave']) + ->getMock(); + + return $address; + } + + /** + * Gets stub object for quote item. + * + * @param Quote $quote + * @return Item + */ + private function getItem(Quote $quote): Item + { + /** @var Item|MockObject $item */ + $item = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->setMethods(['getHasChildren']) + ->getMock(); + $item->setQuote($quote); + $item->setNoDiscount(0); + $item->setParentItemId(0); + $item->method('getHasChildren') + ->willReturn(0); + + return $item; } } diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php index 2f6c5ca600bd3..683790c531265 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php @@ -37,7 +37,7 @@ class RowParserTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->locationDirectoryMock = $this->getMockBuilder(LocationDirectory::class) - ->setMethods(['hasCountryId', 'getCountryId', 'hasRegionId', 'getRegionId']) + ->setMethods(['hasCountryId', 'getCountryId', 'hasRegionId', 'getRegionIds']) ->disableOriginalConstructor() ->getMock(); $this->columnResolverMock = $this->getMockBuilder(ColumnResolver::class) @@ -92,7 +92,7 @@ public function testParse() $conditionShortName, $columnValueMap ); - $this->assertEquals($expectedResult, $result); + $this->assertEquals([$expectedResult], $result); } /** @@ -128,6 +128,9 @@ public function testParseWithException(array $rowData, $conditionFullName, array throw $exception; } + /** + * @return array + */ public function parseWithExceptionDataProvider() { $rowData = ['a', 'b', 'c', 'd', 'e']; @@ -143,7 +146,7 @@ public function parseWithExceptionDataProvider() [$conditionFullName, $rowData, 40], [ColumnResolver::COLUMN_PRICE, $rowData, 350], ], - 'Please correct Country "XX" in the Row #120.', + 'The "XX" country in row number "120" is incorrect. Verify the country and try again.', ], [ $rowData, @@ -155,7 +158,7 @@ public function parseWithExceptionDataProvider() [$conditionFullName, $rowData, 40], [ColumnResolver::COLUMN_PRICE, $rowData, 350], ], - 'Please correct Region/State "AA" in the Row #120.', + 'The "AA" region or state in row number "120" is incorrect. Verify the region or state and try again.', ], [ $rowData, @@ -179,7 +182,7 @@ public function parseWithExceptionDataProvider() [$conditionFullName, $rowData, 40], [ColumnResolver::COLUMN_PRICE, $rowData, 'BBB'], ], - 'Please correct Shipping Price "BBB" in the Row #120.', + 'The "BBB" shipping price in row number "120" is incorrect. Verify the shipping price and try again.', ], ]; } diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php index 14fa8129532fa..722683decb4c2 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php @@ -77,9 +77,6 @@ protected function setUp() ->getMock(); $this->dataHashGeneratorMock = $this->getMockBuilder(DataHashGenerator::class) ->getMock(); - $this->rowParserMock->expects($this->any()) - ->method('parse') - ->willReturnArgument(0); $this->dataHashGeneratorMock->expects($this->any()) ->method('getHash') ->willReturnCallback( @@ -124,6 +121,15 @@ public function testGetData() ['a4', 'b4', 'c4', 'd4', 'e4'], ['a5', 'b5', 'c5', 'd5', 'e5'], ]; + $this->rowParserMock->expects($this->any()) + ->method('parse') + ->willReturn( + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a2', 'b2', 'c2', 'd2', 'e2']], + [['a3', 'b3', 'c3', 'd3', 'e3']], + [['a4', 'b4', 'c4', 'd4', 'e4']], + [['a5', 'b5', 'c5', 'd5', 'e5']] + ); $file = $this->createFileMock($lines); $expectedResult = [ [ @@ -167,6 +173,13 @@ public function testGetDataWithDuplicatedLine() [], ['a2', 'b2', 'c2', 'd2', 'e2'], ]; + $this->rowParserMock->expects($this->any()) + ->method('parse') + ->willReturn( + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a2', 'b2', 'c2', 'd2', 'e2']] + ); $file = $this->createFileMock($lines); $expectedResult = [ [ @@ -193,7 +206,7 @@ public function testGetDataWithDuplicatedLine() /** * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please correct Table Rates File Format. + * @expectedExceptionMessage The Table Rates File Format is incorrect. Verify the format and try again. * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function testGetDataFromEmptyFile() diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/SalesRule/CalculatorTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/SalesRule/CalculatorTest.php index b13a46a8fcdf0..2a886f20c42a7 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/SalesRule/CalculatorTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/SalesRule/CalculatorTest.php @@ -20,6 +20,9 @@ protected function setUp() ); } + /** + * @return bool + */ public function testProcessFreeShipping() { $addressMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json index 2ce66fa368d01..192f1a1e81089 100644 --- a/app/code/Magento/OfflineShipping/composer.json +++ b/app/code/Magento/OfflineShipping/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-offline-shipping", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", @@ -19,7 +19,7 @@ "magento/module-offline-shipping-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.9", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml b/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml index 306aac1769913..4db5f489aa4a2 100644 --- a/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml +++ b/app/code/Magento/OfflineShipping/etc/adminhtml/system.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> <system> - <section id="carriers" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1"> + <section id="carriers" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1"> <group id="flatrate" translate="label" type="text" sortOrder="0" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Flat Rate</label> <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> diff --git a/app/code/Magento/PageCache/Controller/Block.php b/app/code/Magento/PageCache/Controller/Block.php index 7927284763ecb..4fb93a59e877c 100644 --- a/app/code/Magento/PageCache/Controller/Block.php +++ b/app/code/Magento/PageCache/Controller/Block.php @@ -10,6 +10,9 @@ use Magento\Framework\Serialize\Serializer\Base64Json; use Magento\Framework\Serialize\Serializer\Json; +/** + * Page cache block controller abstract class + */ abstract class Block extends \Magento\Framework\App\Action\Action { /** @@ -55,13 +58,12 @@ public function __construct( protected function _getBlocks() { $blocks = $this->getRequest()->getParam('blocks', ''); - $handles = $this->getRequest()->getParam('handles', ''); + $handles = $this->getHandles(); if (!$handles || !$blocks) { return []; } $blocks = $this->jsonSerializer->unserialize($blocks); - $handles = $this->base64jsonSerializer->unserialize($handles); $this->_view->loadLayout($handles, true, true, false); $data = []; @@ -76,4 +78,22 @@ protected function _getBlocks() return $data; } + + /** + * Get handles + * + * @return array + */ + private function getHandles(): array + { + $handles = $this->getRequest()->getParam('handles', ''); + $handles = !$handles ? [] : $this->base64jsonSerializer->unserialize($handles); + $validHandles = []; + foreach ($handles as $handle) { + if (!preg_match('/[@\'\*\.\\\"]/i', $handle)) { + $validHandles[] = $handle; + } + } + return $validHandles; + } } diff --git a/app/code/Magento/PageCache/Controller/Block/Render.php b/app/code/Magento/PageCache/Controller/Block/Render.php index e9a6e06ecb448..6c9f5c6a7375b 100644 --- a/app/code/Magento/PageCache/Controller/Block/Render.php +++ b/app/code/Magento/PageCache/Controller/Block/Render.php @@ -6,6 +6,11 @@ */ namespace Magento\PageCache\Controller\Block; +/** + * Page cache render controller + * + * @deprecated + */ class Render extends \Magento\PageCache\Controller\Block { /** diff --git a/app/code/Magento/PageCache/Model/Cache/Server.php b/app/code/Magento/PageCache/Model/Cache/Server.php index 349e9faffa673..06118446c21bc 100644 --- a/app/code/Magento/PageCache/Model/Cache/Server.php +++ b/app/code/Magento/PageCache/Model/Cache/Server.php @@ -62,8 +62,7 @@ public function getUris() foreach ($configuredHosts as $host) { $servers[] = UriFactory::factory('') ->setHost($host['host']) - ->setPort(isset($host['port']) ? $host['port'] : self::DEFAULT_PORT) - ; + ->setPort(isset($host['port']) ? $host['port'] : self::DEFAULT_PORT); } } elseif ($this->request->getHttpHost()) { $servers[] = UriFactory::factory('')->setHost($this->request->getHttpHost())->setPort(self::DEFAULT_PORT); diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php index 6debbdf3e728a..83db8c0dec3b1 100644 --- a/app/code/Magento/PageCache/Model/Config.php +++ b/app/code/Magento/PageCache/Model/Config.php @@ -113,7 +113,6 @@ public function __construct( * * @return int * @api - * @deprecated 100.2.0 see \Magento\PageCache\Model\VclGeneratorInterface::generateVcl */ public function getType() { @@ -125,7 +124,6 @@ public function getType() * * @return int * @api - * @deprecated 100.2.0 see \Magento\PageCache\Model\VclGeneratorInterface::generateVcl */ public function getTtl() { @@ -255,7 +253,6 @@ protected function _getDesignExceptions() * * @return bool * @api - * @deprecated 100.2.0 see \Magento\PageCache\Model\VclGeneratorInterface::generateVcl */ public function isEnabled() { diff --git a/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php b/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php new file mode 100644 index 0000000000000..e16584b0b17f8 --- /dev/null +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Model\System\Config\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; + +/** + * Access List config field. + */ +class AccessList extends Varnish +{ + /** + * @inheritDoc + */ + public function beforeSave() + { + parent::beforeSave(); + + $value = $this->getValue(); + if (!is_string($value) || !preg_match('/^[\w\s\.\-\,\:]+$/', $value)) { + throw new LocalizedException( + new Phrase( + 'Access List value "%1" is not valid. ' + .'Please use only IP addresses and host names.', + [$value] + ) + ); + } + } +} diff --git a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php index dab8d7be16dd7..95b0ebe72ecd1 100644 --- a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php @@ -6,15 +6,49 @@ namespace Magento\PageCache\Model\System\Config\Backend; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; + /** - * Backend model for processing Public content cache lifetime settings + * Backend model for processing Public content cache lifetime settings. * * Class Ttl */ class Ttl extends \Magento\Framework\App\Config\Value { /** - * Throw exception if Ttl data is invalid or empty + * @var Escaper + */ + private $escaper; + + /** + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param ScopeConfigInterface $config + * @param \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data + * @param Escaper|null $escaper + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + ScopeConfigInterface $config, + \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList, + \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [], + Escaper $escaper = null + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->escaper = $escaper ?: ObjectManager::getInstance()->create(Escaper::class); + } + + /** + * Throw exception if Ttl data is invalid or empty. * * @return $this * @throws \Magento\Framework\Exception\LocalizedException @@ -23,10 +57,14 @@ public function beforeSave() { $value = $this->getValue(); if ($value < 0 || !preg_match('/^[0-9]+$/', $value)) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', $value) + throw new LocalizedException( + __( + 'Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', + $this->escaper->escapeHtml($value) + ) ); } + return $this; } } diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php new file mode 100644 index 0000000000000..7a1cc8934c017 --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php @@ -0,0 +1,108 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer; + +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\App\Cache\Manager; +use Magento\PageCache\Model\Cache\Type as PageCacheType; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; + +/** + * Switch Page Cache on maintenance. + */ +class SwitchPageCacheOnMaintenance implements ObserverInterface +{ + /** + * @var Manager + */ + private $cacheManager; + + /** + * @var PageCacheState + */ + private $pageCacheStateStorage; + + /** + * @param Manager $cacheManager + * @param PageCacheState $pageCacheStateStorage + */ + public function __construct(Manager $cacheManager, PageCacheState $pageCacheStateStorage) + { + $this->cacheManager = $cacheManager; + $this->pageCacheStateStorage = $pageCacheStateStorage; + } + + /** + * Switches Full Page Cache. + * + * Depending on enabling or disabling Maintenance Mode it turns off or restores Full Page Cache state. + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + if ($observer->getData('isOn')) { + $this->pageCacheStateStorage->save($this->isFullPageCacheEnabled()); + $this->turnOffFullPageCache(); + } else { + $this->restoreFullPageCacheState(); + } + } + + /** + * Turns off Full Page Cache. + * + * @return void + */ + private function turnOffFullPageCache() + { + if (!$this->isFullPageCacheEnabled()) { + return; + } + + $this->cacheManager->clean([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], false); + } + + /** + * Full Page Cache state. + * + * @return bool + */ + private function isFullPageCacheEnabled(): bool + { + $cacheStatus = $this->cacheManager->getStatus(); + + if (!array_key_exists(PageCacheType::TYPE_IDENTIFIER, $cacheStatus)) { + return false; + } + + return (bool)$cacheStatus[PageCacheType::TYPE_IDENTIFIER]; + } + + /** + * Restores Full Page Cache state. + * + * Returns FPC to previous state that was before maintenance mode turning on. + * + * @return void + */ + private function restoreFullPageCacheState() + { + $storedPageCacheState = $this->pageCacheStateStorage->isEnabled(); + $this->pageCacheStateStorage->flush(); + + if ($storedPageCacheState) { + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], true); + } + } +} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php new file mode 100644 index 0000000000000..4180885fcbc54 --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -0,0 +1,74 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; + +/** + * Page Cache state. + */ +class PageCacheState +{ + /** + * Full Page Cache Off state file name. + */ + const PAGE_CACHE_STATE_FILENAME = '.maintenance.fpc.state'; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $flagDir; + + /** + * @param Filesystem $fileSystem + */ + public function __construct(Filesystem $fileSystem) + { + $this->flagDir = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + } + + /** + * Saves Full Page Cache state. + * + * Saves FPC state across requests. + * + * @param bool $state + * @return void + */ + public function save(bool $state) + { + $this->flagDir->writeFile(self::PAGE_CACHE_STATE_FILENAME, (string)$state); + } + + /** + * Returns stored Full Page Cache state. + * + * @return bool + */ + public function isEnabled(): bool + { + if (!$this->flagDir->isExist(self::PAGE_CACHE_STATE_FILENAME)) { + return false; + } + + return (bool)$this->flagDir->readFile(self::PAGE_CACHE_STATE_FILENAME); + } + + /** + * Flushes Page Cache state storage. + * + * @return void + */ + public function flush() + { + $this->flagDir->delete(self::PAGE_CACHE_STATE_FILENAME); + } +} diff --git a/app/code/Magento/PageCache/Test/Mftf/ActionGroup/ClearCacheActionGroup.xml b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/ClearCacheActionGroup.xml new file mode 100644 index 0000000000000..966ce16c02f4b --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/ClearCacheActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ClearCacheActionGroup"> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/admin/cache/" stepKey="goToNewCustomVarialePage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminCacheManagementSection.FlushMagentoCache}}" stepKey="clickFlushMagentoCache" /> + <waitForPageLoad stepKey="waitForCacheFlush"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/PageCache/Test/Mftf/LICENSE.txt b/app/code/Magento/PageCache/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/PageCache/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 <insert your license name here>" 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/app/code/Magento/PageCache/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/PageCache/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/PageCache/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 <insert your license name here>" 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/app/code/Magento/PageCache/Test/Mftf/Page/AdminCacheManagementPage.xml b/app/code/Magento/PageCache/Test/Mftf/Page/AdminCacheManagementPage.xml new file mode 100644 index 0000000000000..24fc5ffb04cb7 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Page/AdminCacheManagementPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCacheManagementPage" url="/admin/cache" area="admin" module="PageCache"> + <section name="AdminCacheManagementSection"/> + </page> +</pages> diff --git a/app/code/Magento/PageCache/Test/Mftf/README.md b/app/code/Magento/PageCache/Test/Mftf/README.md new file mode 100644 index 0000000000000..01c6ae4e4f483 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Page Cache Functional Tests + +The Functional Test Module for **Magento Page Cache** module. diff --git a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml new file mode 100644 index 0000000000000..bf0326f68bc97 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCacheManagementSection"> + <element name="FlushMagentoCache" type="button" selector="#flush_magento"/> + <element name="configurationCheckbox" type="checkbox" selector="input[value='config']"/> + <element name="pageCacheCheckbox" type="checkbox" selector="input[value='full_page']"/> + <element name="massActionSelect" type="select" selector="#cache_grid_massaction-form #cache_grid_massaction-select"/> + <element name="massActionSubmit" type="button" selector="#cache_grid_massaction-form button"/> + </section> +</sections> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml new file mode 100644 index 0000000000000..bbb80f9185bd0 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFrontendAreaSessionMustNotAffectAdminAreaTest"> + <annotations> + <stories value="Backend"/> + <features value="Session cookies"/> + <title value="Frontend area session must not affect admin area"/> + <description value="Frontend area session must not affect admin area"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-8838"/> + <group value="backend"/> + <group value="pagecache"/> + <group value="cookie"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="_defaultCategory" stepKey="createCategoryA"/> + <createData entity="SubCategoryWithParent" stepKey="createCategoryB"> + <requiredEntity createDataKey="createCategoryA"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="createCategoryC"> + <requiredEntity createDataKey="createCategoryB"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategoryC"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategoryA"/> + </createData> + + <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearCache"/> + <actionGroup ref="logout" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + + <deleteData createDataKey="createCategoryC" stepKey="deleteCategoryC"/> + <deleteData createDataKey="createCategoryB" stepKey="deleteCategoryB"/> + <deleteData createDataKey="createCategoryA" stepKey="deleteCategoryA"/> + + <actionGroup ref="logout" stepKey="logoutAdmin"/> + </after> + + <!-- 1. Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- 2. Navigate Go to "Catalog"->"Products" --> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="openAdminCatalogPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- 3. Open separate tab with Storefront --> + <openNewTab stepKey="openNewTab"/> + + <!-- 4. Navigate to Men -> "Tops" -> "Jackets" --> + <amOnPage + url="{{StorefrontCategoryPage.url($$createCategoryA.custom_attributes[url_key]$$/$$createCategoryB.custom_attributes[url_key]$$/$$createCategoryC.custom_attributes[url_key]$$)}}" + stepKey="openCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForCategoryPage"/> + + <!-- 5. Open admin tab with page with products. Reload this page twice. --> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <reloadPage stepKey="reloadAdminCatalogPageFirst"/> + <waitForPageLoad stepKey="waitForReloadFirst"/> + <reloadPage stepKey="reloadAdminCatalogPageSecond"/> + <waitForPageLoad stepKey="waitForReloadSecond"/> + + <seeInTitle userInput="Products / Inventory / Catalog / Magento Admin" stepKey="seeAdminProductsPageTitle"/> + <see userInput="Products" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeAdminProductsPageHeader"/> + + <switchToNextTab stepKey="switchToFrontendTab"/> + <closeTab stepKey="closeFrontendTab"/> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Test/Unit/Block/JavascriptTest.php b/app/code/Magento/PageCache/Test/Unit/Block/JavascriptTest.php index a1c66c67b5524..42a9086daa442 100644 --- a/app/code/Magento/PageCache/Test/Unit/Block/JavascriptTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Block/JavascriptTest.php @@ -130,6 +130,9 @@ public function testGetScriptOptions($isSecure, $url, $expectedResult) $this->assertRegExp($expectedResult, $this->blockJavascript->getScriptOptions()); } + /** + * @return array + */ public function getScriptOptionsDataProvider() { return [ @@ -193,6 +196,9 @@ public function testGetScriptOptionsPrivateContent($url, $route, $controller, $a $this->assertRegExp($expectedResult, $this->blockJavascript->getScriptOptions()); } + /** + * @return array + */ public function getScriptOptionsPrivateContentDataProvider() { // @codingStandardsIgnoreStart diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php index 4a7628c7ad839..3167f5e071a06 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Controller\Block; /** @@ -130,6 +127,9 @@ public function testExecute($blockClass, $shouldSetHeaders) $this->action->execute(); } + /** + * @return array + */ public function executeDataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php index 2b55a93473180..5f204c02b86d6 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Controller\Block; /** @@ -111,14 +108,20 @@ public function testExecuteNoParams() public function testExecute() { $blocks = ['block1', 'block2']; - $handles = ['handle1', 'handle2']; + $handles = ['handle1', 'handle2', "'handle'", '@hanle', '"hanle', '*hanle', '.hanle']; $originalRequest = '{"route":"route","controller":"controller","action":"action","uri":"uri"}'; $expectedData = ['block1' => 'data1', 'block2' => 'data2']; - $blockInstance1 = $this->createPartialMock(\Magento\PageCache\Test\Unit\Block\Controller\StubBlock::class, ['toHtml']); + $blockInstance1 = $this->createPartialMock( + \Magento\PageCache\Test\Unit\Block\Controller\StubBlock::class, + ['toHtml'] + ); $blockInstance1->expects($this->once())->method('toHtml')->will($this->returnValue($expectedData['block1'])); - $blockInstance2 = $this->createPartialMock(\Magento\PageCache\Test\Unit\Block\Controller\StubBlock::class, ['toHtml']); + $blockInstance2 = $this->createPartialMock( + \Magento\PageCache\Test\Unit\Block\Controller\StubBlock::class, + ['toHtml'] + ); $blockInstance2->expects($this->once())->method('toHtml')->will($this->returnValue($expectedData['block2'])); $this->requestMock->expects($this->once())->method('isAjax')->will($this->returnValue(true)); @@ -148,7 +151,7 @@ public function testExecute() ->method('getParam') ->with($this->equalTo('handles'), $this->equalTo('')) ->will($this->returnValue(base64_encode(json_encode($handles)))); - $this->viewMock->expects($this->once())->method('loadLayout')->with($this->equalTo($handles)); + $this->viewMock->expects($this->once())->method('loadLayout')->with($this->equalTo(['handle1', 'handle2'])); $this->viewMock->expects($this->any())->method('getLayout')->will($this->returnValue($this->layoutMock)); $this->layoutMock->expects($this->at(0)) ->method('getBlock') diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php index 2811cb5316dc6..db0edfa6bd779 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php @@ -226,6 +226,9 @@ public function testAroundDispatchDisabled($state) ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/PageCachePluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/PageCachePluginTest.php index 033eb2501865b..2e69fdaf47910 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/App/PageCachePluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/PageCachePluginTest.php @@ -57,6 +57,9 @@ public function testAfterSaveDecompression($data, $initResult) $this->assertSame($data, $this->plugin->afterLoad($this->subjectMock, $initResult)); } + /** + * @return array + */ public function afterSaveDataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/Response/HttpPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/Response/HttpPluginTest.php index c9231f118fc75..59591c8ee957f 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/App/Response/HttpPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/Response/HttpPluginTest.php @@ -26,6 +26,9 @@ public function testBeforeSendResponse($responseInstanceClass, $sendVaryCalled) $plugin->beforeSendResponse($responseMock); } + /** + * @return array + */ public function beforeSendResponseDataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Cache/ServerTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Cache/ServerTest.php index 2321a951aafe8..a57effe1f31ad 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Cache/ServerTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Cache/ServerTest.php @@ -90,6 +90,9 @@ public function testGetUris( $this->assertEquals($uris, $this->model->getUris()); } + /** + * @return array + */ public function getUrisDataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Layout/DepersonalizePluginTest.php index e08bb005da53f..c9638a3d5b8f6 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Layout/DepersonalizePluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Layout/DepersonalizePluginTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Model\Layout; /** @@ -45,7 +43,10 @@ protected function setUp() { $this->layoutMock = $this->createMock(\Magento\Framework\View\Layout::class); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\Manager::class); - $this->messageSessionMock = $this->createPartialMock(\Magento\Framework\Message\Session::class, ['clearStorage']); + $this->messageSessionMock = $this->createPartialMock( + \Magento\Framework\Message\Session::class, + ['clearStorage'] + ); $this->depersonalizeCheckerMock = $this->createMock(\Magento\PageCache\Model\DepersonalizeChecker::class); $this->plugin = new \Magento\PageCache\Model\Layout\DepersonalizePlugin( $this->depersonalizeCheckerMock, diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php index 8c04db7cb8390..6c39fe1e7979c 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php @@ -70,6 +70,9 @@ public function testAfterGenerateXml($cacheState, $layoutIsCacheable) $this->assertSame($result, $output); } + /** + * @return array + */ public function afterGenerateXmlDataProvider() { return [ @@ -112,6 +115,9 @@ public function testAfterGetOutput($cacheState, $layoutIsCacheable, $expectedTag $this->assertSame($output, $html); } + /** + * @return array + */ public function afterGetOutputDataProvider() { $tags = 'identity1,identity2'; diff --git a/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/AccessListTest.php b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/AccessListTest.php new file mode 100644 index 0000000000000..1c4f279917c68 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/AccessListTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\PageCache\Test\Unit\Model\System\Config\Backend; + +use Magento\PageCache\Model\System\Config\Backend\AccessList; +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; + +class AccessListTest extends TestCase +{ + /** + * @var AccessList + */ + private $accessList; + + /** + * @inheritDoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $configMock = $this->getMockForAbstractClass( + ScopeConfigInterface::class + ); + $configMock->expects($this->any()) + ->method('getValue') + ->with('system/full_page_cache/default') + ->willReturn(['access_list' => 'localhost']); + $this->accessList = $objectManager->getObject( + AccessList::class, + [ + 'config' => $configMock, + 'data' => ['field' => 'access_list'] + ] + ); + } + + /** + * @return array + */ + public function getValidValues(): array + { + return [ + ['localhost', 'localhost'], + [null, 'localhost'], + ['127.0.0.1', '127.0.0.1'], + ['127.0.0.1, localhost, ::2', '127.0.0.1, localhost, ::2'], + ]; + } + + /** + * @param mixed $value + * @param mixed $expectedValue + * @dataProvider getValidValues + */ + public function testBeforeSave($value, $expectedValue) + { + $this->accessList->setValue($value); + $this->accessList->beforeSave(); + $this->assertEquals($expectedValue, $this->accessList->getValue()); + } + + /** + * @return array + */ + public function getInvalidValues(): array + { + return [ + ['\\bull val\\'], + ['{*I am not an IP*}'], + ['{*I am not an IP*}, 127.0.0.1'], + ]; + } + + /** + * @param mixed $value + * @expectedException \Magento\Framework\Exception\LocalizedException + * @dataProvider getInvalidValues + */ + public function testBeforeSaveInvalid($value) + { + $this->accessList->setValue($value); + $this->accessList->beforeSave(); + } +} diff --git a/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/TtlTest.php b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/TtlTest.php new file mode 100644 index 0000000000000..6fd3307de726c --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/TtlTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Model\System\Config\Backend; + +use Magento\PageCache\Model\System\Config\Backend\Ttl; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; +use PHPUnit\Framework\TestCase; + +/** + * Class for tesing backend model for processing Public content cache lifetime settings. + */ +class TtlTest extends TestCase +{ + /** + * @var Ttl + */ + private $ttl; + + /* + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritDoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $configMock->expects($this->any()) + ->method('getValue') + ->with('system/full_page_cache/default') + ->willReturn(['ttl' => 86400]); + + $this->escaperMock = $this->getMockBuilder(Escaper::class)->disableOriginalConstructor()->getMock(); + + $this->ttl = $objectManager->getObject( + Ttl::class, + [ + 'config' => $configMock, + 'data' => ['field' => 'ttl'], + 'escaper' => $this->escaperMock, + ] + ); + } + + /** + * @return array + */ + public function getValidValues(): array + { + return [ + ['3600', '3600'], + ['10000', '10000'], + ['100000', '100000'], + ['1000000', '1000000'], + ]; + } + + /** + * @param string $value + * @param string $expectedValue + * @return void + * @dataProvider getValidValues + */ + public function testBeforeSave(string $value, string $expectedValue) + { + $this->ttl->setValue($value); + $this->ttl->beforeSave(); + $this->assertEquals($expectedValue, $this->ttl->getValue()); + } + + /** + * @return array + */ + public function getInvalidValues(): array + { + return [ + ['<script>alert(1)</script>'], + ['apple'], + ['123 street'], + ['-123'], + ]; + } + + /** + * @param string $value + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessageRegExp /Ttl value ".+" is not valid. Please .+ only numbers equal or greater than zero./ + * @dataProvider getInvalidValues + */ + public function testBeforeSaveInvalid(string $value) + { + $this->ttl->setValue($value); + $this->escaperMock->expects($this->any())->method('escapeHtml')->with($value)->willReturn($value); + $this->ttl->beforeSave(); + } +} diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/FlushAllCacheTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/FlushAllCacheTest.php index fd5c57500687f..65727682f0a00 100644 --- a/app/code/Magento/PageCache/Test/Unit/Observer/FlushAllCacheTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Observer/FlushAllCacheTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Observer; class FlushAllCacheTest extends \PHPUnit\Framework\TestCase @@ -52,13 +49,9 @@ protected function setUp() */ public function testExecute() { - $this->_configMock->expects( - $this->once() - )->method( - 'getType' - )->will( - $this->returnValue(\Magento\PageCache\Model\Config::BUILT_IN) - ); + $this->_configMock->expects($this->once()) + ->method('getType') + ->willReturn(\Magento\PageCache\Model\Config::BUILT_IN); $this->fullPageCacheMock->expects($this->once())->method('clean'); $this->_model->execute($this->observerMock); diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/FlushCacheByTagsTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/FlushCacheByTagsTest.php index af5d0d0f95e8f..b2bffd27b9a98 100644 --- a/app/code/Magento/PageCache/Test/Unit/Observer/FlushCacheByTagsTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Observer/FlushCacheByTagsTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Observer; class FlushCacheByTagsTest extends \PHPUnit\Framework\TestCase @@ -83,6 +80,9 @@ public function testExecute($cacheState) $this->assertNull($result); } + /** + * @return array + */ public function flushCacheByTagsDataProvider() { return [ diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/ProcessLayoutRenderElementTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/ProcessLayoutRenderElementTest.php index 2e6ca26295cbd..e1894db2375b1 100644 --- a/app/code/Magento/PageCache/Test/Unit/Observer/ProcessLayoutRenderElementTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Observer/ProcessLayoutRenderElementTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\PageCache\Test\Unit\Observer; use Magento\Framework\View\EntitySpecificHandlesList; @@ -49,7 +46,10 @@ protected function setUp() new \Magento\Framework\Serialize\Serializer\Base64Json() ); $this->_observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']); - $this->_layoutMock = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['isCacheable', 'getBlock', 'getUpdate', 'getHandles']); + $this->_layoutMock = $this->createPartialMock( + \Magento\Framework\View\Layout::class, + ['isCacheable', 'getBlock', 'getUpdate', 'getHandles'] + ); $this->_blockMock = $this->getMockForAbstractClass( \Magento\Framework\View\Element\AbstractBlock::class, [], @@ -77,7 +77,10 @@ public function testExecute( $blockTtl, $expectedOutput ) { - $eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getLayout', 'getElementName', 'getTransport']); + $eventMock = $this->createPartialMock( + \Magento\Framework\Event::class, + ['getLayout', 'getElementName', 'getTransport'] + ); $this->_observerMock->expects($this->once())->method('getEvent')->will($this->returnValue($eventMock)); $eventMock->expects($this->once())->method('getLayout')->will($this->returnValue($this->_layoutMock)); $this->_configMock->expects($this->any())->method('isEnabled')->will($this->returnValue($cacheState)); @@ -118,8 +121,16 @@ public function testExecute( ->will($this->returnValue($blockTtl)); $this->_blockMock->expects($this->any()) ->method('getUrl') - ->with('page_cache/block/esi', ['blocks' => '[null]', 'handles' => 'WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==']) - ->will($this->returnValue('page_cache/block/wrapesi/with/handles/WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==')); + ->with( + 'page_cache/block/esi', + [ + 'blocks' => '[null]', + 'handles' => 'WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==', + ] + ) + ->willReturn( + 'page_cache/block/wrapesi/with/handles/WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==' + ); } if ($scopeIsPrivate) { $this->_blockMock->expects($this->once()) @@ -139,7 +150,10 @@ public function testExecute( public function testExecuteWithBase64Encode() { $expectedOutput = '<esi:include src="page_cache/block/wrapesi/with/handles/YW5kL290aGVyL3N0dWZm" />'; - $eventMock = $this->createPartialMock('Magento\Framework\Event', ['getLayout', 'getElementName', 'getTransport']); + $eventMock = $this->createPartialMock( + \Magento\Framework\Event::class, + ['getLayout', 'getElementName', 'getTransport'] + ); $expectedUrl = 'page_cache/block/wrapesi/with/handles/' . base64_encode('and/other/stuff'); $this->_observerMock->expects($this->once())->method('getEvent')->will($this->returnValue($eventMock)); @@ -206,7 +220,8 @@ public function processLayoutRenderDataProvider() true, false, 360, - '<esi:include src="page_cache/block/wrapesi/with/handles/WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==" />', + '<esi:include src="page_cache/block/wrapesi/with/handles/' + . 'WyJkZWZhdWx0IiwiY2F0YWxvZ19wcm9kdWN0X3ZpZXciXQ==" />', ], 'full_page type and Varnish enabled, public scope, ttl is not set' => [ true, diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php new file mode 100644 index 0000000000000..8c4661cddd44c --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php @@ -0,0 +1,161 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Observer; + +use PHPUnit\Framework\TestCase; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Cache\Manager; +use Magento\Framework\Event\Observer; +use Magento\PageCache\Model\Cache\Type as PageCacheType; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; + +/** + * SwitchPageCacheOnMaintenance observer test. + */ +class SwitchPageCacheOnMaintenanceTest extends TestCase +{ + /** + * @var SwitchPageCacheOnMaintenance + */ + private $model; + + /** + * @var Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheManager; + + /** + * @var PageCacheState|\PHPUnit_Framework_MockObject_MockObject + */ + private $pageCacheStateStorage; + + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->cacheManager = $this->createMock(Manager::class); + $this->pageCacheStateStorage = $this->createMock(PageCacheState::class); + $this->observer = $this->createMock(Observer::class); + + $this->model = $objectManager->getObject(SwitchPageCacheOnMaintenance::class, [ + 'cacheManager' => $this->cacheManager, + 'pageCacheStateStorage' => $this->pageCacheStateStorage, + ]); + } + + /** + * Tests execute when setting maintenance mode to on. + * + * @param array $cacheStatus + * @param bool $cacheState + * @param int $flushCacheCalls + * @return void + * @dataProvider enablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceEnabling(array $cacheStatus, bool $cacheState, int $flushCacheCalls) + { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(true); + $this->cacheManager->method('getStatus') + ->willReturn($cacheStatus); + + // Page Cache state will be stored. + $this->pageCacheStateStorage->expects($this->once()) + ->method('save') + ->with($cacheState); + + // Page Cache will be cleaned and disabled + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('clean') + ->with([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER], false); + + $this->model->execute($this->observer); + } + + /** + * Tests execute when setting Maintenance Mode to off. + * + * @param bool $storedCacheState + * @param int $enableCacheCalls + * @return void + * @dataProvider disablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceDisabling(bool $storedCacheState, int $enableCacheCalls) + { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(false); + + $this->pageCacheStateStorage->method('isEnabled') + ->willReturn($storedCacheState); + + // Nullify Page Cache state. + $this->pageCacheStateStorage->expects($this->once()) + ->method('flush'); + + // Page Cache will be enabled. + $this->cacheManager->expects($this->exactly($enableCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER]); + + $this->model->execute($this->observer); + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function enablingPageCacheStateProvider(): array + { + return [ + 'page_cache_is_enable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 1], + 'cache_state' => true, + 'flush_cache_calls' => 1, + ], + 'page_cache_is_missing_in_system' => [ + 'cache_status' => [], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + 'page_cache_is_disable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 0], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + ]; + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function disablingPageCacheStateProvider(): array + { + return [ + ['stored_cache_state' => true, 'enable_cache_calls' => 1], + ['stored_cache_state' => false, 'enable_cache_calls' => 0], + ]; + } +} diff --git a/app/code/Magento/PageCache/composer.json b/app/code/Magento/PageCache/composer.json index cdbd8327b9cdd..ee1f3acdf7467 100644 --- a/app/code/Magento/PageCache/composer.json +++ b/app/code/Magento/PageCache/composer.json @@ -2,14 +2,14 @@ "name": "magento/module-page-cache", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/PageCache/etc/adminhtml/system.xml b/app/code/Magento/PageCache/etc/adminhtml/system.xml index 1d6c0d890737b..2a4439ac6a9cf 100644 --- a/app/code/Magento/PageCache/etc/adminhtml/system.xml +++ b/app/code/Magento/PageCache/etc/adminhtml/system.xml @@ -20,7 +20,7 @@ <label>Access list</label> <comment>IPs access list separated with ',' that can purge Varnish configuration for config file generation. If field is empty default value localhost will be saved.</comment> - <backend_model>Magento\PageCache\Model\System\Config\Backend\Varnish</backend_model> + <backend_model>Magento\PageCache\Model\System\Config\Backend\AccessList</backend_model> <depends> <field id="caching_application">1</field> </depends> @@ -49,7 +49,7 @@ <field id="caching_application">1</field> </depends> </field> - <field id="export_button_version4" type="button" sortOrder="35" showInDefault="1" showInWebsite="0" showInStore="0"> + <field id="export_button_version4" translate="label" type="button" sortOrder="35" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Export Configuration</label> <frontend_model>Magento\PageCache\Block\System\Config\Form\Field\Export\Varnish4</frontend_model> <depends> diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml index 7584f5f36d69c..3f0a2532ae60a 100644 --- a/app/code/Magento/PageCache/etc/events.xml +++ b/app/code/Magento/PageCache/etc/events.xml @@ -57,4 +57,7 @@ <event name="customer_logout"> <observer name="FlushFormKey" instance="Magento\PageCache\Observer\FlushFormKey"/> </event> + <event name="maintenance_mode_changed"> + <observer name="page_cache_switcher_for_maintenance" instance="Magento\PageCache\Observer\SwitchPageCacheOnMaintenance"/> + </event> </config> diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index 793f8f81a03f9..21f48ef76502f 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -91,10 +91,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 4dce6356d1e73..23df172dd3aa4 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -92,10 +92,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/PageCache/view/frontend/templates/js/components.phtml b/app/code/Magento/PageCache/view/frontend/templates/js/components.phtml index 52a280651da78..50778ce81c010 100644 --- a/app/code/Magento/PageCache/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/PageCache/view/frontend/templates/js/components.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag /** @var \Magento\Framework\View\Element\Js\Components $block */ ?> diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index fccc8510ffc70..9ae916356d2b9 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -6,9 +6,10 @@ define([ 'jquery', 'domReady', + 'consoleLogger', 'jquery/ui', 'mage/cookies' -], function ($, domReady) { +], function ($, domReady, consoleLogger) { 'use strict'; /** @@ -41,7 +42,9 @@ define([ * @param {jQuery} element - Comment holder */ (function lookup(element) { - var iframeHostName; + var iframeHostName, + contents, + elementContents; // prevent cross origin iframe content reading if ($(element).prop('tagName') === 'IFRAME') { @@ -53,7 +56,30 @@ define([ } } - $(element).contents().each(function (index, el) { + /** + * Rewrite jQuery contents method + * + * @param {Object} el + * @returns {Object} + * @private + */ + contents = function (el) { + return $.map(el, function (elem) { + try { + return $.nodeName(elem, 'iframe') ? + elem.contentDocument || (elem.contentWindow ? elem.contentWindow.document : []) : + $.merge([], elem.childNodes); + } catch (e) { + consoleLogger.error(e); + + return []; + } + }); + }; + + elementContents = contents($(element)); + + $.each(elementContents, function (index, el) { switch (el.nodeType) { case 1: // ELEMENT_NODE lookup(el); diff --git a/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php new file mode 100644 index 0000000000000..c658baece7779 --- /dev/null +++ b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Payment\Api\Data; + +use Magento\Framework\DataObject\KeyValueObjectInterface; + +/** + * Payment additional info interface. + */ +interface PaymentAdditionalInfoInterface extends KeyValueObjectInterface +{ +} diff --git a/app/code/Magento/Payment/Block/Info/Instructions.php b/app/code/Magento/Payment/Block/Info/Instructions.php index e3c74e020f8f6..687c6b54a2f4f 100644 --- a/app/code/Magento/Payment/Block/Info/Instructions.php +++ b/app/code/Magento/Payment/Block/Info/Instructions.php @@ -23,7 +23,7 @@ class Instructions extends \Magento\Payment\Block\Info /** * @var string */ - protected $_template = 'info/instructions.phtml'; + protected $_template = 'Magento_Payment::info/instructions.phtml'; /** * Get instructions text from order payment diff --git a/app/code/Magento/Payment/Gateway/Command/GatewayCommand.php b/app/code/Magento/Payment/Gateway/Command/GatewayCommand.php index a6f9d4383918c..bb07408ad0e06 100644 --- a/app/code/Magento/Payment/Gateway/Command/GatewayCommand.php +++ b/app/code/Magento/Payment/Gateway/Command/GatewayCommand.php @@ -5,14 +5,13 @@ */ namespace Magento\Payment\Gateway\Command; -use Magento\Framework\Phrase; use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\ErrorMapper\ErrorMessageMapperInterface; use Magento\Payment\Gateway\Http\ClientInterface; use Magento\Payment\Gateway\Http\TransferFactoryInterface; -use Magento\Payment\Gateway\Request; use Magento\Payment\Gateway\Request\BuilderInterface; -use Magento\Payment\Gateway\Response; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ValidatorInterface; use Psr\Log\LoggerInterface; @@ -54,6 +53,11 @@ class GatewayCommand implements CommandInterface */ private $logger; + /** + * @var ErrorMessageMapperInterface + */ + private $errorMessageMapper; + /** * @param BuilderInterface $requestBuilder * @param TransferFactoryInterface $transferFactory @@ -61,6 +65,7 @@ class GatewayCommand implements CommandInterface * @param LoggerInterface $logger * @param HandlerInterface $handler * @param ValidatorInterface $validator + * @param ErrorMessageMapperInterface|null $errorMessageMapper */ public function __construct( BuilderInterface $requestBuilder, @@ -68,7 +73,8 @@ public function __construct( ClientInterface $client, LoggerInterface $logger, HandlerInterface $handler = null, - ValidatorInterface $validator = null + ValidatorInterface $validator = null, + ErrorMessageMapperInterface $errorMessageMapper = null ) { $this->requestBuilder = $requestBuilder; $this->transferFactory = $transferFactory; @@ -76,6 +82,7 @@ public function __construct( $this->handler = $handler; $this->validator = $validator; $this->logger = $logger; + $this->errorMessageMapper = $errorMessageMapper; } /** @@ -98,10 +105,7 @@ public function execute(array $commandSubject) array_merge($commandSubject, ['response' => $response]) ); if (!$result->isValid()) { - $this->logExceptions($result->getFailsDescription()); - throw new CommandException( - __('Transaction has been declined. Please try again later.') - ); + $this->processErrors($result); } } @@ -114,13 +118,33 @@ public function execute(array $commandSubject) } /** - * @param Phrase[] $fails - * @return void + * Tries to map error messages from validation result and logs processed message. + * Throws an exception with mapped message or default error. + * + * @param ResultInterface $result + * @throws CommandException */ - private function logExceptions(array $fails) + private function processErrors(ResultInterface $result) { - foreach ($fails as $failPhrase) { - $this->logger->critical((string) $failPhrase); + $messages = []; + foreach ($result->getFailsDescription() as $failPhrase) { + $message = (string) $failPhrase; + + // error messages mapper can be not configured if payment method doesn't have custom error messages. + if ($this->errorMessageMapper !== null) { + $mapped = (string) $this->errorMessageMapper->getMessage($message); + if (!empty($mapped)) { + $messages[] = $mapped; + $message = $mapped; + } + } + $this->logger->critical('Payment Error: ' . $message); } + + throw new CommandException( + !empty($messages) + ? __(implode(PHP_EOL, $messages)) + : __('Transaction has been declined. Please try again later.') + ); } } diff --git a/app/code/Magento/Payment/Gateway/Command/Result/BoolResult.php b/app/code/Magento/Payment/Gateway/Command/Result/BoolResult.php index 38b02c19cc094..3f9ebf95f94c4 100644 --- a/app/code/Magento/Payment/Gateway/Command/Result/BoolResult.php +++ b/app/code/Magento/Payment/Gateway/Command/Result/BoolResult.php @@ -37,6 +37,6 @@ public function __construct($result = true) */ public function get() { - return (bool) $this->result; + return (bool)$this->result; } } diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php new file mode 100644 index 0000000000000..c5759d41bf4d7 --- /dev/null +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Gateway\ErrorMapper; + +use Magento\Framework\Config\DataInterface; + +/** + * This class can be used for payment integrations which can validate different type of + * error messages per one request. + * For example, during authorization payment operation the payment integration can validate error messages + * related to credit card details and customer address data. + * In that case, this implementation can be extended via di.xml and configured with appropriate mappers. + */ +class ErrorMessageMapper implements ErrorMessageMapperInterface +{ + /** + * @var DataInterface + */ + private $messageMapping; + + /** + * @param DataInterface $messageMapping + */ + public function __construct(DataInterface $messageMapping) + { + $this->messageMapping = $messageMapping; + } + + /** + * @inheritdoc + */ + public function getMessage(string $code) + { + $message = $this->messageMapping->get($code); + return $message ? __($message) : null; + } +} diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php new file mode 100644 index 0000000000000..077226fd9a062 --- /dev/null +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Gateway\ErrorMapper; + +use Magento\Framework\Phrase; + +/** + * Interface to provide customization for payment validation errors. + */ +interface ErrorMessageMapperInterface +{ + /** + * Returns customized error message by provided code. + * If message not found `null` will be returned. + * + * @param string $code + * @return Phrase|null + */ + public function getMessage(string $code); +} diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/MappingData.php b/app/code/Magento/Payment/Gateway/ErrorMapper/MappingData.php new file mode 100644 index 0000000000000..8ae29c7a729e7 --- /dev/null +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/MappingData.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Gateway\ErrorMapper; + +use Magento\Framework\Config\Data\Scoped; + +/** + * Extends Scoped class to override `_scopePriorityScheme` property. + * It allows to load and merge config files from `global` scope and current scope to a single structure. + */ +class MappingData extends Scoped +{ + /** + * @inheritdoc + */ + protected $_scopePriorityScheme = ['global']; +} diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/NullMappingData.php b/app/code/Magento/Payment/Gateway/ErrorMapper/NullMappingData.php new file mode 100644 index 0000000000000..6b3e592d984ce --- /dev/null +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/NullMappingData.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Gateway\ErrorMapper; + +use Magento\Framework\Config\DataInterface; + +/** + * Stub implementation of DataInterface which is used by default for ErrorMessageMapper, because + * each payment method should provide own mapping data source. + */ +class NullMappingData implements DataInterface +{ + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function get($path = null, $default = null) + { + return null; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function merge(array $config) + { + } +} diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/XmlToArrayConverter.php b/app/code/Magento/Payment/Gateway/ErrorMapper/XmlToArrayConverter.php new file mode 100644 index 0000000000000..590a114e22221 --- /dev/null +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/XmlToArrayConverter.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Payment\Gateway\ErrorMapper; + +use Magento\Framework\Config\ConverterInterface; + +/** + * Reads xml in `<message code="code">message</message>` format and converts it to [code => message] array format. + */ +class XmlToArrayConverter implements ConverterInterface +{ + /** + * @inheritdoc + */ + public function convert($source) + { + $result = []; + $messageList = $source->getElementsByTagName('message'); + foreach ($messageList as $messageNode) { + $result[(string) $messageNode->getAttribute('code')] = (string) $messageNode->nodeValue; + } + return $result; + } +} diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index b7f1368ddabce..e8fe23522e8ee 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -10,10 +10,9 @@ use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; /** - * Class ValidatorComposite - * @package Magento\Payment\Gateway\Validator + * Compiles a result using the results of multiple validators + * * @api - * @since 100.0.2 */ class ValidatorComposite extends AbstractValidator { @@ -22,15 +21,22 @@ class ValidatorComposite extends AbstractValidator */ private $validators; + /** + * @var array + */ + private $chainBreakingValidators; + /** * @param ResultInterfaceFactory $resultFactory * @param TMapFactory $tmapFactory * @param array $validators + * @param array $chainBreakingValidators */ public function __construct( ResultInterfaceFactory $resultFactory, TMapFactory $tmapFactory, - array $validators = [] + array $validators = [], + array $chainBreakingValidators = [] ) { $this->validators = $tmapFactory->create( [ @@ -38,6 +44,7 @@ public function __construct( 'type' => ValidatorInterface::class ] ); + $this->chainBreakingValidators = $chainBreakingValidators; parent::__construct($resultFactory); } @@ -51,7 +58,7 @@ public function validate(array $validationSubject) { $isValid = true; $failsDescriptionAggregate = []; - foreach ($this->validators as $validator) { + foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { $isValid = false; @@ -59,6 +66,9 @@ public function validate(array $validationSubject) $failsDescriptionAggregate, $result->getFailsDescription() ); + if (!empty($this->chainBreakingValidators[$key])) { + break; + } } } diff --git a/app/code/Magento/Payment/Helper/Data.php b/app/code/Magento/Payment/Helper/Data.php index e3122913d5dfa..5a3e76e8d89a1 100644 --- a/app/code/Magento/Payment/Helper/Data.php +++ b/app/code/Magento/Payment/Helper/Data.php @@ -147,7 +147,7 @@ public function getStoreMethods($store = null, $quote = null) } $res[] = $methodInstance; } - + // phpcs:ignore Generic.PHP.NoSilencedErrors @uasort( $res, function (MethodInterface $a, MethodInterface $b) { @@ -267,11 +267,12 @@ public function getPaymentMethodList($sorted = true, $asLabelValue = false, $wit $groupRelations = []; foreach ($this->getPaymentMethods() as $code => $data) { - if (isset($data['title'])) { - $methods[$code] = $data['title']; - } else { - $methods[$code] = $this->getMethodInstance($code)->getConfigData('title', $store); + $storeId = $store ? (int)$store->getId() : null; + $storedTitle = $this->getMethodStoreTitle($code, $storeId); + if (!empty($storedTitle)) { + $methods[$code] = $storedTitle; } + if ($asLabelValue && $withGroups && isset($data['group'])) { $groupRelations[$code] = $data['group']; } @@ -293,6 +294,9 @@ public function getPaymentMethodList($sorted = true, $asLabelValue = false, $wit foreach ($methods as $code => $title) { if (isset($groups[$code])) { $labelValues[$code]['label'] = $title; + if (!isset($labelValues[$code]['value'])) { + $labelValues[$code]['value'] = null; + } } elseif (isset($groupRelations[$code])) { unset($labelValues[$code]); $labelValues[$groupRelations[$code]]['value'][$code] = ['value' => $code, 'label' => $title]; @@ -350,4 +354,21 @@ public function getZeroSubTotalPaymentAutomaticInvoice($store = null) $store ); } + + /** + * Get config title of payment method + * + * @param string $code + * @param int|null $storeId + * @return string + */ + private function getMethodStoreTitle(string $code, $storeId = null): string + { + $configPath = sprintf('%s/%s/title', self::XML_PATH_PAYMENT_METHODS, $code); + return (string) $this->scopeConfig->getValue( + $configPath, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + } } diff --git a/app/code/Magento/Payment/Model/CcConfigProvider.php b/app/code/Magento/Payment/Model/CcConfigProvider.php index 15bdd0072a51a..497ce93c30c71 100644 --- a/app/code/Magento/Payment/Model/CcConfigProvider.php +++ b/app/code/Magento/Payment/Model/CcConfigProvider.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -69,7 +69,7 @@ public function getIcons() } $types = $this->ccConfig->getCcAvailableTypes(); - foreach (array_keys($types) as $code) { + foreach ($types as $code => $label) { if (!array_key_exists($code, $this->icons)) { $asset = $this->ccConfig->createAsset('Magento_Payment::images/cc/' . strtolower($code) . '.png'); $placeholder = $this->assetSource->findSource($asset); @@ -78,7 +78,8 @@ public function getIcons() $this->icons[$code] = [ 'url' => $asset->getUrl(), 'width' => $width, - 'height' => $height + 'height' => $height, + 'title' => __($label), ]; } } diff --git a/app/code/Magento/Payment/Model/Method/AbstractMethod.php b/app/code/Magento/Payment/Model/Method/AbstractMethod.php index 5378aa3bf5379..7aa3bb72ffb9b 100644 --- a/app/code/Magento/Payment/Model/Method/AbstractMethod.php +++ b/app/code/Magento/Payment/Model/Method/AbstractMethod.php @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Model\Method; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Payment\Model\InfoInterface; use Magento\Payment\Model\MethodInterface; use Magento\Payment\Observer\AbstractDataAssignObserver; use Magento\Quote\Api\Data\PaymentMethodInterface; use Magento\Sales\Model\Order\Payment; +use Magento\Directory\Helper\Data as DirectoryHelper; /** * Payment method abstract model @@ -219,6 +219,11 @@ abstract class AbstractMethod extends \Magento\Framework\Model\AbstractExtensibl */ protected $logger; + /** + * @var DirectoryHelper + */ + private $directory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -230,6 +235,7 @@ abstract class AbstractMethod extends \Magento\Framework\Model\AbstractExtensibl * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param DirectoryHelper $directory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -242,7 +248,8 @@ public function __construct( \Magento\Payment\Model\Method\Logger $logger, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + DirectoryHelper $directory = null ) { parent::__construct( $context, @@ -256,6 +263,7 @@ public function __construct( $this->_paymentData = $paymentData; $this->_scopeConfig = $scopeConfig; $this->logger = $logger; + $this->directory = $directory ?: ObjectManager::getInstance()->get(DirectoryHelper::class); $this->initializeData($data); } @@ -528,7 +536,9 @@ public function canUseForCurrency($currencyCode) public function getCode() { if (empty($this->_code)) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot retrieve the payment method code.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot retrieve the payment method code.') + ); } return $this->_code; } @@ -568,7 +578,9 @@ public function getInfoInstance() { $instance = $this->getData('info_instance'); if (!$instance instanceof InfoInterface) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot retrieve the payment information object instance.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot retrieve the payment information object instance.') + ); } return $instance; } @@ -605,6 +617,7 @@ public function validate() } else { $billingCountry = $paymentInfo->getQuote()->getBillingAddress()->getCountryId(); } + $billingCountry = $billingCountry ?: $this->directory->getDefaultCountry(); if (!$this->canUseForCountry($billingCountry)) { throw new \Magento\Framework\Exception\LocalizedException( __('You can\'t use the payment type you selected to make payments to the billing country.') diff --git a/app/code/Magento/Payment/Model/Method/Logger.php b/app/code/Magento/Payment/Model/Method/Logger.php index 74068c3b6fef0..90a2a94f92fc2 100644 --- a/app/code/Magento/Payment/Model/Method/Logger.php +++ b/app/code/Magento/Payment/Model/Method/Logger.php @@ -8,9 +8,7 @@ use Psr\Log\LoggerInterface; /** - * Class Logger for payment related information (request, response, etc.) which is used for debug - * - * @author Magento Core Team <core@magentocommerce.com> + * Class Logger for payment related information (request, response, etc.) which is used for debug. * * @api * @since 100.0.2 @@ -69,7 +67,7 @@ public function debug(array $data, array $maskKeys = null, $forceDebug = null) */ private function getDebugReplaceFields() { - if ($this->config and $this->config->getValue('debugReplaceKeys')) { + if ($this->config && $this->config->getValue('debugReplaceKeys')) { return explode(',', $this->config->getValue('debugReplaceKeys')); } return []; @@ -82,7 +80,7 @@ private function getDebugReplaceFields() */ private function isDebugOn() { - return $this->config and (bool)$this->config->getValue('debug'); + return $this->config && (bool)$this->config->getValue('debug'); } /** diff --git a/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php new file mode 100644 index 0000000000000..4ce41181a008a --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Model; + +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; + +/** + * Payment additional info class. + */ +class PaymentAdditionalInfo implements PaymentAdditionalInfoInterface +{ + /** + * @var string + */ + private $key; + + /** + * @var string + */ + private $value; + + /** + * @inheritdoc + */ + public function getKey() + { + return $this->key; + } + + /** + * @inheritdoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritdoc + */ + public function setKey($key) + { + $this->key = $key; + return $this; + } + + /** + * @inheritdoc + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } +} diff --git a/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php b/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php index ed8185e6dedeb..3d520db833164 100644 --- a/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php +++ b/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php @@ -15,12 +15,19 @@ class SalesOrderBeforeSaveObserver implements ObserverInterface * * @param \Magento\Framework\Event\Observer $observer * @return $this + * @throws \Magento\Framework\Exception\LocalizedException in case order has no payment specified. */ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var \Magento\Sales\Model\Order $order */ $order = $observer->getEvent()->getOrder(); + if (!$order->getPayment()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Please provide payment for the order.') + ); + } + if ($order->getPayment()->getMethodInstance()->getCode() != 'free') { return $this; } diff --git a/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml b/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml new file mode 100644 index 0000000000000..14c8bd0fecde7 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/Data/PaymentMethodData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="PaymentMethodCheckMoneyOrder" type="payment_method"> + <data key="method">checkmo</data> + </entity> +</entities> diff --git a/app/code/Magento/Payment/Test/Mftf/Data/ZeroSubtotalCheckoutPaymentMethodData.xml b/app/code/Magento/Payment/Test/Mftf/Data/ZeroSubtotalCheckoutPaymentMethodData.xml new file mode 100644 index 0000000000000..0a9d6b66af6f9 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/Data/ZeroSubtotalCheckoutPaymentMethodData.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ZeroSubtotalCheckoutPaymentMethodConfig" type="zero_subtotal_checkout_payment_method"> + <requiredEntity type="active">Active</requiredEntity> + <requiredEntity type="order_status">OrderStatusProcessing</requiredEntity> + </entity> + + <entity name="Active" type="active"> + <data key="value">1</data> + </entity> + <entity name="OrderStatusProcessing" type="order_status"> + <data key="value">processing</data> + </entity> + <entity name="OrderStatusPending" type="order_status"> + <data key="value">pending</data> + </entity> + + <entity name="ZeroSubtotalCheckoutPaymentMethodDefault" type="zero_subtotal_checkout_payment_method"> + <requiredEntity type="active">Active</requiredEntity> + <requiredEntity type="order_status">OrderStatusPending</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Payment/Test/Mftf/LICENSE.txt b/app/code/Magento/Payment/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Payment/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 <insert your license name here>" 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/app/code/Magento/Payment/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Payment/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Payment/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 <insert your license name here>" 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/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml b/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml new file mode 100644 index 0000000000000..b1e3577b9ccad --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/Metadata/payment_method-meta.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ZeroSubtotalCheckoutPaymentMethodSetup" dataType="zero_subtotal_checkout_payment_method" + type="create" auth="adminFormKey" url="/admin/system_config/save/section/payment/" method="POST" + successRegex="/messages-message-success/"> + <object key="groups" dataType="zero_subtotal_checkout_payment_method"> + <object key="free" dataType="zero_subtotal_checkout_payment_method"> + <object key="fields" dataType="zero_subtotal_checkout_payment_method"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + <object key="order_status" dataType="order_status"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Payment/Test/Mftf/Page/AdminConfigPaymentMethodsPage.xml b/app/code/Magento/Payment/Test/Mftf/Page/AdminConfigPaymentMethodsPage.xml new file mode 100644 index 0000000000000..80de184a4a457 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/Page/AdminConfigPaymentMethodsPage.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminConfigPaymentMethodsPage" url="admin/system_config/edit/section/payment/" area="admin" module="Magento_Payment"> + </page> +</pages> diff --git a/app/code/Magento/Payment/Test/Mftf/README.md b/app/code/Magento/Payment/Test/Mftf/README.md new file mode 100644 index 0000000000000..fc489cbb253a0 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Payment Functional Tests + +The Functional Test Module for **Magento Payment** module. diff --git a/app/code/Magento/Payment/Test/Unit/Block/FormTest.php b/app/code/Magento/Payment/Test/Unit/Block/FormTest.php index c1414c32daa38..0552f6da1fadd 100644 --- a/app/code/Magento/Payment/Test/Unit/Block/FormTest.php +++ b/app/code/Magento/Payment/Test/Unit/Block/FormTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Block; use Magento\Framework\DataObject; @@ -35,23 +33,21 @@ class FormTest extends \PHPUnit\Framework\TestCase protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_storeManager = $this->getMockBuilder( - \Magento\Store\Model\StoreManager::class - )->setMethods( - ['getStore'] - )->disableOriginalConstructor()->getMock(); - $this->_eventManager = $this->getMockBuilder( - \Magento\Framework\Event\ManagerInterface::class - )->setMethods( - ['dispatch'] - )->disableOriginalConstructor()->getMock(); + $this->_storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) + ->setMethods(['getStore']) + ->disableOriginalConstructor() + ->getMock(); + $this->_eventManager = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->setMethods(['dispatch']) + ->disableOriginalConstructor() + ->getMock(); $this->_escaper = $helper->getObject(\Magento\Framework\Escaper::class); $context = $helper->getObject( \Magento\Framework\View\Element\Template\Context::class, [ 'storeManager' => $this->_storeManager, 'eventManager' => $this->_eventManager, - 'escaper' => $this->_escaper + 'escaper' => $this->_escaper, ] ); $this->_object = $helper->getObject(\Magento\Payment\Block\Form::class, ['context' => $context]); diff --git a/app/code/Magento/Payment/Test/Unit/Block/Info/SubstitutionTest.php b/app/code/Magento/Payment/Test/Unit/Block/Info/SubstitutionTest.php index 729da9eb8196d..d20f7b0289616 100644 --- a/app/code/Magento/Payment/Test/Unit/Block/Info/SubstitutionTest.php +++ b/app/code/Magento/Payment/Test/Unit/Block/Info/SubstitutionTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Block\Info; /** @@ -136,29 +134,22 @@ public function testBeforeToHtml() $infoMock->expects($this->once())->method('getMethodInstance')->will($this->returnValue($methodMock)); $this->block->setInfo($infoMock); - $fakeBlock = new \StdClass(); - $this->layout->expects( - $this->any() - )->method( - 'createBlock' - )->with( - \Magento\Framework\View\Element\Template::class, - '', - ['data' => ['method' => $methodMock, 'template' => 'Magento_Payment::info/substitution.phtml']] - )->will( - $this->returnValue( - $fakeBlock - ) - ); + $fakeBlock = new \stdClass(); + $this->layout->expects($this->any()) + ->method('createBlock') + ->with( + \Magento\Framework\View\Element\Template::class, + '', + ['data' => ['method' => $methodMock, 'template' => 'Magento_Payment::info/substitution.phtml']] + ) + ->willReturn($fakeBlock); - $childAbstractBlock->expects( - $this->any() - )->method( - 'setChild' - )->with( - 'order_payment_additional', - $fakeBlock - ); + $childAbstractBlock->expects($this->any()) + ->method('setChild') + ->with( + 'order_payment_additional', + $fakeBlock + ); $this->block->toHtml(); } diff --git a/app/code/Magento/Payment/Test/Unit/Block/InfoTest.php b/app/code/Magento/Payment/Test/Unit/Block/InfoTest.php index 469d7971e21a9..5f9238ca4360a 100644 --- a/app/code/Magento/Payment/Test/Unit/Block/InfoTest.php +++ b/app/code/Magento/Payment/Test/Unit/Block/InfoTest.php @@ -83,6 +83,9 @@ public function testGetIsSecureMode($isSecureMode, $methodInstance, $store, $sto $this->assertEquals($result, $expectedResult); } + /** + * @return array + */ public function getIsSecureModeDataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Command/GatewayCommandTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Command/GatewayCommandTest.php index df8bdc9bca54b..d17a7f302f31b 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Command/GatewayCommandTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Command/GatewayCommandTest.php @@ -6,11 +6,15 @@ namespace Magento\Payment\Test\Unit\Gateway\Command; use Magento\Payment\Gateway\Command\GatewayCommand; +use Magento\Payment\Gateway\ErrorMapper\ErrorMessageMapperInterface; use Magento\Payment\Gateway\Http\ClientInterface; use Magento\Payment\Gateway\Http\TransferFactoryInterface; +use Magento\Payment\Gateway\Http\TransferInterface; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ValidatorInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; /** @@ -18,175 +22,176 @@ */ class GatewayCommandTest extends \PHPUnit\Framework\TestCase { - /** @var GatewayCommand */ - protected $command; + /** + * @var GatewayCommand + */ + private $command; /** - * @var BuilderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BuilderInterface|MockObject */ - protected $requestBuilderMock; + private $requestBuilder; /** - * @var TransferFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var TransferFactoryInterface|MockObject */ - protected $transferFactoryMock; + private $transferFactory; /** - * @var ClientInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ClientInterface|MockObject */ - protected $clientMock; + private $client; /** - * @var HandlerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var HandlerInterface|MockObject */ - protected $responseHandlerMock; + private $responseHandler; /** - * @var ValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ValidatorInterface|MockObject */ - protected $validatorMock; + private $validator; /** - * @var LoggerInterface |\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|MockObject */ private $logger; + /** + * @var ErrorMessageMapperInterface|MockObject + */ + private $errorMessageMapper; + protected function setUp() { - $this->requestBuilderMock = $this->createMock( - BuilderInterface::class - ); - $this->transferFactoryMock = $this->createMock( - TransferFactoryInterface::class - ); - $this->clientMock = $this->createMock( - ClientInterface::class - ); - $this->responseHandlerMock = $this->createMock( - HandlerInterface::class - ); - $this->validatorMock = $this->createMock( - ValidatorInterface::class - ); + $this->requestBuilder = $this->createMock(BuilderInterface::class); + $this->transferFactory = $this->createMock(TransferFactoryInterface::class); + $this->client = $this->createMock(ClientInterface::class); + $this->responseHandler = $this->createMock(HandlerInterface::class); + $this->validator = $this->createMock(ValidatorInterface::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->errorMessageMapper = $this->createMock(ErrorMessageMapperInterface::class); $this->command = new GatewayCommand( - $this->requestBuilderMock, - $this->transferFactoryMock, - $this->clientMock, + $this->requestBuilder, + $this->transferFactory, + $this->client, $this->logger, - $this->responseHandlerMock, - $this->validatorMock + $this->responseHandler, + $this->validator, + $this->errorMessageMapper ); } public function testExecute() { $commandSubject = ['authorize']; - $request = [ - 'request_field1' => 'request_value1', - 'request_field2' => 'request_value2' - ]; - $response = ['response_field1' => 'response_value1']; - $validationResult = $this->getMockBuilder( - \Magento\Payment\Gateway\Validator\ResultInterface::class - ) - ->getMockForAbstractClass(); + $this->processRequest($commandSubject, true); - $transferO = $this->getMockBuilder( - \Magento\Payment\Gateway\Http\TransferInterface::class - ) - ->getMockForAbstractClass(); + $this->responseHandler->method('handle') + ->with($commandSubject, ['response_field1' => 'response_value1']); - $this->requestBuilderMock->expects(static::once()) - ->method('build') - ->with($commandSubject) - ->willReturn($request); + $this->command->execute($commandSubject); + } - $this->transferFactoryMock->expects(static::once()) - ->method('create') - ->with($request) - ->willReturn($transferO); + /** + * Checks a case when request fails. + * + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage Transaction has been declined. Please try again later. + */ + public function testExecuteValidationFail() + { + $commandSubject = ['authorize']; + $validationFailures = [ + __('Failure #1'), + __('Failure #2'), + ]; - $this->clientMock->expects(static::once()) - ->method('placeRequest') - ->with($transferO) - ->willReturn($response); - $this->validatorMock->expects(static::once()) - ->method('validate') - ->with(array_merge($commandSubject, ['response' =>$response])) - ->willReturn($validationResult); - $validationResult->expects(static::once()) - ->method('isValid') - ->willReturn(true); + $this->processRequest($commandSubject, false, $validationFailures); - $this->responseHandlerMock->expects(static::once()) - ->method('handle') - ->with($commandSubject, $response); + $this->logger->expects(self::exactly(count($validationFailures))) + ->method('critical') + ->withConsecutive( + [self::equalTo('Payment Error: ' . $validationFailures[0])], + [self::equalTo('Payment Error: ' . $validationFailures[1])] + ); $this->command->execute($commandSubject); } - public function testExecuteValidationFail() + /** + * Checks a case when request fails and response errors are mapped. + * + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage Failure Mapped + */ + public function testExecuteValidationFailWithMappedErrors() { - $this->expectException( - \Magento\Payment\Gateway\Command\CommandException::class - ); - $commandSubject = ['authorize']; - $request = [ - 'request_field1' => 'request_value1', - 'request_field2' => 'request_value2' - ]; - $response = ['response_field1' => 'response_value1']; $validationFailures = [ __('Failure #1'), __('Failure #2'), ]; - $validationResult = $this->getMockBuilder( - \Magento\Payment\Gateway\Validator\ResultInterface::class - ) - ->getMockForAbstractClass(); - $transferO = $this->getMockBuilder( - \Magento\Payment\Gateway\Http\TransferInterface::class - ) + $this->processRequest($commandSubject, false, $validationFailures); + + $this->errorMessageMapper->method('getMessage') + ->willReturnMap( + [ + ['Failure #1', 'Failure Mapped'], + ['Failure #2', null] + ] + ); + + $this->logger->expects(self::exactly(count($validationFailures))) + ->method('critical') + ->withConsecutive( + [self::equalTo('Payment Error: Failure Mapped')], + [self::equalTo('Payment Error: Failure #2')] + ); + + $this->command->execute($commandSubject); + } + + /** + * Performs command actions like request, response and validation. + * + * @param array $commandSubject + * @param bool $validationResult + * @param array $validationFailures + */ + private function processRequest(array $commandSubject, bool $validationResult, array $validationFailures = []) + { + $request = [ + 'request_field1' => 'request_value1', + 'request_field2' => 'request_value2' + ]; + $response = ['response_field1' => 'response_value1']; + $transferO = $this->getMockBuilder(TransferInterface::class) ->getMockForAbstractClass(); - $this->requestBuilderMock->expects(static::once()) - ->method('build') + $this->requestBuilder->method('build') ->with($commandSubject) ->willReturn($request); - $this->transferFactoryMock->expects(static::once()) - ->method('create') + $this->transferFactory->method('create') ->with($request) ->willReturn($transferO); - $this->clientMock->expects(static::once()) - ->method('placeRequest') + $this->client->method('placeRequest') ->with($transferO) ->willReturn($response); - $this->validatorMock->expects(static::once()) - ->method('validate') - ->with(array_merge($commandSubject, ['response' =>$response])) - ->willReturn($validationResult); - $validationResult->expects(static::once()) - ->method('isValid') - ->willReturn(false); - $validationResult->expects(static::once()) - ->method('getFailsDescription') - ->willReturn( - $validationFailures - ); - $this->logger->expects(static::exactly(count($validationFailures))) - ->method('critical') - ->withConsecutive( - [$validationFailures[0]], - [$validationFailures[1]] - ); + $result = $this->getMockBuilder(ResultInterface::class) + ->getMockForAbstractClass(); - $this->command->execute($commandSubject); + $this->validator->method('validate') + ->with(array_merge($commandSubject, ['response' => $response])) + ->willReturn($result); + $result->method('isValid') + ->willReturn($validationResult); + $result->method('getFailsDescription') + ->willReturn($validationFailures); } } diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Data/Order/AddressAdapterTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Data/Order/AddressAdapterTest.php index 1cf19ff9292e9..faf4818965386 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Data/Order/AddressAdapterTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Data/Order/AddressAdapterTest.php @@ -54,6 +54,9 @@ public function testStreetLine1($street, $expected) $this->assertEquals($expected, $this->model->getStreetLine1()); } + /** + * @return array + */ public function streetLine1DataProvider() { return [ @@ -73,6 +76,9 @@ public function testStreetLine2($street, $expected) $this->assertEquals($expected, $this->model->getStreetLine2()); } + /** + * @return array + */ public function streetLine2DataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Data/PaymentDataObjectTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Data/PaymentDataObjectTest.php index dc9a51b30cfea..e816533d59088 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Data/PaymentDataObjectTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Data/PaymentDataObjectTest.php @@ -40,11 +40,11 @@ protected function setUp() public function testGetOrder() { - $this->assertSame($this->orderMock, $this->model->getOrder()) ; + $this->assertSame($this->orderMock, $this->model->getOrder()); } public function testGetPayment() { - $this->assertSame($this->paymentMock, $this->model->getPayment()) ; + $this->assertSame($this->paymentMock, $this->model->getPayment()); } } diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Data/Quote/AddressAdapterTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Data/Quote/AddressAdapterTest.php index 751d8c51b4410..1f5b1c2d87053 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Data/Quote/AddressAdapterTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Data/Quote/AddressAdapterTest.php @@ -54,6 +54,9 @@ public function testStreetLine1($street, $expected) $this->assertEquals($expected, $this->model->getStreetLine1()); } + /** + * @return array + */ public function streetLine1DataProvider() { return [ @@ -73,6 +76,9 @@ public function testStreetLine2($street, $expected) $this->assertEquals($expected, $this->model->getStreetLine2()); } + /** + * @return array + */ public function streetLine2DataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php index 546dadfb1bf62..af26ea428a235 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Http/Client/SoapTest.php @@ -64,7 +64,7 @@ public function testPlaceRequest() $expectedResult = [ 'result' => [] ]; - $soapResult = new \StdClass(); + $soapResult = new \stdClass(); $this->logger->expects(static::at(0)) ->method('debug') diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/CountryValidatorTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/CountryValidatorTest.php index 0c808cb7c1faa..44d6144200504 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/CountryValidatorTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/CountryValidatorTest.php @@ -68,6 +68,9 @@ public function testValidateAllowspecificTrue($storeId, $country, $allowspecific $this->assertSame($this->resultMock, $this->model->validate($validationSubject)); } + /** + * @return array + */ public function validateAllowspecificTrueDataProvider() { return [ @@ -96,6 +99,9 @@ public function testValidateAllowspecificFalse($storeId, $allowspecific, $isVali $this->assertSame($this->resultMock, $this->model->validate($validationSubject)); } + /** + * @return array + */ public function validateAllowspecificFalseDataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ResultTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ResultTest.php index fb81137dea42b..562835e3199f1 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ResultTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ResultTest.php @@ -29,6 +29,9 @@ public function testResult($isValid, $failsDescription, $expectedIsValid, $expec $this->assertEquals($expectedFailsDescription, $this->model->getFailsDescription()); } + /** + * @return array + */ public function resultDataProvider() { $phraseMock = $this->getMockBuilder(\Magento\Framework\Phrase::class)->disableOriginalConstructor()->getMock(); diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php index 7b86db369b977..303c74b218e37 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php @@ -5,6 +5,10 @@ */ namespace Magento\Payment\Test\Unit\Gateway\Validator; +use Magento\Framework\ObjectManager\TMap; +use Magento\Framework\ObjectManager\TMapFactory; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; use Magento\Payment\Gateway\Validator\ValidatorComposite; use Magento\Payment\Gateway\Validator\ValidatorInterface; @@ -13,15 +17,15 @@ class ValidatorCompositeTest extends \PHPUnit\Framework\TestCase public function testValidate() { $validationSubject = []; - $validator1 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator1 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); - $validator2 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator2 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); - $tMapFactory = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMapFactory::class) + $tMapFactory = $this->getMockBuilder(TMapFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $tMap = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMap::class) + $tMap = $this->getMockBuilder(TMap::class) ->disableOriginalConstructor() ->getMock(); @@ -30,10 +34,10 @@ public function testValidate() ->with( [ 'array' => [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class, ], - 'type' => ValidatorInterface::class + 'type' => ValidatorInterface::class, ] ) ->willReturn($tMap); @@ -41,12 +45,12 @@ public function testValidate() ->method('getIterator') ->willReturn(new \ArrayIterator([$validator1, $validator2])); - $resultSuccess = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + $resultSuccess = $this->getMockBuilder(ResultInterface::class) ->getMockForAbstractClass(); $resultSuccess->expects(static::once()) ->method('isValid') ->willReturn(true); - $resultFail = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + $resultFail = $this->getMockBuilder(ResultInterface::class) ->getMockForAbstractClass(); $resultFail->expects(static::once()) ->method('isValid') @@ -64,9 +68,9 @@ public function testValidate() ->with($validationSubject) ->willReturn($resultFail); - $compositeResult = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + $compositeResult = $this->getMockBuilder(ResultInterface::class) ->getMockForAbstractClass(); - $resultFactory = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterfaceFactory::class) + $resultFactory = $this->getMockBuilder(ResultInterfaceFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -84,10 +88,90 @@ public function testValidate() $resultFactory, $tMapFactory, [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class ] ); static::assertSame($compositeResult, $validatorComposite->validate($validationSubject)); } + + /** + * @return void + */ + public function testValidateChainBreaksCorrectly() + { + $validationSubject = []; + $validator1 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $validator2 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $tMapFactory = $this->getMockBuilder(TMapFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $tMap = $this->getMockBuilder(TMap::class) + ->disableOriginalConstructor() + ->getMock(); + + $tMapFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'array' => [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + 'type' => ValidatorInterface::class + ] + ) + ->willReturn($tMap); + $tMap->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$validator1, $validator2])); + + $resultFail = $this->getMockBuilder(ResultInterface::class) + ->getMockForAbstractClass(); + $resultFail->expects($this->once()) + ->method('isValid') + ->willReturn(false); + $resultFail->expects($this->once()) + ->method('getFailsDescription') + ->willReturn(['Fail']); + + $validator1->expects($this->once()) + ->method('validate') + ->with($validationSubject) + ->willReturn($resultFail); + + // Assert this is never called + $validator2->expects($this->never()) + ->method('validate'); + + $compositeResult = $this->getMockBuilder(ResultInterface::class) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(ResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $resultFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'isValid' => false, + 'failsDescription' => ['Fail'] + ] + ) + ->willReturn($compositeResult); + + $validatorComposite = new ValidatorComposite( + $resultFactory, + $tMapFactory, + [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + ['validator1'] + ); + $this->assertSame($compositeResult, $validatorComposite->validate($validationSubject)); + } } diff --git a/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php b/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php index 931c2b5fd93d5..690f67fe28cac 100644 --- a/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php @@ -6,9 +6,9 @@ namespace Magento\Payment\Test\Unit\Helper; -use \Magento\Payment\Helper\Data; - use Magento\Framework\TestFramework\Unit\Matcher\MethodInvokedAtIndex; +use Magento\Payment\Helper\Data; +use Magento\Store\Model\ScopeInterface; class DataTest extends \PHPUnit\Framework\TestCase { @@ -18,6 +18,9 @@ class DataTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ private $scopeConfig; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $paymentConfig; + /** @var \PHPUnit_Framework_MockObject_MockObject */ private $initialConfig; @@ -34,6 +37,9 @@ class DataTest extends \PHPUnit\Framework\TestCase */ private $appEmulation; + /** + * @inheritDoc + */ protected function setUp() { $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -48,11 +54,15 @@ protected function setUp() $this->methodFactory = $arguments['paymentMethodFactory']; $this->appEmulation = $arguments['appEmulation']; + $this->paymentConfig = $arguments['paymentConfig']; $this->initialConfig = $arguments['initialConfig']; $this->helper = $objectManagerHelper->getObject($className, $arguments); } + /** + * @return void + */ public function testGetMethodInstance() { list($code, $class, $methodInstance) = ['method_code', 'method_class', 'method_instance']; @@ -166,6 +176,9 @@ public function testSortMethods(array $methodA, array $methodB) ); } + /** + * @return void + */ public function testGetMethodFormBlock() { list($blockType, $methodCode) = ['method_block_type', 'method_code']; @@ -191,6 +204,9 @@ public function testGetMethodFormBlock() $this->assertSame($blockMock, $this->helper->getMethodFormBlock($methodMock, $layoutMock)); } + /** + * @return void` + */ public function testGetInfoBlock() { $blockType = 'method_block_type'; @@ -216,6 +232,9 @@ public function testGetInfoBlock() $this->assertSame($blockMock, $this->helper->getInfoBlock($infoMock)); } + /** + * @return void + */ public function testGetInfoBlockHtml() { list($storeId, $blockHtml, $secureMode, $blockType) = [1, 'HTML MARKUP', true, 'method_block_type']; @@ -253,6 +272,9 @@ public function testGetInfoBlockHtml() $this->assertEquals($blockHtml, $this->helper->getInfoBlockHtml($infoMock, $storeId)); } + /** + * @return array + */ public function getSortMethodsDataProvider() { return [ @@ -266,4 +288,117 @@ public function getSortMethodsDataProvider() ] ]; } + + /** + * @param bool $sorted + * @param bool $asLabelValue + * @param bool $withGroups + * @param string|null $configTitle + * @param array $paymentMethod + * @param array $expectedPaymentMethodList + * @return void + * + * @dataProvider paymentMethodListDataProvider + */ + public function testGetPaymentMethodList( + bool $sorted, + bool $asLabelValue, + bool $withGroups, + $configTitle, + array $paymentMethod, + array $expectedPaymentMethodList + ) { + $groups = ['group' => 'Group Title']; + + $this->initialConfig->method('getData') + ->with('default') + ->willReturn( + [ + Data::XML_PATH_PAYMENT_METHODS => [ + $paymentMethod['code'] => $paymentMethod['data'], + ], + ] + ); + + $titlePath = sprintf('%s/%s/title', Data::XML_PATH_PAYMENT_METHODS, $paymentMethod['code']); + $this->scopeConfig->method('getValue') + ->with($titlePath, ScopeInterface::SCOPE_STORE, null) + ->willReturn($configTitle); + + $this->paymentConfig->method('getGroups') + ->willReturn($groups); + + $paymentMethodList = $this->helper->getPaymentMethodList($sorted, $asLabelValue, $withGroups); + $this->assertEquals($expectedPaymentMethodList, $paymentMethodList); + } + + /** + * @return array + */ + public function paymentMethodListDataProvider(): array + { + return [ + 'Payment method with changed title' => + [ + true, + false, + false, + 'Config Payment Title', + [ + 'code' => 'payment_method', + 'data' => [ + 'active' => 1, + 'title' => 'Payment Title', + ], + ], + ['payment_method' => 'Config Payment Title'], + ], + 'Payment method as value => label' => + [ + true, + true, + false, + 'Payment Title', + [ + 'code' => 'payment_method', + 'data' => [ + 'active' => 1, + 'title' => 'Payment Title', + ], + ], + [ + 'payment_method' => [ + 'value' => 'payment_method', + 'label' => 'Payment Title', + ], + ], + ], + 'Payment method with group' => + [ + true, + true, + true, + 'Payment Title', + [ + 'code' => 'payment_method', + 'data' => [ + 'active' => 1, + 'title' => 'Payment Title', + 'group' => 'group', + ], + ], + [ + 'group' => [ + 'label' => 'Group Title', + 'value' => [ + 'payment_method' => [ + 'value' => 'payment_method', + 'label' => 'Payment Title', + ], + ], + ], + ], + ], + ]; + } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/FactoryTest.php b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/FactoryTest.php index c49f3da315908..64069ff4a1941 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/FactoryTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/FactoryTest.php @@ -40,6 +40,9 @@ public function testCreate($salesModelClass, $expectedType) $this->assertEquals('some value', $this->_model->create($salesModel)); } + /** + * @return array + */ public function createDataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/OrderTest.php b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/OrderTest.php index 7594d9ce501ac..3e3c4226d4fbe 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/OrderTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/OrderTest.php @@ -19,6 +19,9 @@ protected function setUp() $this->_model = new \Magento\Payment\Model\Cart\SalesModel\Order($this->_orderMock); } + /** + * @return array + */ public function gettersDataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/QuoteTest.php b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/QuoteTest.php index 95271569e994e..bdcc89840ac19 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/QuoteTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Cart/SalesModel/QuoteTest.php @@ -84,6 +84,9 @@ public function testGetAllItems($pItem, $name, $qty, $price) $this->assertEquals($expected, $this->_model->getAllItems()); } + /** + * @return array + */ public function getAllItemsDataProvider() { return [ @@ -135,6 +138,9 @@ public function testGetter($isVirtual, $getterMethod) $this->assertEquals($getterMethod, $model->{$getterMethod}()); } + /** + * @return array + */ public function getterDataProvider() { return [ diff --git a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php index a8856166995fc..ff6aea44645cf 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php @@ -42,12 +42,14 @@ public function testGetConfig() 'vi' => [ 'url' => 'http://cc.card/vi.png', 'width' => getimagesize($imagesDirectoryPath . 'vi.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1], + 'title' => __('Visa'), ], 'ae' => [ 'url' => 'http://cc.card/ae.png', 'width' => getimagesize($imagesDirectoryPath . 'ae.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1], + 'title' => __('American Express'), ] ] ] @@ -56,11 +58,13 @@ public function testGetConfig() $ccAvailableTypesMock = [ 'vi' => [ + 'title' => 'Visa', 'fileId' => 'Magento_Payment::images/cc/vi.png', 'path' => $imagesDirectoryPath . 'vi.png', 'url' => 'http://cc.card/vi.png' ], 'ae' => [ + 'title' => 'American Express', 'fileId' => 'Magento_Payment::images/cc/ae.png', 'path' => $imagesDirectoryPath . 'ae.png', 'url' => 'http://cc.card/ae.png' @@ -68,7 +72,11 @@ public function testGetConfig() ]; $assetMock = $this->createMock(\Magento\Framework\View\Asset\File::class); - $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes')->willReturn($ccAvailableTypesMock); + $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes') + ->willReturn(array_combine( + array_keys($ccAvailableTypesMock), + array_column($ccAvailableTypesMock, 'title') + )); $this->ccConfigMock->expects($this->atLeastOnce()) ->method('createAsset') diff --git a/app/code/Magento/Payment/Test/Unit/Model/Config/Source/AllmethodsTest.php b/app/code/Magento/Payment/Test/Unit/Model/Config/Source/AllmethodsTest.php index 7cd577bf2ec27..f481d23f83a36 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Config/Source/AllmethodsTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Config/Source/AllmethodsTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Model\Config\Source; use \Magento\Payment\Model\Config\Source\Allmethods; @@ -36,9 +34,10 @@ protected function setUp() public function testToOptionArray() { $expectedArray = ['key' => 'value']; - $this->_paymentData->expects($this->once())->method('getPaymentMethodList')->with( - true, true, true - )->will($this->returnValue($expectedArray)); + $this->_paymentData->expects($this->once()) + ->method('getPaymentMethodList') + ->with(true, true, true) + ->willReturn($expectedArray); $this->assertEquals($expectedArray, $this->_model->toOptionArray()); } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Payment/Test/Unit/Model/ConfigTest.php index cd215e870462c..584da622b1aad 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/ConfigTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Model; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -144,6 +142,9 @@ public function testGetActiveMethods($isActive) static::assertEquals($isActive ? ['active_method' => $adapter] : [], $this->config->getActiveMethods()); } + /** + * @return array + */ public function getActiveMethodsDataProvider() { return [[true], [false]]; diff --git a/app/code/Magento/Payment/Test/Unit/Model/InfoTest.php b/app/code/Magento/Payment/Test/Unit/Model/InfoTest.php index 3203f1498b9a3..93279f308eefa 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/InfoTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/InfoTest.php @@ -176,7 +176,7 @@ public function testDecrypt() */ public function testSetAdditionalInformationException() { - $this->info->setAdditionalInformation('object', new \StdClass()); + $this->info->setAdditionalInformation('object', new \stdClass()); } /** diff --git a/app/code/Magento/Payment/Test/Unit/Model/Method/FactoryTest.php b/app/code/Magento/Payment/Test/Unit/Model/Method/FactoryTest.php deleted file mode 100644 index f0cb19ef0fa0f..0000000000000 --- a/app/code/Magento/Payment/Test/Unit/Model/Method/FactoryTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Payment\Test\Unit\Model\Method; - -class FactoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\ObjectManagerInterface|PHPUnit_Framework_MockObject_MockObject - */ - protected $_objectManagerMock; - - /** - * @var \Magento\Payment\Model\Method\Factory - */ - protected $_factory; - - protected function setUp() - { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->_objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->_factory = $objectManagerHelper->getObject( - \Magento\Payment\Model\Method\Factory::class, - ['objectManager' => $this->_objectManagerMock] - ); - } - - public function testCreateMethod() - { - $className = \Magento\Payment\Model\Method\AbstractMethod::class; - $methodMock = $this->createMock($className); - $this->_objectManagerMock->expects( - $this->once() - )->method( - 'create' - )->with( - $className, - [] - )->will( - $this->returnValue($methodMock) - ); - - $this->assertEquals($methodMock, $this->_factory->create($className)); - } - - public function testCreateMethodWithArguments() - { - $className = \Magento\Payment\Model\Method\AbstractMethod::class; - $data = ['param1', 'param2']; - $methodMock = $this->createMock($className); - $this->_objectManagerMock->expects( - $this->once() - )->method( - 'create' - )->with( - $className, - $data - )->will( - $this->returnValue($methodMock) - ); - - $this->assertEquals($methodMock, $this->_factory->create($className, $data)); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage WrongClass class doesn't implement \Magento\Payment\Model\MethodInterface - */ - public function testWrongTypeException() - { - $className = 'WrongClass'; - $methodMock = $this->createMock($className); - $this->_objectManagerMock->expects( - $this->once() - )->method( - 'create' - )->with( - $className, - [] - )->will( - $this->returnValue($methodMock) - ); - - $this->_factory->create($className); - } -} diff --git a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php index 9b5481dc05270..a8e8c592e3be8 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Model\Method; /** diff --git a/app/code/Magento/Payment/Test/Unit/Model/Method/Specification/FactoryTest.php b/app/code/Magento/Payment/Test/Unit/Model/Method/Specification/FactoryTest.php deleted file mode 100644 index 9bdc90829f6fe..0000000000000 --- a/app/code/Magento/Payment/Test/Unit/Model/Method/Specification/FactoryTest.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Payment\Test\Unit\Model\Method\Specification; - -/** - * Factory Test - */ -class FactoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Payment\Model\Method\Specification\Factory - */ - protected $factory; - - protected function setUp() - { - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->factory = $objectManagerHelper->getObject( - \Magento\Payment\Model\Method\Specification\Factory::class, - ['objectManager' => $this->objectManagerMock] - ); - } - - public function testCreateMethod() - { - $className = \Magento\Payment\Model\Method\SpecificationInterface::class; - $methodMock = $this->createMock($className); - $this->objectManagerMock->expects( - $this->once() - )->method( - 'get' - )->with( - $className - )->will( - $this->returnValue($methodMock) - ); - - $this->assertEquals($methodMock, $this->factory->create($className)); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Specification must implement SpecificationInterface - */ - public function testWrongTypeException() - { - $className = 'WrongClass'; - $methodMock = $this->createMock($className); - $this->objectManagerMock->expects( - $this->once() - )->method( - 'get' - )->with( - $className - )->will( - $this->returnValue($methodMock) - ); - - $this->factory->create($className); - } -} diff --git a/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php b/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php index 0ba72d38b2a03..3f4af635974e8 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/MethodListTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Payment\Test\Unit\Model; use Magento\Payment\Model\MethodList; diff --git a/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php index b86fbc6b18263..75916dd2ea99b 100644 --- a/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php +++ b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php @@ -61,7 +61,7 @@ public function testSalesOrderBeforeSaveCantUnhold() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -86,7 +86,7 @@ public function testSalesOrderBeforeSaveIsCanceled() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -114,7 +114,7 @@ public function testSalesOrderBeforeSaveIsClosed() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -156,6 +156,29 @@ public function testSalesOrderBeforeSaveSetForced() $this->salesOrderBeforeSaveObserver->execute($this->observerMock); } + /** + * The method should check that the payment is available, as this is not always the case. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Please provide payment for the order. + */ + public function testDoesNothingWhenNoPaymentIsAvailable() + { + $this->_prepareEventMockWithMethods(['getOrder']); + + $order = $this->getMockBuilder(\Magento\Sales\Model\Order::class)->disableOriginalConstructor()->setMethods( + array_merge(['__wakeup', 'getPayment']) + )->getMock(); + + $this->eventMock->expects($this->once())->method('getOrder')->will( + $this->returnValue($order) + ); + + $order->expects($this->exactly(1))->method('getPayment')->willReturn(null); + + $this->salesOrderBeforeSaveObserver->execute($this->observerMock); + } + /** * Prepares EventMock with set of methods * @@ -184,7 +207,7 @@ private function _getPreparedOrderMethod($methodCode, $orderMethods = []) $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); diff --git a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php index 5afaa9fcf97b9..71e0c60efaa40 100644 --- a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php +++ b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php @@ -40,6 +40,14 @@ public function toOptionArray() if ($this->options === null) { $this->options = $this->paymentHelper->getPaymentMethodList(true, true); } + + array_walk( + $this->options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); + return $this->options; } } diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index b8dbf6cd7f16f..f840011ce1b49 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-payment", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-sales": "101.0.*", @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.9", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Payment/etc/config.xml b/app/code/Magento/Payment/etc/config.xml index 9fe859c96a941..663734fb066c7 100644 --- a/app/code/Magento/Payment/etc/config.xml +++ b/app/code/Magento/Payment/etc/config.xml @@ -13,6 +13,7 @@ <model>Magento\Payment\Model\Method\Free</model> <order_status>pending</order_status> <title>No Payment Information Required + authorize 0 1 diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index e2de2244bff89..b7422bb00d543 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -7,11 +7,13 @@ --> + + @@ -36,4 +38,47 @@ Magento\Payment\Gateway\Config\Config + + + + Magento_Payment + error_mapping.xsd + + + + + Magento\Payment\Gateway\ErrorMapper\XmlToArrayConverter + Magento\Payment\Gateway\ErrorMapper\VirtualSchemaLocator + error_mapping.xml + + + + + Magento\Payment\Gateway\ErrorMapper\VirtualConfigReader + payment_error_mapper + + + + + Magento\Payment\Gateway\ErrorMapper\NullMappingData + + + + + + /var/log/payment.log + + + + + + Magento\Payment\Model\Method\VirtualDebug + + + + + + Magento\Payment\Model\Method\VirtualLogger + + diff --git a/app/code/Magento/Payment/etc/error_mapping.xsd b/app/code/Magento/Payment/etc/error_mapping.xsd new file mode 100644 index 0000000000000..97f3c181beb37 --- /dev/null +++ b/app/code/Magento/Payment/etc/error_mapping.xsd @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml index 8e4127c4cbb11..678bde815d370 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Payment\Block\Adminhtml\Transparent\Form $block */ @@ -23,8 +22,8 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); - getCcMonths() as $k => $v): ?> + getCcMonths() as $k => $v) : ?>
    escapeHtml(__('Items')) ?>
    escapeHtml(__('Product Name')) ?>escapeHtml(__('Qty')) ?>
    getItemHtml($_item->getQuoteItem()) ?>getQty() ?>getItemHtml($_item->getQuoteItem()) ?>escapeHtml($_item->getQty()) ?>
    - $value):?> + $value) : ?> r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"
    escapeHtml($label) ?>: diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml index e95237cc82021..f60c1d063addf 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/instructions.phtml @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Payment\Block\Info $block * @see \Magento\Payment\Block\Info */ ?>

    escapeHtml($block->getMethod()->getTitle()) ?>

    -getInstructions()): ?> +getInstructions()) : ?> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml index 7acac62f65d38..a8583ea5549fe 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @see \Magento\Payment\Block\Info * @var \Magento\Payment\Block\Info $block @@ -12,8 +11,8 @@ ?> escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} -getSpecificInformation()):?> - $value):?> +getSpecificInformation()) : ?> + $value) : ?> escapeHtml($label) ?>: escapeHtml(implode(' ', $block->getValueAsArray($value))) ?> {{pdf_row_separator}} diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/substitution.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/substitution.phtml index ad24b113ffdea..d187b64683e45 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/substitution.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/substitution.phtml @@ -4,12 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Payment\Block\Info $block */ ?>
    - escapeHtml($block->getMethod()->getTitle());?> + getMethod()->getTitle() + ? $block->escapeHtml($block->getMethod()->getTitle()) + : $block->escapeHtml(__('Payment method')); ?> escapeHtml(__(' is not available. You still can process offline actions.')) ?>
    diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml index 5b65bbaa3d9b7..36b8c978c339f 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Payment\Block\Transparent\Form $block */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); @@ -48,9 +46,9 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#_cc_number"}' class="admin__control-select"> - getCcAvailableTypes() as $typeCode => $typeName): ?> + getCcAvailableTypes() as $typeCode => $typeName) : ?> @@ -88,10 +86,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-container="-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#_expiration_yr"}'> - getCcMonths() as $k => $v): ?> + getCcMonths() as $k => $v) : ?> @@ -100,17 +98,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); - hasVerification()): ?> + hasVerification()) : ?>
    - getItems() as $item): ?> + getItems() as $item) : ?> getItemHtml($item) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml index ca84b0829ea9e..839d278ed227c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml @@ -4,23 +4,22 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** @var $block \Magento\Paypal\Block\Express\Review */ ?>
    - getCanEditShippingMethod() || !$block->getCurrentShippingRate()): ?> - getShippingRateGroups()): ?> + getCanEditShippingMethod() || !$block->getCurrentShippingRate()) : ?> + getShippingRateGroups()) : ?> getCurrentShippingRate(); ?> - +

    escapeHtml(__('Sorry, no quotes are available for this order right now.')) ?>

    - +

    renderShippingRateOption($block->getCurrentShippingRate()) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut.phtml index ce8a0e940230b..1e7787817b558 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Paypal\Block\Express\Shortcut $block */ @@ -37,7 +36,7 @@ if ($block->getIsInCatalogProduct()) { src="escapeUrl($block->getImageUrl()) ?>" alt="escapeHtml(__('Checkout with PayPal')) ?>" title="escapeHtml(__('Checkout with PayPal')) ?>"/> - getAdditionalLinkImage()): ?> + getAdditionalLinkImage()) : ?> getAdditionalLinkImage(); ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut/container.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut/container.phtml index 9a9bac2c74164..acaff92065d7c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut/container.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut/container.phtml @@ -4,11 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var $block Magento\Paypal\Block\Express\Shortcut */ ?> -getIsInCatalogProduct()): ?> +getIsInCatalogProduct()) : ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml index 28daa150b8888..c2339f85b7ca5 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile + /** * @var \Magento\Paypal\Block\Payment\Info $block */ diff --git a/app/code/Magento/Paypal/view/frontend/templates/js/components.phtml b/app/code/Magento/Paypal/view/frontend/templates/js/components.phtml index 7ba9307742031..13f44b97fc789 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/js/components.phtml @@ -4,6 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag ?> getChildHtml() ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml index 06cbc9f3e354c..d5944a6f22f5f 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Paypal\Block\Payflow\Advanced\Form $block */ diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml index d9095c202f88f..75ee08111bd7a 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** * @var \Magento\Paypal\Block\Payment\Form\Billing\Agreement $block */ @@ -18,7 +17,7 @@ $code = $block->escapeHtml($block->getMethodCode());

    + + + CAPTCHA for "Create user", "Forgot password", "Payflow Pro" forms is always enabled if chosen. + + +
    + + diff --git a/app/code/Magento/PaypalCaptcha/etc/config.xml b/app/code/Magento/PaypalCaptcha/etc/config.xml new file mode 100644 index 0000000000000..133a78a42f7b4 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/config.xml @@ -0,0 +1,30 @@ + + + + + + + + 1 + + + 1 + + + + + + + + + + + + + + diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml new file mode 100644 index 0000000000000..c236d5ea04ca0 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\PaypalCaptcha\Model\Checkout\ConfigProviderPayPal + + + + diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml new file mode 100644 index 0000000000000..ae706c4485d61 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/PaypalCaptcha/etc/module.xml b/app/code/Magento/PaypalCaptcha/etc/module.xml new file mode 100644 index 0000000000000..a456cb0584fe6 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Magento/PaypalCaptcha/registration.php b/app/code/Magento/PaypalCaptcha/registration.php new file mode 100644 index 0000000000000..4dac0582a6d1b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/registration.php @@ -0,0 +1,9 @@ + + + + + + + + + + + + + + + + + + + + uiComponent + paypal-captcha + paypal-captcha + checkoutProvider + + Magento_Checkout/payment/before-place-order + + + + Magento_PaypalCaptcha/js/view/checkout/paymentCaptcha + paypal-captcha + co-payment-form + checkoutConfig + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..78e7add4ec690 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Checkout/js/view/payment/list': { + 'Magento_PaypalCaptcha/js/view/payment/list-mixin': true + } + } + } +}; diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js new file mode 100644 index 0000000000000..f8f119e3b3396 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js @@ -0,0 +1,44 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Captcha/js/model/captcha' +], +function ($, defaultCaptcha, captchaList, Captcha) { + 'use strict'; + + return defaultCaptcha.extend({ + + /** @inheritdoc */ + initialize: function () { + var captchaConfigPayment, + currentCaptcha; + + this._super(); + + if (window[this.configSource] && window[this.configSource].captchaPayments) { + captchaConfigPayment = window[this.configSource].captchaPayments; + + $.each(captchaConfigPayment, function (formId, captchaData) { + var captcha; + + captchaData.formId = formId; + captcha = Captcha(captchaData); + captchaList.add(captcha); + }); + } + + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + } + } + }); +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js new file mode 100644 index 0000000000000..60172f696e9ed --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js @@ -0,0 +1,54 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/model/captchaList' +], function ($, captchaList) { + 'use strict'; + + var mixin = { + + formId: 'co-payment-form', + + /** + * Sets custom template for Payflow Pro + * + * @param {Object} payment + * @returns {Object} + */ + createComponent: function (payment) { + + var component = this._super(payment); + + if (component.component === 'Magento_Paypal/js/view/payment/method-renderer/payflowpro-method') { + component.template = 'Magento_PaypalCaptcha/payment/payflowpro-form'; + $(window).off('clearTimeout') + .on('clearTimeout', this.clearTimeout.bind(this)); + } + + return component; + }, + + /** + * Overrides default window.clearTimeout() to catch errors from iframe and reload Captcha. + */ + clearTimeout: function () { + var captcha = captchaList.getCaptchaByFormId(this.formId); + + if (captcha !== null) { + captcha.refresh(); + } + clearTimeout(); + } + }; + + /** + * Overrides `Magento_Checkout/js/view/payment/list::createComponent` + */ + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html new file mode 100644 index 0000000000000..fec5cf96b0324 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html @@ -0,0 +1,90 @@ + +
    +
    + + +
    + +
    + + + +
    + + + +
    + + + + + + +
    + + +
    + + +
    + + + +
    + + +
    + + + +
    +
    +
    + +
    +
    +
    +
    diff --git a/app/code/Magento/Persistent/Block/Header/Additional.php b/app/code/Magento/Persistent/Block/Header/Additional.php index c740f5a3469fb..28e967f201230 100644 --- a/app/code/Magento/Persistent/Block/Header/Additional.php +++ b/app/code/Magento/Persistent/Block/Header/Additional.php @@ -5,6 +5,10 @@ */ namespace Magento\Persistent\Block\Header; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Persistent\Helper\Data; + /** * Remember Me block * @@ -30,20 +34,37 @@ class Additional extends \Magento\Framework\View\Element\Html\Link protected $customerRepository; /** - * Constructor - * + * @var string + */ + protected $_template = 'Magento_Persistent::additional.phtml'; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Data + */ + private $persistentHelper; + + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\View $customerViewHelper * @param \Magento\Persistent\Helper\Session $persistentSessionHelper * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data + * @param Json|null $jsonSerializer + * @param Data|null $persistentHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Customer\Helper\View $customerViewHelper, \Magento\Persistent\Helper\Session $persistentSessionHelper, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - array $data = [] + array $data = [], + Json $jsonSerializer = null, + Data $persistentHelper = null ) { $this->isScopePrivate = true; $this->_customerViewHelper = $customerViewHelper; @@ -51,6 +72,8 @@ public function __construct( $this->customerRepository = $customerRepository; parent::__construct($context, $data); $this->_isScopePrivate = true; + $this->jsonSerializer = $jsonSerializer ?: ObjectManager::getInstance()->get(Json::class); + $this->persistentHelper = $persistentHelper ?: ObjectManager::getInstance()->get(Data::class); } /** @@ -64,17 +87,25 @@ public function getHref() } /** - * Render additional header html + * @return int + */ + public function getCustomerId() + { + return $this->_persistentSessionHelper->getSession()->getCustomerId(); + } + + /** + * Get persistent config. * * @return string */ - protected function _toHtml() + public function getConfig() { - if ($this->_persistentSessionHelper->getSession()->getCustomerId()) { - return '
    getLinkAttributes() . ' >' . __('Not you?') - . ''; - } - - return ''; + return + $this->jsonSerializer->serialize( + [ + 'expirationLifetime' => $this->persistentHelper->getLifeTime(), + ] + ); } } diff --git a/app/code/Magento/Persistent/CustomerData/Persistent.php b/app/code/Magento/Persistent/CustomerData/Persistent.php new file mode 100644 index 0000000000000..98b36b2e36612 --- /dev/null +++ b/app/code/Magento/Persistent/CustomerData/Persistent.php @@ -0,0 +1,71 @@ +persistentSession = $persistentSession; + $this->customerViewHelper = $customerViewHelper; + $this->customerRepository = $customerRepository; + } + + /** + * Get data + * + * @return array + */ + public function getSectionData() + { + if (!$this->persistentSession->isPersistent()) { + return []; + } + + $customerId = $this->persistentSession->getSession()->getCustomerId(); + if (!$customerId) { + return []; + } + + $customer = $this->customerRepository->getById($customerId); + + return [ + 'fullname' => $this->customerViewHelper->getCustomerName($customer), + ]; + } +} diff --git a/app/code/Magento/Persistent/Helper/Data.php b/app/code/Magento/Persistent/Helper/Data.php index 39a9ce7a8ef43..c963084a1ecb1 100644 --- a/app/code/Magento/Persistent/Helper/Data.php +++ b/app/code/Magento/Persistent/Helper/Data.php @@ -136,12 +136,10 @@ public function isShoppingCartPersist($store = null) */ public function getLifeTime($store = null) { - $lifeTime = intval( - $this->scopeConfig->getValue( - self::XML_PATH_LIFE_TIME, - ScopeInterface::SCOPE_STORE, - $store - ) + $lifeTime = (int)$this->scopeConfig->getValue( + self::XML_PATH_LIFE_TIME, + ScopeInterface::SCOPE_STORE, + $store ); return $lifeTime < 0 ? 0 : $lifeTime; } diff --git a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php index 2c0259bd12b00..2641102ca4d72 100644 --- a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php +++ b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php @@ -108,8 +108,8 @@ public function beforeSavePaymentInformationAndPlaceOrder( $this->customerSession->setCustomerId(null); $this->customerSession->setCustomerGroupId(null); $this->quoteManager->convertCustomerCartToGuest(); - /** @var \Magento\Quote\Api\Data\CartInterface $quote */ - $quote = $this->cartRepository->get($this->checkoutSession->getQuote()->getId()); + $quoteId = $this->checkoutSession->getQuoteId(); + $quote = $this->cartRepository->get($quoteId); $quote->setCustomerEmail($email); $quote->getAddressesCollection()->walk('setEmail', ['email' => $email]); $this->cartRepository->save($quote); diff --git a/app/code/Magento/Persistent/Model/Observer.php b/app/code/Magento/Persistent/Model/Observer.php index 53fe5f95531e1..81c2870071a2e 100644 --- a/app/code/Magento/Persistent/Model/Observer.php +++ b/app/code/Magento/Persistent/Model/Observer.php @@ -86,13 +86,8 @@ public function __construct( */ public function emulateWelcomeBlock($block) { - $customerName = $this->_customerViewHelper->getCustomerName( - $this->customerRepository->getById($this->_persistentSession->getSession()->getCustomerId()) - ); + $block->setWelcome(' '); - $this->_applyAccountLinksPersistentData(); - $welcomeMessage = __('Welcome, %1!', $customerName); - $block->setWelcome($welcomeMessage); return $this; } diff --git a/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php new file mode 100644 index 0000000000000..04b4450d93cec --- /dev/null +++ b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php @@ -0,0 +1,39 @@ +persistentSession = $persistentSession; + } + + /** + * @param \Magento\Framework\App\Http\Context $subject + * @return mixed + */ + public function beforeGetVaryString(\Magento\Framework\App\Http\Context $subject) + { + if ($this->persistentSession->isPersistent()) { + $subject->setValue('PERSISTENT', 1, 0); + } + } +} diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 8937a4920cb23..8ae22e4c26c6f 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -7,6 +7,8 @@ /** * Class QuoteManager + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteManager { @@ -87,6 +89,7 @@ public function setGuest($checkQuote = false) ->setCustomerLastname(null) ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) + ->setCustomerIsGuest(true) ->removeAllAddresses(); //Create guest addresses $quote->getShippingAddress(); @@ -108,8 +111,9 @@ public function setGuest($checkQuote = false) */ public function convertCustomerCartToGuest() { + $quoteId = $this->checkoutSession->getQuoteId(); /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->quoteRepository->get($this->checkoutSession->getQuote()->getId()); + $quote = $this->quoteRepository->get($quoteId); if ($quote && $quote->getId()) { $this->_setQuotePersistent = false; $quote->setIsActive(true) diff --git a/app/code/Magento/Persistent/Model/Session.php b/app/code/Magento/Persistent/Model/Session.php index abc344671ab70..3aea20a090dd9 100644 --- a/app/code/Magento/Persistent/Model/Session.php +++ b/app/code/Magento/Persistent/Model/Session.php @@ -354,7 +354,7 @@ public function deleteExpired($websiteId = null) $lifetime = $this->_coreConfig->getValue( \Magento\Persistent\Helper\Data::XML_PATH_LIFE_TIME, 'website', - intval($websiteId) + (int)$websiteId ); if ($lifetime) { diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 2aaf0f30fe71d..d355fbf538874 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -1,13 +1,23 @@ _persistentSession = $persistentSession; $this->quoteManager = $quoteManager; @@ -72,6 +110,8 @@ public function __construct( $this->_checkoutSession = $checkoutSession; $this->_eventManager = $eventManager; $this->_persistentData = $persistentData; + $this->request = $request; + $this->quoteRepository = $quoteRepository; } /** @@ -86,16 +126,101 @@ public function execute(\Magento\Framework\Event\Observer $observer) return; } + //clear persistent when persistent data is disabled + if ($this->isPersistentQuoteOutdated()) { + $this->_eventManager->dispatch('persistent_session_expired'); + $this->quoteManager->expire(); + $this->_checkoutSession->clearQuote(); + $this->_customerSession->setCustomerId(null)->setCustomerGroupId(null); + return; + } + if ($this->_persistentData->isEnabled() && !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$observer->getControllerAction() instanceof \Magento\Checkout\Controller\Onepage - // persistent session does not expire on onepage checkout page to not spoil customer group id + !$this->isRequestFromCheckoutPage($this->request) && + // persistent session does not expire on onepage checkout page + $this->isNeedToExpireSession() ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); $this->_customerSession->setCustomerId(null)->setCustomerGroupId(null); } } + + /** + * Checks if current quote marked as persistent and Persistence Functionality is disabled. + * + * @return bool + */ + private function isPersistentQuoteOutdated(): bool + { + if ((!$this->_persistentData->isEnabled() || !$this->_persistentData->isShoppingCartPersist()) + && !$this->_customerSession->isLoggedIn() + && $this->_checkoutSession->getQuoteId() + && $this->isActiveQuote() + ) { + return (bool)$this->getQuote()->getIsPersistent(); + } + return false; + } + + /** + * Condition checker + * + * @return bool + */ + private function isNeedToExpireSession(): bool + { + return $this->getQuote()->getIsPersistent() || $this->getQuote()->getCustomerIsGuest(); + } + + /** + * Getter for Quote with micro optimization + * + * @return Quote + */ + private function getQuote(): Quote + { + if ($this->quote === null) { + $this->quote = $this->_checkoutSession->getQuote(); + } + return $this->quote; + } + + /** + * Check if quote is active. + * + * @return bool + */ + private function isActiveQuote(): bool + { + try { + $this->quoteRepository->getActive($this->_checkoutSession->getQuoteId()); + return true; + } catch (NoSuchEntityException $e) { + return false; + } + } + + /** + * Check current request is coming from onepage checkout page. + * + * @param \Magento\Framework\App\RequestInterface $request + * @return bool + */ + private function isRequestFromCheckoutPage(\Magento\Framework\App\RequestInterface $request): bool + { + $requestUri = (string)$request->getRequestUri(); + $refererUri = (string)$request->getServer('HTTP_REFERER'); + + /** @var bool $isCheckoutPage */ + $isCheckoutPage = ( + false !== strpos($requestUri, $this->checkoutPagePath) || + false !== strpos($refererUri, $this->checkoutPagePath) + ); + + return $isCheckoutPage; + } } diff --git a/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php new file mode 100644 index 0000000000000..030eca854c801 --- /dev/null +++ b/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php @@ -0,0 +1,88 @@ +persistentSession = $persistentSession; + $this->customerSession = $customerSession; + $this->persistentData = $persistentData; + $this->customerRepository = $customerRepository; + } + + /** + * Pass customer data from persistent session to checkout session and set quote to be loaded even if not active. + * + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + /** @var $checkoutSession \Magento\Checkout\Model\Session */ + $checkoutSession = $observer->getEvent()->getData('checkout_session'); + if ($this->persistentData->isShoppingCartPersist() && $this->persistentSession->isPersistent()) { + $checkoutSession->setCustomerData( + $this->customerRepository->getById($this->persistentSession->getSession()->getCustomerId()) + ); + } + if (!(($this->persistentSession->isPersistent() && !$this->customerSession->isLoggedIn()) + && !$this->persistentData->isShoppingCartPersist() + )) { + return; + } + if ($checkoutSession) { + $checkoutSession->setLoadInactive(); + } + } +} diff --git a/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php deleted file mode 100644 index 6eeab94a91cca..0000000000000 --- a/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php +++ /dev/null @@ -1,78 +0,0 @@ -_persistentSession = $persistentSession; - $this->_customerSession = $customerSession; - $this->_checkoutSession = $checkoutSession; - $this->_persistentData = $persistentData; - } - - /** - * Set quote to be loaded even if not active - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - if (!(($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() - )) { - return; - } - - if ($this->_checkoutSession) { - $this->_checkoutSession->setLoadInactive(); - } - } -} diff --git a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php index db6b6d1ee370d..2803bc998dcbe 100644 --- a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php +++ b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php @@ -1,6 +1,5 @@ _persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() + ($this->_persistentSession->isPersistent()) + && $this->_persistentData->isShoppingCartPersist() ) && $this->quoteManager->isPersistent() ) { diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontAssertPersistentRegistrationPageFieldsActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontAssertPersistentRegistrationPageFieldsActionGroup.xml new file mode 100644 index 0000000000000..34409480c9ecf --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontAssertPersistentRegistrationPageFieldsActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 0000000000000..9e4551cabeb8e --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml new file mode 100644 index 0000000000000..2b2d88248ab8d --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontPersistentAssertCustomerWelcomeMessageActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml new file mode 100644 index 0000000000000..5e01931a2b8bf --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml @@ -0,0 +1,93 @@ + + + + + + persistentDefaultState + + + 0 + + + + persistentEnabledState + + + 1 + + + + LogoutClearActive + + + 1 + + + + LogoutClearInactive + + + 0 + + + + + persistentEnabledState + SecondsOfPersistentLifetime + EnablePersistentRememberMe + EnablePersistentRememberMeDefaultValue + LogoutClearInactive + EnablePersistentShoppingCart + + + + 31536000 + + + 1 + + + 1 + + + 1 + + + + + RestorePersistentOptionsEnabled + RestorePersistentOptionsLifetime + RestorePersistentOptionsRememberEnabled + RestorePersistentOptionsRememberDefault + RestorePersistentOptionsLogout + RestorePersistentOptionsShoppingCart + + + + PersistentOptionsUseInherit + + + PersistentOptionsUseInherit + + + PersistentOptionsUseInherit + + + PersistentOptionsUseInherit + + + PersistentOptionsUseInherit + + + PersistentOptionsUseInherit + + + + 1 + + diff --git a/app/code/Magento/Persistent/Test/Mftf/LICENSE.txt b/app/code/Magento/Persistent/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Persistent/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/app/code/Magento/Persistent/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Persistent/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Persistent/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/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml b/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml new file mode 100644 index 0000000000000..772e68afa314f --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Metadata/perstistent_config-meta.xml @@ -0,0 +1,54 @@ + + + + + + + + + + string + + integer + + + + string + + integer + + + + string + + integer + + + + string + + integer + + + + string + + integer + + + + string + + integer + + + + + + + diff --git a/app/code/Magento/Persistent/Test/Mftf/README.md b/app/code/Magento/Persistent/Test/Mftf/README.md new file mode 100644 index 0000000000000..14b4ba423ba0b --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Persistent Functional Tests + +The Functional Test Module for **Magento Persistent** module. diff --git a/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 0000000000000..c2220c33a6052 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml new file mode 100644 index 0000000000000..1a02300ff4264 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml @@ -0,0 +1,85 @@ + + + + + + + + + <description value="Checkout data must be restored after page checkout reload."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-91015"/> + <group value="persistent"/> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <actionGroup ref="logout" stepKey="adminLogout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <!-- Add simple product to cart --> + <actionGroup stepKey="addProductToCart1" ref="AddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup stepKey="addProductNavigateToCheckout" ref="NavigateToCheckoutActionGroup"/> + <!-- Fill Shipping Address form --> + <fillField selector="{{GuestCheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{GuestCheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{GuestCheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{GuestCheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{GuestCheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{GuestCheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{GuestCheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{GuestCheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{GuestCheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <!-- Check that have the same values after page reload --> + <amOnPage url="{{CheckoutPage.url}}" stepKey="amOnCheckoutPage2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <seeInField stepKey="seeEmailtOnCheckout" selector="{{GuestCheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" /> + <seeInField stepKey="seeFirstnameOnCheckout" selector="{{GuestCheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstName}}" /> + <seeInField stepKey="seeLastnameOnCheckout" selector="{{GuestCheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastName}}" /> + <seeInField stepKey="seeStreetOnCheckout" selector="{{GuestCheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" /> + <seeInField stepKey="seeCityOnCheckout" selector="{{GuestCheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" /> + <seeInField stepKey="seeStateOnCheckout" selector="{{GuestCheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" /> + <seeInField stepKey="seePostcodeOnCheckout" selector="{{GuestCheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" /> + <seeInField stepKey="seePhoneOnCheckout" selector="{{GuestCheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" /> + <waitForElement selector="{{GuestCheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <!-- Click next button to open shipping section --> + <click selector="{{GuestCheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{GuestCheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <!-- Reload shipping section --> + <amOnPage url="{{GuestCheckoutPage.url}}" stepKey="amOnCheckoutShipToPage"/> + <waitForElement selector="{{GuestCheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> + <!-- Check that address block contains correct information --> + <see stepKey="seeBilllingFirstName" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.firstName}}" /> + <see stepKey="seeBilllingLastName" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.lastName}}" /> + <see stepKey="seeBilllingStreet" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" /> + <see stepKey="seeBilllingCity" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.city}}" /> + <see stepKey="seeBilllingState" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.state}}" /> + <see stepKey="seeBilllingPostcode" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.postcode}}" /> + <see stepKey="seeBilllingTelephone" selector="{{PaymentMethodSection.billingAddress}}" userInput="{{CustomerAddressSimple.telephone}}" /> + <!-- Check that "Ship To" block contains correct information --> + <see stepKey="seeShipToFirstName" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.firstName}}" /> + <see stepKey="seeShipToLastName" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.lastName}}" /> + <see stepKey="seeShipToStreet" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.street[0]}}" /> + <see stepKey="seeShipToCity" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.city}}" /> + <see stepKey="seeShipToState" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.state}}" /> + <see stepKey="seeShipToPostcode" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.postcode}}" /> + <see stepKey="seeShipToTelephone" selector="{{ShipToSection.shippingInformation}}" userInput="{{CustomerAddressSimple.telephone}}" /> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCheckShoppingCartBehaviorAfterSessionExpiredTest.xml new file mode 100644 index 0000000000000..de09d4a02fc3e --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckShoppingCartBehaviorAfterSessionExpiredTest"> + <annotations> + <features value="Persistent"/> + <stories value="MAGETWO-86549 - Unusual behavior with the persistent shopping cart after the session is expired"/> + <title value="Checking behavior with the persistent shopping cart after the session is expired"/> + <description value="Checking behavior with the persistent shopping cart after the session is expired"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96637"/> + <group value="persistent"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <!--Create product and customer--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Roll back configuration--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <!--Delete product and customer--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!--Login as a Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefrontAccount"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <!--Reset cookies and refresh the page--> + <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Check product exists in cart--> + <see userInput="$$createProduct.name$$" stepKey="ProductExistsInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml new file mode 100644 index 0000000000000..963e453e9a460 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest"> + <annotations> + <features value="Persistent"/> + <stories value="MAGETWO-95850 - Incorrect use of cookies for customer"/> + <title value="Checking welcome message for persistent customer after logout"/> + <description value="Checking welcome message for persistent customer after logout"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97095"/> + <group value="persistent"/> + <group value="customer"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + + <!--Create customers--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomerForPersistent"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Roll back configuration--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + + <!-- Logout customer on Storefront--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + <!--Delete customers--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomerForPersistent" stepKey="deleteCustomerForPersistent"/> + </after> + <!--Login as a Customer with remember me unchecked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeUnChecked" stepKey="loginToStorefrontAccountWithRememberMeUnchecked"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check customer name and last name in welcome message--> + <seeCurrentUrlMatches regex="~/customer/account/~" stepKey="seeCustomerAccountPageUrl"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage"/> + <!--Logout and check default welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout"/> + <seeCurrentUrlMatches regex="~/customer/account/logoutSuccess/~" stepKey="seeCustomerSignOutPageUrl"/> + <see userInput="Default welcome msg!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeDefaultWelcomeMessage"/> + + <!--Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="customer" value="$$createCustomerForPersistent$$"/> + </actionGroup> + <!--Check customer name and last name in welcome message--> + <seeCurrentUrlMatches regex="~/customer/account/~" stepKey="seeCustomerAccountPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage1"/> + + <!--Logout and check persistent customer welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout1"/> + <seeCurrentUrlMatches regex="~/customer/account/logoutSuccess/~" stepKey="seeCustomerSignOutPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$! Not you?" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seePersistentWelcomeMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml new file mode 100644 index 0000000000000..ce0ccf8f98667 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontQuoteShippingDataPersistedForGuestTest"> + <annotations> + <features value="Persistent"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Estimate Shipping and Tax block sections on shipping cart saving correctly for Guest."/> + <description value="Verify that 'Estimate Shipping and Tax' block sections on shipping cart saving correctly for Guest after switching to another page. And check that the shopping cart is cleared after reset persistent cookie."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-99048"/> + <useCaseId value="MAGETWO-98569"/> + <group value="persistent"/> + <group value="checkout"/> + </annotations> + <before> + <!--Enabled The Persistent Shopping Cart feature --> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + <!--Create category and simple product--> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Revert persistent configuration to default--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> + </after> + <!--Step 1: Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Step 2: Open the Product Page and add the product to shopping cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsLoggedUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsLoggedUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Step 3: Log out, reset persistent cookie and go to homepage--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <resetCookie userInput="persistent_shopping_cart" stepKey="resetPersistentCookie"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePageAfterResetPersistentCookie"/> + <!--Step 4: Add the product to shopping cart and open cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsGuestUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsGuestUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartBeforeChangeShippingAndTaxSection"/> + <!--Step 5: Open Estimate Shipping and Tax block and fill the sections--> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTax" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectUSCountry"/> + <waitForPageLoad stepKey="waitAfterSelectCountry"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> + <waitForPageLoad stepKey="waitAfterSelectRegion"/> + <fillField selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> + <waitForPageLoad stepKey="waitAfterSelectPostcode"/> + <!--Step 6: Go to Homepage--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> + <waitForLoadingMaskToDisappear stepKey="waitEstimateBlock"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml new file mode 100644 index 0000000000000..ffc310fcf30d9 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Shopping Cart Persistence"/> + <title value="Verify Shopping Cart Persistence under long-term cookie"/> + <description value="Verify Shopping Cart Persistence under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10263"/> + <group value="persistent"/> + <group value="customer"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigSettings" stepKey="persistentConfigSetting"/> + <!--Create Simple Product 1 and Product 2 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimple1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimple2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Set Defaults Persistence configuration--> + <createData entity="PersistentConfigUseSystemValue" stepKey="persistentDefaultsConfiguration"/> + <!--Delete Simple Product 1, Product 2 and Category--> + <deleteData createDataKey="createSimple1" stepKey="deleteSimple1"/> + <deleteData createDataKey="createSimple2" stepKey="deleteSimple2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="RemoveCustomerFromAdminActionGroup" stepKey="deleteJohnSmithCustomer"> + <argument name="customer" value="John_Smith_Customer"/> + </actionGroup> + <actionGroup ref="RemoveCustomerFromAdminActionGroup" stepKey="deleteJohnDoeCustomer"> + <argument name="customer" value="Simple_Customer_Without_Address"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- 1. Go to storefront and click the Create an Account link--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <click selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}" stepKey="clickCreateAnAccountLink" /> + <actionGroup ref="StorefrontAssertPersistentRegistrationPageFields" stepKey="assertPersistentRegistrationPageFields"/> + + <!-- 2. Fill fields for registration, set password and unselect the Remember Me checkbox--> + <actionGroup ref="StorefrontCreateCustomerOnRegisterPageDoNotRememberMe" stepKey="registrationJohnSmithCustomer"> + <argument name="Customer" value="John_Smith_Customer"/> + </actionGroup> + <!--Check customer name and last name in welcome message--> + <actionGroup ref="StorefrontAssertMessageCustomerCreateAccountActionGroup" stepKey="customerCreatedSuccessMessageForJohnSmith"/> + <actionGroup ref="StorefrontAssertCustomerWelcomeMessageActionGroup" stepKey="seeWelcomeMessageForJohnSmithCustomer"> + <argument name="customerFullName" value="{{John_Smith_Customer.fullname}}"/> + </actionGroup> + + <!-- 3. Put Simple Product 1 into Shopping Cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimple1ProductToCartForJohnSmithCustomer"> + <argument name="product" value="$$createSimple1$$"/> + </actionGroup> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple1InMiniCartForJohnSmithCustomer"> + <argument name="productName" value="$$createSimple1.name$$"/> + </actionGroup> + + <!-- 4. Click Sign Out --> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnSmithCustomer"/> + <actionGroup ref="StorefrontWaitCustomerLoggedOut" stepKey="waitJohnSmithCustomerLoggedOut"/> + <actionGroup ref="StorefrontAssertPersistentCustomerWelcomeMessageNotPresentActionGroup" stepKey="dontSeeWelcomeJohnSmithCustomerNotYouMessage"> + <argument name="customerFullName" value="{{John_Smith_Customer.fullname}}"/> + </actionGroup> + <actionGroup ref="AssertMiniCartEmpty" stepKey="assertMiniCartEmptyAfterJohnSmithSignOut" /> + + <!-- 5. Click the Create an Account link again and fill fields for registration of another customer, set password and check the Remember Me checkbox --> + <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="amOnCustomerAccountCreatePage"/> + <actionGroup ref="StorefrontRegisterCustomerRememberMe" stepKey="registrationJohnDoeCustomer"> + <argument name="Customer" value="Simple_Customer_Without_Address"/> + </actionGroup> + <!--Check customer name and last name in welcome message--> + <actionGroup ref="StorefrontAssertMessageCustomerCreateAccountActionGroup" stepKey="customerCreatedSuccessMessageForJohnDoe"/> + <actionGroup ref="StorefrontAssertCustomerWelcomeMessageActionGroup" stepKey="seeWelcomeMessageForJohnDoeCustomer"> + <argument name="customerFullName" value="{{Simple_Customer_Without_Address.fullname}}"/> + </actionGroup> + <!-- 6. Add Simple Product 1 to Shopping Cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimple1ProductToCartForJohnDoeCustomer"> + <argument name="product" value="$$createSimple1$$"/> + </actionGroup> + <see selector="{{StorefrontMinicartSection.productCount}}" userInput="1" stepKey="miniCartContainsOneProductForJohnDoeCustomer"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple1InMiniCartForJohnDoeCustomer"> + <argument name="productName" value="$$createSimple1.name$$"/> + </actionGroup> + + <!-- 7. Click Log Out --> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnDoeCustomer"/> + <actionGroup ref="StorefrontWaitCustomerLoggedOut" stepKey="waitJohnDoeCustomerLoggedOut"/> + <actionGroup ref="StorefrontAssertPersistentCustomerWelcomeMessageActionGroup" stepKey="seeWelcomeForJohnDoeCustomer"> + <argument name="customerFullName" value="{{Simple_Customer_Without_Address.fullname}}"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontMinicartSection.productCount}}" stepKey="waitForCartCounterVisible"/> + <see selector="{{StorefrontMinicartSection.productCount}}" userInput="1" stepKey="miniCartContainsOneProductForGuest"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple1InMiniCartForGuestCustomer"> + <argument name="productName" value="$$createSimple1.name$$"/> + </actionGroup> + + <!-- 8. Go to Shopping Cart and verify Simple Product 1 is present there --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCart" /> + <see selector="{{CheckoutCartProductSection.productName}}" userInput="$$createSimple1.name$$" stepKey="checkSimple1InShoppingCart"/> + + <!-- 9. Add Simple Product 2 to Shopping Cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimple2ProductToCartForGuest"> + <argument name="product" value="$$createSimple2$$"/> + </actionGroup> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple1InMiniCartForGuestCustomerSecondTime"> + <argument name="productName" value="$$createSimple1.name$$"/> + </actionGroup> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple2InMiniCartForGuestCustomer"> + <argument name="productName" value="$$createSimple2.name$$"/> + </actionGroup> + <see selector="{{StorefrontMinicartSection.productCount}}" userInput="2" stepKey="miniCartContainsTwoProductForGuest"/> + + <!-- 10. Go to My Account section --> + <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="amOnCustomerAccountPage"/> + <seeInCurrentUrl url="{{StorefrontCustomerSignInPage.url}}" stepKey="redirectToCustomerAccountLoginPage"/> + <seeElement selector="{{StorefrontCustomerSignInFormSection.customerLoginBlock}}" stepKey="checkSystemRequiresToLogIn"/> + + <!-- 11. Log in as John Doe --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="logInAsJohnDoeCustomer"> + <argument name="customer" value="Simple_Customer_Without_Address"/> + </actionGroup> + <see selector="{{StorefrontMinicartSection.productCount}}" userInput="2" stepKey="miniCartContainsTwoProductForJohnDoeCustomer"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple1InMiniCartForJohnDoeCustomerSecondTime"> + <argument name="productName" value="$$createSimple1.name$$"/> + </actionGroup> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="checkSimple2InMiniCartForJohnDoeCustomer"> + <argument name="productName" value="$$createSimple2.name$$"/> + </actionGroup> + + <!-- 12. Sign out and click the Not you? link --> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnDoeCustomerSecondTime"/> + <actionGroup ref="StorefrontWaitCustomerLoggedOut" stepKey="waitJohnDoeCustomerLoggedOutSecondTime"/> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickOnNotYouLink" /> + <waitForPageLoad stepKey="waitForCustomerLoginPageLoad"/> + <actionGroup ref="AssertMiniCartEmpty" stepKey="assertMiniCartEmptyAfterJohnDoeSignOut" /> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml new file mode 100644 index 0000000000000..86af061cc26f8 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -0,0 +1,193 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Catalog widget"/> + <title value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <description value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-9931"/> + <group value="persistent"/> + <group value="widget"/> + <group value="catalog_widget"/> + <skip> + <issueId value="MC-15906"/> + </skip> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + <createData entity="EnableSynchronizeWidgetProductsWithBackendStorage" stepKey="enableSynchronizeWidgetProductsWithBackendStorage"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + </before> + <after> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <createData entity="DisableSynchronizeWidgetProductsWithBackendStorage" stepKey="disableSynchronizeWidgetProductsWithBackendStorage"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromCustomer"/> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="checkWelcomeMessage"/> + + <!--Open the details page of Simple Product 1, Simple Product 2 and add to cart, get to the category--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSecondSimpleProductProductToCart"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterAddedProductToCart"/> + <!--The Recently Viewed widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSecondSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add Simple Product 1 and Simple Product 2 to Wishlist--> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSimpleProductToWishlist"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterProductAddToWishlist"/> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSecondSimpleProductToWishlist"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The My Wishlist widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInWishlistSidebar"/> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSecondSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add to compare Simple Product and Simple Product 2--> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSimpleProductToCompare" > + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSecondSimpleProductToCompare" > + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The Compare Products widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSecondSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Click Clear all in the Compare Products widget--> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearCompareList"/> + <!--The Recently Compared widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!--The Recently Ordered widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInRecentlyOrderedWidget"/> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Sign out and check that widgets persist the information about the items--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromCustomerToCheckThatWidgetsPersist"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterLogoutFromCustomer"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="checkWelcomeMessageAfterLogoutFromCustomer"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYouAfterLogoutFromCustomer"/> + + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebarAfterLogout"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickLinkNotYou"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClickLinkNotYou"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterClickNotYou"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="checkWelcomeMessageAfterClickLinkNotYou"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyViewedWidget"/> + <dontSee selector="{{StorefrontCustomerWishlistSidebarSection.productTitleByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeProductInWishlistWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyComparedWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyOrderedWidget"/> + + <!--Login to storefront from customer again--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="logInFromCustomerAfterClearLongTermCookie"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckWidgets"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="checkWelcomeMessageAfterLogin"/> + + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductNameInWishlistSidebarAfterLogin"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php index b88b02ab4cfb5..a6ad8b1aaab33 100644 --- a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php @@ -34,44 +34,14 @@ class AdditionalTest extends \PHPUnit\Framework\TestCase protected $contextMock; /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; + private $jsonSerializerMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Persistent\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfigMock; - - /** - * @var \Magento\Framework\App\Cache\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheStateMock; - - /** - * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheMock; - - /** - * @var \Magento\Framework\Session\SidResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sidResolverMock; - - /** - * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject - */ - protected $escaperMock; - - /** - * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $urlBuilderMock; + private $persistentHelperMock; /** * @var \Magento\Persistent\Block\Header\Additional @@ -93,17 +63,7 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, [ - 'getEventManager', - 'getScopeConfig', - 'getCacheState', - 'getCache', - 'getInlineTranslation', - 'getSidResolver', - 'getSession', - 'getEscaper', - 'getUrlBuilder' - ]); + $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, []); $this->customerViewHelperMock = $this->createMock(\Magento\Customer\Helper\View::class); $this->persistentSessionHelperMock = $this->createPartialMock( \Magento\Persistent\Helper\Session::class, @@ -119,103 +79,14 @@ protected function setUp() ['getById'] ); - $this->eventManagerMock = $this->getMockForAbstractClass( - \Magento\Framework\Event\ManagerInterface::class, - [], - '', - false, - true, - true, - ['dispatch'] - ); - $this->scopeConfigMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Config\ScopeConfigInterface::class, - [], - '', - false, - true, - true, - ['getValue'] - ); - $this->cacheStateMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Cache\StateInterface::class, - [], - '', - false, - true, - true, - ['isEnabled'] + $this->jsonSerializerMock = $this->createPartialMock( + \Magento\Framework\Serialize\Serializer\Json::class, + ['serialize'] ); - $this->cacheMock = $this->getMockForAbstractClass( - \Magento\Framework\App\CacheInterface::class, - [], - '', - false, - true, - true, - ['load'] + $this->persistentHelperMock = $this->createPartialMock( + \Magento\Persistent\Helper\Data::class, + ['getLifeTime'] ); - $this->sidResolverMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SidResolverInterface::class, - [], - '', - false, - true, - true, - ['getSessionIdQueryParam'] - ); - $this->sessionMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SessionManagerInterface::class, - [], - '', - false, - true, - true, - ['getSessionId'] - ); - $this->escaperMock = $this->getMockForAbstractClass( - \Magento\Framework\Escaper::class, - [], - '', - false, - true, - true, - ['escapeHtml'] - ); - $this->urlBuilderMock = $this->getMockForAbstractClass( - \Magento\Framework\UrlInterface::class, - [], - '', - false, - true, - true, - ['getUrl'] - ); - - $this->contextMock->expects($this->once()) - ->method('getEventManager') - ->willReturn($this->eventManagerMock); - $this->contextMock->expects($this->once()) - ->method('getScopeConfig') - ->willReturn($this->scopeConfigMock); - $this->contextMock->expects($this->once()) - ->method('getCacheState') - ->willReturn($this->cacheStateMock); - $this->contextMock->expects($this->once()) - ->method('getCache') - ->willReturn($this->cacheMock); - $this->contextMock->expects($this->once()) - ->method('getSidResolver') - ->willReturn($this->sidResolverMock); - $this->contextMock->expects($this->once()) - ->method('getSession') - ->willReturn($this->sessionMock); - $this->contextMock->expects($this->once()) - ->method('getEscaper') - ->willReturn($this->escaperMock); - $this->contextMock->expects($this->once()) - ->method('getUrlBuilder') - ->willReturn($this->urlBuilderMock); $this->additional = $this->objectManager->getObject( \Magento\Persistent\Block\Header\Additional::class, @@ -224,91 +95,48 @@ protected function setUp() 'customerViewHelper' => $this->customerViewHelperMock, 'persistentSessionHelper' => $this->persistentSessionHelperMock, 'customerRepository' => $this->customerRepositoryMock, - 'data' => [] + 'data' => [], + 'jsonSerializer' => $this->jsonSerializerMock, + 'persistentHelper' => $this->persistentHelperMock, ] ); } /** - * Run test toHtml method - * - * @param bool $customerId * @return void - * - * @dataProvider dataProviderToHtml */ - public function testToHtml($customerId) + public function testGetCustomerId() { - $cacheData = false; - $idQueryParam = 'id-query-param'; - $sessionId = 'session-id'; - - $this->additional->setData('cache_lifetime', 789); - $this->additional->setData('cache_key', 'cache-key'); - - $this->eventManagerMock->expects($this->at(0)) - ->method('dispatch') - ->with('view_block_abstract_to_html_before', ['block' => $this->additional]); - $this->eventManagerMock->expects($this->at(1)) - ->method('dispatch') - ->with('view_block_abstract_to_html_after'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with( - 'advanced/modules_disable_output/Magento_Persistent', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - )->willReturn(false); - - // get cache - $this->cacheStateMock->expects($this->at(0)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(true); - // save cache - $this->cacheStateMock->expects($this->at(1)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(false); - - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn($cacheData); - $this->sidResolverMock->expects($this->never()) - ->method('getSessionIdQueryParam') - ->with($this->sessionMock) - ->willReturn($idQueryParam); - $this->sessionMock->expects($this->never()) - ->method('getSessionId') - ->willReturn($sessionId); - - // call protected _toHtml method + $customerId = 1; + /** @var \Magento\Persistent\Model\Session|\PHPUnit_Framework_MockObject_MockObject $sessionMock */ $sessionMock = $this->createPartialMock(\Magento\Persistent\Model\Session::class, ['getCustomerId']); - - $this->persistentSessionHelperMock->expects($this->atLeastOnce()) - ->method('getSession') - ->willReturn($sessionMock); - - $sessionMock->expects($this->atLeastOnce()) + $sessionMock->expects($this->once()) ->method('getCustomerId') ->willReturn($customerId); + $this->persistentSessionHelperMock->expects($this->once()) + ->method('getSession') + ->willReturn($sessionMock); - if ($customerId) { - $this->assertEquals('<span><a >Not you?</a></span>', $this->additional->toHtml()); - } else { - $this->assertEquals('', $this->additional->toHtml()); - } + $this->assertEquals($customerId, $this->additional->getCustomerId()); } /** - * Data provider for dataProviderToHtml method - * - * @return array + * @return void */ - public function dataProviderToHtml() + public function testGetConfig() { - return [ - ['customerId' => 2], - ['customerId' => null], - ]; + $lifeTime = 500; + $arrayToSerialize = ['expirationLifetime' => $lifeTime]; + $serializedArray = '{"expirationLifetime":' . $lifeTime . '}'; + + $this->persistentHelperMock->expects($this->once()) + ->method('getLifeTime') + ->willReturn($lifeTime); + $this->jsonSerializerMock->expects($this->once()) + ->method('serialize') + ->with($arrayToSerialize) + ->willReturn($serializedArray); + + $this->assertEquals($serializedArray, $this->additional->getConfig()); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php index b9285715146a5..c7f84b476fa7e 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php @@ -102,8 +102,7 @@ public function testBeforeSavePaymentInformationAndPlaceOrderCartConvertsToGuest ['setCustomerEmail', 'getAddressesCollection'], false ); - $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('getId')->willReturn($cartId); + $this->checkoutSessionMock->method('getQuoteId')->willReturn($cartId); $this->cartRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); $quoteMock->expects($this->once())->method('setCustomerEmail')->with($email); /** @var \Magento\Framework\Data\Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ diff --git a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php index 7008a9eb25e5d..dd299fe30d646 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php @@ -80,31 +80,18 @@ protected function setUp() ); } + /** + * @return void + */ public function testEmulateWelcomeBlock() { - $customerId = 1; - $customerName = 'Test Customer Name'; - $welcomeMessage = __('Welcome, %1!', $customerName); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + $welcomeMessage = __(' '); $block = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) ->disableOriginalConstructor() ->setMethods(['setWelcome']) ->getMock(); - $headerAdditionalBlock = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) - ->disableOriginalConstructor() - ->getMock(); - $this->persistentSessionMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); - $this->customerRepositoryMock - ->expects($this->once()) - ->method('getById') - ->with($customerId)->willReturn($customerMock); - $this->customerViewHelperMock->expects($this->once())->method('getCustomerName')->willReturn($customerName); - $this->layoutMock->expects($this->once()) - ->method('getBlock') - ->with('header.additional') - ->willReturn($headerAdditionalBlock); $block->expects($this->once())->method('setWelcome')->with($welcomeMessage); + $this->observer->emulateWelcomeBlock($block); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 7d3d1c8487627..93244e70fe549 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Persistent\Test\Unit\Model; use Magento\Persistent\Model\QuoteManager; @@ -247,8 +244,8 @@ public function testConvertCustomerCartToGuest() $emailArgs = ['email' => null]; $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->exactly(2))->method('getId')->willReturn($quoteId); + ->method('getQuoteId')->willReturn($quoteId); + $this->quoteMock->expects($this->once())->method('getId')->willReturn($quoteId); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($quoteId)->willReturn($this->quoteMock); $this->quoteMock->expects($this->once()) ->method('setIsActive')->with(true)->willReturn($this->quoteMock); @@ -288,18 +285,15 @@ public function testConvertCustomerCartToGuest() public function testConvertCustomerCartToGuestWithEmptyQuote() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(null); + ->method('getQuoteId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(null)->willReturn(null); - $this->model->convertCustomerCartToGuest(); } public function testConvertCustomerCartToGuestWithEmptyQuoteId() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(1); + ->method('getQuoteId')->willReturn(1); $quoteWithNoId = $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteWithNoId->expects($this->once())->method('getId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(1)->willReturn($quoteWithNoId); diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/ApplyBlockPersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/ApplyBlockPersistentDataObserverTest.php index 4241b9ba3e3a1..f3ef79cb00135 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/ApplyBlockPersistentDataObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/ApplyBlockPersistentDataObserverTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Persistent\Test\Unit\Observer; class ApplyBlockPersistentDataObserverTest extends \PHPUnit\Framework\TestCase @@ -69,10 +66,10 @@ protected function setUp() $this->blockMock = $this->createMock(\Magento\Framework\View\Element\AbstractBlock::class); $this->persistentConfigMock = $this->createMock(\Magento\Persistent\Model\Persistent\Config::class); $this->model = new \Magento\Persistent\Observer\ApplyBlockPersistentDataObserver( - $this->sessionMock, - $this->persistentHelperMock, - $this->customerSessionMock, - $this->configMock + $this->sessionMock, + $this->persistentHelperMock, + $this->customerSessionMock, + $this->configMock ); } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index a52e22a960e0b..3384f4f73436f 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -1,12 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -49,6 +55,21 @@ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase */ protected $eventManagerMock; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\Magento\Framework\App\RequestInterface + */ + private $requestMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Quote + */ + private $quoteMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CartRepositoryInterface + */ + private $quoteRepositoryMock; + protected function setUp() { $this->sessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); @@ -56,18 +77,30 @@ protected function setUp() $this->persistentHelperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); $this->observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getControllerAction', - '__wakeUp']); + '__wakeUp']); $this->quoteManagerMock = $this->createMock(\Magento\Persistent\Model\QuoteManager::class); $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getRequestUri', 'getServer']) + ->getMockForAbstractClass(); + $this->quoteRepositoryMock = $this->createMock(CartRepositoryInterface::class); + $this->model = new \Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver( $this->sessionMock, $this->persistentHelperMock, $this->quoteManagerMock, $this->eventManagerMock, $this->customerSessionMock, - $this->checkoutSessionMock + $this->checkoutSessionMock, + $this->requestMock, + $this->quoteRepositoryMock ); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->setMethods(['getCustomerIsGuest', 'getIsPersistent']) + ->disableOriginalConstructor() + ->getMock(); } public function testExecuteWhenCanNotApplyPersistentData() @@ -83,35 +116,117 @@ public function testExecuteWhenCanNotApplyPersistentData() public function testExecuteWhenPersistentIsNotEnabled() { + $quoteId = 'quote_id_1'; + $this->persistentHelperMock ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) ->will($this->returnValue(true)); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(false)); + $this->persistentHelperMock->expects($this->exactly(2))->method('isEnabled')->will($this->returnValue(false)); + $this->checkoutSessionMock->expects($this->exactly(2))->method('getQuoteId')->willReturn($quoteId); + $this->quoteRepositoryMock->expects($this->once()) + ->method('getActive') + ->with($quoteId) + ->willThrowException(new NoSuchEntityException()); $this->eventManagerMock->expects($this->never())->method('dispatch'); $this->model->execute($this->observerMock); } - public function testExecuteWhenPersistentIsEnabled() - { + /** + * Test method \Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver::execute when persistent is enabled + * + * @param $refererUri + * @param $requestUri + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expireCounter + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $dispatchCounter + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $setCustomerIdCounter + * @dataProvider requestDataProvider + */ + public function testExecuteWhenPersistentIsEnabled( + $refererUri, + $requestUri, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expireCounter, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $dispatchCounter, + \PHPUnit_Framework_MockObject_Matcher_InvokedCount $setCustomerIdCounter + ) { $this->persistentHelperMock ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) ->will($this->returnValue(true)); - $this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true)); + $this->persistentHelperMock->expects($this->atLeastOnce()) + ->method('isEnabled') + ->will($this->returnValue(true)); + $this->persistentHelperMock->expects($this->atLeastOnce()) + ->method('isShoppingCartPersist') + ->willReturn(true); $this->sessionMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->checkoutSessionMock->expects($this->once())->method('getQuoteId')->will($this->returnValue(10)); - $this->observerMock->expects($this->once())->method('getControllerAction'); - $this->eventManagerMock->expects($this->once())->method('dispatch'); - $this->quoteManagerMock->expects($this->once())->method('expire'); + $this->checkoutSessionMock + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->method('getCustomerIsGuest')->willReturn(true); + $this->quoteMock->method('getIsPersistent')->willReturn(true); $this->customerSessionMock - ->expects($this->once()) + ->expects($this->atLeastOnce()) + ->method('isLoggedIn') + ->will($this->returnValue(false)); + $this->checkoutSessionMock + ->expects($this->atLeastOnce()) + ->method('getQuoteId') + ->will($this->returnValue(10)); + $this->eventManagerMock->expects($dispatchCounter)->method('dispatch'); + $this->quoteManagerMock->expects($expireCounter)->method('expire'); + $this->customerSessionMock + ->expects($setCustomerIdCounter) ->method('setCustomerId') ->with(null) ->will($this->returnSelf()); + $this->requestMock->expects($this->atLeastOnce())->method('getRequestUri')->willReturn($refererUri); + $this->requestMock + ->expects($this->atLeastOnce()) + ->method('getServer') + ->with('HTTP_REFERER') + ->willReturn($requestUri); $this->model->execute($this->observerMock); } + + /** + * Request Data Provider + * + * @return array + */ + public function requestDataProvider() + { + return [ + [ + 'refererUri' => 'checkout', + 'requestUri' => 'index', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'checkout', + 'requestUri' => 'checkout', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'index', + 'requestUri' => 'checkout', + 'expireCounter' => $this->never(), + 'dispatchCounter' => $this->never(), + 'setCustomerIdCounter' => $this->never(), + ], + [ + 'refererUri' => 'index', + 'requestUri' => 'index', + 'expireCounter' => $this->once(), + 'dispatchCounter' => $this->once(), + 'setCustomerIdCounter' => $this->once(), + ], + ]; + } } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/PreventExpressCheckoutObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/PreventExpressCheckoutObserverTest.php index 7749377bbfcd0..669728804af1a 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/PreventExpressCheckoutObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/PreventExpressCheckoutObserverTest.php @@ -122,7 +122,7 @@ public function testPreventExpressCheckoutEmpty() $this->_event->setControllerAction(null); $this->_model->execute($this->_observer); - $this->_event->setControllerAction(new \StdClass()); + $this->_event->setControllerAction(new \stdClass()); $this->_model->execute($this->_observer); $expectedActionName = 'realAction'; diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/RefreshCustomerDataTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/RefreshCustomerDataTest.php index da60f8a3162ac..ca415a8fab5b3 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/RefreshCustomerDataTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/RefreshCustomerDataTest.php @@ -80,6 +80,9 @@ public function testBeforeStart($result, $callCount) $this->observer->execute($observerMock); } + /** + * @return array + */ public function beforeStartDataProvider() { return [ diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/RemovePersistentCookieOnRegisterObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/RemovePersistentCookieOnRegisterObserverTest.php index ef3aa446172c0..2e644ea1cbcf1 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/RemovePersistentCookieOnRegisterObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/RemovePersistentCookieOnRegisterObserverTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Persistent\Test\Unit\Observer; use \Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver; @@ -61,7 +58,8 @@ protected function setUp() $this->persistentSessionMock, $this->persistentDataMock, $this->customerSessionMock, - $this->quoteManagerMock); + $this->quoteManagerMock + ); } public function testExecuteWithPersistentDataThatCanNotBeProcess() diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php new file mode 100644 index 0000000000000..2f0e90e330a1e --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php @@ -0,0 +1,147 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Observer; + +/** + * Test for SetCheckoutSessionPersistentDataObserver. + */ +class SetCheckoutSessionPersistentDataObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver + */ + private $model; + + /** + * @var \Magento\Persistent\Helper\Data| \PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * @var \Magento\Persistent\Helper\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $sessionHelperMock; + + /** + * @var \Magento\Checkout\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $checkoutSessionMock; + + /** + * @var \Magento\Customer\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var \Magento\Persistent\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $persistentSessionMock; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface| \PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observerMock; + + /** + * @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->helperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); + $this->sessionHelperMock = $this->createMock(\Magento\Persistent\Helper\Session::class); + $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); + $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); + $this->observerMock = $this->createMock(\Magento\Framework\Event\Observer::class); + $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getData']); + $this->persistentSessionMock = $this->createPartialMock( + \Magento\Persistent\Model\Session::class, + ['getCustomerId'] + ); + $this->customerRepositoryMock = $this->createMock( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->model = new \Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver( + $this->sessionHelperMock, + $this->customerSessionMock, + $this->helperMock, + $this->customerRepositoryMock + ); + } + + /** + * Test execute method when session is not persistent. + * + * @return void + */ + public function testExecuteWhenSessionIsNotPersistent() + { + $this->observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($this->eventMock); + $this->eventMock->expects($this->once()) + ->method('getData') + ->willReturn($this->checkoutSessionMock); + $this->sessionHelperMock->expects($this->once()) + ->method('isPersistent') + ->willReturn(false); + $this->checkoutSessionMock->expects($this->never()) + ->method('setLoadInactive'); + $this->checkoutSessionMock->expects($this->never()) + ->method('setCustomerData'); + $this->model->execute($this->observerMock); + } + + /** + * Test execute method when session is persistent. + * + * @return void + */ + public function testExecute() + { + $this->observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($this->eventMock); + $this->eventMock->expects($this->once()) + ->method('getData') + ->willReturn($this->checkoutSessionMock); + $this->sessionHelperMock->expects($this->exactly(2)) + ->method('isPersistent') + ->willReturn(true); + $this->customerSessionMock->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->helperMock->expects($this->exactly(2)) + ->method('isShoppingCartPersist') + ->willReturn(true); + $this->persistentSessionMock->expects($this->once()) + ->method('getCustomerId') + ->willReturn(123); + $this->sessionHelperMock->expects($this->once()) + ->method('getSession') + ->willReturn($this->persistentSessionMock); + $this->customerRepositoryMock->expects($this->once()) + ->method('getById') + ->willReturn(1); + $this->checkoutSessionMock->expects($this->never()) + ->method('setLoadInactive'); + $this->checkoutSessionMock->expects($this->once()) + ->method('setCustomerData'); + $this->model->execute($this->observerMock); + } +} diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php deleted file mode 100644 index fd78a6852ea59..0000000000000 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Persistent\Test\Unit\Observer; - -class SetLoadPersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Persistent\Observer\SetLoadPersistentQuoteObserver - */ - protected $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionHelperMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $checkoutSessionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSessionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $observerMock; - - protected function setUp() - { - $this->helperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); - $this->sessionHelperMock = $this->createMock(\Magento\Persistent\Helper\Session::class); - $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->observerMock = $this->createMock(\Magento\Framework\Event\Observer::class); - - $this->model = new \Magento\Persistent\Observer\SetLoadPersistentQuoteObserver( - $this->sessionHelperMock, - $this->helperMock, - $this->customerSessionMock, - $this->checkoutSessionMock - ); - } - - public function testExecuteWhenSessionIsNotPersistent() - { - $this->sessionHelperMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false)); - $this->checkoutSessionMock->expects($this->never())->method('setLoadInactive'); - $this->model->execute($this->observerMock); - } - - public function testExecute() - { - $this->sessionHelperMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); - $this->checkoutSessionMock->expects($this->never())->method('setLoadInactive'); - $this->model->execute($this->observerMock); - } -} diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php index 6724743789cea..ffa829e8456cc 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php @@ -7,6 +7,9 @@ namespace Magento\Persistent\Test\Unit\Observer; +/** + * Observer test for setting "is_persistent" value to quote + */ class SetQuotePersistentDataObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -83,7 +86,6 @@ public function testExecuteWhenQuoteNotExist() ->method('getEvent') ->will($this->returnValue($this->eventManagerMock)); $this->eventManagerMock->expects($this->once())->method('getQuote'); - $this->customerSessionMock->expects($this->never())->method('isLoggedIn'); $this->model->execute($this->observerMock); } @@ -98,8 +100,7 @@ public function testExecuteWhenSessionIsPersistent() ->expects($this->once()) ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(false)); + $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); $this->quoteManagerMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); $this->quoteMock->expects($this->once())->method('setIsPersistent')->with(true); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/UpdateCustomerCookiesObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/UpdateCustomerCookiesObserverTest.php index d9a469a3832d3..ebca9097bd0a7 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/UpdateCustomerCookiesObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/UpdateCustomerCookiesObserverTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Persistent\Test\Unit\Observer; /** @@ -69,8 +67,8 @@ protected function setUp() false ); $this->model = new \Magento\Persistent\Observer\UpdateCustomerCookiesObserver( - $this->sessionHelperMock, - $this->customerRepository + $this->sessionHelperMock, + $this->customerRepository ); } @@ -86,7 +84,10 @@ public function testExecuteWhenCustomerCookieExist() $customerId = 1; $customerGroupId = 2; $cookieMock = - $this->createPartialMock(\Magento\Framework\DataObject::class, ['setCustomerId', 'setCustomerGroupId', '__wakeUp']); + $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['setCustomerId', 'setCustomerGroupId', '__wakeUp'] + ); $this->sessionHelperMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); $this->observerMock ->expects($this->once()) diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index ac065ba413cdb..c19c71946575a 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-persistent", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-checkout": "100.2.*", "magento/module-customer": "101.0.*", @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Persistent/etc/di.xml b/app/code/Magento/Persistent/etc/di.xml index f49d4361acb52..c28426b4f25bf 100644 --- a/app/code/Magento/Persistent/etc/di.xml +++ b/app/code/Magento/Persistent/etc/di.xml @@ -12,4 +12,7 @@ <type name="Magento\Customer\CustomerData\Customer"> <plugin name="section_data" type="Magento\Persistent\Model\Plugin\CustomerData" /> </type> + <type name="Magento\Framework\App\Http\Context"> + <plugin name="persistent_page_cache_variation" type="Magento\Persistent\Model\Plugin\PersistentCustomerContext" /> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index f976f4de79c21..3c33f8a51c418 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -35,4 +35,18 @@ <type name="Magento\Checkout\Model\GuestPaymentInformationManagement"> <plugin name="inject_guest_address_for_nologin" type="Magento\Persistent\Model\Checkout\GuestPaymentInformationManagementPlugin" /> </type> + <type name="Magento\Customer\CustomerData\SectionPoolInterface"> + <arguments> + <argument name="sectionSourceMap" xsi:type="array"> + <item name="persistent" xsi:type="string">Magento\Persistent\CustomerData\Persistent</item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\Block\CustomerData"> + <arguments> + <argument name="expirableSectionNames" xsi:type="array"> + <item name="persistent" xsi:type="string">persistent</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/events.xml b/app/code/Magento/Persistent/etc/frontend/events.xml index 193b9a10818e4..79720695ea6f6 100644 --- a/app/code/Magento/Persistent/etc/frontend/events.xml +++ b/app/code/Magento/Persistent/etc/frontend/events.xml @@ -49,7 +49,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml new file mode 100644 index 0000000000000..16b44c502fc47 --- /dev/null +++ b/app/code/Magento/Persistent/etc/frontend/sections.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> + <action name="persistent/index/unsetCookie"> + <section name="persistent"/> + </action> +</config> diff --git a/app/code/Magento/Persistent/etc/webapi_rest/events.xml b/app/code/Magento/Persistent/etc/webapi_rest/events.xml index 1eff845386bf4..79dffa1834563 100644 --- a/app/code/Magento/Persistent/etc/webapi_rest/events.xml +++ b/app/code/Magento/Persistent/etc/webapi_rest/events.xml @@ -22,7 +22,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/etc/webapi_soap/events.xml b/app/code/Magento/Persistent/etc/webapi_soap/events.xml index 1eff845386bf4..79dffa1834563 100644 --- a/app/code/Magento/Persistent/etc/webapi_soap/events.xml +++ b/app/code/Magento/Persistent/etc/webapi_soap/events.xml @@ -22,7 +22,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/view/frontend/requirejs-config.js b/app/code/Magento/Persistent/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..e30e07c454be5 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Customer/js/customer-data': { + 'Magento_Persistent/js/view/customer-data-mixin': true + } + } + } +}; diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml new file mode 100644 index 0000000000000..40c8674bc025a --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<?php if ($block->getCustomerId()) :?> + <span> + <a <?= /* @noEscape */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> + </span> +<?php endif;?> +<script type="application/javascript"> + window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; +</script> +<script type="text/x-magento-init"> + { + "li.greet.welcome > span.not-logged-in": { + "Magento_Persistent/js/view/additional-welcome": {} + } + } +</script> diff --git a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml index 1e04e8ae3b407..0447b3e1b9cef 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Customer "Remember Me" template */ @@ -15,7 +13,7 @@ ?> <div id="remember-me-box" class="field choice persistent"> <?php $rememberMeId = 'remember_me' . $block->getRandomString(10); ?> - <input type="checkbox" name="persistent_remember_me" class="checkbox" id="<?= $block->escapeHtmlAttr($rememberMeId) ?>"<?php if ($block->isRememberMeChecked()): ?> checked="checked"<?php endif; ?> title="<?= $block->escapeHtmlAttr(__('Remember Me')) ?>" /> + <input type="checkbox" name="persistent_remember_me" class="checkbox" id="<?= $block->escapeHtmlAttr($rememberMeId) ?>"<?php if ($block->isRememberMeChecked()) : ?> checked="checked"<?php endif; ?> title="<?= $block->escapeHtmlAttr(__('Remember Me')) ?>" /> <label for="<?= $block->escapeHtmlAttr($rememberMeId) ?>" class="label"><span><?= $block->escapeHtml(__('Remember Me')) ?></span></label> <span class="tooltip wrapper"> <a class="link tooltip toggle" href="#"><?= $block->escapeHtml(__('What\'s this?')) ?></a> diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js new file mode 100644 index 0000000000000..47949671fed52 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -0,0 +1,55 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate', + 'Magento_Customer/js/customer-data' +], function ($, $t, customerData) { + 'use strict'; + + return { + /** + * Init + */ + init: function () { + var persistent = customerData.get('persistent'); + + if (persistent().fullname === undefined) { + customerData.get('persistent').subscribe(this.replacePersistentWelcome); + } else { + this.replacePersistentWelcome(); + } + }, + + /** + * Replace welcome message for customer with persistent cookie. + */ + replacePersistentWelcome: function () { + var persistent = customerData.get('persistent'), + welcomeElems; + + if (persistent().fullname !== undefined) { + welcomeElems = $('li.greet.welcome > span.not-logged-in'); + + if (welcomeElems.length) { + $(welcomeElems).each(function () { + var html = $t('Welcome, %1!').replace('%1', persistent().fullname); + + $(this).attr('data-bind', html); + $(this).html(html); + }); + } + } + }, + + /** + * @constructor + */ + 'Magento_Persistent/js/view/additional-welcome': function () { + this.init(); + } + }; +}); diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js new file mode 100644 index 0000000000000..855404c6f6f32 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js @@ -0,0 +1,51 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/utils/wrapper' +], function ($, wrapper) { + 'use strict'; + + var mixin = { + + /** + * Check if persistent section is expired due to lifetime. + * + * @param {Function} originFn - Original method. + * @return {Array} + */ + getExpiredSectionNames: function (originFn) { + var expiredSections = originFn(), + storage = $.initNamespaceStorage('mage-cache-storage').localStorage, + currentTimestamp = Math.floor(Date.now() / 1000), + persistentIndex = expiredSections.indexOf('persistent'), + persistentLifeTime = 0, + sectionData; + + if (window.persistent !== undefined && window.persistent.expirationLifetime !== undefined) { + persistentLifeTime = window.persistent.expirationLifetime; + } + + if (persistentIndex !== -1) { + sectionData = storage.get('persistent'); + + if (typeof sectionData === 'object' && + sectionData['data_id'] + persistentLifeTime >= currentTimestamp + ) { + expiredSections.splice(persistentIndex, 1); + } + } + + return expiredSections; + } + }; + + /** + * Override default customer-data.getExpiredSectionNames(). + */ + return function (target) { + return wrapper.extend(target, mixin); + }; +}); diff --git a/app/code/Magento/ProductAlert/Block/Email/AbstractEmail.php b/app/code/Magento/ProductAlert/Block/Email/AbstractEmail.php index b2ac2965a94c4..b98ba0d905ef2 100644 --- a/app/code/Magento/ProductAlert/Block/Email/AbstractEmail.php +++ b/app/code/Magento/ProductAlert/Block/Email/AbstractEmail.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ProductAlert\Block\Email; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -67,11 +65,11 @@ public function __construct( } /** - * Filter malicious code before insert content to email - * - * @param string|array $content - * @return string|array - */ + * Filter malicious code before insert content to email + * + * @param string|array $content + * @return string|array + */ public function getFilteredContent($content) { return $this->_maliciousCode->filter($content); @@ -104,7 +102,7 @@ public function setStore($store) */ public function getStore() { - if (is_null($this->_store)) { + if ($this->_store === null) { $this->_store = $this->_storeManager->getStore(); } return $this->_store; diff --git a/app/code/Magento/ProductAlert/Block/Email/Price.php b/app/code/Magento/ProductAlert/Block/Email/Price.php index 982b0f7f63375..0430a21dc8bfd 100644 --- a/app/code/Magento/ProductAlert/Block/Email/Price.php +++ b/app/code/Magento/ProductAlert/Block/Email/Price.php @@ -15,7 +15,7 @@ class Price extends \Magento\ProductAlert\Block\Email\AbstractEmail /** * @var string */ - protected $_template = 'email/price.phtml'; + protected $_template = 'Magento_ProductAlert::email/price.phtml'; /** * Retrieve unsubscribe url for product diff --git a/app/code/Magento/ProductAlert/Block/Email/Stock.php b/app/code/Magento/ProductAlert/Block/Email/Stock.php index f424e7d7125c4..d01960b8eb855 100644 --- a/app/code/Magento/ProductAlert/Block/Email/Stock.php +++ b/app/code/Magento/ProductAlert/Block/Email/Stock.php @@ -15,7 +15,7 @@ class Stock extends \Magento\ProductAlert\Block\Email\AbstractEmail /** * @var string */ - protected $_template = 'email/stock.phtml'; + protected $_template = 'Magento_ProductAlert::email/stock.phtml'; /** * Retrieve unsubscribe url for product diff --git a/app/code/Magento/ProductAlert/Controller/Add/TestObserver.php b/app/code/Magento/ProductAlert/Controller/Add/TestObserver.php deleted file mode 100644 index 74f03220e59d3..0000000000000 --- a/app/code/Magento/ProductAlert/Controller/Add/TestObserver.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\ProductAlert\Controller\Add; - -use Magento\ProductAlert\Controller\Add as AddController; -use Magento\Framework\DataObject; - -class TestObserver extends AddController -{ - /** - * @return void - */ - public function execute() - { - $object = new DataObject(); - /** @var \Magento\ProductAlert\Model\Observer $observer */ - $observer = $this->_objectManager->get(\Magento\ProductAlert\Model\Observer::class); - $observer->process($object); - } -} diff --git a/app/code/Magento/ProductAlert/Model/ResourceModel/Price.php b/app/code/Magento/ProductAlert/Model/ResourceModel/Price.php index 9317c2b0ebbac..98a7c3cd5a6f6 100644 --- a/app/code/Magento/ProductAlert/Model/ResourceModel/Price.php +++ b/app/code/Magento/ProductAlert/Model/ResourceModel/Price.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ProductAlert\Model\ResourceModel; /** @@ -53,7 +51,10 @@ protected function _construct() */ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { - if (is_null($object->getId()) && $object->getCustomerId() && $object->getProductId() && $object->getWebsiteId() + if ($object->getId() === null + && $object->getCustomerId() + && $object->getProductId() + && $object->getWebsiteId() ) { if ($row = $this->_getAlertRow($object)) { $price = $object->getPrice(); @@ -64,7 +65,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) $object->setStatus(0); } } - if (is_null($object->getAddDate())) { + if ($object->getAddDate() === null) { $object->setAddDate($this->_dateFactory->create()->gmtDate()); } return parent::_beforeSave($object); diff --git a/app/code/Magento/ProductAlert/Model/ResourceModel/Stock.php b/app/code/Magento/ProductAlert/Model/ResourceModel/Stock.php index aba0fca0fafd1..43eba19fa545b 100644 --- a/app/code/Magento/ProductAlert/Model/ResourceModel/Stock.php +++ b/app/code/Magento/ProductAlert/Model/ResourceModel/Stock.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\ProductAlert\Model\ResourceModel; /** @@ -53,14 +51,17 @@ protected function _construct() */ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { - if (is_null($object->getId()) && $object->getCustomerId() && $object->getProductId() && $object->getWebsiteId() + if ($object->getId() === null + && $object->getCustomerId() + && $object->getProductId() + && $object->getWebsiteId() ) { if ($row = $this->_getAlertRow($object)) { $object->addData($row); $object->setStatus(0); } } - if (is_null($object->getAddDate())) { + if ($object->getAddDate() === null) { $object->setAddDate($this->_dateFactory->create()->gmtDate()); $object->setStatus(0); } diff --git a/app/code/Magento/ProductAlert/Test/Mftf/LICENSE.txt b/app/code/Magento/ProductAlert/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ProductAlert/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 <insert your license name here>" 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/app/code/Magento/ProductAlert/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ProductAlert/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ProductAlert/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 <insert your license name here>" 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/app/code/Magento/ProductAlert/Test/Mftf/README.md b/app/code/Magento/ProductAlert/Test/Mftf/README.md new file mode 100644 index 0000000000000..c842458d761a1 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Product Alert Functional Tests + +The Functional Test Module for **Magento Product Alert** module. diff --git a/app/code/Magento/ProductAlert/Test/Unit/Block/Email/StockTest.php b/app/code/Magento/ProductAlert/Test/Unit/Block/Email/StockTest.php index 13bcc2273d93b..815c4f941e683 100644 --- a/app/code/Magento/ProductAlert/Test/Unit/Block/Email/StockTest.php +++ b/app/code/Magento/ProductAlert/Test/Unit/Block/Email/StockTest.php @@ -55,6 +55,9 @@ public function testGetFilteredContent($contentToFilter, $contentFiltered) $this->assertEquals($contentFiltered, $this->_block->getFilteredContent($contentToFilter)); } + /** + * @return array + */ public function getFilteredContentDataProvider() { return [ diff --git a/app/code/Magento/ProductAlert/Test/Unit/Block/Product/View/StockTest.php b/app/code/Magento/ProductAlert/Test/Unit/Block/Product/View/StockTest.php index 886ed6eb3fe27..b9f28bde15d07 100644 --- a/app/code/Magento/ProductAlert/Test/Unit/Block/Product/View/StockTest.php +++ b/app/code/Magento/ProductAlert/Test/Unit/Block/Product/View/StockTest.php @@ -126,6 +126,9 @@ public function testSetTemplateStockUrlNotAllowed($stockAlertAllowed, $productAv $this->assertNull($this->_block->getSignupUrl()); } + /** + * @return array + */ public function setTemplateStockUrlNotAllowedDataProvider() { return [ diff --git a/app/code/Magento/ProductAlert/composer.json b/app/code/Magento/ProductAlert/composer.json index 86e67e6bdc8ae..1089131ef518a 100644 --- a/app/code/Magento/ProductAlert/composer.json +++ b/app/code/Magento/ProductAlert/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-product-alert", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-backend": "100.2.*", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", @@ -13,7 +13,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ProductAlert/view/frontend/templates/email/price.phtml b/app/code/Magento/ProductAlert/view/frontend/templates/email/price.phtml index 9942c94ea887d..054871be52613 100644 --- a/app/code/Magento/ProductAlert/view/frontend/templates/email/price.phtml +++ b/app/code/Magento/ProductAlert/view/frontend/templates/email/price.phtml @@ -4,43 +4,42 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\ProductAlert\Block\Email\Price */ ?> -<?php if ($_products = $block->getProducts()): ?> -<p><?= /* @escapeNotVerified */ __('Price change alert! We wanted you to know that prices have changed for these products:') ?></p> -<table> -<?php /** @var $_product \Magento\Catalog\Model\Product */ ?> -<?php foreach ($_products as $_product): ?> - <tr> - <td class="col photo"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" title="<?= $block->escapeHtml($_product->getName()) ?>" class="product photo"> - <?= $block->getImage($_product, 'product_thumbnail_image', ['class' => 'photo image'])->toHtml() ?> - </a> - </td> - <td class="col item"> - <p> - <strong class="product name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>"><?= $block->escapeHtml($_product->getName()) ?></a> - </strong> - </p> - <?php if ($shortDescription = $block->getFilteredContent($_product->getShortDescription())): ?> - <p><small><?= /* @escapeNotVerified */ $shortDescription ?></small></p> - <?php endif; ?> - <?= $block->getProductPriceHtml( +<?php if ($_products = $block->getProducts()) : ?> + <p><?= $block->escapeHtml(__('Price change alert! We wanted you to know that prices have changed for these products:')) ?></p> + <table> + <?php /** @var $_product \Magento\Catalog\Model\Product */ ?> + <?php foreach ($_products as $_product) : ?> + <tr> + <td class="col photo"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" title="<?= $block->escapeHtml($_product->getName()) ?>" class="product photo"> + <?= $block->getImage($_product, 'product_thumbnail_image', ['class' => 'photo image'])->toHtml() ?> + </a> + </td> + <td class="col item"> + <p> + <strong class="product name"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>"><?= $block->escapeHtml($_product->getName()) ?></a> + </strong> + </p> + <?php if ($shortDescription = $block->getFilteredContent($_product->getShortDescription())) : ?> + <p><small><?= /* @noEscape */ $shortDescription ?></small></p> + <?php endif; ?> + <?= + $block->getProductPriceHtml( $_product, \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, \Magento\Framework\Pricing\Render::ZONE_EMAIL, [ - 'display_label' => __('Price:') + 'display_label' => __('Price:'), ] ); ?> - <p><small><a href="<?= /* @escapeNotVerified */ $block->getProductUnsubscribeUrl($_product->getId()) ?>"><?= /* @escapeNotVerified */ __('Click here to stop alerts for this product.') ?></a></small></p> - </td> - </tr> -<?php endforeach; ?> -</table> -<p><a href="<?= /* @escapeNotVerified */ $block->getUnsubscribeUrl() ?>"><?= /* @escapeNotVerified */ __('Unsubscribe from all price alerts') ?></a></p> + <p><small><a href="<?= $block->escapeUrl($block->getProductUnsubscribeUrl($_product->getId())) ?>"><?= $block->escapeHtml(__('Click here to stop alerts for this product.')) ?></a></small></p> + </td> + </tr> + <?php endforeach; ?> + </table> + <p><a href="<?= $block->escapeUrl($block->getUnsubscribeUrl()) ?>"><?= $block->escapeHtml(__('Unsubscribe from all price alerts')) ?></a></p> <?php endif; ?> diff --git a/app/code/Magento/ProductAlert/view/frontend/templates/email/stock.phtml b/app/code/Magento/ProductAlert/view/frontend/templates/email/stock.phtml index ffacd605517bc..d90a458b4422a 100644 --- a/app/code/Magento/ProductAlert/view/frontend/templates/email/stock.phtml +++ b/app/code/Magento/ProductAlert/view/frontend/templates/email/stock.phtml @@ -4,30 +4,29 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\ProductAlert\Block\Email\Stock */ ?> -<?php if ($_products = $block->getProducts()): ?> -<p><?= /* @escapeNotVerified */ __('In stock alert! We wanted you to know that these products are now available:') ?></p> -<table> -<?php foreach ($_products as $_product): ?> - <tr> - <td class="col photo"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" title="<?= $block->escapeHtml($_product->getName()) ?>" class="product photo"> - <?= $block->getImage($_product, 'product_thumbnail_image', ['class' => 'photo image'])->toHtml() ?> - </a> - </td> - <td class="col item"> - <p> - <strong class="product name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>"><?= $block->escapeHtml($_product->getName()) ?></a> - </strong> - </p> - <?php if ($shortDescription = $block->getFilteredContent($_product->getShortDescription())): ?> - <p><small><?= /* @escapeNotVerified */ $shortDescription ?></small></p> - <?php endif; ?> - <?= $block->getProductPriceHtml( +<?php if ($_products = $block->getProducts()) : ?> + <p><?= $block->escapeHtml(__('In stock alert! We wanted you to know that these products are now available:')) ?></p> + <table> + <?php foreach ($_products as $_product) : ?> + <tr> + <td class="col photo"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" title="<?= $block->escapeHtml($_product->getName()) ?>" class="product photo"> + <?= $block->getImage($_product, 'product_thumbnail_image', ['class' => 'photo image'])->toHtml() ?> + </a> + </td> + <td class="col item"> + <p> + <strong class="product name"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>"><?= $block->escapeHtml($_product->getName()) ?></a> + </strong> + </p> + <?php if ($shortDescription = $block->getFilteredContent($_product->getShortDescription())) : ?> + <p><small><?= /* @noEscape */ $shortDescription ?></small></p> + <?php endif; ?> + <?= + $block->getProductPriceHtml( $_product, \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, \Magento\Framework\Pricing\Render::ZONE_EMAIL, @@ -36,10 +35,10 @@ ] ); ?> - <p><small><a href="<?= /* @escapeNotVerified */ $block->getProductUnsubscribeUrl($_product->getId()) ?>"><?= /* @escapeNotVerified */ __('Click here to stop alerts for this product.') ?></a></small></p> - </td> - </tr> -<?php endforeach; ?> -</table> -<p><a href="<?= /* @escapeNotVerified */ $block->getUnsubscribeUrl() ?>"><?= /* @escapeNotVerified */ __('Unsubscribe from all stock alerts') ?></a></p> + <p><small><a href="<?= $block->escapeUrl($block->getProductUnsubscribeUrl($_product->getId())) ?>"><?= $block->escapeHtml(__('Click here to stop alerts for this product.')) ?></a></small></p> + </td> + </tr> + <?php endforeach; ?> + </table> + <p><a href="<?= $block->escapeUrl($block->getUnsubscribeUrl()) ?>"><?= $block->escapeHtml(__('Unsubscribe from all stock alerts')) ?></a></p> <?php endif; ?> diff --git a/app/code/Magento/ProductVideo/Block/Product/View/Gallery.php b/app/code/Magento/ProductVideo/Block/Product/View/Gallery.php index 0278ec8dadf9f..d6a1929db4d13 100644 --- a/app/code/Magento/ProductVideo/Block/Product/View/Gallery.php +++ b/app/code/Magento/ProductVideo/Block/Product/View/Gallery.php @@ -9,6 +9,7 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\ProductVideo\Block\Product\View; /** @@ -85,6 +86,6 @@ public function getVideoSettingsJson() */ public function getOptionsMediaGalleryDataJson() { - return $this->jsonEncoder->encode([]); + return $this->jsonEncoder->encode([]); } } diff --git a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php index 3658e36a82ec3..53705ee9f6e72 100644 --- a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php +++ b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php @@ -107,10 +107,13 @@ public function execute() { $baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath(); try { + if (!$this->getRequest()->isPost()) { + throw new LocalizedException(__('Invalid request type.')); + } $remoteFileUrl = $this->getRequest()->getParam('remote_image'); $this->validateRemoteFile($remoteFileUrl); $localFileName = Uploader::getCorrectFileName(basename($remoteFileUrl)); - $localTmpFileName = Uploader::getDispretionPath($localFileName) . DIRECTORY_SEPARATOR . $localFileName; + $localTmpFileName = Uploader::getDispersionPath($localFileName) . DIRECTORY_SEPARATOR . $localFileName; $localFilePath = $baseTmpMediaPath . ($localTmpFileName); $localUniqFilePath = $this->appendNewFileName($localFilePath); $this->validateRemoteFileExtensions($localUniqFilePath); @@ -174,7 +177,7 @@ private function validateRemoteFileExtensions($filePath) protected function appendResultSaveRemoteImage($fileName) { $fileInfo = pathinfo($fileName); - $tmpFileName = Uploader::getDispretionPath($fileInfo['basename']) . DIRECTORY_SEPARATOR . $fileInfo['basename']; + $tmpFileName = Uploader::getDispersionPath($fileInfo['basename']) . DIRECTORY_SEPARATOR . $fileInfo['basename']; $result['name'] = $fileInfo['basename']; $result['type'] = $this->imageAdapter->getMimeType(); $result['error'] = 0; diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php index c06ce8355d024..d554b5dd68db2 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ProductVideo\Model\Plugin\Catalog\Product\Gallery; use Magento\ProductVideo\Model\Product\Attribute\Media\ExternalVideoEntryConverter; @@ -19,6 +20,8 @@ class CreateHandler extends AbstractHandler const ADDITIONAL_STORE_DATA_KEY = 'additional_store_data'; /** + * Execute before Plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @param array $arguments @@ -30,6 +33,7 @@ public function beforeExecute( \Magento\Catalog\Model\Product $product, array $arguments = [] ) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ $attribute = $mediaGalleryCreateHandler->getAttribute(); $mediaCollection = $this->getMediaEntriesDataCollection($product, $attribute); if (!empty($mediaCollection)) { @@ -37,12 +41,14 @@ public function beforeExecute( $mediaCollection = $this->addAdditionalStoreData($mediaCollection, $storeDataCollection); $product->setData( $attribute->getAttributeCode(), - $mediaCollection + $product->getData($attribute->getAttributeCode()) + $mediaCollection ); } } /** + * Execute plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product @@ -57,6 +63,12 @@ public function afterExecute( ); if (!empty($mediaCollection)) { + if ($product->getIsDuplicate() === true) { + $mediaCollection = $this->makeAllNewVideos($product->getId(), $mediaCollection); + } + $newVideoCollection = $this->collectNewVideos($mediaCollection); + $this->saveVideoData($newVideoCollection, 0); + $videoDataCollection = $this->collectVideoData($mediaCollection); $this->saveVideoData($videoDataCollection, $product->getStoreId()); $this->saveAdditionalStoreData($videoDataCollection); @@ -66,6 +78,8 @@ public function afterExecute( } /** + * Saves video data + * * @param array $videoDataCollection * @param int $storeId * @return void @@ -79,6 +93,8 @@ protected function saveVideoData(array $videoDataCollection, $storeId) } /** + * Saves additioanal video data + * * @param array $videoDataCollection * @return void */ @@ -95,6 +111,8 @@ protected function saveAdditionalStoreData(array $videoDataCollection) } /** + * Saves video data + * * @param array $item * @return void */ @@ -107,6 +125,8 @@ protected function saveVideoValuesItem(array $item) } /** + * Excludes current store data + * * @param array $mediaCollection * @param int $currentStoreId * @return array @@ -122,6 +142,8 @@ function ($item) use ($currentStoreId) { } /** + * Prepare video data for saving + * * @param array $rowData * @return array */ @@ -139,6 +161,8 @@ protected function prepareVideoRowDataForSave(array $rowData) } /** + * Loads video data + * * @param array $mediaCollection * @param int $excludedStore * @return array @@ -161,6 +185,8 @@ protected function loadStoreViewVideoData(array $mediaCollection, $excludedStore } /** + * Collect video data + * * @param array $mediaCollection * @return array */ @@ -168,10 +194,7 @@ protected function collectVideoData(array $mediaCollection) { $videoDataCollection = []; foreach ($mediaCollection as $item) { - if (!empty($item['media_type']) - && empty($item['removed']) - && $item['media_type'] == ExternalVideoEntryConverter::MEDIA_TYPE_CODE - ) { + if ($this->isVideoItem($item)) { $videoData = $this->extractVideoDataFromRowData($item); $videoDataCollection[] = $videoData; } @@ -181,6 +204,8 @@ protected function collectVideoData(array $mediaCollection) } /** + * Extract video data + * * @param array $rowData * @return array */ @@ -193,6 +218,8 @@ protected function extractVideoDataFromRowData(array $rowData) } /** + * Collect items for additional data adding + * * @param array $mediaCollection * @return array */ @@ -200,11 +227,7 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection { $ids = []; foreach ($mediaCollection as $item) { - if (!empty($item['media_type']) - && empty($item['removed']) - && $item['media_type'] == ExternalVideoEntryConverter::MEDIA_TYPE_CODE - && isset($item['save_data_from']) - ) { + if ($this->isVideoItem($item) && isset($item['save_data_from'])) { $ids[] = $item['save_data_from']; } } @@ -212,30 +235,35 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection } /** + * Add additional data + * * @param array $mediaCollection * @param array $data * @return array */ - protected function addAdditionalStoreData(array $mediaCollection, array $data) + protected function addAdditionalStoreData(array $mediaCollection, array $data): array { - foreach ($mediaCollection as &$mediaItem) { + $return = []; + foreach ($mediaCollection as $key => $mediaItem) { if (!empty($mediaItem['save_data_from'])) { $additionalData = $this->createAdditionalStoreDataCollection($data, $mediaItem['save_data_from']); if (!empty($additionalData)) { $mediaItem[self::ADDITIONAL_STORE_DATA_KEY] = $additionalData; } } + $return[$key] = $mediaItem; } - - return ['images' => $mediaCollection]; + return ['images' => $return]; } /** + * Creates additional video data + * * @param array $storeData * @param int $valueId * @return array */ - protected function createAdditionalStoreDataCollection(array $storeData, $valueId) + protected function createAdditionalStoreDataCollection(array $storeData, $valueId): array { $result = []; foreach ($storeData as $item) { @@ -247,4 +275,66 @@ protected function createAdditionalStoreDataCollection(array $storeData, $valueI return $result; } + + /** + * Collect new videos + * + * @param array $mediaCollection + * @return array + */ + private function collectNewVideos(array $mediaCollection): array + { + $return = []; + foreach ($mediaCollection as $item) { + if ($this->isVideoItem($item) && $this->isNewVideo($item)) { + $return[] = $this->extractVideoDataFromRowData($item); + } + } + return $return; + } + + /** + * Checks if gallery item is video + * + * @param $item + * @return bool + */ + private function isVideoItem($item): bool + { + return !empty($item['media_type']) + && empty($item['removed']) + && $item['media_type'] == ExternalVideoEntryConverter::MEDIA_TYPE_CODE; + } + + /** + * Checks if video is new + * + * @param $item + * @return bool + */ + private function isNewVideo($item): bool + { + return !isset($item['video_url_default'], $item['video_title_default']) + || empty($item['video_url_default']) + || empty($item['video_title_default']); + } + + /** + * Mark all videos as new + * + * @param int $entityId + * @param array $mediaCollection + * @return array + */ + private function makeAllNewVideos($entityId, array $mediaCollection): array + { + foreach ($mediaCollection as $key => $video) { + if ($this->isVideoItem($video)) { + unset($video['video_url_default'], $video['video_title_default']); + $video['entity_id'] = $entityId; + $mediaCollection[$key] = $video; + } + } + return $mediaCollection; + } } diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/ReadHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/ReadHandler.php index a6225fa40c1b3..cd785a583b6b5 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/ReadHandler.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/ReadHandler.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ProductVideo\Model\Plugin\Catalog\Product\Gallery; use Magento\ProductVideo\Model\Product\Attribute\Media\ExternalVideoEntryConverter; @@ -56,8 +57,8 @@ protected function collectVideoEntriesIds(array $mediaCollection) { $ids = []; foreach ($mediaCollection as $item) { - if ($item['media_type'] == ExternalVideoEntryConverter::MEDIA_TYPE_CODE - && !array_key_exists('video_url', $item) + if ($item['media_type'] === ExternalVideoEntryConverter::MEDIA_TYPE_CODE + && !isset($item['video_url']) ) { $ids[] = $item['value_id']; } @@ -73,7 +74,7 @@ protected function collectVideoEntriesIds(array $mediaCollection) protected function loadVideoDataById(array $ids, $storeId = null) { $mainTableAlias = $this->resourceModel->getMainTableAlias(); - $joinConditions = $mainTableAlias.'.value_id = store_value.value_id'; + $joinConditions = $mainTableAlias . '.value_id = store_value.value_id'; if (null !== $storeId) { $joinConditions = implode( ' AND ', @@ -135,10 +136,10 @@ protected function addVideoDataToMediaEntries(array $mediaCollection, array $dat protected function substituteNullsWithDefaultValues(array $rowData) { foreach ($this->getVideoProperties(false) as $key) { - if (empty($rowData[$key]) && !empty($rowData[$key.'_default'])) { - $rowData[$key] = $rowData[$key.'_default']; + if (empty($rowData[$key]) && !empty($rowData[$key . '_default'])) { + $rowData[$key] = $rowData[$key . '_default']; } - unset($rowData[$key.'_default']); + unset($rowData[$key . '_default']); } return $rowData; @@ -151,8 +152,7 @@ protected function substituteNullsWithDefaultValues(array $rowData) protected function getVideoProperties($withDbMapping = true) { $properties = $this->videoPropertiesDbMapping; - unset($properties['value_id']); - unset($properties['store_id']); + unset($properties['value_id'], $properties['store_id']); return $withDbMapping ? $properties : array_keys($properties); } diff --git a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php index a46a90f24d9fa..166963739fe6d 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php @@ -61,13 +61,7 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec 'value.store_id = value_video.store_id', ] ), - [ - 'video_provider' => 'provider', - 'video_url' => 'url', - 'video_title' => 'title', - 'video_description' => 'description', - 'video_metadata' => 'metadata' - ] + [] )->joinLeft( ['default_value_video' => $originalResourceModel->getTable(InstallSchema::GALLERY_VALUE_VIDEO_TABLE)], implode( @@ -77,14 +71,24 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec 'default_value.store_id = default_value_video.store_id', ] ), - [ - 'video_provider_default' => 'provider', - 'video_url_default' => 'url', - 'video_title_default' => 'title', - 'video_description_default' => 'description', - 'video_metadata_default' => 'metadata', - ] - ); + [] + )->columns([ + 'video_provider' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`provider`', '`default_value_video`.`provider`'), + 'video_url' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`url`', '`default_value_video`.`url`'), + 'video_title' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`title`', '`default_value_video`.`title`'), + 'video_description' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`description`', '`default_value_video`.`description`'), + 'video_metadata' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`metadata`', '`default_value_video`.`metadata`'), + 'video_provider_default' => 'default_value_video.provider', + 'video_url_default' => 'default_value_video.url', + 'video_title_default' => 'default_value_video.title', + 'video_description_default' => 'default_value_video.description', + 'video_metadata_default' => 'default_value_video.metadata', + ]); return $select; } diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml new file mode 100644 index 0000000000000..4e14285c8ee56 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AdminProductVideoActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add video in Admin Product page --> + <actionGroup name="addProductVideo"> + <arguments> + <argument name="video" defaultValue="mftfTestProductVideo"/> + </arguments> + + <scrollTo selector="{{AdminProductImagesSection.productImagesToggle}}" x="0" y="-100" stepKey="scrollToArea"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" + dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" + visible="false" + stepKey="openProductVideoSection"/> + <waitForElementVisible selector="{{AdminProductImagesSection.addVideoButton}}" stepKey="waitForAddVideoButtonVisible"/> + <click selector="{{AdminProductImagesSection.addVideoButton}}" stepKey="addVideo"/> + <waitForElementVisible selector="{{AdminProductNewVideoSection.videoUrlTextField}}" stepKey="waitForUrlElementVisible"/> + <fillField selector="{{AdminProductNewVideoSection.videoUrlTextField}}" userInput="{{video.videoUrl}}" stepKey="fillFieldVideoUrl"/> + <fillField selector="{{AdminProductNewVideoSection.videoTitleTextField}}" userInput="{{video.videoTitle}}" stepKey="fillFieldVideoTitle"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementNotVisible selector="{{AdminProductNewVideoSection.saveButtonDisabled}}" time="30" stepKey="waitForSaveButtonVisible"/> + <click selector="{{AdminProductNewVideoSection.saveButton}}" stepKey="saveVideo"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> + + <!-- Assert product video in Admin Product page --> + <actionGroup name="assertProductVideoAdminProductPage"> + <arguments> + <argument name="video" defaultValue="mftfTestProductVideo"/> + </arguments> + <scrollTo selector="{{AdminProductImagesSection.productImagesToggle}}" x="0" y="-100" stepKey="scrollToArea"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" + dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" + visible="false" + stepKey="openProductVideoSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{AdminProductImagesSection.videoTitleText(video.videoShortTitle)}}" stepKey="seeVideoTitle"/> + <seeElementInDOM selector="{{AdminProductImagesSection.videoUrlHiddenField(video.videoUrl)}}" stepKey="seeVideoItem"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml new file mode 100644 index 0000000000000..28634f41deec1 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/StorefrontProductVideoActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Assert product video in Storefront Product page --> + <actionGroup name="assertProductVideoStorefrontProductPage"> + <arguments> + <argument name="dataTypeAttribute" defaultValue="'youtube'"/> + </arguments> + <seeElement selector="{{StorefrontProductInfoMainSection.productVideo(dataTypeAttribute)}}" stepKey="seeProductVideoDataType"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml new file mode 100644 index 0000000000000..8fe5899e91ef8 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoConfigData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- mftf test youtube api key configuration --> + <entity name="ProductVideoYoutubeApiKeyConfig" type="product_video_config"> + <requiredEntity type="youtube_api_key_config">YouTubeApiKey</requiredEntity> + </entity> + <entity name="YouTubeApiKey" type="youtube_api_key_config"> + <data key="value">AIzaSyDwqDWuw1lra-LnpJL2Mr02DYuFmkuRSns</data> + </entity> + + <!-- default configuration used to restore Magento config --> + <entity name="DefaultProductVideoConfig" type="product_video_config"> + <requiredEntity type="youtube_api_key_config">DefaultYouTubeApiKey</requiredEntity> + </entity> + <entity name="DefaultYouTubeApiKey" type="youtube_api_key_config"> + <data key="value"/> + </entity> +</entities> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml new file mode 100644 index 0000000000000..5bc4ad86e0f06 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Data/ProductVideoData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="mftfTestProductVideo" type="product_video"> + <data key="videoUrl">https://youtu.be/bpOSxM0rNPM</data> + <data key="videoTitle">Arctic Monkeys - Do I Wanna Know? (Official Video)</data> + <data key="videoShortTitle">Arctic Monkeys</data> + </entity> +</entities> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/LICENSE.txt b/app/code/Magento/ProductVideo/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/ProductVideo/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 <insert your license name here>" 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/app/code/Magento/ProductVideo/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ProductVideo/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..496bf10ad440f --- /dev/null +++ b/app/code/Magento/ProductVideo/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 © 2016 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 <insert your license name here>" 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/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml b/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml new file mode 100644 index 0000000000000..dc6d3af1c52c5 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Metadata/product_video_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductVideoYouTubeApiKeyConfig" dataType="product_video_config" type="create" auth="adminFormKey" url="admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="product_video_config"> + <object key="product_video" dataType="product_video_config"> + <object key="fields" dataType="product_video_config"> + <object key="youtube_api_key" dataType="youtube_api_key_config"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml new file mode 100644 index 0000000000000..52f13a1da5188 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Page/AdminProductCreatePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <section name="AdminProductNewVideoSection"/> + </page> +</pages> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/README.md b/app/code/Magento/ProductVideo/Test/Mftf/README.md new file mode 100644 index 0000000000000..b2243ad32074c --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Product Video Functional Tests + +The Functional Test Module for **Magento Product Video** module. diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml new file mode 100644 index 0000000000000..913ae8c955340 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductImagesSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductImagesSection"> + <element name="addVideoButton" type="button" selector="#add_video_button" timeout="60"/> + <element name="videoUrlHiddenField" type="text" selector="#media_gallery_content input[value*='{{title}}']" parameterized="true"/> + <element name="videoTitleText" type="text" selector="//*[@id='media_gallery_content']//div[contains(text(), '{{title}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml new file mode 100644 index 0000000000000..8df254aea7c50 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/AdminProductNewVideoSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductNewVideoSection"> + <element name="saveButton" type="button" selector=".action-primary.video-create-button" timeout="30"/> + <element name="saveButtonDisabled" type="text" selector="button.action-primary.video-create-button[disabled='disabled']"/> + <element name="videoUrlTextField" type="input" selector="#video_url"/> + <element name="videoTitleTextField" type="input" selector="#video_title"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..f33f9d8f23244 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="productVideo" type="text" selector=".product-video[data-type='{{videoType}}']" parameterized="true"/> + <element name="clickInVideo" type="button" selector=".fotorama__stage__shaft"/> + <element name="videoPausedMode" type="button" selector="[class*='paused-mode']"/> + <element name="videoPlayedMode" type="button" selector="[class*='playing-mode']"/> + <element name="frameVideo" type="button" selector="widget2"/> + </section> +</sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml new file mode 100644 index 0000000000000..15888d1af8ee5 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontYoutubeVideoWindowOnProductPageTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontYoutubeVideoWindowOnProductPageTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="MAGETWO-87734: [Sigma Beauty]Cannot pause Youtube video in IE 11"/> + <testCaseId value="MAGETWO-96677"/> + <title value="Youtube video window on the product page"/> + <description value="Check Youtube video window on the product page"/> + <severity value="MAJOR"/> + <group value="productVideo"/> + </annotations> + + <before> + <!--Log In--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Set product video Youtube api key configuration --> + <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setStoreConfig"/> + </before> + + <after> + <!--Clear all filters on products grid page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductsIndex"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearExistingProductFilters"/> + <!--Delete created product and category--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategoryFirst"/> + <!-- Set product video configuration to default --> + <createData entity="DefaultProductVideoConfig" stepKey="setStoreDefaultConfig"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open simple product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <actionGroup ref="AdminGridFilterSearchResultsByInput" stepKey="filterProductGridBySku"> + <argument name="inputName" value="sku"/> + <argument name="value" value="$$createProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="openFirstProductForEdit"/> + <waitForPageLoad stepKey="waitProductPageIsLoaded"/> + + <!-- Add product video --> + <actionGroup ref="addProductVideo" stepKey="addProductVideo"/> + <!-- Assert product video in admin product form --> + <actionGroup ref="assertProductVideoAdminProductPage" stepKey="assertProductVideoAdminProductPage"/> + + <!-- Save the product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + + <!-- Assert product video in storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="goToStorefrontProductPage"/> + <actionGroup ref="assertProductVideoStorefrontProductPage" stepKey="assertProductVideoStorefrontProductPage"/> + + <!--Click Play video button--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickToPlayVideo"/> + <wait time="5" stepKey="waitFiveSecondToPlayVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayed"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="assertVideoIsPlayed"/> + <switchToIFrame stepKey="switchBack"/> + + <!--Click Pause button--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickToStopVideo"/> + <wait time="5" stepKey="waitFiveSecondToStopVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame1"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="waitForVideoPaused"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="assertVideoIsPaused"/> + <switchToIFrame stepKey="switchBack1"/> + + <!--Click Play video button again. Make sure that Video continued playing--> + <click selector="{{StorefrontProductInfoMainSection.clickInVideo}}" stepKey="clickAgainToPlayVideo"/> + <wait time="5" stepKey="waitAgainFiveSecondToPlayVideo"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame2"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayedAgain"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="assertVideoIsPlayedAgain"/> + <switchToIFrame stepKey="switchBack2"/> + </test> +</tests> + diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index 28c6db7d31379..cc38e55d26af2 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -6,6 +6,7 @@ namespace Magento\ProductVideo\Test\Unit\Controller\Adminhtml\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -101,14 +102,16 @@ protected function setUp() $this->adapterFactoryMock->expects($this->once())->method('create')->willReturn($this->abstractAdapter); $this->curlMock = $this->createMock(\Magento\Framework\HTTP\Adapter\Curl::class); $this->storageFileMock = $this->createMock(\Magento\MediaStorage\Model\ResourceModel\File\Storage\File::class); - $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->fileDriverMock = $this->createMock(\Magento\Framework\Filesystem\DriverInterface::class); - $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request)); $managerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() ->setMethods(['get']) ->getMockForAbstractClass(); - $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request)); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->request); $this->contextMock->expects($this->any())->method('getObjectManager')->willReturn($managerMock); $this->image = $objectManager->getObject( diff --git a/app/code/Magento/ProductVideo/Test/Unit/Model/Plugin/Catalog/Product/Gallery/CreateHandlerTest.php b/app/code/Magento/ProductVideo/Test/Unit/Model/Plugin/Catalog/Product/Gallery/CreateHandlerTest.php index 5770ea8b5689d..f6a72ee94b577 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Model/Plugin/Catalog/Product/Gallery/CreateHandlerTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Model/Plugin/Catalog/Product/Gallery/CreateHandlerTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ProductVideo\Test\Unit\Model\Plugin\Catalog\Product\Gallery; /** @@ -37,6 +38,9 @@ class CreateHandlerTest extends \PHPUnit\Framework\TestCase */ protected $mediaGalleryCreateHandler; + /** + * {@inheritDoc} + */ protected function setUp() { $this->product = $this->createMock(\Magento\Catalog\Model\Product::class); @@ -62,72 +66,18 @@ protected function setUp() ); } - public function testAfterExecute() + /** + * @dataProvider provideImageForAfterExecute + * @param array $image + * @param array $expectedSave + * @param int $rowSaved + */ + public function testAfterExecute($image, $expectedSave, $rowSaved) { - $mediaData = [ - 'images' => [ - '72mljfhmasfilp9cuq' => [ - 'position' => '3', - 'media_type' => 'external-video', - 'file' => '/i/n/index111111.jpg', - 'value_id' => '4', - 'label' => '', - 'disabled' => '0', - 'removed' => '', - 'video_provider' => 'youtube', - 'video_url' => 'https://www.youtube.com/watch?v=ab123456', - 'video_title' => 'Some second title', - 'video_description' => 'Description second', - 'video_metadata' => 'meta two', - 'role' => '', - ], - 'w596fi79hv1p6wj21u' => [ - 'position' => '4', - 'media_type' => 'image', - 'video_provider' => '', - 'file' => '/h/d/hd_image.jpg', - 'value_id' => '7', - 'label' => '', - 'disabled' => '0', - 'removed' => '', - 'video_url' => '', - 'video_title' => '', - 'video_description' => '', - 'video_metadata' => '', - 'role' => '', - ], - 'tcodwd7e0dirifr64j' => [ - 'position' => '4', - 'media_type' => 'external-video', - 'file' => '/s/a/sample_3.jpg', - 'value_id' => '5', - 'label' => '', - 'disabled' => '0', - 'removed' => '', - 'video_provider' => 'youtube', - 'video_url' => 'https://www.youtube.com/watch?v=ab123456', - 'video_title' => 'Some second title', - 'video_description' => 'Description second', - 'video_metadata' => 'meta two', - 'role' => '', - 'additional_store_data' => [ - 0 => [ - 'store_id' => '0', - 'video_provider' => null, - 'video_url' => 'https://www.youtube.com/watch?v=ab123456', - 'video_title' => 'New Title', - 'video_description' => 'New Description', - 'video_metadata' => 'New metadata', - ], - ] - ], - ], - ]; - $this->product->expects($this->once()) ->method('getData') ->with('media_gallery') - ->willReturn($mediaData); + ->willReturn(['images' => $image]); $this->product->expects($this->once()) ->method('getStoreId') ->willReturn(0); @@ -136,12 +86,149 @@ public function testAfterExecute() ->method('getAttribute') ->willReturn($this->attribute); - $this->subject->afterExecute( - $this->mediaGalleryCreateHandler, - $this->product - ); + $this->resourceModel->expects($this->exactly($rowSaved)) + ->method('saveDataRow') + ->with('catalog_product_entity_media_gallery_value_video', $expectedSave) + ->willReturn(1); + + $this->subject->afterExecute($this->mediaGalleryCreateHandler, $this->product); + } + + /** + * DataProvider for testAfterExecute + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function provideImageForAfterExecute(): array + { + return [ + 'new_video' => [ + [ + '72mljfhmasfilp9cuq' => [ + 'position' => '3', + 'media_type' => 'external-video', + 'file' => '/i/n/index111111.jpg', + 'value_id' => '4', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'video_provider' => 'youtube', + 'video_url' => 'https://www.youtube.com/watch?v=ab123456', + 'video_title' => 'Some second title', + 'video_description' => 'Description second', + 'video_metadata' => 'meta two', + 'role' => '', + ], + ], + [ + 'value_id' => '4', + 'store_id' => 0, + 'provider' => 'youtube', + 'url' => 'https://www.youtube.com/watch?v=ab123456', + 'title' => 'Some second title', + 'description' => 'Description second', + 'metadata' => 'meta two', + ], + 2 + ], + 'image' => [ + [ + 'w596fi79hv1p6wj21u' => [ + 'position' => '4', + 'media_type' => 'image', + 'video_provider' => '', + 'file' => '/h/d/hd_image.jpg', + 'value_id' => '7', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'video_url' => '', + 'video_title' => '', + 'video_description' => '', + 'video_metadata' => '', + 'role' => '', + ], + ], + [], + 0 + ], + 'new_video_with_additional_data' => [ + [ + 'tcodwd7e0dirifr64j' => [ + 'position' => '4', + 'media_type' => 'external-video', + 'file' => '/s/a/sample_3.jpg', + 'value_id' => '5', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'video_provider' => 'youtube', + 'video_url' => 'https://www.youtube.com/watch?v=ab123456', + 'video_title' => 'Some second title', + 'video_description' => 'Description second', + 'video_metadata' => 'meta two', + 'role' => '', + 'additional_store_data' => [ + 0 => [ + 'store_id' => 0, + 'video_provider' => 'youtube', + 'video_url' => 'https://www.youtube.com/watch?v=ab123456', + 'video_title' => 'Some second title', + 'video_description' => 'Description second', + 'video_metadata' => 'meta two', + ], + ] + ], + ], + [ + 'value_id' => '5', + 'store_id' => 0, + 'provider' => 'youtube', + 'url' => 'https://www.youtube.com/watch?v=ab123456', + 'title' => 'Some second title', + 'description' => 'Description second', + 'metadata' => 'meta two', + ], + 3 + ], + 'not_new_video' => [ + [ + '72mljfhmasfilp9cuq' => [ + 'position' => '3', + 'media_type' => 'external-video', + 'file' => '/i/n/index111111.jpg', + 'value_id' => '4', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'video_provider' => 'youtube', + 'video_url' => 'https://www.youtube.com/watch?v=ab123456', + 'video_url_default' => 'https://www.youtube.com/watch?v=ab123456', + 'video_title' => 'Some second title', + 'video_title_default' => 'Some second title', + 'video_description' => 'Description second', + 'video_metadata' => 'meta two', + 'role' => '', + ], + ], + [ + 'value_id' => '4', + 'store_id' => 0, + 'provider' => 'youtube', + 'url' => 'https://www.youtube.com/watch?v=ab123456', + 'title' => 'Some second title', + 'description' => 'Description second', + 'metadata' => 'meta two', + ], + 1 + ], + ]; } + /** + * Tests empty media gallery + */ public function testAfterExecuteEmpty() { $this->product->expects($this->once()) diff --git a/app/code/Magento/ProductVideo/composer.json b/app/code/Magento/ProductVideo/composer.json index d40707a06de8a..6553ac5cb3605 100644 --- a/app/code/Magento/ProductVideo/composer.json +++ b/app/code/Magento/ProductVideo/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-product-video", "description": "Add Video to Products", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-backend": "100.2.*", "magento/module-eav": "101.0.*", @@ -16,7 +16,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.8", "license": [ "proprietary" ], diff --git a/app/code/Magento/ProductVideo/i18n/de_DE.csv b/app/code/Magento/ProductVideo/i18n/de_DE.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/de_DE.csv +++ b/app/code/Magento/ProductVideo/i18n/de_DE.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/en_US.csv b/app/code/Magento/ProductVideo/i18n/en_US.csv index 3277843424bcf..debcab151cc91 100644 --- a/app/code/Magento/ProductVideo/i18n/en_US.csv +++ b/app/code/Magento/ProductVideo/i18n/en_US.csv @@ -40,4 +40,4 @@ Delete,Delete "Autostart base video","Autostart base video" "Show related video","Show related video" "Auto restart video","Auto restart video" -"Images And Videos","Images And Videos" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/es_ES.csv b/app/code/Magento/ProductVideo/i18n/es_ES.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/es_ES.csv +++ b/app/code/Magento/ProductVideo/i18n/es_ES.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/fr_FR.csv b/app/code/Magento/ProductVideo/i18n/fr_FR.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/fr_FR.csv +++ b/app/code/Magento/ProductVideo/i18n/fr_FR.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/nl_NL.csv b/app/code/Magento/ProductVideo/i18n/nl_NL.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/nl_NL.csv +++ b/app/code/Magento/ProductVideo/i18n/nl_NL.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/pt_BR.csv b/app/code/Magento/ProductVideo/i18n/pt_BR.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/pt_BR.csv +++ b/app/code/Magento/ProductVideo/i18n/pt_BR.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv +++ b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml index f5a22c50e6d0d..63bd5321ad30b 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml +++ b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <css src="Magento_ProductVideo::css/gallery-delete-tooltip.css"/> + </head> <body> <referenceContainer name="content"> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index f0ae057bc724d..1548770d4032f 100755 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -4,17 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Files.LineLength +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ -$elementName = $block->getElement()->getName() . '[images]'; -$formName = $block->getFormName(); +$elementNameEscaped = $block->escapeHtmlAttr($block->getElement()->getName()) . '[images]'; +$formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); ?> <div class="row"> <div class="add-video-button-container"> <button id="add_video_button" - title="<?= $block->escapeHtml(__('Add Video')) ?>" + title="<?= $block->escapeHtmlAttr(__('Add Video')) ?>" data-role="add-video-button" type="button" class="action-secondary" @@ -29,109 +30,94 @@ $formName = $block->getFormName(); $element = $block->getElement(); $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> - -<div id="<?= $block->getHtmlId() ?>" +<div id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>" class="gallery" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" - data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" - data-types="<?= $block->escapeHtml( - $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) - ) ?>" + data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" + data-types='<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) ?>' > - - <?php - if (!$block->getElement()->getReadonly()): - ?> + <?php if (!$block->getElement()->getReadonly()) : ?> <div class="image image-placeholder"> - <?php /* @escapeNotVerified */ echo $block->getUploaderHtml(); - ?> + <?= $block->getUploaderHtml(); ?> <div class="product-image-wrapper"> <p class="image-placeholder-text"> - <?= $block->escapeHtml( - __('Browse to find or drag image here') - ); ?> + <?= $block->escapeHtml(__('Browse to find or drag image here')); ?> </p> </div> </div> - <?= /* @escapeNotVerified */ $block->getChildHtml('additional_buttons') ?> - <?php - endif; - ?> - <?php - foreach ($block->getImageTypes() as $typeData): - ?> - <input name="<?= $block->escapeHtml($typeData['name']) ?>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" - class="image-<?= $block->escapeHtml($typeData['code']) ?>" + <?= $block->getChildHtml('additional_buttons') ?> + <?php endif; ?> + <?php foreach ($block->getImageTypes() as $typeData) : ?> + <input name="<?= $block->escapeHtmlAttr($typeData['name']) ?>" + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" + class="image-<?= $block->escapeHtmlAttr($typeData['code']) ?>" type="hidden" - value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php - endforeach; - ?> - <script id="<?= /* @escapeNotVerified */ $block->getHtmlId() ?>-template" data-template="image" type="text/x-magento-template"> + value="<?= $block->escapeHtmlAttr($typeData['value']) ?>"/> + <?php endforeach; ?> + <script id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-template" data-template="image" type="text/x-magento-template"> <div class="image item <% if (data.disabled == 1) { %>hidden-for-front<% } %> <% if (data.video_url) { %>video-item<% } %>" data-role="image"> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][position]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][position]" value="<%- data.position %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" class="position"/> <% if (data.media_type !== 'external-video') {%> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][media_type]" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][media_type]" + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" value="image"/> <% } else { %> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][media_type]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][media_type]" value="<%- data.media_type %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <% } %> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][video_provider]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][video_provider]" value="<%- data.video_provider %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][file]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][file]" value="<%- data.file %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][value_id]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][value_id]" value="<%- data.value_id %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][label]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][label]" value="<%- data.label %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][disabled]" value="<%- data.disabled %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][removed]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][removed]" value="" class="is-removed" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][video_url]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][video_url]" value="<%- data.video_url %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][video_title]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][video_title]" value="<%- data.video_title %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][video_description]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][video_description]" value="<%- data.video_description %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][video_metadata]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][video_metadata]" value="<%- data.video_metadata %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <input type="hidden" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][role]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][role]" value="<%- data.video_description %>" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>"/> + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>"/> <div class="product-image-wrapper"> <img class="product-image" @@ -140,35 +126,30 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to alt="<%- data.label %>"/> <div class="actions"> - <button type="button" - class="action-remove" - data-role="delete-button" - title="<% if (data.media_type == 'external-video') {%> - <?= $block->escapeHtml( - __('Delete video') - ); ?> - <%} else {%> - <?= $block->escapeHtml( - __('Delete image') - ); ?> - <%}%>"> - <span> - <% if (data.media_type == 'external-video') { %> - <?= $block->escapeHtml( - __('Delete video') - ); ?> - <% } else {%> - <?= $block->escapeHtml( - __('Delete image') - ); ?> - <%} %> - </span> - </button> + <div class="tooltip"> + <span class="delete-tooltiptext"> + <?= $block->escapeHtml(__('Delete image in all store views')); ?> + </span> + <button type="button" + class="action-remove" + data-role="delete-button" + title="<% if (data.media_type == 'external-video') {%> + <?= $block->escapeHtmlAttr(__('Delete video')); ?> + <%} else {%> + <?= $block->escapeHtmlAttr(__('Delete image')); ?> + <%}%>"> + <span> + <% if (data.media_type == 'external-video') { %> + <?= $block->escapeHtml(__('Delete video')); ?> + <% } else {%> + <?= $block->escapeHtml(__('Delete image')); ?> + <%} %> + </span> + </button> + </div> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml( - __('Hidden') - ); ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span></div> </div> <div class="item-description"> @@ -183,19 +164,11 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <ul class="item-roles" data-role="roles-labels"> - <?php - foreach ($block->getImageTypes() as $typeData): - ?> - <li data-role-code="<?= $block->escapeHtml( - $typeData['code'] - ) ?>" class="item-role item-role-<?= $block->escapeHtml( - $typeData['code'] - ) ?>"> + <?php foreach ($block->getImageTypes() as $typeData) : ?> + <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> <?= $block->escapeHtml($typeData['label']) ?> </li> - <?php - endforeach; - ?> + <?php endforeach; ?> </ul> </div> </script> @@ -215,24 +188,20 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <fieldset class="admin__fieldset fieldset-image-panel"> <div class="admin__field field-image-description"> <label class="admin__field-label" for="image-description"> - <span><?= /* @escapeNotVerified */ __('Alt Text') ?></span> + <span><?= $block->escapeHtml(__('Alt Text')) ?></span> </label> <div class="admin__field-control"> <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?php /* @escapeNotVerified */ - echo $elementName - ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> </div> </div> <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml( - __('Role') - ); ?></span> + <span><?= $block->escapeHtml(__('Role')); ?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> @@ -243,36 +212,30 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <label> <input class="image-type" data-role="type-selector" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" type="checkbox" - value="<?= $block->escapeHtml( - $attribute->getAttributeCode() - ) ?>" + value="<?= $block->escapeHtmlAttr($attribute->getAttributeCode()) ?>" /> - <?php /* @escapeNotVerified */ echo $block->escapeHtml( - $attribute->getFrontendLabel() - ) ?> + <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> </label> </li> - <?php - endforeach; - ?> + <?php endforeach; ?> </ul> </div> </div> <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Image Size') ?></span> + <span><?= $block->escapeHtml(__('Image Size')) ?></span> </label> - <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{size}') ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{size}')) ?>"></div> </div> <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> <label class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Image Resolution') ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> </label> - <div class="admin__field-value" data-message="<?= /* @escapeNotVerified */ __('{width}^{height} px') ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -281,16 +244,14 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <input type="checkbox" id="hide-from-product-page" data-role="visibility-trigger" - data-form-part="<?= /* @escapeNotVerified */ $formName ?>" + data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" value="1" class="admin__control-checkbox" - name="<?= /* @escapeNotVerified */ $elementName ?>[<%- data.file_id %>][disabled]" + name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][disabled]" <% if (data.disabled == 1) { %>checked="checked"<% } %> /> <label for="hide-from-product-page" class="admin__field-label"> - <?= $block->escapeHtml( - __('Hide from Product Page') - ); ?> + <?= $block->escapeHtml(__('Hide from Product Page')); ?> </label> </div> </div> @@ -299,28 +260,20 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> </script> <div id="<?= /* @noEscape */ $block->getNewVideoBlockName() ?>" style="display:none"> - <?= /* @escapeNotVerified */ $block->getFormHtml() ?> + <?= $block->getFormHtml() ?> <div id="video-player-preview-location" class="video-player-sidebar"> <div class="video-player-container"></div> <div class="video-information title"> - <label><?= $block->escapeHtml( - __('Title:') - ); ?> </label><span></span> + <label><?= $block->escapeHtml(__('Title:')); ?> </label><span></span> </div> <div class="video-information uploaded"> - <label><?= $block->escapeHtml( - __('Uploaded:') - ); ?> </label><span></span> + <label><?= $block->escapeHtml(__('Uploaded:')); ?> </label><span></span> </div> <div class="video-information uploader"> - <label><?= $block->escapeHtml( - __('Uploader:') - ); ?> </label><span></span> + <label><?= $block->escapeHtml(__('Uploader:')); ?> </label><span></span> </div> <div class="video-information duration"> - <label><?= $block->escapeHtml( - __('Duration:') - ); ?> </label><span></span> + <label><?= $block->escapeHtml(__('Duration:')); ?> </label><span></span> </div> </div> </div> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml index dc0d8206571dd..e1dcab9e8b2d4 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml @@ -8,30 +8,30 @@ <div class="add-video-button-container"> <button id="add_video_button" - title="<?= /* @escapeNotVerified */ $addVideoTitle ?>" + title="<?= $block->escapeHtmlAttr($addVideoTitle) ?>" type="button" class="action-secondary" onclick="jQuery('#new-video').modal('openModal'); jQuery('#new_video_form')[0].reset();" data-ui-id="widget-button-1"> - <span><?= /* @escapeNotVerified */ __('Add Video') ?></span> + <span><?= $block->escapeHtml(__('Add Video')) ?></span> </button> </div> </div> -<div id="<?= /* @escapeNotVerified */ $htmlId ?>-container" +<div id="<?= $block->escapeHtmlAttr($htmlId) ?>-container" class="images" data-mage-init='{"baseImage":{}}' - data-max-file-size="<?= /* @escapeNotVerified */ $fileMaxSize ?>" + data-max-file-size="<?= $block->escapeHtmlAttr($fileMaxSize) ?>" > <div class="image image-placeholder"> - <input type="file" name="image" data-url="<?= /* @escapeNotVerified */ $uploadUrl ?>" multiple="multiple" /> - <img class="spacer" src="<?= /* @escapeNotVerified */ $spacerImage ?>"/> - <p class="image-placeholder-text"><?= /* @escapeNotVerified */ $imagePlaceholderText ?></p> + <input type="file" name="image" data-url="<?= $block->escapeUrl($uploadUrl) ?>" multiple="multiple" /> + <img class="spacer" src="<?= $block->escapeUrl($spacerImage) ?>"/> + <p class="image-placeholder-text"><?= $block->escapeHtml($imagePlaceholderText) ?></p> </div> - <script id="<?= /* @escapeNotVerified */ $htmlId ?>-template" + <script id="<?= $block->escapeHtmlAttr($htmlId) ?>-template" data-template="image" type="text/x-magento-template"> <div class="image"> - <img class="spacer" src="<?= /* @escapeNotVerified */ $spacerImage ?>"/> + <img class="spacer" src="<?= $block->escapeUrl($spacerImage) ?>"/> <img class="product-image" src="<%- data.url %>" @@ -42,25 +42,25 @@ type="button" class="action-delete" data-role="delete-button" - title="<?= /* @escapeNotVerified */ $deleteImageText ?>"> - <span><?= /* @escapeNotVerified */ $deleteImageText ?></span> + title="<?= $block->escapeHtmlAttr($deleteImageText) ?>"> + <span><?= $block->escapeHtml($deleteImageText) ?></span> </button> <button type="button" class="action-make-base" data-role="make-base-button" - title="<?= /* @escapeNotVerified */ $makeBaseText ?>"> - <span><?= /* @escapeNotVerified */ $makeBaseText ?></span> + title="<?= $block->escapeHtmlAttr($makeBaseText) ?>"> + <span><?= $block->escapeHtml($makeBaseText) ?></span> </button> <div class="draggable-handle"></div> </div> <div class="image-label"></div> - <div class="image-fade"><span><?= /* @escapeNotVerified */ $hiddenText ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml($hiddenText) ?></span></div> </div> </script> </div> <span class="action-manage-images" data-activate-tab="image-management"> - <span><?= /* @escapeNotVerified */ $imageManagementText ?></span> + <span><?= $block->escapeHtml($imageManagementText) ?></span> </span> <script> require([ diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml index e9421f8e74a87..7de3042b56ab5 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml @@ -5,23 +5,23 @@ */ /* @var Magento\ProductVideo\Block\Adminhtml\Product\Edit\NewVideo $block */ ?> -<div id="<?= /* @escapeNotVerified */ $block->getNameInLayout() ?>" style="display:none" - data-modal-info='<?= /* @escapeNotVerified */ $block->getWidgetOptions() ?>' +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none" + data-modal-info='<?= /* @noEscape */ $block->getWidgetOptions() ?>' > - <?= /* @escapeNotVerified */ $block->getFormHtml() ?> + <?= $block->getFormHtml() ?> <div id="video-player-preview-location" class="video-player-sidebar"> <div class="video-player-container"></div> <div class="video-information title"> - <label><?= /* @escapeNotVerified */ __('Title:') ?> </label><span></span> + <label><?= $block->escapeHtml(__('Title:')) ?> </label><span></span> </div> <div class="video-information uploaded"> - <label><?= /* @escapeNotVerified */ __('Uploaded:') ?> </label><span></span> + <label><?= $block->escapeHtml(__('Uploaded:')) ?> </label><span></span> </div> <div class="video-information uploader"> - <label><?= /* @escapeNotVerified */ __('Uploader:') ?> </label><span></span> + <label><?= $block->escapeHtml(__('Uploader:')) ?> </label><span></span> </div> <div class="video-information duration"> - <label><?= /* @escapeNotVerified */ __('Duration:') ?> </label><span></span> + <label><?= $block->escapeHtml(__('Duration:')) ?> </label><span></span> </div> </div> </div> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css new file mode 100644 index 0000000000000..835a22683b157 --- /dev/null +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +.gallery .tooltip .delete-tooltiptext { + visibility: hidden; + width: 112px; + background-color: #373330; + color: #F7F3EB; + text-align: center; + padding: 5px 0; + position: absolute; + z-index: 1; + left: 30px; + top: 91px; +} + +.gallery .tooltip:hover .delete-tooltiptext { + visibility: visible; +} diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js index ca920e8740978..653434f1008ca 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js @@ -86,6 +86,7 @@ define([ this._height = this.element.data('height'); this._autoplay = !!this.element.data('autoplay'); this._playing = this._autoplay || false; + this.useYoutubeNocookie = this.element.data('youtubenocookie') || false; this._responsive = this.element.data('responsive') !== false; @@ -163,6 +164,12 @@ define([ * @private */ 'youtubeapiready': function () { + var host = 'https://www.youtube.com'; + + if (self.useYoutubeNocookie) { + host = 'https://www.youtube-nocookie.com'; + } + if (self._player !== undefined) { return; } @@ -177,6 +184,7 @@ define([ width: self._width, videoId: self._code, playerVars: self._params, + host: host, events: { /** @@ -302,7 +310,7 @@ define([ additionalParams += '&autoplay=1'; } - src = window.location.protocol + '//player.vimeo.com/video/' + + src = 'https://player.vimeo.com/video/' + this._code + '?api=1&player_id=vimeo' + this._code + timestamp + @@ -469,7 +477,8 @@ define([ description: tmp.snippet.description, thumbnail: tmp.snippet.thumbnails.high.url, videoId: videoInfo.id, - videoProvider: videoInfo.type + videoProvider: videoInfo.type, + useYoutubeNocookie: videoInfo.useYoutubeNocookie }; this._videoInformation = respData; this.element.trigger(this._UPDATE_VIDEO_INFORMATION_TRIGGER, respData); @@ -525,7 +534,7 @@ define([ ); } else if (type === 'vimeo') { $.ajax({ - url: window.location.protocol + '//www.vimeo.com/api/v2/video/' + id + '.json', + url: 'https://www.vimeo.com/api/v2/video/' + id + '.json', dataType: 'jsonp', data: { format: 'json' @@ -600,7 +609,8 @@ define([ var id, type, ampersandPosition, - vimeoRegex; + vimeoRegex, + useYoutubeNocookie = false; if (typeof href !== 'string') { return href; @@ -620,9 +630,13 @@ define([ id = id.substring(0, ampersandPosition); } - } else if (href.host.match(/youtube\.com|youtu\.be/)) { + } else if (href.host.match(/youtube\.com|youtu\.be|youtube-nocookie.com/)) { id = href.pathname.replace(/^\/(embed\/|v\/)?/, '').replace(/\/.*/, ''); type = 'youtube'; + + if (href.host.match(/youtube-nocookie.com/)) { + useYoutubeNocookie = true; + } } else if (href.host.match(/vimeo\.com/)) { type = 'vimeo'; vimeoRegex = new RegExp(['https?:\\/\\/(?:www\\.|player\\.)?vimeo.com\\/(?:channels\\/(?:\\w+\\/)', @@ -640,7 +654,7 @@ define([ } return id ? { - id: id, type: type, s: href.search.replace(/^\?/, '') + id: id, type: type, s: href.search.replace(/^\?/, ''), useYoutubeNocookie: useYoutubeNocookie } : false; } }); diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js index 1ab10c95a51bc..e9b234c5f1160 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js @@ -21,6 +21,7 @@ define([ container: '.video-player-container', videoClass: 'product-video', reset: false, + useYoutubeNocookie: false, metaData: { DOM: { title: '.video-information.title span', @@ -86,34 +87,36 @@ define([ * @private */ _doUpdate: function () { + var uploaderLinkUrl, + uploaderLink; + this.reset(); - this.element.find(this.options.container).append('<div class="' + - this.options.videoClass + - '" data-type="' + - this.options.videoProvider + - '" data-code="' + - this.options.videoId + - '" data-width="100%" data-height="100%"></div>'); + this.element.find(this.options.container).append( + '<div class="' + + this.options.videoClass + + '" data-type="' + + this.options.videoProvider + + '" data-code="' + + this.options.videoId + + '" data-youtubenocookie="' + + this.options.useYoutubeNocookie + + '" data-width="100%" data-height="100%"></div>' + ); this.element.find(this.options.metaData.DOM.wrapper).show(); this.element.find(this.options.metaData.DOM.title).text(this.options.metaData.data.title); this.element.find(this.options.metaData.DOM.uploaded).text(this.options.metaData.data.uploaded); this.element.find(this.options.metaData.DOM.duration).text(this.options.metaData.data.duration); if (this.options.videoProvider === 'youtube') { - this.element.find(this.options.metaData.DOM.uploader).html( - '<a href="https://youtube.com/channel/' + - this.options.metaData.data.uploaderUrl + - '" target="_blank">' + - this.options.metaData.data.uploader + - '</a>' - ); + uploaderLinkUrl = 'https://youtube.com/channel/' + this.options.metaData.data.uploaderUrl; } else if (this.options.videoProvider === 'vimeo') { - this.element.find(this.options.metaData.DOM.uploader).html( - '<a href="' + - this.options.metaData.data.uploaderUrl + - '" target="_blank">' + this.options.metaData.data.uploader + - '</a>'); + uploaderLinkUrl = this.options.metaData.data.uploaderUrl; } + uploaderLink = document.createElement('a'); + uploaderLink.setAttribute('href', uploaderLinkUrl); + uploaderLink.setAttribute('target', '_blank'); + uploaderLink.innerText = this.options.metaData.data.uploader; + this.element.find(this.options.metaData.DOM.uploader)[0].appendChild(uploaderLink); this.element.find('.' + this.options.videoClass).productVideoLoader(); }, @@ -337,6 +340,7 @@ define([ .createVideoPlayer({ videoId: data.videoId, videoProvider: data.videoProvider, + useYoutubeNocookie: data.useYoutubeNocookie, reset: false, metaData: { DOM: { diff --git a/app/code/Magento/ProductVideo/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/ProductVideo/view/frontend/templates/product/view/gallery.phtml index 55486dbbb0cfe..bfa394b5e7007 100644 --- a/app/code/Magento/ProductVideo/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/frontend/templates/product/view/gallery.phtml @@ -14,8 +14,8 @@ { "[data-gallery-role=gallery-placeholder]": { "Magento_ProductVideo/js/fotorama-add-video-events": { - "videoData": <?= /* @escapeNotVerified */ $block->getMediaGalleryDataJson() ?>, - "videoSettings": <?= /* @escapeNotVerified */ $block->getVideoSettingsJson() ?>, + "videoData": <?= /* @noEscape */ $block->getMediaGalleryDataJson() ?>, + "videoSettings": <?= /* @noEscape */ $block->getVideoSettingsJson() ?>, "optionsVideoData": <?= /* @noEscape */ $block->getOptionsMediaGalleryDataJson() ?> } } diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index 1dfcc95a552c6..b7f4adb857a91 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -34,7 +34,8 @@ define([ var id, type, ampersandPosition, - vimeoRegex; + vimeoRegex, + useYoutubeNocookie = false; /** * Get youtube ID @@ -68,9 +69,13 @@ define([ id = _getYoutubeId(id); type = 'youtube'; } - } else if (href.host.match(/youtube\.com|youtu\.be/)) { + } else if (href.host.match(/youtube\.com|youtu\.be|youtube-nocookie.com/)) { id = href.pathname.replace(/^\/(embed\/|v\/)?/, '').replace(/\/.*/, ''); type = 'youtube'; + + if (href.host.match(/youtube-nocookie.com/)) { + useYoutubeNocookie = true; + } } else if (href.host.match(/vimeo\.com/)) { type = 'vimeo'; vimeoRegex = new RegExp(['https?:\\/\\/(?:www\\.|player\\.)?vimeo.com\\/(?:channels\\/(?:\\w+\\/)', @@ -85,7 +90,7 @@ define([ } return id ? { - id: id, type: type, s: href.search.replace(/^\?/, '') + id: id, type: type, s: href.search.replace(/^\?/, ''), useYoutubeNocookie: useYoutubeNocookie } : false; } @@ -174,12 +179,14 @@ define([ * @private */ clearEvents: function () { - this.fotoramaItem.off( - 'fotorama:show ' + - 'fotorama:showend ' + - 'fotorama:fullscreenenter ' + - 'fotorama:fullscreenexit' - ); + if (this.fotoramaItem !== undefined) { + this.fotoramaItem.off( + 'fotorama:show.' + this.PV + + ' fotorama:showend.' + this.PV + + ' fotorama:fullscreenenter.' + this.PV + + ' fotorama:fullscreenexit.' + this.PV + ); + } }, /** @@ -207,7 +214,7 @@ define([ if (options.dataMergeStrategy === 'prepend') { this.options.videoData = [].concat( this.options.optionsVideoData[options.selectedOption], - this.options.videoData + this.defaultVideoData ); } else { this.options.videoData = this.options.optionsVideoData[options.selectedOption]; @@ -232,11 +239,11 @@ define([ * @private */ _listenForFullscreen: function () { - this.fotoramaItem.on('fotorama:fullscreenenter', $.proxy(function () { + this.fotoramaItem.on('fotorama:fullscreenenter.' + this.PV, $.proxy(function () { this.isFullscreen = true; }, this)); - this.fotoramaItem.on('fotorama:fullscreenexit', $.proxy(function () { + this.fotoramaItem.on('fotorama:fullscreenexit.' + this.PV, $.proxy(function () { this.isFullscreen = false; this._hideVideoArrows(); }, this)); @@ -283,6 +290,7 @@ define([ tmpVideoData.id = dataUrl.id; tmpVideoData.provider = dataUrl.type; tmpVideoData.videoUrl = tmpInputData.videoUrl; + tmpVideoData.useYoutubeNocookie = dataUrl.useYoutubeNocookie; } videoData.push(tmpVideoData); @@ -444,7 +452,7 @@ define([ scriptTag = document.getElementsByTagName('script')[0]; element.async = true; - element.src = 'https://secure-a.vimeocdn.com/js/froogaloop2.min.js'; + element.src = 'https://f.vimeocdn.com/js/froogaloop2.min.js'; /** * Vimeo js framework on load callback. @@ -468,7 +476,7 @@ define([ t; if (!fotorama.activeFrame.$navThumbFrame) { - this.fotoramaItem.on('fotorama:showend', $.proxy(function (evt, fotoramaData) { + this.fotoramaItem.on('fotorama:showend.' + this.PV, $.proxy(function (evt, fotoramaData) { $(fotoramaData.activeFrame.$stageFrame).removeAttr('href'); }, this)); @@ -486,7 +494,7 @@ define([ this._checkForVideo(e, fotorama, t + 1); } - this.fotoramaItem.on('fotorama:showend', $.proxy(function (evt, fotoramaData) { + this.fotoramaItem.on('fotorama:showend.' + this.PV, $.proxy(function (evt, fotoramaData) { $(fotoramaData.activeFrame.$stageFrame).removeAttr('href'); }, this)); }, @@ -528,15 +536,15 @@ define([ * @private */ _attachFotoramaEvents: function () { - this.fotoramaItem.on('fotorama:showend', $.proxy(function (e, fotorama) { + this.fotoramaItem.on('fotorama:showend.' + this.PV, $.proxy(function (e, fotorama) { this._startPrepareForPlayer(e, fotorama); }, this)); - this.fotoramaItem.on('fotorama:show', $.proxy(function (e, fotorama) { + this.fotoramaItem.on('fotorama:show.' + this.PV, $.proxy(function (e, fotorama) { this._unloadVideoPlayer(fotorama.activeFrame.$stageFrame.parent(), fotorama, true); }, this)); - this.fotoramaItem.on('fotorama:fullscreenexit', $.proxy(function (e, fotorama) { + this.fotoramaItem.on('fotorama:fullscreenexit.' + this.PV, $.proxy(function (e, fotorama) { fotorama.activeFrame.$stageFrame.find('.' + this.PV).remove(); this._startPrepareForPlayer(e, fotorama); }, this)); @@ -631,6 +639,8 @@ define([ videoData.provider + '" data-code="' + videoData.id + + '" data-youtubenocookie="' + + videoData.useYoutubeNocookie + '" data-width="100%" data-height="100%"></div>' ); }, diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js index 5a9f6a3eca941..75a2c1d75da15 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/load-player.js @@ -88,6 +88,7 @@ define(['jquery', 'jquery/ui'], function ($) { this._playing = this._autoplay || false; this._loop = this.element.data('loop'); this._rel = this.element.data('related'); + this.useYoutubeNocookie = this.element.data('youtubenocookie') || false; this._responsive = this.element.data('responsive') !== false; @@ -164,6 +165,12 @@ define(['jquery', 'jquery/ui'], function ($) { * Handle event */ 'youtubeapiready': function () { + var host = 'https://www.youtube.com'; + + if (self.useYoutubeNocookie) { + host = 'https://www.youtube-nocookie.com'; + } + if (self._player !== undefined) { return; } @@ -182,6 +189,7 @@ define(['jquery', 'jquery/ui'], function ($) { width: self._width, videoId: self._code, playerVars: self._params, + host: host, events: { /** @@ -317,7 +325,7 @@ define(['jquery', 'jquery/ui'], function ($) { if (this._loop) { additionalParams += '&loop=1'; } - src = window.location.protocol + '//player.vimeo.com/video/' + + src = 'https://player.vimeo.com/video/' + this._code + '?api=1&player_id=vimeo' + this._code + timestamp + diff --git a/app/code/Magento/Quote/Api/ChangeQuoteControlInterface.php b/app/code/Magento/Quote/Api/ChangeQuoteControlInterface.php new file mode 100644 index 0000000000000..ef6ecf746df76 --- /dev/null +++ b/app/code/Magento/Quote/Api/ChangeQuoteControlInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Quote\Api; + +use Magento\Quote\Api\Data\CartInterface; + +/** + * Service checks if the user has ability to change the quote. + */ +interface ChangeQuoteControlInterface +{ + /** + * Checks if user is allowed to change the quote. + * + * @param CartInterface $quote + * @return bool + */ + public function isAllowed(CartInterface $quote): bool; +} diff --git a/app/code/Magento/Quote/Api/Data/CartInterface.php b/app/code/Magento/Quote/Api/Data/CartInterface.php index 551833e3effb1..b87869de6b3df 100644 --- a/app/code/Magento/Quote/Api/Data/CartInterface.php +++ b/app/code/Magento/Quote/Api/Data/CartInterface.php @@ -223,14 +223,14 @@ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $bill /** * Returns the reserved order ID for the cart. * - * @return int|null Reserved order ID. Otherwise, null. + * @return string|null Reserved order ID. Otherwise, null. */ public function getReservedOrderId(); /** * Sets the reserved order ID for the cart. * - * @param int $reservedOrderId + * @param string $reservedOrderId * @return $this */ public function setReservedOrderId($reservedOrderId); diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index 2cbca917c26a1..c89fc37b44689 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -76,6 +76,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); + $address->setCustomerId($quote->getCustomerId()); $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($address); try { diff --git a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php index 9fe69b691424d..e18ab8587fc71 100644 --- a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php +++ b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php @@ -10,6 +10,7 @@ use Magento\Quote\Api\CartTotalRepositoryInterface; use Magento\Catalog\Helper\Product\ConfigurationPool; use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Quote\Model\Cart\Totals\ItemConverter; use Magento\Quote\Api\CouponManagementInterface; @@ -94,6 +95,7 @@ public function get($cartId) $addressTotalsData = $quote->getShippingAddress()->getData(); $addressTotals = $quote->getShippingAddress()->getTotals(); } + unset($addressTotalsData[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]); /** @var \Magento\Quote\Api\Data\TotalsInterface $quoteTotals */ $quoteTotals = $this->totalsFactory->create(); diff --git a/app/code/Magento/Quote/Model/ChangeQuoteControl.php b/app/code/Magento/Quote/Model/ChangeQuoteControl.php new file mode 100644 index 0000000000000..a4ed0345f207d --- /dev/null +++ b/app/code/Magento/Quote/Model/ChangeQuoteControl.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Quote\Model; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Quote\Api\ChangeQuoteControlInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * {@inheritdoc} + */ +class ChangeQuoteControl implements ChangeQuoteControlInterface +{ + /** + * @var UserContextInterface $userContext + */ + private $userContext; + + /** + * @param UserContextInterface $userContext + */ + public function __construct(UserContextInterface $userContext) + { + $this->userContext = $userContext; + } + + /** + * {@inheritdoc} + */ + public function isAllowed(CartInterface $quote): bool + { + switch ($this->userContext->getUserType()) { + case UserContextInterface::USER_TYPE_CUSTOMER: + $isAllowed = ($quote->getCustomerId() == $this->userContext->getUserId()); + break; + case UserContextInterface::USER_TYPE_GUEST: + $isAllowed = ($quote->getCustomerId() === null); + break; + case UserContextInterface::USER_TYPE_ADMIN: + case UserContextInterface::USER_TYPE_INTEGRATION: + $isAllowed = true; + break; + default: + $isAllowed = false; + } + + return $isAllowed; + } +} diff --git a/app/code/Magento/Quote/Model/CouponManagement.php b/app/code/Magento/Quote/Model/CouponManagement.php index 7701e41e0b55a..a46cc344cadb7 100644 --- a/app/code/Magento/Quote/Model/CouponManagement.php +++ b/app/code/Magento/Quote/Model/CouponManagement.php @@ -50,11 +50,15 @@ public function get($cartId) */ public function set($cartId, $couponCode) { + $couponCode = trim($couponCode); /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); if (!$quote->getItemsCount()) { throw new NoSuchEntityException(__('Cart %1 doesn\'t contain products', $cartId)); } + if (!$quote->getStoreId()) { + throw new NoSuchEntityException(__('Cart isn\'t assigned to correct store')); + } $quote->getShippingAddress()->setCollectShippingRates(true); try { diff --git a/app/code/Magento/Quote/Model/PaymentMethodManagement.php b/app/code/Magento/Quote/Model/PaymentMethodManagement.php index f12b9e5d1fb7f..5a0a3e8608a88 100644 --- a/app/code/Magento/Quote/Model/PaymentMethodManagement.php +++ b/app/code/Magento/Quote/Model/PaymentMethodManagement.php @@ -51,36 +51,35 @@ public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->get($cartId); - + $quote->setTotalsCollectedFlag(false); $method->setChecks([ \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_CHECKOUT, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_COUNTRY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_CURRENCY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]); - $payment = $quote->getPayment(); - - $data = $method->getData(); - $payment->importData($data); if ($quote->isVirtual()) { - $quote->getBillingAddress()->setPaymentMethod($payment->getMethod()); + $address = $quote->getBillingAddress(); } else { + $address = $quote->getShippingAddress(); // check if shipping address is set - if ($quote->getShippingAddress()->getCountryId() === null) { + if ($address->getCountryId() === null) { throw new InvalidTransitionException(__('Shipping address is not set')); } - $quote->getShippingAddress()->setPaymentMethod($payment->getMethod()); - } - if (!$quote->isVirtual() && $quote->getShippingAddress()) { - $quote->getShippingAddress()->setCollectShippingRates(true); + $address->setCollectShippingRates(true); } + $paymentData = $method->getData(); + $payment = $quote->getPayment(); + $payment->importData($paymentData); + $address->setPaymentMethod($payment->getMethod()); + if (!$this->zeroTotalValidator->isApplicable($payment->getMethodInstance(), $quote)) { throw new InvalidTransitionException(__('The requested Payment Method is not available.')); } - $quote->setTotalsCollectedFlag(false)->collectTotals()->save(); + $quote->save(); return $quote->getPayment()->getId(); } diff --git a/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php new file mode 100644 index 0000000000000..f18bb46fa63fb --- /dev/null +++ b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +/** + * Remove quote items after mass disabling products + */ +class MarkQuotesRecollectMassDisabled +{ + /** @var QuoteResource$quoteResource */ + private $quoteResource; + + /** + * @param QuoteResource $quoteResource + */ + public function __construct( + QuoteResource $quoteResource + ) { + $this->quoteResource = $quoteResource; + } + + /** + * Clean quote items after mass disabling product + * + * @param \Magento\Catalog\Model\Product\Action $subject + * @param \Magento\Catalog\Model\Product\Action $result + * @param int[] $productIds + * @param int[] $attrData + * @param int $storeId + * @return \Magento\Catalog\Model\Product\Action + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateAttributes( + ProductAction $subject, + ProductAction $result, + $productIds, + $attrData, + $storeId + ): ProductAction { + if (isset($attrData['status']) && $attrData['status'] === Status::STATUS_DISABLED) { + $this->quoteResource->markQuotesRecollect($productIds); + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 7741d3b0f7657..064b0c60e199c 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -7,6 +7,7 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\GroupInterface; +use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Model\AbstractExtensibleModel; @@ -14,6 +15,7 @@ use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total as AddressTotal; use Magento\Sales\Model\Status; +use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; use Magento\Sales\Model\OrderIncrementIdChecker; @@ -360,6 +362,11 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C */ private $orderIncrementIdChecker; + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -402,6 +409,7 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param OrderIncrementIdChecker|null $orderIncrementIdChecker + * @param AllowedCountries|null $allowedCountriesReader * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -445,7 +453,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - OrderIncrementIdChecker $orderIncrementIdChecker = null + OrderIncrementIdChecker $orderIncrementIdChecker = null, + AllowedCountries $allowedCountriesReader = null ) { $this->quoteValidator = $quoteValidator; $this->_catalogProduct = $catalogProduct; @@ -482,6 +491,8 @@ public function __construct( $this->shippingAssignmentFactory = $shippingAssignmentFactory; $this->orderIncrementIdChecker = $orderIncrementIdChecker ?: ObjectManager::getInstance() ->get(OrderIncrementIdChecker::class); + $this->allowedCountriesReader = $allowedCountriesReader + ?: ObjectManager::getInstance()->get(AllowedCountries::class); parent::__construct( $context, $registry, @@ -941,7 +952,7 @@ public function assignCustomerWithAddressChange( /** @var \Magento\Quote\Model\Quote\Address $billingAddress */ $billingAddress = $this->_quoteAddressFactory->create(); $billingAddress->importCustomerAddressData($defaultBillingAddress); - $this->setBillingAddress($billingAddress); + $this->assignAddress($billingAddress); } } @@ -959,7 +970,7 @@ public function assignCustomerWithAddressChange( $shippingAddress = $this->_quoteAddressFactory->create(); } } - $this->setShippingAddress($shippingAddress); + $this->assignAddress($shippingAddress, false); } return $this; @@ -1313,8 +1324,7 @@ public function addAddress(\Magento\Quote\Api\Data\AddressInterface $address) */ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $address = null) { - $old = $this->getBillingAddress(); - + $old = $this->getAddressesCollection()->getItemById($address->getId()) ?? $this->getBillingAddress(); if (!empty($old)) { $old->addData($address->getData()); } else { @@ -1334,7 +1344,7 @@ public function setShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add if ($this->getIsMultiShipping()) { $this->addAddress($address->setAddressType(Address::TYPE_SHIPPING)); } else { - $old = $this->getShippingAddress(); + $old = $this->getAddressesCollection()->getItemById($address->getId()) ?? $this->getShippingAddress(); if (!empty($old)) { $old->addData($address->getData()); } else { @@ -1355,7 +1365,7 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add } /** - * Retrieve quote items collection + * Retrieve quote items collection. * * @param bool $useCache * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection @@ -1363,10 +1373,10 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add */ public function getItemsCollection($useCache = true) { - if ($this->hasItemsCollection()) { + if ($this->hasItemsCollection() && $useCache) { return $this->getData('items_collection'); } - if (null === $this->_items) { + if (null === $this->_items || !$useCache) { $this->_items = $this->_quoteItemCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($this->_items); $this->_items->setQuote($this); @@ -1400,7 +1410,7 @@ public function getAllVisibleItems() { $items = []; foreach ($this->getItemsCollection() as $item) { - if (!$item->isDeleted() && !$item->getParentItemId()) { + if (!$item->isDeleted() && !$item->getParentItemId() && !$item->getParentItem()) { $items[] = $item; } } @@ -1609,7 +1619,7 @@ public function addProduct( * Error message */ if (is_string($cartCandidates) || $cartCandidates instanceof \Magento\Framework\Phrase) { - return strval($cartCandidates); + return (string)$cartCandidates; } /** @@ -2022,7 +2032,7 @@ public function getErrors() foreach ($this->getMessages() as $message) { /* @var $error \Magento\Framework\Message\AbstractMessage */ if ($message->getType() == \Magento\Framework\Message\MessageInterface::TYPE_ERROR) { - array_push($errors, $message); + $errors[] = $message; } } return $errors; @@ -2219,6 +2229,11 @@ public function validateMinimumAmount($multishipping = false) if (!$minOrderActive) { return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $minOrderMulti = $this->_scopeConfig->isSetFlag( 'sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -2252,7 +2267,10 @@ public function validateMinimumAmount($multishipping = false) $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; foreach ($address->getQuote()->getItemsCollection() as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ - $amount = $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes; + $amount = $includeDiscount ? + $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes : + $item->getBaseRowTotal() + $taxes; + if ($amount < $minAmount) { return false; } @@ -2262,7 +2280,9 @@ public function validateMinimumAmount($multishipping = false) $baseTotal = 0; foreach ($addresses as $address) { $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; - $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; + $baseTotal += $includeDiscount ? + $address->getBaseSubtotalWithDiscount() + $taxes : + $address->getBaseSubtotal() + $taxes; } if ($baseTotal < $minAmount) { return false; @@ -2302,7 +2322,7 @@ public function isVirtual() */ public function getIsVirtual() { - return intval($this->isVirtual()); + return (int)$this->isVirtual(); } /** @@ -2342,6 +2362,7 @@ public function merge(Quote $quote) foreach ($this->getAllItems() as $quoteItem) { if ($quoteItem->compare($item)) { $quoteItem->setQty($quoteItem->getQty() + $item->getQty()); + $this->itemProcessor->merge($item, $quoteItem); $found = true; break; } @@ -2389,8 +2410,9 @@ protected function _afterLoad() { // collect totals and save me, if required if (1 == $this->getTriggerRecollect()) { - $this->collectTotals()->save(); - $this->setTriggerRecollect(0); + $this->collectTotals() + ->setTriggerRecollect(0) + ->save(); } return parent::_afterLoad(); } @@ -2559,4 +2581,34 @@ public function setExtensionAttributes(\Magento\Quote\Api\Data\CartExtensionInte { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Check is address allowed for store + * + * @param Address $address + * @param int|null $storeId + * @return bool + */ + private function isAddressAllowedForWebsite(Address $address, $storeId): bool + { + $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); + + return in_array($address->getCountryId(), $allowedCountries); + } + + /** + * Assign address to quote + * + * @param Address $address + * @param bool $isBillingAddress + * @return void + */ + private function assignAddress(Address $address, bool $isBillingAddress = true) + { + if ($this->isAddressAllowedForWebsite($address, $this->getStoreId())) { + $isBillingAddress + ? $this->setBillingAddress($address) + : $this->setShippingAddress($address); + } + } } diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 87c5feaba8f2e..38a97783ca012 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -872,13 +872,7 @@ public function getGroupedAllShippingRates() */ protected function _sortRates($firstItem, $secondItem) { - if ((int)$firstItem[0]->carrier_sort_order < (int)$secondItem[0]->carrier_sort_order) { - return -1; - } elseif ((int)$firstItem[0]->carrier_sort_order > (int)$secondItem[0]->carrier_sort_order) { - return 1; - } else { - return 0; - } + return (int) $firstItem[0]->carrier_sort_order <=> (int) $secondItem[0]->carrier_sort_order; } /** @@ -971,6 +965,7 @@ public function collectShippingRates() /** * Request shipping rates for entire address or specified address item + * * Returns true if current selected shipping method code corresponds to one of the found rates * * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item @@ -1008,8 +1003,15 @@ public function requestShippingRates(\Magento\Quote\Model\Quote\Item\AbstractIte /** * Store and website identifiers specified from StoreManager */ - $request->setStoreId($this->storeManager->getStore()->getId()); - $request->setWebsiteId($this->storeManager->getWebsite()->getId()); + $request->setQuoteStoreId($this->getQuote()->getStoreId()); + if ($this->getQuote()->getStoreId()) { + $storeId = $this->getQuote()->getStoreId(); + $request->setStoreId($storeId); + $request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId()); + } else { + $request->setStoreId($this->storeManager->getStore()->getId()); + $request->setWebsiteId($this->storeManager->getWebsite()->getId()); + } $request->setFreeShipping($this->getFreeShipping()); /** * Currencies need to convert in free shipping @@ -1148,6 +1150,11 @@ public function validateMinimumAmount() return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $amount = $this->_scopeConfig->getValue( 'sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -1158,9 +1165,12 @@ public function validateMinimumAmount() \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeId ); + $taxes = $taxInclude ? $this->getBaseTaxAmount() : 0; - return ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount); + return $includeDiscount ? + ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) : + ($this->getBaseSubtotal() + $taxes >= $amount); } /** @@ -1354,7 +1364,7 @@ public function getAllBaseTotalAmounts() /******************************* End Total Collector Interface *******************************************/ /** - * {@inheritdoc} + * @inheritdoc */ protected function _getValidationRulesBeforeSave() { @@ -1362,7 +1372,7 @@ protected function _getValidationRulesBeforeSave() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCountryId() { @@ -1370,7 +1380,7 @@ public function getCountryId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCountryId($countryId) { @@ -1378,7 +1388,7 @@ public function setCountryId($countryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getStreet() { @@ -1387,7 +1397,7 @@ public function getStreet() } /** - * {@inheritdoc} + * @inheritdoc */ public function setStreet($street) { @@ -1395,7 +1405,7 @@ public function setStreet($street) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCompany() { @@ -1403,7 +1413,7 @@ public function getCompany() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCompany($company) { @@ -1411,7 +1421,7 @@ public function setCompany($company) } /** - * {@inheritdoc} + * @inheritdoc */ public function getTelephone() { @@ -1419,7 +1429,7 @@ public function getTelephone() } /** - * {@inheritdoc} + * @inheritdoc */ public function setTelephone($telephone) { @@ -1427,7 +1437,7 @@ public function setTelephone($telephone) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFax() { @@ -1435,7 +1445,7 @@ public function getFax() } /** - * {@inheritdoc} + * @inheritdoc */ public function setFax($fax) { @@ -1443,7 +1453,7 @@ public function setFax($fax) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPostcode() { @@ -1451,7 +1461,7 @@ public function getPostcode() } /** - * {@inheritdoc} + * @inheritdoc */ public function setPostcode($postcode) { @@ -1459,7 +1469,7 @@ public function setPostcode($postcode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCity() { @@ -1467,7 +1477,7 @@ public function getCity() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCity($city) { @@ -1475,7 +1485,7 @@ public function setCity($city) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFirstname() { @@ -1483,7 +1493,7 @@ public function getFirstname() } /** - * {@inheritdoc} + * @inheritdoc */ public function setFirstname($firstname) { @@ -1491,7 +1501,7 @@ public function setFirstname($firstname) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLastname() { @@ -1499,7 +1509,7 @@ public function getLastname() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLastname($lastname) { @@ -1507,7 +1517,7 @@ public function setLastname($lastname) } /** - * {@inheritdoc} + * @inheritdoc */ public function getMiddlename() { @@ -1515,7 +1525,7 @@ public function getMiddlename() } /** - * {@inheritdoc} + * @inheritdoc */ public function setMiddlename($middlename) { @@ -1523,7 +1533,7 @@ public function setMiddlename($middlename) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrefix() { @@ -1531,7 +1541,7 @@ public function getPrefix() } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrefix($prefix) { @@ -1539,7 +1549,7 @@ public function setPrefix($prefix) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSuffix() { @@ -1547,7 +1557,7 @@ public function getSuffix() } /** - * {@inheritdoc} + * @inheritdoc */ public function setSuffix($suffix) { @@ -1555,7 +1565,7 @@ public function setSuffix($suffix) } /** - * {@inheritdoc} + * @inheritdoc */ public function getVatId() { @@ -1563,7 +1573,7 @@ public function getVatId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatId($vatId) { @@ -1571,7 +1581,7 @@ public function setVatId($vatId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomerId() { @@ -1579,7 +1589,7 @@ public function getCustomerId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerId($customerId) { @@ -1587,7 +1597,7 @@ public function setCustomerId($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getEmail() { @@ -1600,7 +1610,7 @@ public function getEmail() } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmail($email) { @@ -1608,7 +1618,7 @@ public function setEmail($email) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegion($region) { @@ -1616,7 +1626,7 @@ public function setRegion($region) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionId($regionId) { @@ -1624,7 +1634,7 @@ public function setRegionId($regionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionCode($regionCode) { @@ -1632,7 +1642,7 @@ public function setRegionCode($regionCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSameAsBilling() { @@ -1640,7 +1650,7 @@ public function getSameAsBilling() } /** - * {@inheritdoc} + * @inheritdoc */ public function setSameAsBilling($sameAsBilling) { @@ -1648,7 +1658,7 @@ public function setSameAsBilling($sameAsBilling) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomerAddressId() { @@ -1656,7 +1666,7 @@ public function getCustomerAddressId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerAddressId($customerAddressId) { @@ -1687,9 +1697,7 @@ public function setSaveInAddressBook($saveInAddressBook) //@codeCoverageIgnoreEnd /** - * {@inheritdoc} - * - * @return \Magento\Quote\Api\Data\AddressExtensionInterface|null + * @inheritdoc */ public function getExtensionAttributes() { @@ -1697,10 +1705,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} - * - * @param \Magento\Quote\Api\Data\AddressExtensionInterface $extensionAttributes - * @return $this + * @inheritdoc */ public function setExtensionAttributes(\Magento\Quote\Api\Data\AddressExtensionInterface $extensionAttributes) { @@ -1718,7 +1723,7 @@ public function getShippingMethod() } /** - * {@inheritdoc} + * @inheritdoc */ protected function getCustomAttributesCodes() { diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index c5b8dc1c4b124..81b0bed2592ea 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -12,6 +12,9 @@ use Magento\Quote\Model\QuoteAddressValidator; use Magento\Customer\Api\AddressRepositoryInterface; +/** + * Saves billing address for quotes. + */ class BillingAddressPersister { /** @@ -37,17 +40,17 @@ public function __construct( } /** + * Save address for billing. + * * @param CartInterface $quote * @param AddressInterface $address * @param bool $useForShipping * @return void - * @throws NoSuchEntityException - * @throws InputException */ public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; $addressData = []; diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 42224c970ed27..00060c15c10d8 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -6,6 +6,8 @@ namespace Magento\Quote\Model\Quote\Address; /** + * Class Total + * * @method string getCode() * * @api @@ -54,6 +56,8 @@ public function __construct( */ public function setTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->totalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -72,6 +76,8 @@ public function setTotalAmount($code, $amount) */ public function setBaseTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->baseTotalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -167,6 +173,7 @@ public function getAllBaseTotalAmounts() /** * Set the full info, which is used to capture tax related information. + * * If a string is used, it is assumed to be serialized. * * @param array|string $info diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 6e6aa27ec5f30..e9a63dad6e169 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -6,8 +6,12 @@ namespace Magento\Quote\Model\Quote\Address\Total; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address\FreeShippingInterface; +/** + * Collect totals for shipping. + */ class Shipping extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { /** @@ -40,7 +44,6 @@ public function __construct( * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment * @param \Magento\Quote\Model\Quote\Address\Total $total * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -54,13 +57,6 @@ public function collect( $address = $shippingAssignment->getShipping()->getAddress(); $method = $shippingAssignment->getShipping()->getMethod(); - $address->setWeight(0); - $address->setFreeMethodWeight(0); - - $addressWeight = $address->getWeight(); - $freeMethodWeight = $address->getFreeMethodWeight(); - $addressFreeShipping = $address->getFreeShipping(); - $total->setTotalAmount($this->getCode(), 0); $total->setBaseTotalAmount($this->getCode(), 0); @@ -68,97 +64,20 @@ public function collect( return $this; } - $addressQty = 0; - foreach ($shippingAssignment->getItems() as $item) { - /** - * Skip if this item is virtual - */ - if ($item->getProduct()->isVirtual()) { - continue; - } - - /** - * Children weight we calculate for parent - */ - if ($item->getParentItem()) { - continue; - } - - if ($item->getHasChildren() && $item->isShipSeparately()) { - foreach ($item->getChildren() as $child) { - if ($child->getProduct()->isVirtual()) { - continue; - } - $addressQty += $child->getTotalQty(); - - if (!$item->getProduct()->getWeightType()) { - $itemWeight = $child->getWeight(); - $itemQty = $child->getTotalQty(); - $rowWeight = $itemWeight * $itemQty; - $addressWeight += $rowWeight; - if ($addressFreeShipping || $child->getFreeShipping() === true) { - $rowWeight = 0; - } elseif (is_numeric($child->getFreeShipping())) { - $freeQty = $child->getFreeShipping(); - if ($itemQty > $freeQty) { - $rowWeight = $itemWeight * ($itemQty - $freeQty); - } else { - $rowWeight = 0; - } - } - $freeMethodWeight += $rowWeight; - $item->setRowWeight($rowWeight); - } - } - if ($item->getProduct()->getWeightType()) { - $itemWeight = $item->getWeight(); - $rowWeight = $itemWeight * $item->getQty(); - $addressWeight += $rowWeight; - if ($addressFreeShipping || $item->getFreeShipping() === true) { - $rowWeight = 0; - } elseif (is_numeric($item->getFreeShipping())) { - $freeQty = $item->getFreeShipping(); - if ($item->getQty() > $freeQty) { - $rowWeight = $itemWeight * ($item->getQty() - $freeQty); - } else { - $rowWeight = 0; - } - } - $freeMethodWeight += $rowWeight; - $item->setRowWeight($rowWeight); - } - } else { - if (!$item->getProduct()->isVirtual()) { - $addressQty += $item->getQty(); - } - $itemWeight = $item->getWeight(); - $rowWeight = $itemWeight * $item->getQty(); - $addressWeight += $rowWeight; - if ($addressFreeShipping || $item->getFreeShipping() === true) { - $rowWeight = 0; - } elseif (is_numeric($item->getFreeShipping())) { - $freeQty = $item->getFreeShipping(); - if ($item->getQty() > $freeQty) { - $rowWeight = $itemWeight * ($item->getQty() - $freeQty); - } else { - $rowWeight = 0; - } - } - $freeMethodWeight += $rowWeight; - $item->setRowWeight($rowWeight); - } - } - - if (isset($addressQty)) { - $address->setItemQty($addressQty); + $data = $this->getAssignmentWeightData($address, $shippingAssignment->getItems()); + $address->setItemQty($data['addressQty']); + $address->setWeight($data['addressWeight']); + $address->setFreeMethodWeight($data['freeMethodWeight']); + $addressFreeShipping = (bool)$address->getFreeShipping(); + $isFreeShipping = $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()); + $address->setFreeShipping($isFreeShipping); + if (!$addressFreeShipping && $isFreeShipping) { + $data = $this->getAssignmentWeightData($address, $shippingAssignment->getItems()); + $address->setItemQty($data['addressQty']); + $address->setWeight($data['addressWeight']); + $address->setFreeMethodWeight($data['freeMethodWeight']); } - $address->setWeight($addressWeight); - $address->setFreeMethodWeight($freeMethodWeight); - $address->setFreeShipping( - $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()) - ); - $address->collectShippingRates(); if ($method) { @@ -195,7 +114,7 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu { $amount = $total->getShippingAmount(); $shippingDescription = $total->getShippingDescription(); - $title = ($amount != 0 && $shippingDescription) + $title = ($shippingDescription) ? __('Shipping & Handling (%1)', $shippingDescription) : __('Shipping & Handling'); @@ -215,4 +134,122 @@ public function getLabel() { return __('Shipping'); } + + /** + * Gets shipping assignments data like items weight, address weight, items quantity. + * + * @param AddressInterface $address + * @param array $items + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function getAssignmentWeightData(AddressInterface $address, array $items): array + { + $address->setWeight(0); + $address->setFreeMethodWeight(0); + $addressWeight = $address->getWeight(); + $freeMethodWeight = $address->getFreeMethodWeight(); + $addressFreeShipping = (bool)$address->getFreeShipping(); + $addressQty = 0; + foreach ($items as $item) { + /** + * Skip if this item is virtual + */ + if ($item->getProduct()->isVirtual()) { + continue; + } + + /** + * Children weight we calculate for parent + */ + if ($item->getParentItem()) { + continue; + } + + $itemQty = (float)$item->getQty(); + $itemWeight = (float)$item->getWeight(); + + if ($item->getHasChildren() && $item->isShipSeparately()) { + foreach ($item->getChildren() as $child) { + if ($child->getProduct()->isVirtual()) { + continue; + } + $addressQty += $child->getTotalQty(); + + if (!$item->getProduct()->getWeightType()) { + $itemWeight = (float)$child->getWeight(); + $itemQty = (float)$child->getTotalQty(); + $addressWeight += ($itemWeight * $itemQty); + $rowWeight = $this->getItemRowWeight( + $addressFreeShipping, + $itemWeight, + $itemQty, + $child->getFreeShipping() + ); + $freeMethodWeight += $rowWeight; + $item->setRowWeight($rowWeight); + } + } + if ($item->getProduct()->getWeightType()) { + $addressWeight += ($itemWeight * $itemQty); + $rowWeight = $this->getItemRowWeight( + $addressFreeShipping, + $itemWeight, + $itemQty, + $item->getFreeShipping() + ); + $freeMethodWeight += $rowWeight; + $item->setRowWeight($rowWeight); + } + } else { + if (!$item->getProduct()->isVirtual()) { + $addressQty += $itemQty; + } + $addressWeight += ($itemWeight * $itemQty); + $rowWeight = $this->getItemRowWeight( + $addressFreeShipping, + $itemWeight, + $itemQty, + $item->getFreeShipping() + ); + $freeMethodWeight += $rowWeight; + $item->setRowWeight($rowWeight); + } + } + + return [ + 'addressQty' => $addressQty, + 'addressWeight' => $addressWeight, + 'freeMethodWeight' => $freeMethodWeight + ]; + } + + /** + * Calculates item row weight. + * + * @param bool $addressFreeShipping + * @param float $itemWeight + * @param float $itemQty + * @param bool $freeShipping + * @return float + */ + private function getItemRowWeight( + bool $addressFreeShipping, + float $itemWeight, + float $itemQty, + $freeShipping + ): float { + $rowWeight = $itemWeight * $itemQty; + if ($addressFreeShipping || $freeShipping === true) { + $rowWeight = 0; + } elseif (is_numeric($freeShipping)) { + $freeQty = $freeShipping; + if ($itemQty > $freeQty) { + $rowWeight = $itemWeight * ($itemQty - $freeQty); + } else { + $rowWeight = 0; + } + } + return (float)$rowWeight; + } } diff --git a/app/code/Magento/Quote/Model/Quote/Item.php b/app/code/Magento/Quote/Model/Quote/Item.php index d8177ddfe5236..fe6d712500bcd 100644 --- a/app/code/Magento/Quote/Model/Quote/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Item.php @@ -745,6 +745,9 @@ public function saveItemOptions() unset($this->_options[$index]); unset($this->_optionsByCode[$option->getCode()]); } else { + if (!$option->getItem() || !$option->getItem()->getId()) { + $option->setItem($this); + } $option->save(); } } diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php index ddaa636ef32b3..76ba324518dc1 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php @@ -50,7 +50,7 @@ protected function getOptionValues($value) if (is_string($value) && $this->jsonValidator->isValid($value)) { $value = $this->serializer->unserialize($value); if (is_array($value)) { - unset($value['qty'], $value['uenc']); + unset($value['qty'], $value['uenc'], $value['related_product'], $value['item']); $value = array_filter($value, function ($optionValue) { return !empty($optionValue); }); diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php index f34591cfad143..757b81b984a46 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php @@ -97,12 +97,27 @@ public function prepare(Item $item, DataObject $request, Product $candidate) $item->addQty($candidate->getCartQty()); $customPrice = $request->getCustomPrice(); + $item->setPrice($candidate->getFinalPrice()); if (!empty($customPrice)) { $item->setCustomPrice($customPrice); $item->setOriginalCustomPrice($customPrice); } } + /** + * Merge two quote items. + * + * @param Item $source + * @param Item $target + * @return Item + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function merge(Item $source, Item $target): Item + { + return $target; + } + /** * Set store_id value to quote item * diff --git a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php index 32687499274f8..6192d3471ccb0 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php +++ b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Convert quote item(quote address item) into order item. + * * @param Item|AddressItem $item * @param array $data * @return OrderItemInterface @@ -63,6 +65,16 @@ public function convert($item, $data = []) 'to_order_item', $item ); + if ($item instanceof \Magento\Quote\Model\Quote\Address\Item) { + $orderItemData = array_merge( + $orderItemData, + $this->objectCopyService->getDataFromFieldset( + 'quote_convert_address_item', + 'to_order_item', + $item + ) + ); + } if (!$item->getNoDiscount()) { $data = array_merge( $data, diff --git a/app/code/Magento/Quote/Model/Quote/Item/Updater.php b/app/code/Magento/Quote/Model/Quote/Item/Updater.php index 6a7a3c1c1839e..05244d4ecc43a 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Updater.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Updater.php @@ -145,8 +145,8 @@ protected function unsetCustomPrice(Item $item) $item->addOption($infoBuyRequest); } - $item->unsetData('custom_price'); - $item->unsetData('original_custom_price'); + $item->setData('custom_price', null); + $item->setData('original_custom_price', null); } /** diff --git a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php index 8fa03232a0e8d..8f18a04a102fa 100644 --- a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php +++ b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php @@ -203,11 +203,12 @@ protected function _validateCouponCode(\Magento\Quote\Model\Quote $quote) */ protected function _collectItemsQtys(\Magento\Quote\Model\Quote $quote) { + $quoteItems = $quote->getAllVisibleItems(); $quote->setItemsCount(0); $quote->setItemsQty(0); $quote->setVirtualItemsQty(0); - foreach ($quote->getAllVisibleItems() as $item) { + foreach ($quoteItems as $item) { if ($item->getParentItem()) { continue; } diff --git a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php index 3113721f8a597..38bfcbf1d30ca 100644 --- a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php +++ b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php @@ -19,22 +19,32 @@ class ValidationMessage /** * @var \Magento\Framework\Locale\CurrencyInterface + * @deprecated since 101.0.0 */ private $currency; + /** + * @var \Magento\Framework\Pricing\Helper\Data + */ + private $priceHelper; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Locale\CurrencyInterface $currency + * @param \Magento\Framework\Pricing\Helper\Data $priceHelper */ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\CurrencyInterface $currency + \Magento\Framework\Locale\CurrencyInterface $currency, + \Magento\Framework\Pricing\Helper\Data $priceHelper = null ) { $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; $this->currency = $currency; + $this->priceHelper = $priceHelper ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Pricing\Helper\Data::class); } /** @@ -50,13 +60,11 @@ public function getMessage() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); if (!$message) { - $currencyCode = $this->storeManager->getStore()->getCurrentCurrencyCode(); - $minimumAmount = $this->currency->getCurrency($currencyCode)->toCurrency( - $this->scopeConfig->getValue( - 'sales/minimum_order/amount', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ); + $minimumAmount = $this->priceHelper->currency($this->scopeConfig->getValue( + 'sales/minimum_order/amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ), true, false); + $message = __('Minimum order amount is %1', $minimumAmount); } else { //Added in order to address the issue: https://github.com/magento/magento2/issues/8287 diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index 9a86829bfc4ce..06f21ee119bd5 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -6,10 +6,13 @@ namespace Magento\Quote\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Quote shipping/billing address validator service. * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteAddressValidator { @@ -28,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @var \Magento\Customer\Model\Session + * @deprecated This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; @@ -50,44 +53,68 @@ public function __construct( } /** - * Validates the fields in a specified address data object. + * Validate address. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. - * @return bool - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @param AddressInterface $address + * @param int|null $customerId Cart belongs to + * @return void + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. */ - public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData) + private function doValidate(AddressInterface $address, $customerId) { //validate customer id - if ($addressData->getCustomerId()) { - $customer = $this->customerRepository->getById($addressData->getCustomerId()); - if (!$customer->getId()) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer id %1', $addressData->getCustomerId()) - ); + if ($customerId) { + try { + $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $exception) { + throw new NoSuchEntityException(__('Invalid customer id %1', $customerId)); } } - if ($addressData->getCustomerAddressId()) { + if ($address->getCustomerAddressId()) { + //Existing address cannot belong to a guest + if (!$customerId) { + throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId())); + } + //Validating address ID try { - $this->addressRepository->getById($addressData->getCustomerAddressId()); + $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid address id %1', $addressData->getId()) - ); + throw new NoSuchEntityException(__('Invalid address id %1', $address->getId())); } - + //Finding available customer's addresses $applicableAddressIds = array_map(function ($address) { /** @var \Magento\Customer\Api\Data\AddressInterface $address */ return $address->getId(); - }, $this->customerRepository->getById($addressData->getCustomerId())->getAddresses()); - if (!in_array($addressData->getCustomerAddressId(), $applicableAddressIds)) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer address id %1', $addressData->getCustomerAddressId()) - ); + }, $this->customerRepository->getById($customerId)->getAddresses()); + if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { + throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId())); } } + } + + /** + * Validates the fields in a specified address data object. + * + * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @return bool + */ + public function validate(AddressInterface $addressData): bool + { + $this->doValidate($addressData, $addressData->getCustomerId()); + return true; } + + /** + * Validate address to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + */ + public function validateForCart(CartInterface $cart, AddressInterface $address) + { + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 6f03bba5072a2..79170ad90832c 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -25,6 +25,7 @@ /** * Class QuoteManagement * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -36,9 +37,9 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface protected $eventManager; /** - * @var QuoteValidator + * @var SubmitQuoteValidator */ - protected $quoteValidator; + private $submitQuoteValidator; /** * @var OrderFactory @@ -147,7 +148,7 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface /** * @param EventManager $eventManager - * @param QuoteValidator $quoteValidator + * @param SubmitQuoteValidator $submitQuoteValidator * @param OrderFactory $orderFactory * @param OrderManagement $orderManagement * @param CustomerManagement $customerManagement @@ -172,7 +173,7 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface */ public function __construct( EventManager $eventManager, - QuoteValidator $quoteValidator, + SubmitQuoteValidator $submitQuoteValidator, OrderFactory $orderFactory, OrderManagement $orderManagement, CustomerManagement $customerManagement, @@ -195,7 +196,7 @@ public function __construct( \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null ) { $this->eventManager = $eventManager; - $this->quoteValidator = $quoteValidator; + $this->submitQuoteValidator = $submitQuoteValidator; $this->orderFactory = $orderFactory; $this->orderManagement = $orderManagement; $this->customerManagement = $customerManagement; @@ -281,6 +282,7 @@ public function assignCustomer($cartId, $customerId, $storeId) throw new StateException( __('Cannot assign customer to the given cart. Customer already has active cart.') ); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } @@ -355,6 +357,13 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) if ($quote->getCheckoutMethod() === self::METHOD_GUEST) { $quote->setCustomerId(null); $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); + if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) { + $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname()); + $quote->setCustomerLastname($quote->getBillingAddress()->getLastname()); + if ($quote->getCustomerMiddlename() === null) { + $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename()); + } + } $quote->setCustomerIsGuest(true); $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); } @@ -446,7 +455,7 @@ protected function resolveItems(QuoteEntity $quote) protected function submitQuote(QuoteEntity $quote, $orderData = []) { $order = $this->orderFactory->create(); - $this->quoteValidator->validateBeforeSubmit($quote); + $this->submitQuoteValidator->validateQuote($quote); if (!$quote->getCustomerIsGuest()) { if ($quote->getCustomerId()) { $this->_prepareCustomerQuote($quote); @@ -475,6 +484,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) 'email' => $quote->getCustomerEmail() ] ); + $shippingAddress->setData('quote_address_id', $quote->getShippingAddress()->getId()); $addresses[] = $shippingAddress; $order->setShippingAddress($shippingAddress); $order->setShippingMethod($quote->getShippingAddress()->getShippingMethod()); @@ -486,6 +496,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) 'email' => $quote->getCustomerEmail() ] ); + $billingAddress->setData('quote_address_id', $quote->getBillingAddress()->getId()); $addresses[] = $billingAddress; $order->setBillingAddress($billingAddress); $order->setAddresses($addresses); @@ -499,6 +510,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) $order->setCustomerFirstname($quote->getCustomerFirstname()); $order->setCustomerMiddlename($quote->getCustomerMiddlename()); $order->setCustomerLastname($quote->getCustomerLastname()); + $this->submitQuoteValidator->validateOrder($order); $this->eventManager->dispatch( 'sales_model_service_quote_submit_before', @@ -519,19 +531,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - if (!empty($this->addressesToSync)) { - foreach ($this->addressesToSync as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } - $this->eventManager->dispatch( - 'sales_model_service_quote_submit_failure', - [ - 'order' => $order, - 'quote' => $quote, - 'exception' => $e - ] - ); + $this->rollbackAddresses($quote, $order, $e); throw $e; } return $order; @@ -563,6 +563,10 @@ protected function _prepareCustomerQuote($quote) //Make provided address as default shipping address $shippingAddress->setIsDefaultShipping(true); $hasDefaultShipping = true; + if (!$hasDefaultBilling && !$billing->getSaveInAddressBook()) { + $shippingAddress->setIsDefaultBilling(true); + $hasDefaultBilling = true; + } } //save here new customer address $shippingAddress->setCustomerId($quote->getCustomerId()); @@ -594,4 +598,43 @@ protected function _prepareCustomerQuote($quote) $shipping->setIsDefaultBilling(true); } } + + /** + * Remove related to order and quote addresses and submit exception to further processing. + * + * @param Quote $quote + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Exception $e + * @throws \Exception + * @return void + */ + private function rollbackAddresses( + QuoteEntity $quote, + \Magento\Sales\Api\Data\OrderInterface $order, + \Exception $e + ) { + try { + if (!empty($this->addressesToSync)) { + foreach ($this->addressesToSync as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + $this->eventManager->dispatch( + 'sales_model_service_quote_submit_failure', + [ + 'order' => $order, + 'quote' => $quote, + 'exception' => $e, + ] + ); + // phpcs:ignore Magento2.Exceptions.ThrowCatch + } catch (\Exception $consecutiveException) { + $message = sprintf( + "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", + $consecutiveException->getMessage() + ); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($message, 0, $e); + } + } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index d3967794b300a..30931821ddc7d 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -3,27 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Quote\Model; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Api\SortOrder; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\Api\Search\FilterGroup; -use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; -use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\QuoteRepository\LoadHandler; +use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; /** + * Quote repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface +class QuoteRepository implements CartRepositoryInterface { /** * @var Quote[] @@ -37,6 +43,7 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface /** * @var QuoteFactory + * @deprecated */ protected $quoteFactory; @@ -46,13 +53,13 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface protected $storeManager; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Collection + * @var QuoteCollection * @deprecated 100.2.0 */ protected $quoteCollection; /** - * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory + * @var CartSearchResultsInterfaceFactory */ protected $searchResultsDataFactory; @@ -77,43 +84,51 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface private $collectionProcessor; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory + * @var QuoteCollectionFactory */ private $quoteCollectionFactory; + /** + * @var CartInterfaceFactory + */ + private $cartFactory; + /** * Constructor * * @param QuoteFactory $quoteFactory * @param StoreManagerInterface $storeManager - * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection - * @param \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory + * @param QuoteCollection $quoteCollection + * @param CartSearchResultsInterfaceFactory $searchResultsDataFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface|null $collectionProcessor - * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|null $quoteCollectionFactory + * @param QuoteCollectionFactory|null $quoteCollectionFactory + * @param CartInterfaceFactory|null $cartFactory * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( QuoteFactory $quoteFactory, StoreManagerInterface $storeManager, - \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection, - \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory, + QuoteCollection $quoteCollection, + CartSearchResultsInterfaceFactory $searchResultsDataFactory, JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory = null + QuoteCollectionFactory $quoteCollectionFactory = null, + CartInterfaceFactory $cartFactory = null ) { $this->quoteFactory = $quoteFactory; $this->storeManager = $storeManager; $this->searchResultsDataFactory = $searchResultsDataFactory; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; - $this->collectionProcessor = $collectionProcessor ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\SearchCriteria\CollectionProcessor::class); - $this->quoteCollectionFactory = $quoteCollectionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class); + $this->collectionProcessor = $collectionProcessor ?: ObjectManager::getInstance() + ->get(CollectionProcessor::class); + $this->quoteCollectionFactory = $quoteCollectionFactory ?: ObjectManager::getInstance() + ->get(QuoteCollectionFactory::class); + $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function get($cartId, array $sharedStoreIds = []) { @@ -126,7 +141,7 @@ public function get($cartId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getForCustomer($customerId, array $sharedStoreIds = []) { @@ -140,7 +155,7 @@ public function getForCustomer($customerId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActive($cartId, array $sharedStoreIds = []) { @@ -152,7 +167,7 @@ public function getActive($cartId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) { @@ -164,9 +179,9 @@ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ - public function save(\Magento\Quote\Api\Data\CartInterface $quote) + public function save(CartInterface $quote) { if ($quote->getId()) { $currentQuote = $this->get($quote->getId(), [$quote->getStoreId()]); @@ -184,9 +199,9 @@ public function save(\Magento\Quote\Api\Data\CartInterface $quote) } /** - * {@inheritdoc} + * @inheritdoc */ - public function delete(\Magento\Quote\Api\Data\CartInterface $quote) + public function delete(CartInterface $quote) { $quoteId = $quote->getId(); $customerId = $quote->getCustomerId(); @@ -203,16 +218,16 @@ public function delete(\Magento\Quote\Api\Data\CartInterface $quote) * @param int $identifier * @param int[] $sharedStoreIds * @throws NoSuchEntityException - * @return Quote + * @return CartInterface */ protected function loadQuote($loadMethod, $loadField, $identifier, array $sharedStoreIds = []) { - /** @var Quote $quote */ - $quote = $this->quoteFactory->create(); - if ($sharedStoreIds) { + /** @var CartInterface $quote */ + $quote = $this->cartFactory->create(); + if ($sharedStoreIds && method_exists($quote, 'setSharedStoreIds')) { $quote->setSharedStoreIds($sharedStoreIds); } - $quote->$loadMethod($identifier)->setStoreId($this->storeManager->getStore()->getId()); + $quote->setStoreId($this->storeManager->getStore()->getId())->$loadMethod($identifier); if (!$quote->getId()) { throw NoSuchEntityException::singleField($loadField, $identifier); } @@ -220,9 +235,9 @@ protected function loadQuote($loadMethod, $loadField, $identifier, array $shared } /** - * {@inheritdoc} + * @inheritdoc */ - public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) + public function getList(SearchCriteriaInterface $searchCriteria) { $this->quoteCollection = $this->quoteCollectionFactory->create(); /** @var \Magento\Quote\Api\Data\CartSearchResultsInterface $searchData */ @@ -265,6 +280,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol /** * Get new SaveHandler dependency for application code. + * * @return SaveHandler * @deprecated 100.1.0 */ @@ -277,6 +293,8 @@ private function getSaveHandler() } /** + * Get load handler instance. + * * @return LoadHandler * @deprecated 100.1.0 */ diff --git a/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php b/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php index 3eff09faac1f5..79b518fc54534 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/Plugin/AccessChangeQuoteControl.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Quote\Model\QuoteRepository\Plugin; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Quote\Model\Quote; +use Magento\Quote\Api\ChangeQuoteControlInterface; use Magento\Framework\Exception\StateException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; @@ -17,24 +17,23 @@ class AccessChangeQuoteControl { /** - * @var UserContextInterface + * @var ChangeQuoteControlInterface $changeQuoteControl */ - private $userContext; + private $changeQuoteControl; /** - * @param UserContextInterface $userContext + * @param ChangeQuoteControlInterface $changeQuoteControl */ - public function __construct( - UserContextInterface $userContext - ) { - $this->userContext = $userContext; + public function __construct(ChangeQuoteControlInterface $changeQuoteControl) + { + $this->changeQuoteControl = $changeQuoteControl; } /** * Checks if change quote's customer id is allowed for current user. * * @param CartRepositoryInterface $subject - * @param Quote $quote + * @param CartInterface $quote * @throws StateException if Guest has customer_id or Customer's customer_id not much with user_id * or unknown user's type * @return void @@ -42,34 +41,8 @@ public function __construct( */ public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote) { - if (!$this->isAllowed($quote)) { + if (! $this->changeQuoteControl->isAllowed($quote)) { throw new StateException(__("Invalid state change requested")); } } - - /** - * Checks if user is allowed to change the quote. - * - * @param Quote $quote - * @return bool - */ - private function isAllowed(Quote $quote) - { - switch ($this->userContext->getUserType()) { - case UserContextInterface::USER_TYPE_CUSTOMER: - $isAllowed = ($quote->getCustomerId() == $this->userContext->getUserId()); - break; - case UserContextInterface::USER_TYPE_GUEST: - $isAllowed = ($quote->getCustomerId() === null); - break; - case UserContextInterface::USER_TYPE_ADMIN: - case UserContextInterface::USER_TYPE_INTEGRATION: - $isAllowed = true; - break; - default: - $isAllowed = false; - } - - return $isAllowed; - } } diff --git a/app/code/Magento/Quote/Model/QuoteValidator.php b/app/code/Magento/Quote/Model/QuoteValidator.php index 8d46832df8db2..1d5ff86b17429 100644 --- a/app/code/Magento/Quote/Model/QuoteValidator.php +++ b/app/code/Magento/Quote/Model/QuoteValidator.php @@ -6,11 +6,13 @@ namespace Magento\Quote\Model; -use Magento\Framework\Exception\LocalizedException; -use Magento\Quote\Model\Quote as QuoteEntity; use Magento\Directory\Model\AllowedCountries; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\Error; +use Magento\Quote\Model\Quote as QuoteEntity; use Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage as OrderAmountValidationMessage; +use Magento\Quote\Model\ValidationRules\QuoteValidationRuleInterface; /** * @api @@ -33,20 +35,29 @@ class QuoteValidator */ private $minimumAmountMessage; + /** + * @var QuoteValidationRuleInterface + */ + private $quoteValidationRule; + /** * QuoteValidator constructor. * * @param AllowedCountries|null $allowedCountryReader * @param OrderAmountValidationMessage|null $minimumAmountMessage + * @param QuoteValidationRuleInterface|null $quoteValidationRule */ public function __construct( AllowedCountries $allowedCountryReader = null, - OrderAmountValidationMessage $minimumAmountMessage = null + OrderAmountValidationMessage $minimumAmountMessage = null, + QuoteValidationRuleInterface $quoteValidationRule = null ) { $this->allowedCountryReader = $allowedCountryReader ?: ObjectManager::getInstance() ->get(AllowedCountries::class); $this->minimumAmountMessage = $minimumAmountMessage ?: ObjectManager::getInstance() ->get(OrderAmountValidationMessage::class); + $this->quoteValidationRule = $quoteValidationRule ?: ObjectManager::getInstance() + ->get(QuoteValidationRuleInterface::class); } /** @@ -62,59 +73,57 @@ public function validateQuoteAmount(QuoteEntity $quote, $amount) $quote->setHasError(true); $quote->addMessage(__('This item price or quantity is not valid for checkout.')); } + return $this; } /** - * Validate quote before submit + * Validates quote before submit. * * @param Quote $quote * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function validateBeforeSubmit(QuoteEntity $quote) { - if (!$quote->isVirtual()) { - if ($quote->getShippingAddress()->validate() !== true) { - throw new \Magento\Framework\Exception\LocalizedException( - __( - 'Please check the shipping address information. %1', - implode(' ', $quote->getShippingAddress()->validate()) - ) - ); - } + if ($quote->getHasError()) { + $errors = $this->getQuoteErrors($quote); + throw new LocalizedException(__($errors ?: 'Something went wrong. Please try to place the order again.')); + } - // Checks if country id present in the allowed countries list. - if (!in_array( - $quote->getShippingAddress()->getCountryId(), - $this->allowedCountryReader->getAllowedCountries() - )) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Some addresses cannot be used due to country-specific configurations.') - ); + foreach ($this->quoteValidationRule->validate($quote) as $validationResult) { + if ($validationResult->isValid()) { + continue; } - $method = $quote->getShippingAddress()->getShippingMethod(); - $rate = $quote->getShippingAddress()->getShippingRateByCode($method); - if (!$method || !$rate) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please specify a shipping method.')); + $messages = $validationResult->getErrors(); + $defaultMessage = array_shift($messages); + if ($defaultMessage && !empty($messages)) { + $defaultMessage .= ' %1'; + } + if ($defaultMessage) { + throw new LocalizedException(__($defaultMessage, implode(' ', $messages))); } - } - if ($quote->getBillingAddress()->validate() !== true) { - throw new \Magento\Framework\Exception\LocalizedException( - __( - 'Please check the billing address information. %1', - implode(' ', $quote->getBillingAddress()->validate()) - ) - ); - } - if (!$quote->getPayment()->getMethod()) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please select a valid payment method.')); - } - if (!$quote->validateMinimumAmount($quote->getIsMultiShipping())) { - throw new LocalizedException($this->minimumAmountMessage->getMessage()); } return $this; } + + /** + * Parses quote error messages and concatenates them into single string. + * + * @param Quote $quote + * @return string + */ + private function getQuoteErrors(QuoteEntity $quote): string + { + $errors = array_map( + function (Error $error) { + return $error->getText(); + }, + $quote->getErrors() + ); + + return implode(PHP_EOL, $errors); + } } diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 9f491b749a812..946c0e0c5f3b8 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\VersionControl\AbstractDb; @@ -167,7 +165,7 @@ public function getReservedOrderId($quote) { return $this->sequenceManager->getSequence( \Magento\Sales\Model\Order::ENTITY, - $quote->getStore()->getGroup()->getDefaultStoreId() + $quote->getStoreId() ) ->getNextValue(); } @@ -211,7 +209,7 @@ public function markQuotesRecollectOnCatalogRules() * @param \Magento\Catalog\Model\Product $product * @return $this */ - public function substractProductFromQuotes($product) + public function subtractProductFromQuotes($product) { $productId = (int)$product->getId(); if (!$productId) { @@ -251,6 +249,21 @@ public function substractProductFromQuotes($product) return $this; } + /** + * Subtract product from all quotes quantities + * + * @param \Magento\Catalog\Model\Product $product + * + * @deprecated 101.0.1 + * @see \Magento\Quote\Model\ResourceModel\Quote::subtractProductFromQuotes + * + * @return $this + */ + public function substractProductFromQuotes($product) + { + return $this->subtractProductFromQuotes($product); + } + /** * Mark recollect contain product(s) quotes * diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index 0487d7e46eb26..959604592c848 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -3,9 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model\ResourceModel\Quote\Item; -use \Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\ResourceModel\Quote\Item as ResourceQuoteItem; /** * Quote item resource collection @@ -45,6 +52,16 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionContro */ protected $_quoteConfig; + /** + * @var \Magento\Store\Model\StoreManagerInterface|null + */ + private $storeManager; + + /** + * @var bool $recollectQuote + */ + private $recollectQuote = false; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -56,6 +73,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionContro * @param \Magento\Quote\Model\Quote\Config $quoteConfig * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -68,7 +86,8 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, \Magento\Quote\Model\Quote\Config $quoteConfig, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { parent::__construct( $entityFactory, @@ -82,6 +101,10 @@ public function __construct( $this->_itemOptionCollectionFactory = $itemOptionCollectionFactory; $this->_productCollectionFactory = $productCollectionFactory; $this->_quoteConfig = $quoteConfig; + + // Backward compatibility constructor parameters + $this->storeManager = $storeManager ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -91,7 +114,7 @@ public function __construct( */ protected function _construct() { - $this->_init(\Magento\Quote\Model\Quote\Item::class, \Magento\Quote\Model\ResourceModel\Quote\Item::class); + $this->_init(QuoteItem::class, ResourceQuoteItem::class); } /** @@ -99,18 +122,21 @@ protected function _construct() * * @return int */ - public function getStoreId() + public function getStoreId(): int { - return (int)$this->_productCollectionFactory->create()->getStoreId(); + // Fallback to current storeId if no quote is provided + // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721) + return $this->_quote === null ? + (int) $this->storeManager->getStore()->getId() : (int) $this->_quote->getStoreId(); } /** - * Set Quote object to Collection + * Set Quote object to Collection. * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @return $this */ - public function setQuote($quote) + public function setQuote($quote): self { $this->_quote = $quote; $quoteId = $quote->getId(); @@ -124,14 +150,15 @@ public function setQuote($quote) } /** - * Reset the collection and inner join it to quotes table + * Reset the collection and inner join it to quotes table. + * * Optionally can select items with specified product id only * * @param string $quotesTableName * @param int $productId * @return $this */ - public function resetJoinQuotes($quotesTableName, $productId = null) + public function resetJoinQuotes($quotesTableName, $productId = null): self { $this->getSelect()->reset()->from( ['qi' => $this->getResource()->getMainTable()], @@ -148,11 +175,11 @@ public function resetJoinQuotes($quotesTableName, $productId = null) } /** - * After load processing + * After load processing. * * @return $this */ - protected function _afterLoad() + protected function _afterLoad(): self { parent::_afterLoad(); @@ -181,11 +208,11 @@ protected function _afterLoad() } /** - * Add options to items + * Add options to items. * * @return $this */ - protected function _assignOptions() + protected function _assignOptions(): self { $itemIds = array_keys($this->_items); $optionCollection = $this->_itemOptionCollectionFactory->create()->addItemFilter($itemIds); @@ -199,12 +226,12 @@ protected function _assignOptions() } /** - * Add products to items and item options + * Add products to items and item options. * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _assignProducts() + protected function _assignProducts(): self { \Magento\Framework\Profiler::start('QUOTE:' . __METHOD__, ['group' => 'QUOTE', 'method' => __METHOD__]); $productCollection = $this->_productCollectionFactory->create()->setStoreId( @@ -216,7 +243,6 @@ protected function _assignProducts() ); $this->skipStockStatusFilter($productCollection); $productCollection->addOptionsToResult()->addStoreFilter()->addUrlRewrite(); - $this->addTierPriceData($productCollection); $this->_eventManager->dispatch( 'prepare_catalog_product_collection_prices', @@ -227,46 +253,30 @@ protected function _assignProducts() ['collection' => $productCollection] ); - $recollectQuote = false; foreach ($this as $item) { + /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); - if ($product) { + $isValidProduct = $this->isValidProduct($product); + $qtyOptions = []; + if ($isValidProduct) { $product->setCustomOptions([]); - $qtyOptions = []; - $optionProductIds = []; - foreach ($item->getOptions() as $option) { - /** - * Call type-specific logic for product associated with quote item - */ - $product->getTypeInstance()->assignProductToOption( - $productCollection->getItemById($option->getProductId()), - $option, - $product - ); - - if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { - $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); + $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); + foreach ($optionProductIds as $optionProductId) { + $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); + if ($qtyOption) { + $qtyOptions[$optionProductId] = $qtyOption; } } - - if ($optionProductIds) { - foreach ($optionProductIds as $optionProductId) { - $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); - if ($qtyOption) { - $qtyOptions[$optionProductId] = $qtyOption; - } - } - } - - $item->setQtyOptions($qtyOptions)->setProduct($product); } else { $item->isDeleted(true); - $recollectQuote = true; + $this->recollectQuote = true; + } + if (!$item->isDeleted()) { + $item->setQtyOptions($qtyOptions)->setProduct($product); + $item->checkData(); } - $item->checkData(); } - - if ($recollectQuote && $this->_quote) { + if ($this->recollectQuote && $this->_quote) { $this->_quote->collectTotals(); } \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__); @@ -275,31 +285,67 @@ protected function _assignProducts() } /** - * Prevents adding stock status filter to the collection of products. + * Get product Ids from option. * + * @param QuoteItem $item + * @param ProductInterface $product * @param ProductCollection $productCollection - * @return void + * @return array + */ + private function getOptionProductIds( + QuoteItem $item, + ProductInterface $product, + ProductCollection $productCollection + ): array { + $optionProductIds = []; + foreach ($item->getOptions() as $option) { + /** + * Call type-specific logic for product associated with quote item + */ + $product->getTypeInstance()->assignProductToOption( + $productCollection->getItemById($option->getProductId()), + $option, + $product + ); + + if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { + $isValidProduct = $this->isValidProduct($option->getProduct()); + if (!$isValidProduct && !$item->isDeleted()) { + $item->isDeleted(true); + $this->recollectQuote = true; + continue; + } + $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); + } + } + + return $optionProductIds; + } + + /** + * Check is valid product. * - * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection + * @param ProductInterface|null $product + * @return bool */ - private function skipStockStatusFilter(ProductCollection $productCollection) + private function isValidProduct(ProductInterface $product = null): bool { - $productCollection->setFlag('has_stock_status_filter', true); + $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); + + return $result; } /** - * Add tier prices to product collection. + * Prevents adding stock status filter to the collection of products. * * @param ProductCollection $productCollection * @return void + * + * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection */ - private function addTierPriceData(ProductCollection $productCollection) + private function skipStockStatusFilter(ProductCollection $productCollection) { - if (empty($this->_quote)) { - $productCollection->addTierPriceData(); - } else { - $productCollection->addTierPriceDataByGroupId($this->_quote->getCustomerGroupId()); - } + $productCollection->setFlag('has_stock_status_filter', true); } /** @@ -309,6 +355,10 @@ private function addTierPriceData(ProductCollection $productCollection) */ private function removeItemsWithAbsentProducts() { + if (count($this->_productIds) === 0) { + return; + } + $productCollection = $this->_productCollectionFactory->create()->addIdFilter($this->_productIds); $existingProductsIds = $productCollection->getAllIds(); $absentProductsIds = array_diff($this->_productIds, $existingProductsIds); diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php index 0e2be5c9e3692..71a93e4604200 100644 --- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php +++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php @@ -78,7 +78,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address) @@ -94,7 +94,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; $sameAsBilling = $address->getSameAsBilling() ? 1 : 0; $customerAddressId = $address->getCustomerAddressId(); - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $quote->setShippingAddress($address); $address = $quote->getShippingAddress(); @@ -122,7 +122,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritdoc */ public function get($cartId) { diff --git a/app/code/Magento/Quote/Model/ShippingMethodManagement.php b/app/code/Magento/Quote/Model/ShippingMethodManagement.php index 5d7160d8c384a..7c87ca26832ef 100644 --- a/app/code/Magento/Quote/Model/ShippingMethodManagement.php +++ b/app/code/Magento/Quote/Model/ShippingMethodManagement.php @@ -15,6 +15,7 @@ use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\EstimateAddressInterface; use Magento\Quote\Api\ShipmentEstimationInterface; +use Magento\Quote\Model\ResourceModel\Quote\Address as QuoteAddressResource; /** * Shipping method read service @@ -62,6 +63,11 @@ class ShippingMethodManagement implements */ private $addressFactory; + /** + * @var QuoteAddressResource + */ + private $quoteAddressResource; + /** * Constructor * @@ -70,13 +76,15 @@ class ShippingMethodManagement implements * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository * @param Quote\TotalsCollector $totalsCollector * @param AddressInterfaceFactory|null $addressFactory + * @param QuoteAddressResource|null $quoteAddressResource */ public function __construct( \Magento\Quote\Api\CartRepositoryInterface $quoteRepository, Cart\ShippingMethodConverter $converter, \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector, - AddressInterfaceFactory $addressFactory = null + AddressInterfaceFactory $addressFactory = null, + QuoteAddressResource $quoteAddressResource = null ) { $this->quoteRepository = $quoteRepository; $this->converter = $converter; @@ -84,6 +92,8 @@ public function __construct( $this->totalsCollector = $totalsCollector; $this->addressFactory = $addressFactory ?: ObjectManager::getInstance() ->get(AddressInterfaceFactory::class); + $this->quoteAddressResource = $quoteAddressResource ?: ObjectManager::getInstance() + ->get(QuoteAddressResource::class); } /** @@ -170,9 +180,9 @@ public function set($cartId, $carrierCode, $methodCode) * @param string $methodCode The shipping method code. * @return void * @throws InputException The shipping method is not valid for an empty cart. - * @throws CouldNotSaveException The shipping method could not be saved. * @throws NoSuchEntityException Cart contains only virtual products. Shipping method is not applicable. * @throws StateException The billing or shipping address is not set. + * @throws \Exception */ public function apply($cartId, $carrierCode, $methodCode) { @@ -188,6 +198,8 @@ public function apply($cartId, $carrierCode, $methodCode) } $shippingAddress = $quote->getShippingAddress(); if (!$shippingAddress->getCountryId()) { + // Remove empty quote address + $this->quoteAddressResource->delete($shippingAddress); throw new StateException(__('Shipping address is not set')); } $shippingAddress->setShippingMethod($carrierCode . '_' . $methodCode); diff --git a/app/code/Magento/Quote/Model/SubmitQuoteValidator.php b/app/code/Magento/Quote/Model/SubmitQuoteValidator.php new file mode 100644 index 0000000000000..76d31f94d2a62 --- /dev/null +++ b/app/code/Magento/Quote/Model/SubmitQuoteValidator.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address\Validator as OrderAddressValidator; + +/** + * Validates quote and order before quote submit. + */ +class SubmitQuoteValidator +{ + /** + * @var QuoteValidator + */ + private $quoteValidator; + + /** + * @var OrderAddressValidator + */ + private $orderAddressValidator; + + /** + * @param QuoteValidator $quoteValidator + * @param OrderAddressValidator $orderAddressValidator + */ + public function __construct( + QuoteValidator $quoteValidator, + OrderAddressValidator $orderAddressValidator + ) { + $this->quoteValidator = $quoteValidator; + $this->orderAddressValidator = $orderAddressValidator; + } + + /** + * Validates quote. + * + * @param Quote $quote + * @return void + * @throws LocalizedException + */ + public function validateQuote(Quote $quote) + { + $this->quoteValidator->validateBeforeSubmit($quote); + } + + /** + * Validates order. + * + * @param Order $order + * @return void + * @throws LocalizedException + */ + public function validateOrder(Order $order) + { + foreach ($order->getAddresses() as $address) { + $errors = $this->orderAddressValidator->validate($address); + if (!empty($errors)) { + throw new LocalizedException( + __("Failed address validation:\n%1", implode("\n", $errors)) + ); + } + } + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php new file mode 100644 index 0000000000000..2498e9976f009 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; +use Magento\Store\Model\ScopeInterface; + +/** + * @inheritdoc + */ +class AllowedCountryValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var AllowedCountries + */ + private $allowedCountryReader; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param AllowedCountries $allowedCountryReader + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + AllowedCountries $allowedCountryReader, + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->allowedCountryReader = $allowedCountryReader; + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + + if (!$quote->isVirtual()) { + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); + $validationResult = + in_array( + $shippingAddress->getCountryId(), + $this->allowedCountryReader->getAllowedCountries( + ScopeInterface::SCOPE_STORE, + $quote->getStoreId() + ) + ); + if (!$validationResult) { + $validationErrors = [__($this->generalMessage)]; + } + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php new file mode 100644 index 0000000000000..2c02c9f9eacc4 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class BillingAddressValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + + $billingAddress = $quote->getBillingAddress(); + $billingAddress->setStoreId($quote->getStoreId()); + $validationResult = $billingAddress->validate(); + + if ($validationResult !== true) { + $validationErrors = [__($this->generalMessage)]; + } + if (is_array($validationResult)) { + $validationErrors = array_merge($validationErrors, $validationResult); + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/MinimumAmountValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/MinimumAmountValidationRule.php new file mode 100644 index 0000000000000..34e953be43c74 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/MinimumAmountValidationRule.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage; + +/** + * @inheritdoc + */ +class MinimumAmountValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var ValidationMessage + */ + private $amountValidationMessage; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param ValidationMessage $amountValidationMessage + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + ValidationMessage $amountValidationMessage, + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->amountValidationMessage = $amountValidationMessage; + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + * @throws \Zend_Currency_Exception + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + $validationResult = $quote->validateMinimumAmount($quote->getIsMultiShipping()); + if (!$validationResult) { + if (!$this->generalMessage) { + $this->generalMessage = $this->amountValidationMessage->getMessage(); + } + $validationErrors = [__($this->generalMessage)]; + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/PaymentMethodValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/PaymentMethodValidationRule.php new file mode 100644 index 0000000000000..bf2b813541fb0 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/PaymentMethodValidationRule.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class PaymentMethodValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + $validationResult = $quote->getPayment()->getMethod(); + if (!$validationResult) { + $validationErrors = [__($this->generalMessage)]; + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationComposite.php b/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationComposite.php new file mode 100644 index 0000000000000..6a75be3acce89 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationComposite.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class QuoteValidationComposite implements QuoteValidationRuleInterface +{ + /** + * @var QuoteValidationRuleInterface[] + */ + private $validationRules = []; + + /** + * @param QuoteValidationRuleInterface[] $validationRules + * @throws \InvalidArgumentException + */ + public function __construct(array $validationRules) + { + foreach ($validationRules as $validationRule) { + if (!($validationRule instanceof QuoteValidationRuleInterface)) { + throw new \InvalidArgumentException( + sprintf( + 'Instance of the ValidationRuleInterface is expected, got %s instead.', + get_class($validationRule) + ) + ); + } + } + $this->validationRules = $validationRules; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $aggregateResult = []; + + foreach ($this->validationRules as $validationRule) { + $ruleValidationResult = $validationRule->validate($quote); + foreach ($ruleValidationResult as $item) { + if (!$item->isValid()) { + array_push($aggregateResult, $item); + } + } + } + + return $aggregateResult; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationRuleInterface.php b/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationRuleInterface.php new file mode 100644 index 0000000000000..1a777e3f1a5fe --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/QuoteValidationRuleInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResult; +use Magento\Quote\Model\Quote; + +/** + * Provides validation of Quote model. + */ +interface QuoteValidationRuleInterface +{ + /** + * Validate Quote model. + * + * @param Quote $quote + * @return ValidationResult[] + */ + public function validate(Quote $quote): array; +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php new file mode 100644 index 0000000000000..1ced36b9bccf3 --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class ShippingAddressValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + + if (!$quote->isVirtual()) { + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); + $validationResult = $shippingAddress->validate(); + + if ($validationResult !== true) { + $validationErrors = [__($this->generalMessage)]; + } + if (is_array($validationResult)) { + $validationErrors = array_merge($validationErrors, $validationResult); + } + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php new file mode 100644 index 0000000000000..3ef079f5a019a --- /dev/null +++ b/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ValidationRules; + +use Magento\Framework\Validation\ValidationResultFactory; +use Magento\Quote\Model\Quote; + +/** + * @inheritdoc + */ +class ShippingMethodValidationRule implements QuoteValidationRuleInterface +{ + /** + * @var string + */ + private $generalMessage; + + /** + * @var ValidationResultFactory + */ + private $validationResultFactory; + + /** + * @param ValidationResultFactory $validationResultFactory + * @param string $generalMessage + */ + public function __construct( + ValidationResultFactory $validationResultFactory, + string $generalMessage = '' + ) { + $this->validationResultFactory = $validationResultFactory; + $this->generalMessage = $generalMessage; + } + + /** + * @inheritdoc + */ + public function validate(Quote $quote): array + { + $validationErrors = []; + + if (!$quote->isVirtual()) { + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); + $shippingMethod = $shippingAddress->getShippingMethod(); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingMethod); + $validationResult = $shippingMethod && $shippingRate; + if (!$validationResult) { + $validationErrors = [__($this->generalMessage)]; + } + } + + return [$this->validationResultFactory->create(['errors' => $validationErrors])]; + } +} diff --git a/app/code/Magento/Quote/Observer/Webapi/SubmitObserver.php b/app/code/Magento/Quote/Observer/SubmitObserver.php similarity index 94% rename from app/code/Magento/Quote/Observer/Webapi/SubmitObserver.php rename to app/code/Magento/Quote/Observer/SubmitObserver.php index 4f1e66dcc724b..1213636e5966b 100644 --- a/app/code/Magento/Quote/Observer/Webapi/SubmitObserver.php +++ b/app/code/Magento/Quote/Observer/SubmitObserver.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Quote\Observer\Webapi; +namespace Magento\Quote\Observer; use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\Framework\Event\ObserverInterface; @@ -13,12 +13,12 @@ class SubmitObserver implements ObserverInterface /** * @var \Psr\Log\LoggerInterface */ - protected $logger; + private $logger; /** * @var OrderSender */ - protected $orderSender; + private $orderSender; /** * @param \Psr\Log\LoggerInterface $logger diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php new file mode 100644 index 0000000000000..0ac589b11b53b --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Checkout\Model\Session; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcherInterface; + +/** + * Updates quote items store id. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class UpdateQuoteItemStore +{ + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param QuoteRepository $quoteRepository + * @param Session $checkoutSession + */ + public function __construct( + QuoteRepository $quoteRepository, + Session $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreSwitcherInterface $subject + * @param string $result + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSwitch( + StoreSwitcherInterface $subject, + string $result, + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ): string { + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + $quote->setStoreId($targetStore->getId()); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteStore.php new file mode 100644 index 0000000000000..de2fc76b9179f --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteStore.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Checkout\Model\Session; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreCookieManagerInterface; + +/** + * Updates quote store id. + */ +class UpdateQuoteStore +{ + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param QuoteRepository $quoteRepository + * @param Session $checkoutSession + */ + public function __construct( + QuoteRepository $quoteRepository, + Session $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreCookieManagerInterface $subject + * @param null $result + * @param StoreInterface $store + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSetStoreCookie( + StoreCookieManagerInterface $subject, + $result, + StoreInterface $store + ) { + $storeCodeFromCookie = $subject->getStoreCodeFromCookie(); + if (null === $storeCodeFromCookie) { + return; + } + + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive() && $store->getCode() != $storeCodeFromCookie) { + $quote->setStoreId( + $store->getId() + ); + $this->quoteRepository->save($quote); + } + } +} diff --git a/app/code/Magento/Quote/Setup/UpgradeSchema.php b/app/code/Magento/Quote/Setup/UpgradeSchema.php index 1bb20a669bdf2..1689bc55954e2 100644 --- a/app/code/Magento/Quote/Setup/UpgradeSchema.php +++ b/app/code/Magento/Quote/Setup/UpgradeSchema.php @@ -5,6 +5,7 @@ */ namespace Magento\Quote\Setup; +use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\UpgradeSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; @@ -25,7 +26,6 @@ class UpgradeSchema implements UpgradeSchemaInterface public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) { $setup->startSetup(); - if (version_compare($context->getVersion(), '2.0.1', '<')) { $setup->getConnection(self::$connectionName)->addIndex( $setup->getTable('quote_id_mask', self::$connectionName), @@ -33,14 +33,13 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con ['masked_id'] ); } - if (version_compare($context->getVersion(), '2.0.2', '<')) { $setup->getConnection(self::$connectionName)->changeColumn( $setup->getTable('quote_address', self::$connectionName), 'street', 'street', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'type' => Table::TYPE_TEXT, 'length' => 255, 'comment' => 'Street' ] @@ -61,7 +60,7 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $setup->getTable('quote_address', self::$connectionName), 'shipping_method', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'type' => Table::TYPE_TEXT, 'length' => 120 ] ); @@ -72,33 +71,79 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $setup->getTable('quote_address', self::$connectionName), 'firstname', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'type' => Table::TYPE_TEXT, 'length' => 255, ] )->modifyColumn( $setup->getTable('quote_address', self::$connectionName), 'middlename', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'type' => Table::TYPE_TEXT, 'length' => 40, ] )->modifyColumn( $setup->getTable('quote_address', self::$connectionName), 'lastname', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'type' => Table::TYPE_TEXT, 'length' => 255, ] )->modifyColumn( $setup->getTable('quote', self::$connectionName), 'updated_at', [ - 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + 'type' => Table::TYPE_TIMESTAMP, 'nullable' => false, - 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE, + 'default' => Table::TIMESTAMP_INIT_UPDATE, ] ); } + if (version_compare($context->getVersion(), '2.0.7', '<')) { + $this->expandQuoteAddressFields($setup); + } + if (version_compare($context->getVersion(), '2.0.8', '<')) { + $this->expandRemoteIpField($setup); + } $setup->endSetup(); } + + /** + * @param SchemaSetupInterface $setup + * @return void + */ + private function expandRemoteIpField(SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(self::$connectionName); + $connection->modifyColumn( + $setup->getTable('quote', self::$connectionName), + 'remote_ip', + ['type' => Table::TYPE_TEXT, 'length' => 45] + ); + } + + /** + * @param SchemaSetupInterface $setup + * @return void + */ + private function expandQuoteAddressFields(SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(self::$connectionName); + $connection->modifyColumn( + $setup->getTable('quote_address', self::$connectionName), + 'telephone', + ['type' => Table::TYPE_TEXT, 'length' => 255] + )->modifyColumn( + $setup->getTable('quote_address', self::$connectionName), + 'fax', + ['type' => Table::TYPE_TEXT, 'length' => 255] + )->modifyColumn( + $setup->getTable('quote_address', self::$connectionName), + 'region', + ['type' => Table::TYPE_TEXT, 'length' => 255] + )->modifyColumn( + $setup->getTable('quote_address', self::$connectionName), + 'city', + ['type' => Table::TYPE_TEXT, 'length' => 255] + ); + } } diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml new file mode 100644 index 0000000000000..4cc2c4f392419 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SimpleCartItem" type="CartItem"> + <data key="qty">1</data> + <var key="quote_id" entityKey="return" entityType="GuestCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Data/GuestCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/GuestCartData.xml new file mode 100644 index 0000000000000..37c9ed0d1c43e --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/GuestCartData.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="GuestCart" type="GuestCart"> + </entity> + + <entity name="GuestAddressInformation" type="AddressInformation"> + <var key="quote_id" entityKey="return" entityType="GuestCart"/> + <requiredEntity type="shipping_address">ShippingAddressTX</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + <data key="shipping_method_code">flatrate</data> + <data key="shipping_carrier_code">flatrate</data> + </entity> + + <entity name="GuestOrderPaymentMethod" type="PaymentInformation"> + <requiredEntity type="payment_method">PaymentMethodCheckMoneyOrder</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/LICENSE.txt b/app/code/Magento/Quote/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Quote/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 <insert your license name here>" 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/app/code/Magento/Quote/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Quote/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Quote/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 <insert your license name here>" 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/app/code/Magento/Quote/Test/Mftf/Metadata/billing_address-meta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/billing_address-meta.xml new file mode 100644 index 0000000000000..d4a4c4345d738 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/billing_address-meta.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateBillingAddress" dataType="billing_address" type="create"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">int</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/guest-cart-meta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/guest-cart-meta.xml new file mode 100644 index 0000000000000..54b53d6646781 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/guest-cart-meta.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateGuestCart" dataType="GuestCart" type="create" auth="anonymous" url="/V1/guest-carts" method="POST"> + <contentType>application/json</contentType> + </operation> + + <operation name="AddProductToGuestCart" dataType="CartItem" type="create" auth="anonymous" url="V1/guest-carts/{return}/items" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + <field key="sku" type="string">string</field> + <field key="qty">integer</field> + </object> + </operation> + + <operation name="AddAddressInfoToGuestCart" dataType="AddressInformation" type="create" auth="anonymous" url="/V1/guest-carts/{return}/shipping-information" method="POST"> + <contentType>application/json</contentType> + <object key="addressInformation" dataType="AddressInformation"> + <object key="shipping_address" dataType="shipping_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <object key="billing_address" dataType="billing_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <field key="shipping_method_code">string</field> + <field key="shipping_carrier_code">string</field> + </object> + </operation> + + <operation name="SendGuestPaymentInformation" dataType="PaymentInformation" type="update" auth="anonymous" url="/V1/guest-carts/{return}/order" method="PUT"> + <contentType>application/json</contentType> + <object key="paymentMethod" dataType="payment_method"> + <field key="method">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/shipping_address-meta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/shipping_address-meta.xml new file mode 100644 index 0000000000000..803681252a9e9 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/shipping_address-meta.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateShippingAddress" dataType="shipping_address" type="create"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">int</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/README.md b/app/code/Magento/Quote/Test/Mftf/README.md new file mode 100644 index 0000000000000..f1021b8435cba --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Quote Functional Tests + +The Functional Test Module for **Magento Quote** module. diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml new file mode 100644 index 0000000000000..e5e7c3834bf7d --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutDisabledProductTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Remove item from cart if product disabled"/> + <description value="Remove item from cart if simple or configurable product is disabled"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95844"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="AdminResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + <!-- Step 1: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> + <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart($$createConfigProduct.name$$)}}" time="30" stepKey="assertMessage"/> + <!--Disabled via admin panel--> + <openNewTab stepKey="openNewTab"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!-- Disabled child configurable product --> + <scrollToTopOfPage stepKey="scrollToShowEnableDisableControl"/> + <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductPageSaved"/> + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab"/> + <!-- Check cart --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem"/> + <!-- Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct2.name$$)}}" stepKey="amOnSimpleProductPage2"/> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addToCart2"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartPage"/> + <!-- Disabled via admin panel --> + <openNewTab stepKey="openNewTab2"/> + <!-- Find the first simple product that we just created using the product grid and go to its page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab2"/> + <!--Check cart--> + <reloadPage stepKey="reloadPage2"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart2"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem2"/> + </test> +</tests> diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalManagementTest.php index 287d2b3c8d4f0..d6338eb428657 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalManagementTest.php @@ -92,6 +92,9 @@ public function testCollectTotalsNoShipping($shippingCarrierCode, $shippingMetho ); } + /** + * @return array + */ public function collectTotalsShippingData() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php index b1cec90e61e01..1e999cb5e523e 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php @@ -163,6 +163,9 @@ public function testGet($isVirtual, $getAddressType) $this->assertEquals($totalsMock, $this->model->get($cartId)); } + /** + * @return array + */ public function getDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/ShippingMethodConverterTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/ShippingMethodConverterTest.php index ec6cfc0228cec..25dda4a82970c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/ShippingMethodConverterTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/ShippingMethodConverterTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model\Cart; use Magento\Quote\Model\Cart\ShippingMethodConverter; @@ -56,7 +53,10 @@ class ShippingMethodConverterTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->shippingMethodDataFactoryMock = $this->createPartialMock(\Magento\Quote\Api\Data\ShippingMethodInterfaceFactory::class, ['create']); + $this->shippingMethodDataFactoryMock = $this->createPartialMock( + \Magento\Quote\Api\Data\ShippingMethodInterfaceFactory::class, + ['create'] + ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->currencyMock = $this->createMock(\Magento\Directory\Model\Currency::class); $this->shippingMethodMock = $this->createPartialMock(\Magento\Quote\Model\Cart\ShippingMethod::class, [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php index 27824ed44ef3f..011dc2f45c7ce 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php @@ -1,12 +1,9 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model; use Magento\Quote\Model\CouponManagement; @@ -49,6 +46,7 @@ protected function setUp() 'save', 'getShippingAddress', 'getCouponCode', + 'getStoreId', '__wakeup' ]); $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ @@ -100,6 +98,9 @@ public function testSetWhenCouldNotApplyCoupon() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); @@ -127,6 +128,9 @@ public function testSetWhenCouponCodeIsInvalid() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); @@ -146,6 +150,9 @@ public function testSet() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); diff --git a/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php index ee0ffd3bcc666..73ed2e65b41a9 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php @@ -94,7 +94,7 @@ public function testCreateEmptyCart() $cartId = 1; $this->quoteIdMaskMock->expects($this->once())->method('setQuoteId')->with($cartId)->willReturnSelf(); $this->quoteIdMaskMock->expects($this->once())->method('save')->willReturnSelf(); - $this->quoteIdMaskMock->expects($this->once())->method('getMaskedId')->willreturn($maskedCartId); + $this->quoteIdMaskMock->expects($this->once())->method('getMaskedId')->willReturn($maskedCartId); $this->quoteIdMaskFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteIdMaskMock); $this->quoteManagementMock->expects($this->once())->method('createEmptyCart')->willReturn($cartId); diff --git a/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php b/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php index 9cde4bbc19184..d67ebdd5a0fc2 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php @@ -36,7 +36,7 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\StateException - * @expectedMessage Cannot assign customer to the given cart. You don't have permission for this operation. + * @expectedExceptionMessage Cannot assign customer to the given cart. You don't have permission for this operation. */ public function testBeforeAssignCustomer() { diff --git a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php index 8143e0e417ead..3d6ef2dd2882c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php @@ -151,8 +151,8 @@ public function testSetVirtualProduct() ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->expects($this->once())->method('isVirtual')->willReturn(true); $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); @@ -164,7 +164,6 @@ public function testSetVirtualProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -217,9 +216,9 @@ public function testSetVirtualProductThrowsExceptionIfPaymentMethodNotAvailable( ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); - $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(true); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -267,17 +266,20 @@ public function testSetSimpleProduct() $shippingAddressMock = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['getCountryId', 'setPaymentMethod'] + ['getCountryId', 'setPaymentMethod', 'setCollectShippingRates'] ); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(100); $shippingAddressMock->expects($this->once()) ->method('setPaymentMethod') ->with($paymentMethod) ->willReturnSelf(); + $shippingAddressMock->expects($this->once()) + ->method('setCollectShippingRates') + ->with(true); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->exactly(4))->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -288,7 +290,6 @@ public function testSetSimpleProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -302,7 +303,6 @@ public function testSetSimpleProduct() public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() { $cartId = 100; - $methodData = ['method' => 'data']; $quoteMock = $this->createPartialMock( \Magento\Quote\Model\Quote::class, @@ -310,6 +310,7 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() ); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); + /** @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject $methodMock */ $methodMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['setChecks', 'getData']); $methodMock->expects($this->once()) ->method('setChecks') @@ -320,17 +321,13 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]) ->willReturnSelf(); - $methodMock->expects($this->once())->method('getData')->willReturn($methodData); - - $paymentMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['importData']); - $paymentMock->expects($this->once())->method('importData')->with($methodData)->willReturnSelf(); + $methodMock->expects($this->never())->method('getData'); $shippingAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getCountryId']); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(null); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $this->model->set($cartId, $methodMock); } diff --git a/app/code/Magento/Quote/Test/Unit/Model/Product/Plugin/UpdateQuoteItemsTest.php b/app/code/Magento/Quote/Test/Unit/Model/Product/Plugin/UpdateQuoteItemsTest.php index 38f303dd23582..279b2c4b4091f 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Product/Plugin/UpdateQuoteItemsTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -15,7 +15,7 @@ class UpdateQuoteItemsTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\ResourceModel\Quote */ - private $quoteResource ; + private $quoteResource; protected function setUp() { @@ -49,6 +49,9 @@ public function testAfterUpdate($originalPrice, $newPrice, $callMethod, $tierPri $this->assertEquals($result, $productResourceMock); } + /** + * @return array + */ public function aroundUpdateDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/ShippingTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/ShippingTest.php index 6c572b1e86d08..57ecb9534b0fc 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/ShippingTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/ShippingTest.php @@ -158,12 +158,10 @@ public function testCollect() $this->shippingAssignment->expects($this->atLeastOnce()) ->method('getItems') ->willReturn([$this->cartItem]); - $this->freeShipping->expects($this->once()) - ->method('isFreeShipping') + $this->freeShipping->method('isFreeShipping') ->with($this->quote, [$this->cartItem]) ->willReturn(true); - $this->address->expects($this->once()) - ->method('setFreeShipping') + $this->address->method('setFreeShipping') ->with(true); $this->total->expects($this->atLeastOnce()) ->method('setTotalAmount'); @@ -175,24 +173,19 @@ public function testCollect() $this->cartItem->expects($this->atLeastOnce()) ->method('isVirtual') ->willReturn(false); - $this->cartItem->expects($this->once()) - ->method('getParentItem') + $this->cartItem->method('getParentItem') ->willReturn(false); - $this->cartItem->expects($this->once()) - ->method('getHasChildren') + $this->cartItem->method('getHasChildren') ->willReturn(false); - $this->cartItem->expects($this->once()) - ->method('getWeight') + $this->cartItem->method('getWeight') ->willReturn(2); $this->cartItem->expects($this->atLeastOnce()) ->method('getQty') ->willReturn(2); $this->freeShippingAssertions(); - $this->cartItem->expects($this->once()) - ->method('setRowWeight') + $this->cartItem->method('setRowWeight') ->with(0); - $this->address->expects($this->once()) - ->method('setItemQty') + $this->address->method('setItemQty') ->with(2); $this->address->expects($this->atLeastOnce()) ->method('setWeight'); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php index 78a87b59d9f62..c4a8081fbb8fa 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php @@ -49,6 +49,9 @@ protected function setUp() ); } + /** + * @return array + */ public function collectDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/TotalTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/TotalTest.php index f884981424d65..e1971fa9833a3 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/TotalTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/TotalTest.php @@ -49,6 +49,9 @@ public function testSetTotalAmount($code, $amount, $storedCode) $this->assertSame($this->model, $result); } + /** + * @return array + */ public function setTotalAmountDataProvider() { return [ @@ -80,6 +83,9 @@ public function testSetBaseTotalAmount($code, $amount, $storedCode) $this->assertSame($this->model, $result); } + /** + * @return array + */ public function setBaseTotalAmountDataProvider() { return [ @@ -111,6 +117,9 @@ public function testAddTotalAmount($initialAmount, $delta, $updatedAmount) $this->assertEquals($updatedAmount, $this->model->getTotalAmount($code)); } + /** + * @return array + */ public function addTotalAmountDataProvider() { return [ @@ -142,6 +151,9 @@ public function testAddBaseTotalAmount($initialAmount, $delta, $updatedAmount) $this->assertEquals($updatedAmount, $this->model->getBaseTotalAmount($code)); } + /** + * @return array + */ public function addBaseTotalAmountDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index e25b770b7a81e..140833d84aa8d 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model\Quote; use Magento\Directory\Model\Currency; @@ -172,7 +170,9 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->attributeList = $this->createMock(\Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::class); + $this->attributeList = $this->createMock( + \Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::class + ); $this->attributeList->method('getAttributes')->willReturn([]); $this->address = $objectManager->getObject( @@ -216,6 +216,7 @@ public function testValidateMiniumumAmountVirtual() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -240,6 +241,31 @@ public function testValidateMiniumumAmount() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], + ]; + + $this->quote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->quote->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->willReturnMap($scopeConfigValues); + + $this->assertTrue($this->address->validateMinimumAmount()); + } + + public function testValidateMiniumumAmountWithoutDiscount() + { + $storeId = 1; + $scopeConfigValues = [ + ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, false], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -263,6 +289,7 @@ public function testValidateMiniumumAmountNegative() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php index c14d6fa066103..e9c2077a09e98 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model\Quote\Item; /** @@ -43,9 +41,18 @@ class CompareTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->itemMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getProductId', 'getOptions']); - $this->comparedMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getProductId', 'getOptions']); - $this->optionMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item\Option::class, ['__wakeup', 'getCode', 'getValue']); + $this->itemMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Item::class, + ['__wakeup', 'getProductId', 'getOptions'] + ); + $this->comparedMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Item::class, + ['__wakeup', 'getProductId', 'getOptions'] + ); + $this->optionMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Item\Option::class, + ['__wakeup', 'getCode', 'getValue'] + ); $serializer = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class) ->setMethods(['unserialize']) ->disableOriginalConstructor() @@ -118,25 +125,37 @@ public function testCompareProductWithDifferentOptions() $this->itemMock->expects($this->any()) ->method('getOptions') - ->will($this->returnValue([ + ->willReturn( + [ $this->getOptionMock('option-1', 1), $this->getOptionMock('option-2', 'option-value'), - $this->getOptionMock('option-3', json_encode([ - 'value' => 'value-1', - 'qty' => 2, - ]) - ), ] - )); + $this->getOptionMock( + 'option-3', + json_encode( + [ + 'value' => 'value-1', + 'qty' => 2, + ] + ) + ), + ] + ); $this->comparedMock->expects($this->any()) ->method('getOptions') - ->will($this->returnValue([ + ->willReturn( + [ $this->getOptionMock('option-4', 1), $this->getOptionMock('option-2', 'option-value'), - $this->getOptionMock('option-3', json_encode([ - 'value' => 'value-1', - 'qty' => 2, - ])), - ]) + $this->getOptionMock( + 'option-3', + json_encode( + [ + 'value' => 'value-1', + 'qty' => 2, + ] + ) + ), + ] ); $this->assertFalse($this->helper->compare($this->itemMock, $this->comparedMock)); } @@ -154,15 +173,21 @@ public function testCompareItemWithComparedWithoutOption() ->will($this->returnValue(1)); $this->itemMock->expects($this->any()) ->method('getOptions') - ->will($this->returnValue([ + ->willReturn( + [ $this->getOptionMock('option-1', 1), $this->getOptionMock('option-2', 'option-value'), - $this->getOptionMock('option-3', json_encode([ - 'value' => 'value-1', - 'qty' => 2, - ]) - ), ] - )); + $this->getOptionMock( + 'option-3', + json_encode( + [ + 'value' => 'value-1', + 'qty' => 2, + ] + ) + ), + ] + ); $this->comparedMock->expects($this->any()) ->method('getOptions') ->will($this->returnValue([])); @@ -182,15 +207,21 @@ public function testCompareItemWithoutOptionWithCompared() ->will($this->returnValue(1)); $this->comparedMock->expects($this->any()) ->method('getOptions') - ->will($this->returnValue([ + ->willReturn( + [ $this->getOptionMock('option-1', 1), $this->getOptionMock('option-2', 'option-value'), - $this->getOptionMock('option-3', json_encode([ - 'value' => 'value-1', - 'qty' => 2, - ]) - ), ] - )); + $this->getOptionMock( + 'option-3', + json_encode( + [ + 'value' => 'value-1', + 'qty' => 2, + ] + ) + ), + ] + ); $this->itemMock->expects($this->any()) ->method('getOptions') ->will($this->returnValue([])); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php index a56de35b70f75..e8cfb05ca89b9 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php @@ -76,7 +76,8 @@ protected function setUp() 'addQty', 'setCustomPrice', 'setOriginalCustomPrice', - 'setData' + 'setData', + 'setprice' ]); $this->quoteItemFactoryMock->expects($this->any()) ->method('create') @@ -98,7 +99,13 @@ protected function setUp() $this->productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, - ['getCustomOptions', '__wakeup', 'getParentProductId', 'getCartQty', 'getStickWithinParent'] + [ + 'getCustomOptions', + '__wakeup', + 'getParentProductId', + 'getCartQty', + 'getStickWithinParent', + 'getFinalPrice'] ); $this->objectMock = $this->createPartialMock( \Magento\Framework\DataObject::class, @@ -239,6 +246,7 @@ public function testPrepare() $customPrice = 400000000; $itemId = 1; $requestItemId = 1; + $finalPrice = 1000000000; $this->productMock->expects($this->any()) ->method('getCartQty') @@ -246,6 +254,9 @@ public function testPrepare() $this->productMock->expects($this->any()) ->method('getStickWithinParent') ->will($this->returnValue(false)); + $this->productMock->expects($this->once()) + ->method('getFinalPrice') + ->will($this->returnValue($finalPrice)); $this->itemMock->expects($this->once()) ->method('addQty') @@ -255,6 +266,9 @@ public function testPrepare() ->will($this->returnValue($itemId)); $this->itemMock->expects($this->never()) ->method('setData'); + $this->itemMock->expects($this->once()) + ->method('setPrice') + ->will($this->returnValue($this->itemMock)); $this->objectMock->expects($this->any()) ->method('getCustomPrice') @@ -282,6 +296,7 @@ public function testPrepareWithResetCountAndStick() $customPrice = 400000000; $itemId = 1; $requestItemId = 1; + $finalPrice = 1000000000; $this->productMock->expects($this->any()) ->method('getCartQty') @@ -289,6 +304,9 @@ public function testPrepareWithResetCountAndStick() $this->productMock->expects($this->any()) ->method('getStickWithinParent') ->will($this->returnValue(true)); + $this->productMock->expects($this->once()) + ->method('getFinalPrice') + ->will($this->returnValue($finalPrice)); $this->itemMock->expects($this->once()) ->method('addQty') @@ -298,6 +316,9 @@ public function testPrepareWithResetCountAndStick() ->will($this->returnValue($itemId)); $this->itemMock->expects($this->never()) ->method('setData'); + $this->itemMock->expects($this->once()) + ->method('setPrice') + ->will($this->returnValue($this->itemMock)); $this->objectMock->expects($this->any()) ->method('getCustomPrice') @@ -325,6 +346,7 @@ public function testPrepareWithResetCountAndNotStickAndOtherItemId() $customPrice = 400000000; $itemId = 1; $requestItemId = 2; + $finalPrice = 1000000000; $this->productMock->expects($this->any()) ->method('getCartQty') @@ -332,6 +354,9 @@ public function testPrepareWithResetCountAndNotStickAndOtherItemId() $this->productMock->expects($this->any()) ->method('getStickWithinParent') ->will($this->returnValue(false)); + $this->productMock->expects($this->once()) + ->method('getFinalPrice') + ->will($this->returnValue($finalPrice)); $this->itemMock->expects($this->once()) ->method('addQty') @@ -341,6 +366,9 @@ public function testPrepareWithResetCountAndNotStickAndOtherItemId() ->will($this->returnValue($itemId)); $this->itemMock->expects($this->never()) ->method('setData'); + $this->itemMock->expects($this->once()) + ->method('setPrice') + ->will($this->returnValue($this->itemMock)); $this->objectMock->expects($this->any()) ->method('getCustomPrice') @@ -368,6 +396,7 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId() $customPrice = 400000000; $itemId = 1; $requestItemId = 1; + $finalPrice = 1000000000; $this->objectMock->expects($this->any()) ->method('getResetCount') @@ -386,10 +415,16 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId() $this->productMock->expects($this->any()) ->method('getStickWithinParent') ->will($this->returnValue(false)); + $this->productMock->expects($this->once()) + ->method('getFinalPrice') + ->will($this->returnValue($finalPrice)); $this->itemMock->expects($this->once()) ->method('addQty') ->with($qty); + $this->itemMock->expects($this->once()) + ->method('setPrice') + ->will($this->returnValue($this->itemMock)); $this->objectMock->expects($this->any()) ->method('getCustomPrice') diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RelatedProductsTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RelatedProductsTest.php index a73a241922a9c..8be4479598907 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RelatedProductsTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/RelatedProductsTest.php @@ -61,6 +61,9 @@ public function testGetRelatedProductIds($optionValue, $productId, $expectedResu * * @return array */ + /** + * @return array + */ public function getRelatedProductIdsDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php index 03c3e32c20cb9..8e6a3723caa7c 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php @@ -67,7 +67,7 @@ protected function setUp() 'addOption', 'setCustomPrice', 'setOriginalCustomPrice', - 'unsetData', + 'setData', 'hasData', 'setIsQtyDecimal' ]); @@ -92,7 +92,7 @@ protected function setUp() /** * @expectedException \InvalidArgumentException - * @ExceptedExceptionMessage The qty value is required to update quote item. + * @expectedExceptionMessage The qty value is required to update quote item. */ public function testUpdateNoQty() { @@ -130,6 +130,9 @@ public function testUpdateNotQtyDecimal($qty, $expectedQty) $this->assertEquals($result, $this->object); } + /** + * @return array + */ public function qtyProvider() { return [ @@ -142,6 +145,9 @@ public function qtyProvider() ]; } + /** + * @return array + */ public function qtyProviderDecimal() { return [ @@ -295,7 +301,7 @@ public function testUpdateUnsetCustomPrice() 'setProduct', 'getData', 'unsetData', - 'hasData' + 'hasData', ]); $buyRequestMock->expects($this->never())->method('setCustomPrice'); $buyRequestMock->expects($this->once())->method('getData')->will($this->returnValue([])); @@ -347,7 +353,11 @@ public function testUpdateUnsetCustomPrice() ->will($this->returnValue($buyRequestMock)); $this->itemMock->expects($this->exactly(2)) - ->method('unsetData'); + ->method('setData') + ->withConsecutive( + ['custom_price', null], + ['original_custom_price', null] + ); $this->itemMock->expects($this->once()) ->method('hasData') diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/ItemTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/ItemTest.php index 1da659f6b59b7..0e3d6c99efcaf 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/ItemTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/ItemTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model\Quote; /** @@ -112,7 +110,10 @@ protected function setUp() $this->compareHelper = $this->createMock(\Magento\Quote\Model\Quote\Item\Compare::class); - $this->stockItemMock = $this->createPartialMock(\Magento\CatalogInventory\Model\Stock\Item::class, ['getIsQtyDecimal', '__wakeup']); + $this->stockItemMock = $this->createPartialMock( + \Magento\CatalogInventory\Model\Stock\Item::class, + ['getIsQtyDecimal', '__wakeup'] + ); $this->serializer = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class) ->setMethods(['unserialize']) @@ -846,6 +847,12 @@ public function testSetOptionsWithNull() $this->assertEquals($this->model, $this->model->setOptions(null)); } + /** + * @param $optionCode + * @param array $optionData + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function createOptionMock($optionCode, $optionData = []) { $optionMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item\Option::class) @@ -918,7 +925,7 @@ public function testUpdateQtyOption() false, true, ['updateQtyOption'] - ); + ); $productMock->expects($this->once()) ->method('getTypeInstance') ->will($this->returnValue($typeInstanceMock)); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Payment/ToOrderPaymentTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Payment/ToOrderPaymentTest.php index ec6c9cb11dbac..9bdfa10094b47 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Payment/ToOrderPaymentTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Payment/ToOrderPaymentTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model\Quote\Payment; use Magento\Payment\Model\Method\Substitution; @@ -43,7 +41,10 @@ class ToOrderPaymentTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->paymentMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['getCcNumber', 'getCcCid', 'getMethodInstance', 'getAdditionalInformation']); + $this->paymentMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Payment::class, + ['getCcNumber', 'getCcCid', 'getMethodInstance', 'getAdditionalInformation'] + ); $this->objectCopyMock = $this->createMock(\Magento\Framework\DataObject\Copy::class); $this->orderPaymentRepositoryMock = $this->getMockForAbstractClass( \Magento\Sales\Api\OrderPaymentRepositoryInterface::class, @@ -119,7 +120,11 @@ public function testConvert() $this->orderPaymentRepositoryMock->expects($this->once())->method('create')->willReturn($orderPayment); $this->dataObjectHelper->expects($this->once()) ->method('populateWithArray') - ->with($orderPayment, array_merge($paymentData, $data), \Magento\Sales\Api\Data\OrderPaymentInterface::class) + ->with( + $orderPayment, + array_merge($paymentData, $data), + \Magento\Sales\Api\Data\OrderPaymentInterface::class + ) ->willReturnSelf(); $this->assertSame($orderPayment, $this->converter->convert($this->paymentMock, $data)); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Validator/MinimumOrderAmount/ValidationMessageTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Validator/MinimumOrderAmount/ValidationMessageTest.php index 64204ea1fb93d..272a4e3a4ba49 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Validator/MinimumOrderAmount/ValidationMessageTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Validator/MinimumOrderAmount/ValidationMessageTest.php @@ -26,19 +26,27 @@ class ValidationMessageTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject + * @deprecated since 101.0.0 */ private $currencyMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $priceHelperMock; + protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->currencyMock = $this->createMock(\Magento\Framework\Locale\CurrencyInterface::class); + $this->priceHelperMock = $this->createMock(\Magento\Framework\Pricing\Helper\Data::class); $this->model = new \Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage( $this->scopeConfigMock, $this->storeManagerMock, - $this->currencyMock + $this->currencyMock, + $this->priceHelperMock ); } @@ -46,8 +54,6 @@ public function testGetMessage() { $minimumAmount = 20; $minimumAmountCurrency = '$20'; - $currencyCode = 'currency_code'; - $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') ->with('sales/minimum_order/description', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) @@ -58,27 +64,13 @@ public function testGetMessage() ->with('sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ->willReturn($minimumAmount); - $storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getCurrentCurrencyCode']); - $storeMock->expects($this->once())->method('getCurrentCurrencyCode')->willReturn($currencyCode); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $this->priceHelperMock->expects($this->once()) + ->method('currency') + ->with($minimumAmount, true, false) + ->will($this->returnValue($minimumAmountCurrency)); - $currencyMock = $this->createMock(\Magento\Framework\Currency::class); - $this->currencyMock->expects($this->once()) - ->method('getCurrency') - ->with($currencyCode) - ->willReturn($currencyMock); - - $currencyMock->expects($this->once()) - ->method('toCurrency') - ->with($minimumAmount) - ->willReturn($minimumAmountCurrency); - - $this->assertEquals( - __('Minimum order amount is %1', $minimumAmountCurrency), - $this->model->getMessage() - ); + $this->assertEquals(__('Minimum order amount is %1', $minimumAmountCurrency), $this->model->getMessage()); } - public function testGetConfigMessage() { $configMessage = 'config_message'; diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php index 08f5f6a808561..c0ffbc997590e 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php @@ -67,27 +67,22 @@ public function testValidateInvalidCustomer() { $customerId = 100; $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); $this->customerRepositoryMock->expects($this->once())->method('getById')->with($customerId) - ->willReturn($customerMock); + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); $this->model->validate($address); } /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid address id 101 + * @expectedExceptionMessage Invalid customer address id 101 */ public function testValidateInvalidAddress() { $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); $this->customerRepositoryMock->expects($this->never())->method('getById'); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn(101); - $address->expects($this->once())->method('getId')->willReturn(101); - - $this->addressRepositoryMock->expects($this->once())->method('getById') - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $address->expects($this->exactly(2))->method('getCustomerAddressId')->willReturn(101); $this->model->validate($address); } @@ -115,7 +110,6 @@ public function testValidateWithValidAddress() $customerAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); $this->customerRepositoryMock->expects($this->exactly(2))->method('getById')->willReturn($customerMock); - $customerMock->expects($this->once())->method('getId')->willReturn($addressCustomer); $this->addressRepositoryMock->expects($this->once())->method('getById')->willReturn($this->quoteAddressMock); $this->quoteAddressMock->expects($this->any())->method('getCustomerId')->willReturn($addressCustomer); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index 8316885e9b45c..aca490ae2b1a1 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -7,12 +7,14 @@ namespace Magento\Quote\Test\Unit\Model; use Magento\Framework\Exception\NoSuchEntityException; - use Magento\Quote\Model\CustomerManagement; +use Magento\Sales\Api\Data\OrderAddressInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) */ class QuoteManagementTest extends \PHPUnit\Framework\TestCase { @@ -22,9 +24,9 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Quote\Model\QuoteValidator|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\SubmitQuoteValidator|\PHPUnit_Framework_MockObject_MockObject */ - protected $quoteValidator; + protected $submitQuoteValidator; /** * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject @@ -143,7 +145,7 @@ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->quoteValidator = $this->createMock(\Magento\Quote\Model\QuoteValidator::class); + $this->submitQuoteValidator = $this->createMock(\Magento\Quote\Model\SubmitQuoteValidator::class); $this->eventManager = $this->getMockForAbstractClass(\Magento\Framework\Event\ManagerInterface::class); $this->orderFactory = $this->createPartialMock( \Magento\Sales\Api\Data\OrderInterfaceFactory::class, @@ -210,7 +212,7 @@ protected function setUp() \Magento\Quote\Model\QuoteManagement::class, [ 'eventManager' => $this->eventManager, - 'quoteValidator' => $this->quoteValidator, + 'submitQuoteValidator' => $this->submitQuoteValidator, 'orderFactory' => $this->orderFactory, 'orderManagement' => $this->orderManagement, 'customerManagement' => $this->customerManagement, @@ -540,12 +542,12 @@ public function testSubmit() $shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); $payment = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); $baseOrder = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class); - $convertedBillingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); - $convertedShippingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); + $convertedBilling = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']); + $convertedShipping = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']); $convertedPayment = $this->createMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class); $convertedQuoteItem = $this->createMock(\Magento\Sales\Api\Data\OrderItemInterface::class); - $addresses = [$convertedShippingAddress, $convertedBillingAddress]; + $addresses = [$convertedShipping, $convertedBilling]; $quoteItems = [$quoteItem]; $convertedItems = [$convertedQuoteItem]; @@ -560,7 +562,9 @@ public function testSubmit() $shippingAddress ); - $this->quoteValidator->expects($this->once())->method('validateBeforeSubmit')->with($quote); + $this->submitQuoteValidator->expects($this->once()) + ->method('validateQuote') + ->with($quote); $this->quoteAddressToOrder->expects($this->once()) ->method('convert') ->with($shippingAddress, $orderData) @@ -574,7 +578,7 @@ public function testSubmit() 'email' => 'customer@example.com' ] ) - ->willReturn($convertedShippingAddress); + ->willReturn($convertedShipping); $this->quoteAddressToOrderAddress->expects($this->at(1)) ->method('convert') ->with( @@ -584,22 +588,27 @@ public function testSubmit() 'email' => 'customer@example.com' ] ) - ->willReturn($convertedBillingAddress); + ->willReturn($convertedBilling); + + $billingAddress->expects($this->once())->method('getId')->willReturn(4); + $convertedBilling->expects($this->once())->method('setData')->with('quote_address_id', 4); $this->quoteItemToOrderItem->expects($this->once())->method('convert') ->with($quoteItem, ['parent_item' => null]) ->willReturn($convertedQuoteItem); $this->quotePaymentToOrderPayment->expects($this->once())->method('convert')->with($payment) ->willReturn($convertedPayment); $shippingAddress->expects($this->once())->method('getShippingMethod')->willReturn('free'); + $shippingAddress->expects($this->once())->method('getId')->willReturn(5); + $convertedShipping->expects($this->once())->method('setData')->with('quote_address_id', 5); $order = $this->prepareOrderFactory( $baseOrder, - $convertedBillingAddress, + $convertedBilling, $addresses, $convertedPayment, $convertedItems, $quoteId, - $convertedShippingAddress + $convertedShipping ); $this->orderManagement->expects($this->once()) @@ -637,7 +646,7 @@ public function testPlaceOrderIfCustomerIsGuest() $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']); $addressMock->expects($this->once())->method('getEmail')->willReturn($email); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock); + $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock); $this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf(); $this->quoteMock->expects($this->once()) @@ -650,7 +659,7 @@ public function testPlaceOrderIfCustomerIsGuest() ->setConstructorArgs( [ 'eventManager' => $this->eventManager, - 'quoteValidator' => $this->quoteValidator, + 'quoteValidator' => $this->submitQuoteValidator, 'orderFactory' => $this->orderFactory, 'orderManagement' => $this->orderManagement, 'customerManagement' => $this->customerManagement, @@ -707,7 +716,7 @@ public function testPlaceOrder() ->setConstructorArgs( [ 'eventManager' => $this->eventManager, - 'quoteValidator' => $this->quoteValidator, + 'quoteValidator' => $this->submitQuoteValidator, 'orderFactory' => $this->orderFactory, 'orderManagement' => $this->orderManagement, 'customerManagement' => $this->customerManagement, @@ -929,6 +938,9 @@ protected function prepareOrderFactory( return $order; } + /** + * @throws NoSuchEntityException + */ public function testGetCartForCustomer() { $customerId = 100; @@ -974,7 +986,7 @@ protected function setPropertyValue(&$object, $property, $value) } /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Magento\Framework\Exception\LocalizedException */ public function testSubmitForCustomer() { @@ -988,16 +1000,12 @@ public function testSubmitForCustomer() $shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); $payment = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); $baseOrder = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class); - $convertedBillingAddress = $this->createMock( - \Magento\Sales\Api\Data\OrderAddressInterface::class - ); - $convertedShippingAddress = $this->createMock( - \Magento\Sales\Api\Data\OrderAddressInterface::class - ); + $convertedBilling = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']); + $convertedShipping = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['setData']); $convertedPayment = $this->createMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class); $convertedQuoteItem = $this->createMock(\Magento\Sales\Api\Data\OrderItemInterface::class); - $addresses = [$convertedShippingAddress, $convertedBillingAddress]; + $addresses = [$convertedShipping, $convertedBilling]; $quoteItems = [$quoteItem]; $convertedItems = [$convertedQuoteItem]; @@ -1012,7 +1020,8 @@ public function testSubmitForCustomer() $shippingAddress ); - $this->quoteValidator->expects($this->once())->method('validateBeforeSubmit')->with($quote); + $this->submitQuoteValidator->method('validateQuote') + ->with($quote); $this->quoteAddressToOrder->expects($this->once()) ->method('convert') ->with($shippingAddress, $orderData) @@ -1026,7 +1035,7 @@ public function testSubmitForCustomer() 'email' => 'customer@example.com' ] ) - ->willReturn($convertedShippingAddress); + ->willReturn($convertedShipping); $this->quoteAddressToOrderAddress->expects($this->at(1)) ->method('convert') ->with( @@ -1036,22 +1045,24 @@ public function testSubmitForCustomer() 'email' => 'customer@example.com' ] ) - ->willReturn($convertedBillingAddress); + ->willReturn($convertedBilling); $this->quoteItemToOrderItem->expects($this->once())->method('convert') ->with($quoteItem, ['parent_item' => null]) ->willReturn($convertedQuoteItem); $this->quotePaymentToOrderPayment->expects($this->once())->method('convert')->with($payment) ->willReturn($convertedPayment); $shippingAddress->expects($this->once())->method('getShippingMethod')->willReturn('free'); + $shippingAddress->expects($this->once())->method('getId')->willReturn(5); + $convertedShipping->expects($this->once())->method('setData')->with('quote_address_id', 5); $order = $this->prepareOrderFactory( $baseOrder, - $convertedBillingAddress, + $convertedBilling, $addresses, $convertedPayment, $convertedItems, $quoteId, - $convertedShippingAddress + $convertedShipping ); $customerAddressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) ->getMockForAbstractClass(); @@ -1060,6 +1071,8 @@ public function testSubmitForCustomer() $quote->expects($this->any())->method('addCustomerAddress')->with($customerAddressMock); $billingAddress->expects($this->once())->method('getCustomerId')->willReturn(2); $billingAddress->expects($this->once())->method('getSaveInAddressBook')->willReturn(false); + $billingAddress->expects($this->once())->method('getId')->willReturn(4); + $convertedBilling->expects($this->once())->method('setData')->with('quote_address_id', 4); $this->orderManagement->expects($this->once()) ->method('place') ->with($order) @@ -1073,4 +1086,25 @@ public function testSubmitForCustomer() $this->quoteRepositoryMock->expects($this->once())->method('save')->with($quote); $this->assertEquals($order, $this->model->submit($quote, $orderData)); } + + /** + * Get mock for abstract class with methods. + * + * @param string $className + * @param array $methods + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function createPartialMockForAbstractClass($className, $methods = []) + { + return $this->getMockForAbstractClass( + $className, + [], + '', + true, + true, + true, + $methods + ); + } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php index f330ebda17317..043e04319362d 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/Plugin/AccessChangeQuoteControlTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Quote\Test\Unit\Model\QuoteRepository\Plugin; use Magento\Authorization\Model\UserContextInterface; +use Magento\Quote\Model\ChangeQuoteControl; use Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl; use Magento\Quote\Model\Quote; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -34,6 +36,11 @@ class AccessChangeQuoteControlTest extends \PHPUnit\Framework\TestCase */ private $quoteRepositoryMock; + /** + * @var ChangeQuoteControl|MockObject + */ + private $changeQuoteControlMock; + protected function setUp() { $this->userContextMock = $this->getMockBuilder(UserContextInterface::class) @@ -50,15 +57,19 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->changeQuoteControlMock = $this->getMockBuilder(ChangeQuoteControl::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerHelper = new ObjectManager($this); $this->accessChangeQuoteControl = $objectManagerHelper->getObject( AccessChangeQuoteControl::class, - ['userContext' => $this->userContextMock] + ['changeQuoteControl' => $this->changeQuoteControlMock] ); } /** - * User with role Customer and customer_id much with context user_id. + * User with role Customer and customer_id matches context user_id. */ public function testBeforeSaveForCustomer() { @@ -68,6 +79,9 @@ public function testBeforeSaveForCustomer() $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(true); + $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); $this->assertNull($result); @@ -81,11 +95,15 @@ public function testBeforeSaveForCustomer() */ public function testBeforeSaveException() { - $this->userContextMock->method('getUserType') - ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); $this->quoteMock->method('getCustomerId') ->willReturn(2); + $this->userContextMock->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); + + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(false); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } @@ -100,6 +118,9 @@ public function testBeforeSaveForAdmin() $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_ADMIN); + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(true); + $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); $this->assertNull($result); @@ -116,6 +137,9 @@ public function testBeforeSaveForGuest() $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_GUEST); + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(true); + $result = $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); $this->assertNull($result); @@ -135,6 +159,9 @@ public function testBeforeSaveForGuestException() $this->userContextMock->method('getUserType') ->willReturn(UserContextInterface::USER_TYPE_GUEST); + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(false); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } @@ -152,6 +179,9 @@ public function testBeforeSaveForUnknownUserTypeException() $this->userContextMock->method('getUserType') ->willReturn(10); + $this->changeQuoteControlMock->method('isAllowed') + ->willReturn(false); + $this->accessChangeQuoteControl->beforeSave($this->quoteRepositoryMock, $this->quoteMock); } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php index 3101c7d0677a9..095e1760df86f 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php @@ -5,17 +5,31 @@ */ namespace Magento\Quote\Test\Unit\Model; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartSearchResultsInterface; +use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteRepository; use Magento\Quote\Model\QuoteRepository\LoadHandler; use Magento\Quote\Model\QuoteRepository\SaveHandler; +use Magento\Quote\Model\ResourceModel\Quote\Collection; use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyMethods) */ -class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase +class QuoteRepositoryTest extends TestCase { /** * @var \Magento\Quote\Api\CartRepositoryInterface @@ -23,32 +37,32 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var \Magento\Quote\Model\QuoteFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CartInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteFactoryMock; + private $cartFactoryMock; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + * @var Store|\PHPUnit_Framework_MockObject_MockObject */ private $storeMock; /** - * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ private $quoteMock; /** - * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $searchResultsDataFactory; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ private $quoteCollectionMock; @@ -78,21 +92,21 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $objectManagerMock; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ private $quoteCollectionFactoryMock; protected function setUp() { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $objectManager = new ObjectManager($this); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); - $this->quoteFactoryMock = $this->createPartialMock(\Magento\Quote\Model\QuoteFactory::class, ['create']); - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->cartFactoryMock = $this->createPartialMock(CartInterfaceFactory::class, ['create']); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'load', 'loadByIdWithoutStore', @@ -108,35 +122,35 @@ protected function setUp() 'getData' ] ); - $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $this->storeMock = $this->createMock(Store::class); $this->searchResultsDataFactory = $this->createPartialMock( - \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory::class, + CartSearchResultsInterfaceFactory::class, ['create'] ); $this->quoteCollectionMock = - $this->createMock(\Magento\Quote\Model\ResourceModel\Quote\Collection::class); + $this->createMock(Collection::class); $this->extensionAttributesJoinProcessorMock = $this->createMock( - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class + JoinProcessorInterface::class ); $this->collectionProcessor = $this->createMock( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class + CollectionProcessorInterface::class ); $this->quoteCollectionFactoryMock = $this->createPartialMock( - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class, + CollectionFactory::class, ['create'] ); $this->model = $objectManager->getObject( - \Magento\Quote\Model\QuoteRepository::class, + QuoteRepository::class, [ - 'quoteFactory' => $this->quoteFactoryMock, 'storeManager' => $this->storeManagerMock, 'searchResultsDataFactory' => $this->searchResultsDataFactory, 'quoteCollection' => $this->quoteCollectionMock, 'extensionAttributesJoinProcessor' => $this->extensionAttributesJoinProcessorMock, 'collectionProcessor' => $this->collectionProcessor, - 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock + 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock, + 'cartFactory' => $this->cartFactoryMock ] ); @@ -161,7 +175,7 @@ public function testGetWithExceptionById() { $cartId = 14; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -178,7 +192,7 @@ public function testGet() { $cartId = 15; - $this->quoteFactoryMock->expects(static::once()) + $this->cartFactoryMock->expects(static::once()) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::once()) @@ -211,7 +225,7 @@ public function testGetForCustomerAfterGet() $cartId = 15; $customerId = 23; - $this->quoteFactoryMock->expects(static::exactly(2)) + $this->cartFactoryMock->expects(static::exactly(2)) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::exactly(2)) @@ -249,7 +263,7 @@ public function testGetWithSharedStoreIds() $cartId = 16; $sharedStoreIds = [1, 2]; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once()) @@ -275,7 +289,7 @@ public function testGetForCustomer() $cartId = 17; $customerId = 23; - $this->quoteFactoryMock->expects(static::once()) + $this->cartFactoryMock->expects(static::once()) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::once()) @@ -310,7 +324,7 @@ public function testGetActiveWithExceptionById() { $cartId = 14; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -332,7 +346,7 @@ public function testGetActiveWithExceptionByIsActive() { $cartId = 15; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -355,7 +369,7 @@ public function testGetActive() { $cartId = 15; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -379,7 +393,7 @@ public function testGetActiveWithSharedStoreIds() $cartId = 16; $sharedStoreIds = [1, 2]; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once()) @@ -406,7 +420,7 @@ public function testGetActiveForCustomer() $cartId = 17; $customerId = 23; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -430,14 +444,14 @@ public function testSave() { $cartId = 100; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getId', 'getCustomerId', 'getStoreId', 'hasData', 'setData'] ); $quoteMock->expects($this->exactly(3))->method('getId')->willReturn($cartId); $quoteMock->expects($this->once())->method('getCustomerId')->willReturn(2); $quoteMock->expects($this->once())->method('getStoreId')->willReturn(5); - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once())->method('getId')->willReturn($cartId); @@ -481,8 +495,8 @@ public function testGetList() ->method('load') ->with($cartMock); - $searchResult = $this->createMock(\Magento\Quote\Api\Data\CartSearchResultsInterface::class); - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); + $searchResult = $this->createMock(CartSearchResultsInterface::class); + $searchCriteriaMock = $this->createMock(SearchCriteria::class); $this->searchResultsDataFactory ->expects($this->once()) ->method('create') @@ -495,7 +509,7 @@ public function testGetList() $this->extensionAttributesJoinProcessorMock->expects($this->once()) ->method('process') ->with( - $this->isInstanceOf(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) + $this->isInstanceOf(Collection::class) ); $this->quoteCollectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$cartMock]); $searchResult->expects($this->once())->method('setTotalCount')->with($pageSize); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index c0056e2c8338f..08e4862cd122d 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; @@ -157,13 +155,21 @@ class QuoteTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->quoteAddressFactoryMock = $this->createPartialMock(\Magento\Quote\Model\Quote\AddressFactory::class, ['create']); + $this->quoteAddressFactoryMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\AddressFactory::class, + ['create'] + ); $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ 'isDeleted', 'getCollection', 'getId', 'getCustomerAddressId', '__wakeup', 'getAddressType', 'getDeleteImmediately', 'validateMinimumAmount', 'setData' ]); - $this->quoteAddressCollectionMock = $this->createMock(\Magento\Quote\Model\ResourceModel\Quote\Address\Collection::class); - $this->extensibleDataObjectConverterMock = $this->createPartialMock(\Magento\Framework\Api\ExtensibleDataObjectConverter::class, ['toFlatArray']); + $this->quoteAddressCollectionMock = $this->createMock( + \Magento\Quote\Model\ResourceModel\Quote\Address\Collection::class + ); + $this->extensibleDataObjectConverterMock = $this->createPartialMock( + \Magento\Framework\Api\ExtensibleDataObjectConverter::class, + ['toFlatArray'] + ); $this->customerRepositoryMock = $this->getMockForAbstractClass( \Magento\Customer\Api\CustomerRepositoryInterface::class, [], @@ -173,7 +179,10 @@ protected function setUp() true, ['getById', 'save'] ); - $this->objectCopyServiceMock = $this->createPartialMock(\Magento\Framework\DataObject\Copy::class, ['copyFieldsetToTarget']); + $this->objectCopyServiceMock = $this->createPartialMock( + \Magento\Framework\DataObject\Copy::class, + ['copyFieldsetToTarget'] + ); $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); $this->objectFactoryMock = $this->createPartialMock(\Magento\Framework\DataObject\Factory::class, ['create']); $this->quoteAddressFactoryMock->expects( @@ -212,9 +221,18 @@ protected function setUp() $this->contextMock->expects($this->any()) ->method('getEventDispatcher') ->will($this->returnValue($this->eventManagerMock)); - $this->quoteItemCollectionFactoryMock = $this->createPartialMock(\Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class, ['create']); - $this->quotePaymentCollectionFactoryMock = $this->createPartialMock(\Magento\Quote\Model\ResourceModel\Quote\Payment\CollectionFactory::class, ['create']); - $this->paymentFactoryMock = $this->createPartialMock(\Magento\Quote\Model\Quote\PaymentFactory::class, ['create']); + $this->quoteItemCollectionFactoryMock = $this->createPartialMock( + \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class, + ['create'] + ); + $this->quotePaymentCollectionFactoryMock = $this->createPartialMock( + \Magento\Quote\Model\ResourceModel\Quote\Payment\CollectionFactory::class, + ['create'] + ); + $this->paymentFactoryMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\PaymentFactory::class, + ['create'] + ); $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config::class) ->disableOriginalConstructor() ->getMock(); @@ -236,8 +254,13 @@ protected function setUp() $this->itemProcessor = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item\Processor::class) ->disableOriginalConstructor() ->getMock(); - $this->extensionAttributesJoinProcessorMock = $this->createMock(\Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class); - $this->customerDataFactoryMock = $this->createPartialMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create']); + $this->extensionAttributesJoinProcessorMock = $this->createMock( + \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class + ); + $this->customerDataFactoryMock = $this->createPartialMock( + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, + ['create'] + ); $this->orderIncrementIdChecker = $this->getMockBuilder(\Magento\Sales\Model\OrderIncrementIdChecker::class) ->disableOriginalConstructor() ->getMock(); @@ -346,7 +369,10 @@ public function isMultipleShippingAddressesDataProvider() */ protected function getAddressMock($type) { - $shippingAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getAddressType', '__wakeup', 'isDeleted']); + $shippingAddressMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address::class, + ['getAddressType', '__wakeup', 'isDeleted'] + ); $shippingAddressMock->expects($this->any())->method('getAddressType')->will($this->returnValue($type)); $shippingAddressMock->expects($this->any())->method('isDeleted')->will($this->returnValue(false)); @@ -615,6 +641,9 @@ public function testGetAddressById($addressId, $expected) $this->assertEquals((bool)$expected, (bool)$result); } + /** + * @return array + */ public static function dataProviderGetAddress() { return [ @@ -656,6 +685,9 @@ public function testGetAddressByCustomerAddressId($isDeleted, $customerAddressId $this->assertEquals((bool)$expected, (bool)$result); } + /** + * @return array + */ public static function dataProviderGetAddressByCustomer() { return [ @@ -702,6 +734,9 @@ public function testGetShippingAddressByCustomerAddressId($isDeleted, $addressTy $this->assertEquals($expected, (bool)$result); } + /** + * @return array + */ public static function dataProviderShippingAddress() { return [ @@ -942,6 +977,7 @@ public function testValidateMiniumumAmount() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) @@ -968,6 +1004,7 @@ public function testValidateMiniumumAmountNegative() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) @@ -988,13 +1025,19 @@ public function testValidateMiniumumAmountNegative() public function testGetPaymentIsNotDeleted() { $this->quote->setId(1); - $payment = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['setQuote', 'isDeleted', '__wakeup']); + $payment = $this->createPartialMock( + \Magento\Quote\Model\Quote\Payment::class, + ['setQuote', 'isDeleted', '__wakeup'] + ); $payment->expects($this->once()) ->method('setQuote'); $payment->expects($this->once()) ->method('isDeleted') ->willReturn(false); - $quotePaymentCollectionMock = $this->createPartialMock(\Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, ['setQuoteFilter', 'getFirstItem']); + $quotePaymentCollectionMock = $this->createPartialMock( + \Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, + ['setQuoteFilter', 'getFirstItem'] + ); $quotePaymentCollectionMock->expects($this->once()) ->method('setQuoteFilter') ->with(1) @@ -1012,7 +1055,10 @@ public function testGetPaymentIsNotDeleted() public function testGetPaymentIsDeleted() { $this->quote->setId(1); - $payment = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['setQuote', 'isDeleted', 'getId', '__wakeup']); + $payment = $this->createPartialMock( + \Magento\Quote\Model\Quote\Payment::class, + ['setQuote', 'isDeleted', 'getId', '__wakeup'] + ); $payment->expects($this->exactly(2)) ->method('setQuote'); $payment->expects($this->once()) @@ -1021,7 +1067,10 @@ public function testGetPaymentIsDeleted() $payment->expects($this->once()) ->method('getId') ->willReturn(1); - $quotePaymentCollectionMock = $this->createPartialMock(\Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, ['setQuoteFilter', 'getFirstItem']); + $quotePaymentCollectionMock = $this->createPartialMock( + \Magento\Quote\Model\ResourceModel\Quote\Payment\Collection::class, + ['setQuoteFilter', 'getFirstItem'] + ); $quotePaymentCollectionMock->expects($this->once()) ->method('setQuoteFilter') ->with(1) @@ -1048,7 +1097,10 @@ public function testAddItem() $item->expects($this->once()) ->method('getId') ->willReturn(false); - $itemsMock = $this->createPartialMock(\Magento\Eav\Model\Entity\Collection\AbstractCollection::class, ['setQuote', 'addItem']); + $itemsMock = $this->createPartialMock( + \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, + ['setQuote', 'addItem'] + ); $itemsMock->expects($this->once()) ->method('setQuote'); $itemsMock->expects($this->once()) @@ -1102,7 +1154,10 @@ public function testBeforeSaveIsVirtualQuote(array $productTypes, $expected) $productMock = $this->createMock(\Magento\Catalog\Model\Product::class); $productMock->expects($this->any())->method('getIsVirtual')->willReturn($type); - $itemMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['isDeleted', 'getParentItemId', 'getProduct']); + $itemMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Item::class, + ['isDeleted', 'getParentItemId', 'getProduct'] + ); $itemMock->expects($this->any()) ->method('isDeleted') ->willReturn(false); @@ -1204,6 +1259,9 @@ public function testReserveOrderId($isReservedOrderIdExist, $reservedOrderId) $this->assertEquals($reservedOrderId, $this->quote->getReservedOrderId()); } + /** + * @return array + */ public function reservedOrderIdDataProvider() { return [ diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteValidatorTest.php index bd53e44afe9db..41a8d73d2de26 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteValidatorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteValidatorTest.php @@ -6,33 +6,37 @@ namespace Magento\Quote\Test\Unit\Model; use Magento\Directory\Model\AllowedCountries; +use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Payment; use Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage as OrderAmountValidationMessage; use Magento\Quote\Model\QuoteValidator; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class QuoteValidatorTest */ class QuoteValidatorTest extends \PHPUnit\Framework\TestCase { + private static $storeId = 2; + /** * @var \Magento\Quote\Model\QuoteValidator */ - protected $quoteValidator; + private $quoteValidator; /** - * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Quote\Model\Quote + * @var Quote|MockObject */ - protected $quoteMock; + private $quote; /** - * @var AllowedCountries|\PHPUnit_Framework_MockObject_MockObject + * @var AllowedCountries|MockObject */ private $allowedCountryReader; /** - * @var OrderAmountValidationMessage|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAmountValidationMessage|MockObject */ private $orderAmountValidationMessage; @@ -48,13 +52,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->quoteValidator = new \Magento\Quote\Model\QuoteValidator( + $this->quoteValidator = new QuoteValidator( $this->allowedCountryReader, $this->orderAmountValidationMessage ); - $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + $this->quote = $this->createPartialMock( + Quote::class, [ 'getShippingAddress', 'getBillingAddress', @@ -65,204 +69,27 @@ protected function setUp() 'isVirtual', 'validateMinimumAmount', 'getIsMultiShipping', - '__wakeup' + 'getStoreId' ] ); + $this->quote->method('getStoreId') + ->willReturn(self::$storeId); } public function testCheckQuoteAmountExistingError() { - $this->quoteMock->expects($this->once()) - ->method('getHasError') - ->will($this->returnValue(true)); - - $this->quoteMock->expects($this->never()) - ->method('setHasError'); - - $this->quoteMock->expects($this->never()) - ->method('addMessage'); - - $this->assertSame( - $this->quoteValidator, - $this->quoteValidator->validateQuoteAmount($this->quoteMock, QuoteValidator::MAXIMUM_AVAILABLE_NUMBER + 1) - ); - } - - public function testCheckQuoteAmountAmountLessThanAvailable() - { - $this->quoteMock->expects($this->once()) - ->method('getHasError') - ->will($this->returnValue(false)); + $this->quote->method('getHasError') + ->willReturn(true); - $this->quoteMock->expects($this->never()) + $this->quote->expects(self::never()) ->method('setHasError'); - $this->quoteMock->expects($this->never()) + $this->quote->expects(self::never()) ->method('addMessage'); - $this->assertSame( - $this->quoteValidator, - $this->quoteValidator->validateQuoteAmount($this->quoteMock, QuoteValidator::MAXIMUM_AVAILABLE_NUMBER - 1) - ); - } - - public function testCheckQuoteAmountAmountGreaterThanAvailable() - { - $this->quoteMock->expects($this->once()) - ->method('getHasError') - ->will($this->returnValue(false)); - - $this->quoteMock->expects($this->once()) - ->method('setHasError') - ->with(true); - - $this->quoteMock->expects($this->once()) - ->method('addMessage') - ->with(__('This item price or quantity is not valid for checkout.')); - - $this->assertSame( + self::assertSame( $this->quoteValidator, - $this->quoteValidator->validateQuoteAmount($this->quoteMock, QuoteValidator::MAXIMUM_AVAILABLE_NUMBER + 1) + $this->quoteValidator->validateQuoteAmount($this->quote, QuoteValidator::MAXIMUM_AVAILABLE_NUMBER + 1) ); } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please check the shipping address information. - */ - public function testValidateBeforeSubmitThrowsExceptionIfShippingAddressIsInvalid() - { - $shippingAddressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $this->quoteMock->expects($this->any())->method('getShippingAddress')->willReturn($shippingAddressMock); - $this->quoteMock->expects($this->any())->method('isVirtual')->willReturn(false); - $shippingAddressMock->expects($this->any())->method('validate')->willReturn(['Invalid Shipping Address']); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please specify a shipping method. - */ - public function testValidateBeforeSubmitThrowsExceptionIfShippingRateIsNotSelected() - { - $shippingMethod = 'checkmo'; - $shippingAddressMock = $this->getMockBuilder(Address::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->allowedCountryReader->method('getAllowedCountries') - ->willReturn(['US' => 'US']); - - $this->quoteMock->expects($this->any())->method('getShippingAddress')->willReturn($shippingAddressMock); - $this->quoteMock->expects($this->any())->method('isVirtual')->willReturn(false); - $shippingAddressMock->expects($this->any())->method('validate')->willReturn(true); - $shippingAddressMock->method('getCountryId') - ->willReturn('US'); - $shippingAddressMock->expects($this->any())->method('getShippingMethod')->willReturn($shippingMethod); - $shippingAddressMock->expects($this->once())->method('getShippingRateByCode')->with($shippingMethod); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please check the billing address information. - */ - public function testValidateBeforeSubmitThrowsExceptionIfBillingAddressIsNotValid() - { - $billingAddressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $this->quoteMock->expects($this->any())->method('getBillingAddress')->willReturn($billingAddressMock); - $this->quoteMock->expects($this->any())->method('isVirtual')->willReturn(true); - $billingAddressMock->expects($this->any())->method('validate')->willReturn(['Invalid Billing Address']); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please select a valid payment method. - */ - public function testValidateBeforeSubmitThrowsExceptionIfPaymentMethodIsNotSelected() - { - $paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); - $billingAddressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $billingAddressMock->expects($this->any())->method('validate')->willReturn(true); - - $this->quoteMock->expects($this->any())->method('getBillingAddress')->willReturn($billingAddressMock); - $this->quoteMock->expects($this->any())->method('getPayment')->willReturn($paymentMock); - $this->quoteMock->expects($this->any())->method('isVirtual')->willReturn(true); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Minimum Order Amount Exceeded. - */ - public function testValidateBeforeSubmitThrowsExceptionIfMinimumOrderAmount() - { - $paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); - $paymentMock->expects($this->once())->method('getMethod')->willReturn('checkmo'); - - $billingAddressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $billingAddressMock->expects($this->any())->method('validate')->willReturn(true); - - $this->quoteMock->expects($this->any())->method('getBillingAddress')->willReturn($billingAddressMock); - $this->quoteMock->expects($this->any())->method('getPayment')->willReturn($paymentMock); - $this->quoteMock->expects($this->any())->method('isVirtual')->willReturn(true); - - $this->quoteMock->expects($this->any())->method('getIsMultiShipping')->willReturn(false); - $this->quoteMock->expects($this->any())->method('validateMinimumAmount')->willReturn(false); - - $this->orderAmountValidationMessage->expects($this->once())->method('getMessage') - ->willReturn(__("Minimum Order Amount Exceeded.")); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } - - /** - * Test case when country id not present in allowed countries list. - * - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Some addresses cannot be used due to country-specific configurations. - */ - public function testValidateBeforeSubmitThrowsExceptionIfCountrySpecificConfigurations() - { - $this->allowedCountryReader->method('getAllowedCountries') - ->willReturn(['EE' => 'EE']); - - $addressMock = $this->getMockBuilder(Address::class) - ->disableOriginalConstructor() - ->getMock(); - $addressMock->method('validate') - ->willReturn(true); - $addressMock->method('getCountryId') - ->willReturn('EU'); - - $paymentMock = $this->getMockBuilder(Payment::class) - ->setMethods(['getMethod']) - ->disableOriginalConstructor() - ->getMock(); - $paymentMock->method('getMethod') - ->willReturn(true); - - $billingAddressMock = $this->getMockBuilder(Address::class) - ->disableOriginalConstructor() - ->setMethods(['validate']) - ->getMock(); - $billingAddressMock->method('validate') - ->willReturn(true); - - $this->quoteMock->method('getShippingAddress') - ->willReturn($addressMock); - $this->quoteMock->method('isVirtual') - ->willReturn(false); - $this->quoteMock->method('getBillingAddress') - ->willReturn($billingAddressMock); - $this->quoteMock->method('getPayment') - ->willReturn($paymentMock); - - $this->quoteValidator->validateBeforeSubmit($this->quoteMock); - } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/ResourceModel/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/ResourceModel/QuoteTest.php new file mode 100644 index 0000000000000..ab36746da5e73 --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Model/ResourceModel/QuoteTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Quote\Test\Unit\Model\ResourceModel; + +use Magento\Framework\DB\Sequence\SequenceInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; +use Magento\Quote\Model\Quote; +use Magento\SalesSequence\Model\Manager; + +class QuoteTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $sequenceManagerMock; + + /** + * @var SequenceInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sequenceMock; + + /** + * @var \Magento\Quote\Model\ResourceModel\Quote + */ + private $quote; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $snapshot = $this->getMockBuilder(Snapshot::class) + ->disableOriginalConstructor() + ->getMock(); + $relationComposite = $this->getMockBuilder(RelationComposite::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->sequenceManagerMock = $this->getMockBuilder(Manager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->sequenceMock = $this->getMockBuilder(SequenceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quote = new \Magento\Quote\Model\ResourceModel\Quote( + $context, + $snapshot, + $relationComposite, + $this->sequenceManagerMock, + null + ); + } + + /** + * @param $entityType + * @param $storeId + * @param $reservedOrderId + * @dataProvider getReservedOrderIdDataProvider + */ + public function testGetReservedOrderId($entityType, $storeId, $reservedOrderId) + { + $this->sequenceManagerMock->expects($this->once()) + ->method('getSequence') + ->with($entityType, $storeId) + ->willReturn($this->sequenceMock); + $this->quoteMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->sequenceMock->expects($this->once()) + ->method('getNextValue') + ->willReturn($reservedOrderId); + + $this->assertEquals($reservedOrderId, $this->quote->getReservedOrderId($this->quoteMock)); + } + + /** + * @return array + */ + public function getReservedOrderIdDataProvider(): array + { + return [ + [\Magento\Sales\Model\Order::ENTITY, 1, '1000000001'], + [\Magento\Sales\Model\Order::ENTITY, 2, '2000000001'], + [\Magento\Sales\Model\Order::ENTITY, 3, '3000000001'] + ]; + } +} diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php index 1870fa9dc81ba..bcb6fa8014858 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php @@ -1,15 +1,13 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Model; use \Magento\Quote\Model\ShippingAddressManagement; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -83,7 +81,10 @@ protected function setUp() $this->totalsCollectorMock = $this->createMock(\Magento\Quote\Model\Quote\TotalsCollector::class); $this->addressRepository = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->amountErrorMessageMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage::class, ['getMessage']); + $this->amountErrorMessageMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage::class, + ['getMessage'] + ); $this->service = $this->objectManager->getObject( \Magento\Quote\Model\ShippingAddressManagement::class, @@ -100,7 +101,7 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expected ExceptionMessage error345 + * @expectedExceptionMessage error345 */ public function testSetAddressValidationFailed() { @@ -110,7 +111,7 @@ public function testSetAddressValidationFailed() ->with('cart654') ->will($this->returnValue($quoteMock)); - $this->validatorMock->expects($this->once())->method('validate') + $this->validatorMock->expects($this->once())->method('validateForCart') ->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345')))); $this->service->assign('cart654', $this->quoteAddressMock); @@ -121,7 +122,10 @@ public function testSetAddress() $addressId = 1; $customerAddressId = 150; - $quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress']); + $quoteMock = $this->createPartialMock( + \Magento\Quote\Model\Quote::class, + ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] + ); $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with('cart867') @@ -143,8 +147,8 @@ public function testSetAddress() ->with($customerAddressId) ->willReturn($customerAddressMock); - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) + $this->validatorMock->expects($this->once())->method('validateForCart') + ->with($quoteMock, $this->quoteAddressMock) ->willReturn(true); $quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock); @@ -200,7 +204,10 @@ public function testSetAddressWithInabilityToSaveQuote() $customerAddressId = 150; - $quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress']); + $quoteMock = $this->createPartialMock( + \Magento\Quote\Model\Quote::class, + ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] + ); $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with('cart867') @@ -218,8 +225,8 @@ public function testSetAddressWithInabilityToSaveQuote() ->with($customerAddressId) ->willReturn($customerAddressMock); - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) + $this->validatorMock->expects($this->once())->method('validateForCart') + ->with($quoteMock, $this->quoteAddressMock) ->willReturn(true); $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingMethodManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingMethodManagementTest.php index af68dcc30a50f..349352175a047 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingMethodManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/ShippingMethodManagementTest.php @@ -15,7 +15,9 @@ use Magento\Quote\Model\Quote\Address\Rate; use Magento\Quote\Model\Quote\TotalsCollector; use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Model\ResourceModel\Quote\Address as QuoteAddressResource; use Magento\Quote\Model\ShippingMethodManagement; +use Magento\Store\Model\Store; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -83,6 +85,16 @@ class ShippingMethodManagementTest extends \PHPUnit\Framework\TestCase */ private $totalsCollector; + /** + * @var Store|MockObject + */ + private $storeMock; + + /** + * @var QuoteAddressResource|MockObject + */ + private $quoteAddressResource; + protected function setUp() { $this->objectManager = new ObjectManager($this); @@ -98,7 +110,8 @@ protected function setUp() $className = \Magento\Framework\Reflection\DataObjectProcessor::class; $this->dataProcessor = $this->createMock($className); - $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $this->quoteAddressResource = $this->createMock(QuoteAddressResource::class); + $this->storeMock = $this->createMock(Store::class); $this->quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods([ @@ -150,6 +163,7 @@ protected function setUp() 'converter' => $this->converter, 'totalsCollector' => $this->totalsCollector, 'addressRepository' => $this->addressRepository, + 'quoteAddressResource' => $this->quoteAddressResource, ] ); @@ -362,6 +376,7 @@ public function testSetMethodWithoutShippingAddress() $this->quote->expects($this->once()) ->method('getShippingAddress')->will($this->returnValue($this->shippingAddress)); $this->shippingAddress->expects($this->once())->method('getCountryId')->will($this->returnValue(null)); + $this->quoteAddressResource->expects($this->once())->method('delete')->with($this->shippingAddress); $this->model->set($cartId, $carrierCode, $methodCode); } @@ -421,6 +436,7 @@ public function testSetMethodWithoutAddress() ->method('getShippingAddress') ->willReturn($this->shippingAddress); $this->shippingAddress->expects($this->once())->method('getCountryId'); + $this->quoteAddressResource->expects($this->once())->method('delete')->with($this->shippingAddress); $this->model->set($cartId, $carrierCode, $methodCode); } diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Backend/CustomerQuoteObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Backend/CustomerQuoteObserverTest.php index d10e1531d4ec3..f5ef9fed4ff84 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Backend/CustomerQuoteObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Backend/CustomerQuoteObserverTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Observer\Backend; class CustomerQuoteObserverTest extends \PHPUnit\Framework\TestCase @@ -132,15 +130,14 @@ public function testDispatch($isWebsiteScope, $websites) $quoteMock = $this->getMockBuilder( \Magento\Quote\Model\Quote::class )->setMethods( - [ - 'setWebsite', - 'setCustomerGroupId', - 'getCustomerGroupId', - 'collectTotals', - '__wakeup', - ] - )->disableOriginalConstructor( - )->getMock(); + [ + 'setWebsite', + 'setCustomerGroupId', + 'getCustomerGroupId', + 'collectTotals', + '__wakeup', + ] + )->disableOriginalConstructor()->getMock(); $websiteCount = count($websites); $this->quoteRepositoryMock->expects($this->once()) ->method('getForCustomer') @@ -160,11 +157,14 @@ public function testDispatch($isWebsiteScope, $websites) $this->customerQuote->execute($this->observerMock); } + /** + * @return array + */ public function dispatchDataProvider() { return [ - [true, ['website1']], - [true, ['website1', 'website2']], + [true, [['website1']]], + [true, [['website1'], ['website2']]], [false, ['website1']], [false, ['website1', 'website2']], ]; diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 2968fba74478b..5794d67a3ff27 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Quote\Test\Unit\Observer\Frontend\Quote\Address; /** @@ -108,13 +106,25 @@ protected function setUp() ); $this->customerAddressMock = $this->createMock(\Magento\Customer\Helper\Address::class); $this->customerVatMock = $this->createMock(\Magento\Customer\Model\Vat::class); - $this->customerDataFactoryMock = $this->createPartialMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['mergeDataObjectWithArray', 'create']); + $this->customerDataFactoryMock = $this->createPartialMock( + \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, + ['mergeDataObjectWithArray', 'create'] + ); $this->vatValidatorMock = $this->createMock(\Magento\Quote\Observer\Frontend\Quote\Address\VatValidator::class); - $this->observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getShippingAssignment', 'getQuote']); + $this->observerMock = $this->createPartialMock( + \Magento\Framework\Event\Observer::class, + ['getShippingAssignment', 'getQuote'] + ); - $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getCountryId', 'getVatId', 'getQuote', 'setPrevQuoteCustomerGroupId', '__wakeup']); + $this->quoteAddressMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address::class, + ['getCountryId', 'getVatId', 'getQuote', 'setPrevQuoteCustomerGroupId', '__wakeup'] + ); - $this->quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['setCustomerGroupId', 'getCustomerGroupId', 'getCustomer', '__wakeup', 'setCustomer']); + $this->quoteMock = $this->createPartialMock( + \Magento\Quote\Model\Quote::class, + ['setCustomerGroupId', 'getCustomerGroupId', 'getCustomer', '__wakeup', 'setCustomer'] + ); $this->groupManagementMock = $this->getMockForAbstractClass( \Magento\Customer\Api\GroupManagementInterface::class, @@ -238,8 +248,7 @@ public function testDispatchWithDefaultCustomerGroupId() $this->quoteMock->expects($this->once()) ->method('getCustomerGroupId') - ->will($this->returnValue('customerGroupId') - ); + ->willReturn('customerGroupId'); $this->customerMock->expects($this->once())->method('getId')->will($this->returnValue('1')); $this->groupManagementMock->expects($this->once()) ->method('getDefaultGroup') @@ -376,6 +385,5 @@ public function testDispatchWithEmptyShippingAddress() ->method('create') ->willReturn($this->customerMock); $this->model->execute($this->observerMock); - } } diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Webapi/SubmitObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php similarity index 95% rename from app/code/Magento/Quote/Test/Unit/Observer/Webapi/SubmitObserverTest.php rename to app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php index 618a633fd62e0..c19606a7b8f5d 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Webapi/SubmitObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Quote\Test\Unit\Observer\Webapi; +namespace Magento\Quote\Test\Unit\Observer; class SubmitObserverTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Quote\Observer\Webapi\SubmitObserver + * @var \Magento\Quote\Observer\SubmitObserver */ protected $model; @@ -59,7 +59,7 @@ protected function setUp() $eventMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock); $eventMock->expects($this->once())->method('getOrder')->willReturn($this->orderMock); $this->quoteMock->expects($this->once())->method('getPayment')->willReturn($this->paymentMock); - $this->model = new \Magento\Quote\Observer\Webapi\SubmitObserver( + $this->model = new \Magento\Quote\Observer\SubmitObserver( $this->loggerMock, $this->orderSenderMock ); diff --git a/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php new file mode 100644 index 0000000000000..f6146c824aabc --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Test\Unit\Plugin; + +use Magento\Quote\Model\ResourceModel\Quote\Item; +use Magento\Store\Model\StoreSwitcherInterface; +use Magento\Quote\Plugin\UpdateQuoteItemStore; +use Magento\Quote\Model\QuoteRepository; +use Magento\Checkout\Model\Session; +use Magento\Store\Model\Store; +use Magento\Quote\Model\Quote; + +/** + * Unit test for Magento\Quote\Plugin\UpdateQuoteItemStore. + */ +class UpdateQuoteItemStoreTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var UpdateQuoteItemStore + */ + private $model; + + /** + * @var StoreSwitcherInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $subjectMock; + + /** + * @var QuoteRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteRepositoryMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $checkoutSessionMock; + + /** + * @var Store|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->checkoutSessionMock = $this->createPartialMock( + Session::class, + ['getQuote'] + ); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['getIsActive', 'setStoreId', 'getItemsCollection'] + ); + $this->storeMock = $this->createPartialMock( + Store::class, + ['getId'] + ); + $this->quoteRepositoryMock = $this->createPartialMock( + QuoteRepository::class, + ['save'] + ); + $this->subjectMock = $this->createMock(StoreSwitcherInterface::class); + + $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock); + + $this->model = $objectManager->getObject( + UpdateQuoteItemStore::class, + [ + 'quoteRepository' => $this->quoteRepositoryMock, + 'checkoutSession' => $this->checkoutSessionMock, + ] + ); + } + + /** + * Unit test for afterSwitch method with active quote. + * + * @return void + */ + public function testWithActiveQuote() + { + $storeId = 1; + $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn(true); + $this->storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->quoteMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf(); + $quoteItem = $this->createMock(Item::class); + $this->quoteMock->expects($this->once())->method('getItemsCollection')->willReturnSelf($quoteItem); + + $this->model->afterSwitch( + $this->subjectMock, + 'magento2.loc', + $this->storeMock, + $this->storeMock, + 'magento2.loc' + ); + } + + /** + * Unit test for afterSwitch method without active quote. + * + * @dataProvider getIsActive + * @param bool|null $isActive + * @return void + */ + public function testWithoutActiveQuote($isActive) + { + $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn($isActive); + $this->quoteRepositoryMock->expects($this->never())->method('save'); + + $this->model->afterSwitch( + $this->subjectMock, + 'magento2.loc', + $this->storeMock, + $this->storeMock, + 'magento2.loc' + ); + } + + /** + * Data provider for method testWithoutActiveQuote. + * @return array + */ + public function getIsActive() + { + return [ + [false], + [null], + ]; + } +} diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json index 31f875a0f9a35..a3ed9086aa69f 100644 --- a/app/code/Magento/Quote/composer.json +++ b/app/code/Magento/Quote/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-quote", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-customer": "101.0.*", @@ -23,7 +23,7 @@ "magento/module-webapi": "100.2.*" }, "type": "magento2-module", - "version": "101.0.2", + "version": "101.0.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Quote/etc/adminhtml/di.xml b/app/code/Magento/Quote/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..fb6c118fb92e9 --- /dev/null +++ b/app/code/Magento/Quote/etc/adminhtml/di.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\ValidationRules\QuoteValidationComposite"> + <arguments> + <argument name="validationRules" xsi:type="array"> + <item name="ShippingAddressValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\ShippingAddressValidationRule</item> + <item name="AllowedCountryValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\AllowedCountryValidationRule</item> + <item name="ShippingMethodValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule</item> + <item name="BillingAddressValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\BillingAddressValidationRule</item> + <item name="PaymentMethodValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\PaymentMethodValidationRule</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index 674e0eea46e97..8add3786eb9a3 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -22,6 +22,7 @@ <preference for="Magento\Quote\Api\CouponManagementInterface" type="Magento\Quote\Model\CouponManagement" /> <preference for="Magento\Quote\Api\CartManagementInterface" type="Magento\Quote\Model\QuoteManagement" /> <preference for="Magento\Quote\Api\CartTotalRepositoryInterface" type="Magento\Quote\Model\Cart\CartTotalRepository" /> + <preference for="Magento\Quote\Api\ChangeQuoteControlInterface" type="Magento\Quote\Model\ChangeQuoteControl" /> <preference for="Magento\Quote\Api\CartTotalManagementInterface" type="Magento\Quote\Model\Cart\CartTotalManagement" /> <preference for="Magento\Quote\Api\Data\TotalsInterface" type="Magento\Quote\Model\Cart\Totals" /> <preference for="Magento\Quote\Api\Data\TotalSegmentInterface" type="Magento\Quote\Model\Cart\TotalSegment" /> @@ -40,6 +41,7 @@ <preference for="Magento\Quote\Api\GuestCartTotalManagementInterface" type="Magento\Quote\Model\GuestCart\GuestCartTotalManagement" /> <preference for="Magento\Quote\Api\Data\EstimateAddressInterface" type="Magento\Quote\Model\EstimateAddress" /> <preference for="Magento\Quote\Api\Data\ProductOptionInterface" type="Magento\Quote\Model\Quote\ProductOption" /> + <preference for="Magento\Quote\Model\ValidationRules\QuoteValidationRuleInterface" type="Magento\Quote\Model\ValidationRules\QuoteValidationComposite\Proxy"/> <type name="Magento\Webapi\Controller\Rest\ParamsOverrider"> <arguments> <argument name="paramOverriders" xsi:type="array"> @@ -94,4 +96,44 @@ <plugin name="clean_quote_items_after_product_delete" type="Magento\Quote\Model\Product\Plugin\RemoveQuoteItems"/> <plugin name="update_quote_items_after_product_save" type="Magento\Quote\Model\Product\Plugin\UpdateQuoteItems"/> </type> + <type name="Magento\Catalog\Model\Product\Action"> + <plugin name="quoteProductMassChange" type="Magento\Quote\Model\Product\Plugin\MarkQuotesRecollectMassDisabled"/> + </type> + <type name="Magento\Quote\Model\ValidationRules\QuoteValidationComposite"> + <arguments> + <argument name="validationRules" xsi:type="array"> + <item name="AllowedCountryValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\AllowedCountryValidationRule</item> + <item name="ShippingAddressValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\ShippingAddressValidationRule</item> + <item name="ShippingMethodValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule</item> + <item name="BillingAddressValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\BillingAddressValidationRule</item> + <item name="PaymentMethodValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\PaymentMethodValidationRule</item> + <item name="MinimumAmountValidationRule" xsi:type="object">Magento\Quote\Model\ValidationRules\MinimumAmountValidationRule</item> + </argument> + </arguments> + </type> + <type name="Magento\Quote\Model\ValidationRules\AllowedCountryValidationRule"> + <arguments> + <argument name="generalMessage" xsi:type="string" translatable="true">Some addresses can't be used due to the configurations for specific countries.</argument> + </arguments> + </type> + <type name="Magento\Quote\Model\ValidationRules\ShippingAddressValidationRule"> + <arguments> + <argument name="generalMessage" xsi:type="string" translatable="true">Please check the shipping address information.</argument> + </arguments> + </type> + <type name="Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule"> + <arguments> + <argument name="generalMessage" xsi:type="string" translatable="true">The shipping method is missing. Select the shipping method and try again.</argument> + </arguments> + </type> + <type name="Magento\Quote\Model\ValidationRules\BillingAddressValidationRule"> + <arguments> + <argument name="generalMessage" xsi:type="string" translatable="true">Please check the billing address information.</argument> + </arguments> + </type> + <type name="Magento\Quote\Model\ValidationRules\PaymentMethodValidationRule"> + <arguments> + <argument name="generalMessage" xsi:type="string" translatable="true">Enter a valid payment method and try again.</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Quote/etc/fieldset.xml b/app/code/Magento/Quote/etc/fieldset.xml index 55ec76a647fcd..85ee20c7f8520 100644 --- a/app/code/Magento/Quote/etc/fieldset.xml +++ b/app/code/Magento/Quote/etc/fieldset.xml @@ -186,6 +186,11 @@ <aspect name="to_order_address" /> </field> </fieldset> + <fieldset id="quote_convert_address_item"> + <field name="quote_item_id"> + <aspect name="to_order_item" /> + </field> + </fieldset> <fieldset id="quote_convert_item"> <field name="sku"> <aspect name="to_order_item" /> diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml new file mode 100644 index 0000000000000..91f4cfbf60aba --- /dev/null +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Plugin\UpdateQuoteStore"> + <arguments> + <argument name="quoteRepository" xsi:type="object">Magento\Quote\Model\QuoteRepository\Proxy</argument> + <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> + </arguments> + </type> + <type name="Magento\Store\Model\StoreSwitcherInterface"> + <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> + </type> + <type name="Magento\Store\Api\StoreCookieManagerInterface"> + <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> + </type> +</config> diff --git a/app/code/Magento/Quote/etc/frontend/events.xml b/app/code/Magento/Quote/etc/frontend/events.xml new file mode 100644 index 0000000000000..1e9822bbf3ef8 --- /dev/null +++ b/app/code/Magento/Quote/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="sales_model_service_quote_submit_success"> + <observer name="sendEmail" instance="Magento\Quote\Observer\SubmitObserver" /> + </event> +</config> diff --git a/app/code/Magento/Quote/etc/module.xml b/app/code/Magento/Quote/etc/module.xml index f682568e63d02..6720a77a79e15 100644 --- a/app/code/Magento/Quote/etc/module.xml +++ b/app/code/Magento/Quote/etc/module.xml @@ -6,6 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Quote" setup_version="2.0.6"> + <module name="Magento_Quote" setup_version="2.0.8"> </module> </config> diff --git a/app/code/Magento/Quote/etc/webapi_rest/events.xml b/app/code/Magento/Quote/etc/webapi_rest/events.xml index 7b94434f3f20a..1e9822bbf3ef8 100644 --- a/app/code/Magento/Quote/etc/webapi_rest/events.xml +++ b/app/code/Magento/Quote/etc/webapi_rest/events.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="sales_model_service_quote_submit_success"> - <observer name="sendEmail" instance="Magento\Quote\Observer\Webapi\SubmitObserver" /> + <observer name="sendEmail" instance="Magento\Quote\Observer\SubmitObserver" /> </event> </config> diff --git a/app/code/Magento/Quote/etc/webapi_soap/events.xml b/app/code/Magento/Quote/etc/webapi_soap/events.xml index 7b94434f3f20a..1e9822bbf3ef8 100644 --- a/app/code/Magento/Quote/etc/webapi_soap/events.xml +++ b/app/code/Magento/Quote/etc/webapi_soap/events.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="sales_model_service_quote_submit_success"> - <observer name="sendEmail" instance="Magento\Quote\Observer\Webapi\SubmitObserver" /> + <observer name="sendEmail" instance="Magento\Quote\Observer\SubmitObserver" /> </event> </config> diff --git a/app/code/Magento/QuoteAnalytics/Test/Mftf/LICENSE.txt b/app/code/Magento/QuoteAnalytics/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/QuoteAnalytics/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 <insert your license name here>" 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/app/code/Magento/QuoteAnalytics/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/QuoteAnalytics/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/QuoteAnalytics/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 <insert your license name here>" 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/app/code/Magento/QuoteAnalytics/Test/Mftf/README.md b/app/code/Magento/QuoteAnalytics/Test/Mftf/README.md new file mode 100644 index 0000000000000..617b175d7ed07 --- /dev/null +++ b/app/code/Magento/QuoteAnalytics/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Quote Analytics Functional Tests + +The Functional Test Module for **Magento Quote Analytics** module. diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json index c75abc5bb5da2..6d633a7040837 100644 --- a/app/code/Magento/QuoteAnalytics/composer.json +++ b/app/code/Magento/QuoteAnalytics/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-quote-analytics", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-quote": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ReleaseNotification/Controller/Adminhtml/Notification/MarkUserNotified.php b/app/code/Magento/ReleaseNotification/Controller/Adminhtml/Notification/MarkUserNotified.php index 572fb7695b20f..56f836fff9522 100644 --- a/app/code/Magento/ReleaseNotification/Controller/Adminhtml/Notification/MarkUserNotified.php +++ b/app/code/Magento/ReleaseNotification/Controller/Adminhtml/Notification/MarkUserNotified.php @@ -85,6 +85,9 @@ public function execute() return $resultJson->setData($responseContent); } + /** + * @return bool + */ protected function _isAllowed() { return parent::_isAllowed(); diff --git a/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php b/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php old mode 100644 new mode 100755 index 07e26bb1a4d8d..527c1b4fd15f9 --- a/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php +++ b/app/code/Magento/ReleaseNotification/Model/Condition/CanViewNotification.php @@ -10,6 +10,8 @@ use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\View\Layout\Condition\VisibilityConditionInterface; use Magento\Framework\App\CacheInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DataInterfaceFactory; /** * Class CanViewNotification @@ -53,6 +55,11 @@ class CanViewNotification implements VisibilityConditionInterface */ private $cacheStorage; + /** + * @var DataInterfaceFactory + */ + private $configFactory; + /** * CanViewNotification constructor. * @@ -60,17 +67,20 @@ class CanViewNotification implements VisibilityConditionInterface * @param Session $session * @param ProductMetadataInterface $productMetadata * @param CacheInterface $cacheStorage + * @param DataInterfaceFactory|null $configFactory */ public function __construct( Logger $viewerLogger, Session $session, ProductMetadataInterface $productMetadata, - CacheInterface $cacheStorage + CacheInterface $cacheStorage, + DataInterfaceFactory $configFactory = null ) { $this->viewerLogger = $viewerLogger; $this->session = $session; $this->productMetadata = $productMetadata; $this->cacheStorage = $cacheStorage; + $this->configFactory = $configFactory ?? ObjectManager::getInstance()->get(DataInterfaceFactory::class); } /** @@ -80,6 +90,8 @@ public function __construct( */ public function isVisible(array $arguments) { + $config = $this->configFactory->create(['componentName' => 'release_notification']); + $releaseContentVerion = $config->get('release_notification/arguments/data/releaseContentVersion'); $userId = $this->session->getUser()->getId(); $cacheKey = self::$cachePrefix . $userId; $value = $this->cacheStorage->load($cacheKey); @@ -91,6 +103,14 @@ public function isVisible(array $arguments) ); $this->cacheStorage->save(false, $cacheKey); } + if ($value) { + $value = version_compare( + $this->productMetadata->getVersion(), + $releaseContentVerion, + '<=' + ); + } + return (bool)$value; } diff --git a/app/code/Magento/ReleaseNotification/Test/Mftf/LICENSE.txt b/app/code/Magento/ReleaseNotification/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ReleaseNotification/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 <insert your license name here>" 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/app/code/Magento/ReleaseNotification/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ReleaseNotification/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ReleaseNotification/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 <insert your license name here>" 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/app/code/Magento/ReleaseNotification/Test/Mftf/README.md b/app/code/Magento/ReleaseNotification/Test/Mftf/README.md new file mode 100644 index 0000000000000..a8c47e35e0c85 --- /dev/null +++ b/app/code/Magento/ReleaseNotification/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Release Notification Functional Tests + +The Functional Test Module for **Magento Release Notification** module. diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php old mode 100644 new mode 100755 index 3ec00697507c1..082c7b4712c5c --- a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php +++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php @@ -12,6 +12,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Backend\Model\Auth\Session; use Magento\Framework\App\CacheInterface; +use Magento\Framework\Config\DataInterfaceFactory; /** * Class CanViewNotificationTest @@ -36,8 +37,15 @@ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase /** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */ private $cacheStorageMock; + /** @var $dataInterfaceFactoryMock \PHPUnit_Framework_MockObject_MockObject|DataInterfaceFactory */ + private $dataInterfaceFactoryMock; + public function setUp() { + $this->dataInterfaceFactoryMock = $this->getMockBuilder(DataInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create', 'get']) + ->getMock(); $this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class) ->getMockForAbstractClass(); $this->logMock = $this->getMockBuilder(Log::class) @@ -60,12 +68,21 @@ public function setUp() 'session' => $this->sessionMock, 'productMetadata' => $this->productMetadataMock, 'cacheStorage' => $this->cacheStorageMock, + 'configFactory' => $this->dataInterfaceFactoryMock, ] ); } public function testIsVisibleLoadDataFromCache() { + $this->dataInterfaceFactoryMock->expects($this->once()) + ->method('create') + ->with(['componentName' => 'release_notification']) + ->willReturn($this->dataInterfaceFactoryMock); + $this->dataInterfaceFactoryMock->expects($this->once()) + ->method('get') + ->with('release_notification/arguments/data/releaseContentVersion') + ->willReturn('2.2.4'); $this->sessionMock->expects($this->once()) ->method('getUser') ->willReturn($this->sessionMock); @@ -83,10 +100,19 @@ public function testIsVisibleLoadDataFromCache() * @param bool $expected * @param string $version * @param string|null $lastViewVersion + * @param string $releaseContentVersion * @dataProvider isVisibleProvider */ - public function testIsVisible($expected, $version, $lastViewVersion) + public function testIsVisible(bool $expected, string $version, $lastViewVersion, string $releaseContentVersion) { + $this->dataInterfaceFactoryMock->expects($this->once()) + ->method('create') + ->with(['componentName' => 'release_notification']) + ->willReturn($this->dataInterfaceFactoryMock); + $this->dataInterfaceFactoryMock->expects($this->once()) + ->method('get') + ->with('release_notification/arguments/data/releaseContentVersion') + ->willReturn($releaseContentVersion); $this->cacheStorageMock->expects($this->once()) ->method('load') ->with('release-notification-popup-1') @@ -97,7 +123,7 @@ public function testIsVisible($expected, $version, $lastViewVersion) $this->sessionMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->productMetadataMock->expects($this->once()) + $this->productMetadataMock->expects($this->any()) ->method('getVersion') ->willReturn($version); $this->logMock->expects($this->once()) @@ -113,16 +139,22 @@ public function testIsVisible($expected, $version, $lastViewVersion) $this->assertEquals($expected, $this->canViewNotification->isVisible([])); } + /** + * @return array + */ public function isVisibleProvider() { return [ - [false, '2.2.1-dev', '999.999.999-alpha'], - [true, '2.2.1-dev', '2.0.0'], - [true, '2.2.1-dev', null], - [false, '2.2.1-dev', '2.2.1'], - [true, '2.2.1-dev', '2.2.0'], - [true, '2.3.0', '2.2.0'], - [false, '2.2.2', '2.2.2'], + [false, '2.2.1-dev', '999.999.999-alpha', '2.2.0'], + [true, '2.2.1-dev', '2.0.0', '2.2.1'], + [true, '2.2.1-dev', null, '2.2.1'], + [false, '2.2.1-dev', '2.2.1', '2.2.0'], + [true, '2.2.1-dev', '2.2.0', '2.2.1'], + [true, '2.3.0', '2.2.0', '2.3.0'], + [false, '2.2.2', '2.2.2', '2.2.2'], + [false, '2.2.5', '2.2.4', '2.2.4'], + [true, '2.2.6', '2.2.5', '2.2.6'], + [true, '2.2.7', '2.2.6', '2.2.7'], ]; } } diff --git a/app/code/Magento/ReleaseNotification/composer.json b/app/code/Magento/ReleaseNotification/composer.json index 40e9e02db9217..66b3af1258884 100644 --- a/app/code/Magento/ReleaseNotification/composer.json +++ b/app/code/Magento/ReleaseNotification/composer.json @@ -2,13 +2,13 @@ "name": "magento/module-release-notification", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-user": "101.0.*", "magento/module-backend": "100.2.*", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ReleaseNotification/etc/adminhtml/di.xml b/app/code/Magento/ReleaseNotification/etc/adminhtml/di.xml new file mode 100755 index 0000000000000..3bbf2effe1a5e --- /dev/null +++ b/app/code/Magento/ReleaseNotification/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="uiComponentConfigFactory" type="Magento\Framework\Config\DataInterfaceFactory"> + <arguments> + <argument xsi:type="string" name="instanceName">Magento\Ui\Config\Data</argument> + </arguments> + </virtualType> + <type name="Magento\ReleaseNotification\Model\Condition\CanViewNotification"> + <arguments> + <argument name="configFactory" xsi:type="object">uiComponentConfigFactory</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv index 4a3cd02782b9c..7af356627b1ea 100644 --- a/app/code/Magento/ReleaseNotification/i18n/en_US.csv +++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv @@ -1,112 +1,7 @@ "Next >","Next >" "< Back","< Back" "Done","Done" -"What's new with Magento 2.2.2","What's new with Magento 2.2.2" -"<p>Magento 2.2.2 offers advanced new features, including:</p> - <br /> - <div class=""analytics-highlight""> - <h3>Advanced Reporting</h3> - <p>Gain valuable insights through a dynamic suite of product, order, and customer reports, - powered by Magento Business Intelligence.</p> - </div> - <div class=""instant-purchase-highlight""> - <h3>Instant Purchase</h3> - <p>Simplify ordering and boost conversion rates by allowing your customers to use stored - payment and shipping information to skip tedious checkout steps.</p> - </div> - <div class=""email-marketing-highlight""> - <h3>Email Marketing Automation</h3> - <p>Send smarter, faster email campaigns with marketing automation from dotmailer, powered by - your Magento store's live data.</p> - </div> - <p>Release notes and additional details can be found at - <a href=""http://devdocs.magento.com/"" target=""_blank"">Magento DevDocs</a>. - </p>","<p>Magento 2.2.2 offers advanced new features, including:</p> - <br /> - <div class=""analytics-highlight""> - <h3>Advanced Reporting</h3> - <p>Gain valuable insights through a dynamic suite of product, order, and customer reports, - powered by Magento Business Intelligence.</p> - </div> - <div class=""instant-purchase-highlight""> - <h3>Instant Purchase</h3> - <p>Simplify ordering and boost conversion rates by allowing your customers to use stored - payment and shipping information to skip tedious checkout steps.</p> - </div> - <div class=""email-marketing-highlight""> - <h3>Email Marketing Automation</h3> - <p>Send smarter, faster email campaigns with marketing automation from dotmailer, powered by - your Magento store's live data.</p> - </div> - <p>Release notes and additional details can be found at - <a href=""http://devdocs.magento.com/"" target=""_blank"">Magento DevDocs</a>. - </p>" -"Advanced Reporting","Advanced Reporting" -"<p>Advanced Reporting - provides you with a dynamic suite of reports with rich insights about the health of your - business.</p><br /><p>As part of the Advanced Reporting service, we may also use your customer - data for such purposes as benchmarking, improving our products and services, and providing you - with new and improved analytics.</p><br /><p>By using Magento 2.2.2, you agree to the Advanced - Reporting <a href=""https://magento.com/legal/terms/privacy"" target=""_blank"">Privacy Policy</a> and - <a href=""https://magento.com/legal/terms/cloud-terms"" target=""_blank"">Terms - of Service</a>. You may opt out at any time from the Stores Configuration page.</p> - ","<p>Advanced Reporting - provides you with a dynamic suite of reports with rich insights about the health of your - business.</p><br /><p>As part of the Advanced Reporting service, we may also use your customer - data for such purposes as benchmarking, improving our products and services, and providing you - with new and improved analytics.</p><br /><p>By using Magento 2.2.2, you agree to the Advanced - Reporting <a href=""https://magento.com/legal/terms/privacy"" target=""_blank"">Privacy Policy</a> and - <a href=""https://magento.com/legal/terms/cloud-terms"" target=""_blank"">Terms - of Service</a>. You may opt out at any time from the Stores Configuration page.</p>" -"Instant Purchase","Instant Purchase" -"<p>Now you can deliver an Amazon-like experience with a new, streamlined checkout option. - Logged-in customers can use previously-stored payment credentials and shipping information - to skip steps, making the process faster and easier, especially for mobile shoppers. Key - features include: - </p> - <ul> - <li><span>Configurable “Instant Purchase” button to place orders.</span></li> - <li><span>Support for all payment solutions using Braintree Vault, including Braintree Credit - Card, Braintree PayPal, and PayPal Payflow Pro.</span></li> - <li><span>Shipping to the customer’s default address using the lowest cost, available shipping - method.</span></li> - <li><span>Ability for developers to customize the Instant Purchase business logic to meet - merchant needs.</span></li> - </ul>","<p>Now you can deliver an Amazon-like experience with a new, streamlined checkout option. - Logged-in customers can use previously-stored payment credentials and shipping information - to skip steps, making the process faster and easier, especially for mobile shoppers. Key - features include: - </p> - <ul> - <li><span>Configurable “Instant Purchase” button to place orders.</span></li> - <li><span>Support for all payment solutions using Braintree Vault, including Braintree Credit - Card, Braintree PayPal, and PayPal Payflow Pro.</span></li> - <li><span>Shipping to the customer’s default address using the lowest cost, available shipping - method.</span></li> - <li><span>Ability for developers to customize the Instant Purchase business logic to meet - merchant needs.</span></li> - </ul>" -"Email Marketing Automation","Email Marketing Automation" -"<p>Unlock an unparalleled level of insight and control of your eCommerce marketing with - dotmailer Email Marketing Automation. Included with Magento 2.2.2 for easy set-up, dotmailer - ensures every customer’s journey is captured, segmented, and personalized, enabling you to - deliver customer-centric campaigns that beat your results over and over again. Benefits include: - </p> - <ul> - <li><span>No obligation 14-day trial.</span></li> - <li><span>Automation campaigns using your live Magento store data that drive revenue, - including abandoned cart, abandoned browse, product replenishment, and many more</span></li> - <li><span>Built-in solution for transactional emails.</span></li> - <li><span>Telephone support and advice from marketing experts included.</span></li> - </ul>","<p>Unlock an unparalleled level of insight and control of your eCommerce marketing with - dotmailer Email Marketing Automation. Included with Magento 2.2.2 for easy set-up, dotmailer - ensures every customer’s journey is captured, segmented, and personalized, enabling you to - deliver customer-centric campaigns that beat your results over and over again. Benefits include: - </p> - <ul> - <li><span>No obligation 14-day trial.</span></li> - <li><span>Automation campaigns using your live Magento store data that drive revenue, - including abandoned cart, abandoned browse, product replenishment, and many more</span></li> - <li><span>Built-in solution for transactional emails.</span></li> - <li><span>Telephone support and advice from marketing experts included.</span></li> - </ul>" +"Flexible Payment Terms","Flexible Payment Terms" +"Trusted Payment Options","Trusted Payment Options" +"Sales and Use Tax Automation","Sales and Use Tax Automation" +"What’s new with Magento 2.2.4","What’s new with Magento 2.2.4" diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml old mode 100644 new mode 100755 index 0364750d56a38..26db532f89db9 --- a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml +++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml @@ -12,6 +12,7 @@ </item> <item name="label" xsi:type="string" translate="true">Release Notification</item> <item name="template" xsi:type="string">templates/form/collapsible</item> + <item name="releaseContentVersion" xsi:type="string">2.2.4</item> </argument> <settings> <namespace>release_notification</namespace> @@ -43,7 +44,7 @@ <state>true</state> <options> <option name="modalClass" xsi:type="string">release-notification-modal</option> - <option name="title" xsi:type="string" translate="true">What's new with Magento 2.2.2</option> + <option name="title" xsi:type="string" translate="true">What’s new with Magento 2.2.4</option> <option name="type" xsi:type="string">popup</option> <option name="responsive" xsi:type="boolean">true</option> <option name="innerScroll" xsi:type="boolean">true</option> @@ -60,26 +61,21 @@ <item name="label" xsi:type="string"/> <item name="additionalClasses" xsi:type="string">release-notification-text</item> <item name="text" xsi:type="string" translate="true"><![CDATA[ - <p>Magento 2.2.2 offers advanced new features, including:</p> + <p>Magento 2.2.4 provides powerful new tools to help reduce cart abandonment and simplify store operations:</p> <br /> - <div class="analytics-highlight"> - <h3>Advanced Reporting</h3> - <p>Gain valuable insights through a dynamic suite of product, order, and customer reports, - powered by Magento Business Intelligence.</p> + <div class="trusted-payments-highlight"> + <h3>Trusted Payment Options</h3> + <p>Online or on the go, your shoppers can easily purchase in a familiar, trusted way with Amazon Pay.</p> </div> - <div class="instant-purchase-highlight"> - <h3>Instant Purchase</h3> - <p>Simplify ordering and boost conversion rates by allowing your customers to use stored - payment and shipping information to skip tedious checkout steps.</p> + <div class="flexible-payments-highlight"> + <h3>Flexible Payment Terms</h3> + <p>Increase sales by providing shoppers with the flexibility to pay now, later, or in installments using Klarna’s integrated payment option.</p> </div> - <div class="email-marketing-highlight"> - <h3>Email Marketing Automation</h3> - <p>Send smarter, faster email campaigns with marketing automation from dotmailer, powered by - your Magento store's live data.</p> + <div class="tax-automation-highlight"> + <h3>Sales and Use Tax Automation</h3> + <p>Merchants can now automatically perform instant and accurate sales tax calculations on shopping cart items with Vertex Cloud.</p> </div> - <p>Release notes and additional details can be found at - <a href="http://devdocs.magento.com/" target="_blank">Magento DevDocs</a>. - </p>]]> + <p>To learn more, see the <a href="http://devdocs.magento.com/guides/v2.2/release-notes/bk-release-notes.html" target="_blank">Release Notes</a> and the <a href="http://docs.magento.com/m2/ce/user_guide/getting-started.html" target="_blank">Magento User Guide</a>.</p>]]> </item> </item> </argument> @@ -100,7 +96,7 @@ <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = analytics_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = trusted_payments_modal</item> <item name="actionName" xsi:type="string">openModal</item> </item> </item> @@ -114,12 +110,12 @@ </container> </fieldset> </modal> - <modal name="analytics_modal" component="Magento_ReleaseNotification/js/modal/component"> + <modal name="trusted_payments_modal" component="Magento_ReleaseNotification/js/modal/component"> <settings> <onCancel>actionCancel</onCancel> <options> - <option name="modalClass" xsi:type="string">analytics-subscription-modal</option> - <option name="title" xsi:type="string" translate="true">Advanced Reporting</option> + <option name="modalClass" xsi:type="string">trusted-payments-modal</option> + <option name="title" xsi:type="string" translate="true">Trusted Payment Options</option> <option name="type" xsi:type="string">popup</option> <option name="responsive" xsi:type="boolean">true</option> <option name="innerScroll" xsi:type="boolean">true</option> @@ -135,14 +131,13 @@ <item name="config" xsi:type="array"> <item name="label" xsi:type="string"/> <item name="additionalClasses" xsi:type="string">release-notification-text</item> - <item name="text" xsi:type="string" translate="true"><![CDATA[<p>Advanced Reporting - provides you with a dynamic suite of reports with rich insights about the health of your - business.</p><br /><p>As part of the Advanced Reporting service, we may also use your customer - data for such purposes as benchmarking, improving our products and services, and providing you - with new and improved analytics.</p><br /><p>By using Magento 2.2.2, you agree to the Advanced - Reporting <a href="https://magento.com/legal/terms/privacy" target="_blank">Privacy Policy</a> - and <a href="https://magento.com/legal/terms/cloud-terms" target="_blank">Terms - of Service</a>. You may opt out at any time from the Stores Configuration page.</p>]]> + <item name="text" xsi:type="string" translate="true"><![CDATA[<p>With <a href="http://docs.magento.com/m2/ce/user_guide/payment/amazon-pay.html" target="_blank">Amazon Pay</a>, we make it simple for the hundreds of millions of Amazon customers worldwide to easily log in and purchase from your store using information already stored their Amazon account. With easy setup in Magento 2.2.4, Amazon Pay provides your customers with a trusted and familiar checkout experience. In just a few taps, they can complete a transaction without ever leaving your site.</p> + <ul> + <li><span>Your customers use one familiar login to identify themselves and transact anywhere Amazon Pay is offered.</span></li> + <li><span>Checkout experience is covered by the Amazon A-to-z Guarantee at no additional cost.</span></li> + <li><span>Built on years of Amazon eCommerce innovation, and trusted by merchants and buyers alike.</span></li> + <li><span>Available across platforms and devices, in the home and on the go.</span></li> + </ul>]]> </item> </item> </argument> @@ -159,7 +154,7 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-back</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = analytics_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = trusted_payments_modal</item> <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> @@ -180,11 +175,11 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-next</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = analytics_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = trusted_payments_modal</item> <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = instant_purchase_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = flexible_payments_modal</item> <item name="actionName" xsi:type="string">openModal</item> </item> </item> @@ -198,12 +193,12 @@ </container> </fieldset> </modal> - <modal name="instant_purchase_modal" component="Magento_ReleaseNotification/js/modal/component"> + <modal name="flexible_payments_modal" component="Magento_ReleaseNotification/js/modal/component"> <settings> <onCancel>actionCancel</onCancel> <options> - <option name="modalClass" xsi:type="string">instant-purchase-modal</option> - <option name="title" xsi:type="string" translate="true">Instant Purchase</option> + <option name="modalClass" xsi:type="string">flexible-payments-modal</option> + <option name="title" xsi:type="string" translate="true">Flexible Payment Terms</option> <option name="type" xsi:type="string">popup</option> <option name="responsive" xsi:type="boolean">true</option> <option name="innerScroll" xsi:type="boolean">true</option> @@ -220,19 +215,13 @@ <item name="label" xsi:type="string"/> <item name="additionalClasses" xsi:type="string">release-notification-text</item> <item name="text" xsi:type="string" translate="true"><![CDATA[ - <p>Now you can deliver an Amazon-like experience with a new, streamlined checkout option. - Logged-in customers can use previously-stored payment credentials and shipping information - to skip steps, making the process faster and easier, especially for mobile shoppers. Key - features include: - </p> + <p>With <a href="http://docs.magento.com/m2/ce/user_guide/payment/klarna.html" target="_blank">Klarna</a>, reduce purchase stress and improve your shopping experience with several retail finance and direct-payment options that are easily integrated into your existing buying journey. Shoppers can choose to pay now by online payment or direct debit, pay later by invoice, or pay in installments.</p> <ul> - <li><span>Configurable “Instant Purchase” button to place orders.</span></li> - <li><span>Support for all payment solutions using Braintree Vault, including Braintree Credit - Card, Braintree PayPal, and PayPal Payflow Pro.</span></li> - <li><span>Shipping to the customer’s default address using the lowest cost, available shipping - method.</span></li> - <li><span>Ability for developers to customize the Instant Purchase business logic to meet - merchant needs.</span></li> + <li><span>Customers never leave your site, and enjoy a smoother shopping experience.</span></li> + <li><span>Easy application process with the ability to pay over time increases average order value and conversion rates.</span></li> + <li><span>Assumes all credit and fraud risk, and ensures you are paid for ordered goods.</span></li> + <li><span>Ready for all devices, and enables shoppers to use their Klarna account at any Klarna merchant with just a few clicks.</span></li> + <li><span>Fast and easy setup, right from the Magento Admin.</span></li> </ul>]]> </item> </item> @@ -250,11 +239,11 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-back</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = instant_purchase_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = flexible_payments_modal</item> <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = analytics_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = trusted_payments_modal</item> <item name="actionName" xsi:type="string">openModal</item> </item> </item> @@ -271,11 +260,11 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-next</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = instant_purchase_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = flexible_payments_modal</item> <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = email_marketing_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = tax_automation_modal</item> <item name="actionName" xsi:type="string">openModal</item> </item> </item> @@ -289,12 +278,12 @@ </container> </fieldset> </modal> - <modal name="email_marketing_modal" component="Magento_ReleaseNotification/js/modal/component"> + <modal name="tax_automation_modal" component="Magento_ReleaseNotification/js/modal/component"> <settings> <onCancel>actionCancel</onCancel> <options> - <option name="modalClass" xsi:type="string">email-marketing-modal</option> - <option name="title" xsi:type="string" translate="true">Email Marketing Automation</option> + <option name="modalClass" xsi:type="string">tax-automation-modal</option> + <option name="title" xsi:type="string" translate="true">Sales and Use Tax Automation</option> <option name="type" xsi:type="string">popup</option> <option name="responsive" xsi:type="boolean">true</option> <option name="innerScroll" xsi:type="boolean">true</option> @@ -311,17 +300,13 @@ <item name="label" xsi:type="string"/> <item name="additionalClasses" xsi:type="string">release-notification-text</item> <item name="text" xsi:type="string" translate="true"><![CDATA[ - <p>Unlock an unparalleled level of insight and control of your eCommerce marketing with - dotmailer Email Marketing Automation. Included with Magento 2.2.2 for easy set-up, dotmailer - ensures every customer’s journey is captured, segmented, and personalized, enabling you to - deliver customer-centric campaigns that beat your results over and over again. Benefits include: - </p> + <p><a href="http://docs.magento.com/m2/ce/user_guide/tax/vertex.html" target="_blank">Vertex Cloud</a> combines sophisticated software with an exceptional team of tax experts to help Magento merchants simplify the sales tax process, and comply with ever-changing tax laws. Merchants can easily access and configure Vertex Cloud from the Magento Admin to meet complex tax challenges.</p> <ul> - <li><span>No obligation 14-day trial.</span></li> - <li><span>Automation campaigns using your live Magento store data that drive revenue, - including abandoned cart, abandoned browse, product replenishment, and many more.</span></li> - <li><span>Built-in solution for transactional emails.</span></li> - <li><span>Telephone support and advice from marketing experts included.</span></li> + <li><span>Seamless integration with award-winning support for simple-to-complex tax needs.</span></li> + <li><span>Automatically comply with the latest tax rules for each product and every jurisdiction.</span></li> + <li><span>Perform tax calculations and generate signature-ready PDF returns in one solution.</span></li> + <li><span>No penalty to merchants for tax estimates from abandoned carts.</span></li> + <li><span>Flexible deployment options fit the needs of your organization in the cloud, or on premises.</span></li> </ul>]]> </item> </item> @@ -339,11 +324,11 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-back</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = email_marketing_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = tax_automation_modal</item> <item name="actionName" xsi:type="string">closeModal</item> </item> <item name="1" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = instant_purchase_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = flexible_payments_modal</item> <item name="actionName" xsi:type="string">openModal</item> </item> </item> @@ -360,7 +345,7 @@ <item name="buttonClasses" xsi:type="string">release-notification-button-next</item> <item name="actions" xsi:type="array"> <item name="0" xsi:type="array"> - <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = email_marketing_modal</item> + <item name="targetName" xsi:type="string">ns = ${ $.ns }, index = tax_automation_modal</item> <item name="actionName" xsi:type="string">closeReleaseNotes</item> </item> </item> diff --git a/app/code/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStart.php b/app/code/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStart.php index 5a27b4dc7666f..4ac12501aa90f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStart.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStart.php @@ -23,9 +23,8 @@ protected function _getElementHtml(AbstractElement $element) { $_months = []; for ($i = 1; $i <= 12; $i++) { - $_months[$i] = $this->_localeDate->date(mktime(null, null, null, $i))->format('m'); + $_months[$i] = $this->_localeDate->date(mktime(null, null, null, $i, 1))->format('m'); } - $_days = []; for ($i = 1; $i <= 31; $i++) { $_days[$i] = $i < 10 ? '0' . $i : $i; diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid.php index 2bb4dcd1efbf1..c1197a608b19a 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid.php @@ -6,15 +6,32 @@ namespace Magento\Reports\Block\Adminhtml; +use Magento\Backend\Block\Template\Context; +use Magento\Backend\Helper\Data; +use Magento\Framework\Url\DecoderInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\Parameters; + /** * Backend report grid block * * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ class Grid extends \Magento\Backend\Block\Widget\Grid { + /** + * @var DecoderInterface + */ + private $urlDecoder; + + /** + * @var Parameters + */ + private $parameters; + /** * Should Store Switcher block be visible * @@ -71,6 +88,31 @@ class Grid extends \Magento\Backend\Block\Widget\Grid */ protected $_filterValues; + /** + * @param Context $context + * @param Data $backendHelper + * @param array $data + * @param DecoderInterface|null $urlDecoder + * @param Parameters|null $parameters + */ + public function __construct( + Context $context, + Data $backendHelper, + array $data = [], + DecoderInterface $urlDecoder = null, + Parameters $parameters = null + ) { + $this->urlDecoder = $urlDecoder ?? ObjectManager::getInstance()->get( + DecoderInterface::class + ); + + $this->parameters = $parameters ?? ObjectManager::getInstance()->get( + Parameters::class + ); + + parent::__construct($context, $backendHelper, $data); + } + /** * Apply sorting and filtering to collection * @@ -86,9 +128,12 @@ protected function _prepareCollection() } if (is_string($filter)) { - $data = []; - $filter = base64_decode($filter); - parse_str(urldecode($filter), $data); + // this is a replacement for base64_decode() + $filter = $this->urlDecoder->decode($filter); + + // this is a replacement for parse_str() + $this->parameters->fromString(urldecode($filter)); + $data = $this->parameters->toArray(); if (!isset($data['report_from'])) { // getting all reports from 2001 year @@ -113,7 +158,7 @@ protected function _prepareCollection() $this->_setFilterValues($data); } elseif ($filter && is_array($filter)) { $this->_setFilterValues($filter); - } elseif (0 !== sizeof($this->_defaultFilter)) { + } elseif (0 !== count($this->_defaultFilter)) { $this->_setFilterValues($this->_defaultFilter); } @@ -127,9 +172,8 @@ protected function _prepareCollection() * Validate from and to date */ try { - $from = $this->_localeDate->scopeDate(null, $this->getFilter('report_from'), false); - $to = $this->_localeDate->scopeDate(null, $this->getFilter('report_to'), false); - + $from = $this->_localeDate->date($this->getFilter('report_from'), null, true, false); + $to = $this->_localeDate->date($this->getFilter('report_to'), null, true, false); $collection->setInterval($from, $to); } catch (\Exception $e) { $this->_errors[] = __('Invalid date specified'); @@ -328,7 +372,7 @@ public function getFilter($name) if (isset($this->_filters[$name])) { return $this->_filters[$name]; } else { - return $this->getRequest()->getParam($name) ? htmlspecialchars($this->getRequest()->getParam($name)) : ''; + return $this->getRequest()->getParam($name) ? $this->escapeHtml($this->getRequest()->getParam($name)) : ''; } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php index 158455db26455..76c110c41b98e 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Reports\Block\Adminhtml\Grid; class AbstractGrid extends \Magento\Backend\Block\Widget\Grid\Extended @@ -170,12 +171,7 @@ public function addColumn($columnId, $column) */ protected function _getStoreIds() { - $filterData = $this->getFilterData(); - if ($filterData) { - $storeIds = explode(',', $filterData->getData('store_ids')); - } else { - $storeIds = []; - } + $storeIds = $this->getFilteredStores(); // By default storeIds array contains only allowed stores $allowedStoreIds = array_keys($this->_storeManager->getStores()); // And then array_intersect with post data for prevent unauthorized stores reports @@ -302,6 +298,7 @@ public function getCountTotals() ); $this->_addOrderStatusFilter($totalsCollection, $filterData); + $this->_addCustomFilter($totalsCollection, $filterData); if ($totalsCollection->load()->getSize() < 1 || !$filterData->getData('from')) { $this->setTotals(new \Magento\Framework\DataObject()); @@ -313,6 +310,7 @@ public function getCountTotals() } } } + return parent::getCountTotals(); } @@ -409,4 +407,33 @@ protected function _addCustomFilter($collection, $filterData) { return $this; } + + /** + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getFilteredStores(): array + { + $storeIds = []; + + $filterData = $this->getFilterData(); + if ($filterData) { + if ($filterData->getWebsite()) { + $storeIds = array_keys( + $this->_storeManager->getWebsite($filterData->getWebsite())->getStores() + ); + } + + if ($filterData->getGroup()) { + $storeIds = array_keys( + $this->_storeManager->getGroup($filterData->getGroup())->getStores() + ); + } + + if ($filterData->getData('store_ids')) { + $storeIds = explode(',', $filterData->getData('store_ids')); + } + } + return is_array($storeIds) ? $storeIds : []; + } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php index 260d7bb50679d..f22b3e7bb963b 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php @@ -30,7 +30,7 @@ public function render(\Magento\Framework\DataObject $row) return $data; } - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); return $data; diff --git a/app/code/Magento/Reports/Block/Adminhtml/Product/Lowstock/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Product/Lowstock/Grid.php index 653dabb71e21d..5460dab3a7ff8 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Product/Lowstock/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Product/Lowstock/Grid.php @@ -53,14 +53,12 @@ protected function _prepareCollection() } elseif ($store) { $storeId = (int)$store; } else { - $storeId = ''; + $storeId = null; } /** @var $collection \Magento\Reports\Model\ResourceModel\Product\Lowstock\Collection */ $collection = $this->_lowstocksFactory->create()->addAttributeToSelect( '*' - )->setStoreId( - $storeId )->filterByIsQtyProductTypes()->joinInventoryItem( 'qty' )->useManageStockFilter( diff --git a/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php b/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php index fc4cffbdca408..f901b32d8b12f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php @@ -17,7 +17,7 @@ class Viewed extends \Magento\Backend\Block\Widget\Grid\Container /** * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * @return void diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php index d70930d2395ae..b773184408a7f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php @@ -19,7 +19,7 @@ class Bestsellers extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php index b8f71158877bb..fe85af58b34f6 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php @@ -19,7 +19,7 @@ class Coupons extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php index c96483e33ebe5..57594a11bd997 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php @@ -19,7 +19,7 @@ class Invoiced extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php index 7ff80f62f6bee..994b29e6eb0dd 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php @@ -19,7 +19,7 @@ class Refunded extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php index 5abea45e657d7..64375ace3e94d 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php @@ -19,7 +19,7 @@ class Sales extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php index 21836f1a8c276..9c80f6aa423b8 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php @@ -6,21 +6,58 @@ namespace Magento\Reports\Block\Adminhtml\Sales\Sales; +use Magento\Framework\DataObject; +use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Order\ConfigFactory; +use Magento\Sales\Model\Order; + /** * Adminhtml sales report grid block * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid { /** - * GROUP BY criteria - * * @var string */ protected $_columnGroupBy = 'period'; + /** + * @var ConfigFactory + */ + private $configFactory; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Helper\Data $backendHelper + * @param \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory + * @param \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory + * @param \Magento\Reports\Helper\Data $reportsData + * @param array $data + * @param ConfigFactory|null $configFactory + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Helper\Data $backendHelper, + \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory, + \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory, + \Magento\Reports\Helper\Data $reportsData, + array $data = [], + ConfigFactory $configFactory = null + ) { + parent::__construct( + $context, + $backendHelper, + $resourceFactory, + $collectionFactory, + $reportsData, + $data + ); + $this->configFactory = $configFactory ?: ObjectManager::getInstance()->get(ConfigFactory::class); + } + /** * {@inheritdoc} * @codeCoverageIgnore @@ -36,7 +73,7 @@ protected function _construct() */ public function getResourceCollectionName() { - return $this->getFilterData()->getData('report_type') == 'updated_at_order' + return $this->getFilterData()->getData('report_type') === 'updated_at_order' ? \Magento\Sales\Model\ResourceModel\Report\Order\Updatedat\Collection::class : \Magento\Sales\Model\ResourceModel\Report\Order\Collection::class; } @@ -103,9 +140,7 @@ protected function _prepareColumns() ] ); - if ($this->getFilterData()->getStoreIds()) { - $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); - } + $this->setStoreIds($this->_getStoreIds()); $currencyCode = $this->getCurrentCurrencyCode(); $rate = $this->getRate($currencyCode); @@ -118,6 +153,7 @@ protected function _prepareColumns() 'index' => 'total_income_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-total', 'column_css_class' => 'col-sales-total' @@ -133,6 +169,7 @@ protected function _prepareColumns() 'index' => 'total_revenue_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-revenue', @@ -149,6 +186,7 @@ protected function _prepareColumns() 'index' => 'total_profit_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-profit', @@ -165,6 +203,7 @@ protected function _prepareColumns() 'index' => 'total_invoiced_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-invoiced', 'column_css_class' => 'col-invoiced' @@ -180,6 +219,7 @@ protected function _prepareColumns() 'index' => 'total_paid_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-paid', @@ -196,6 +236,7 @@ protected function _prepareColumns() 'index' => 'total_refunded_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-refunded', 'column_css_class' => 'col-refunded' @@ -211,6 +252,7 @@ protected function _prepareColumns() 'index' => 'total_tax_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-tax', 'column_css_class' => 'col-sales-tax' @@ -226,6 +268,7 @@ protected function _prepareColumns() 'index' => 'total_tax_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-tax', @@ -242,6 +285,7 @@ protected function _prepareColumns() 'index' => 'total_shipping_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-shipping', 'column_css_class' => 'col-sales-shipping' @@ -257,6 +301,7 @@ protected function _prepareColumns() 'index' => 'total_shipping_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-shipping', @@ -273,6 +318,7 @@ protected function _prepareColumns() 'index' => 'total_discount_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-discount', 'column_css_class' => 'col-sales-discount' @@ -288,6 +334,7 @@ protected function _prepareColumns() 'index' => 'total_discount_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-discount', @@ -304,6 +351,7 @@ protected function _prepareColumns() 'index' => 'total_canceled_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-canceled', 'column_css_class' => 'col-canceled' @@ -315,4 +363,31 @@ protected function _prepareColumns() return parent::_prepareColumns(); } + + /** + * @inheritdoc + * + * Filter canceled statuses for orders. + * + * @return Grid + */ + protected function _prepareCollection() + { + /** @var DataObject $filterData */ + $filterData = $this->getData('filter_data'); + if (!$filterData->hasData('order_statuses')) { + $orderConfig = $this->configFactory->create(); + $statusValues = []; + $canceledStatuses = $orderConfig->getStateStatuses(Order::STATE_CANCELED); + $statusCodes = array_keys($orderConfig->getStatuses()); + foreach ($statusCodes as $code) { + if (!isset($canceledStatuses[$code])) { + $statusValues[] = $code; + } + } + $filterData->setData('order_statuses', $statusValues); + } + + return parent::_prepareCollection(); + } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php index 44dd4521c7bbe..e4dbdc2737745 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php @@ -19,7 +19,7 @@ class Shipping extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php index 38de08314d257..fa9e63745a87d 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php @@ -19,7 +19,7 @@ class Tax extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php index 79deb27423be5..7ca6b803f5b2b 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php @@ -123,7 +123,6 @@ protected function _prepareColumns() [ 'header' => __('Orders'), 'index' => 'orders_count', - 'total' => 'sum', 'type' => 'number', 'sortable' => false, 'header_css_class' => 'col-qty', diff --git a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php index f81176b7a1124..ff76702592196 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php @@ -67,6 +67,9 @@ protected function _prepareCollection() $this->setCollection($collection); parent::_prepareCollection(); + if ($this->_isExport) { + $collection->setPageSize(null); + } $this->getCollection()->resolveCustomerNames(); return $this; } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php index 28f2011de3365..dd42874b55795 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php @@ -9,6 +9,7 @@ /** * Adminhtml wishlist report page content block * + * @deprecated * @author Magento Core Team <core@magentocommerce.com> */ class Wishlist extends \Magento\Backend\Block\Template @@ -18,7 +19,7 @@ class Wishlist extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'report/wishlist.phtml'; + protected $_template = 'Magento_Reports::report/wishlist.phtml'; /** * Reports wishlist collection factory @@ -44,7 +45,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _beforeToHtml() { diff --git a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php index 04a402c9ac41e..12959f083d376 100644 --- a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php +++ b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php @@ -8,6 +8,7 @@ /** * Reports Recently Viewed Products Widget * + * @deprecated * @author Magento Core Team <core@magentocommerce.com> */ class Item extends \Magento\Catalog\Block\Product\AbstractProduct implements \Magento\Widget\Block\BlockInterface diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php index 3dbced45e0a69..b1f5628a0b7a3 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php @@ -9,8 +9,10 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\Reports\Controller\Adminhtml\Report; +use Magento\Backend\Helper\Data as BackendHelper; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** @@ -41,22 +43,30 @@ abstract class AbstractReport extends \Magento\Backend\App\Action */ protected $timezone; + /** + * @var BackendHelper + */ + private $backendHelper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param TimezoneInterface $timezone + * @param BackendHelper|null $backendHelperData */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - TimezoneInterface $timezone + TimezoneInterface $timezone, + BackendHelper $backendHelperData = null ) { parent::__construct($context); $this->_fileFactory = $fileFactory; $this->_dateFilter = $dateFilter; $this->timezone = $timezone; + $this->backendHelper = $backendHelperData ?: $this->_objectManager->get(BackendHelper::class); } /** @@ -103,25 +113,7 @@ public function _initReportAction($blocks) $blocks = [$blocks]; } - $requestData = $this->_objectManager->get( - \Magento\Backend\Helper\Data::class - )->prepareFilterString( - $this->getRequest()->getParam('filter') - ); - $inputFilter = new \Zend_Filter_Input( - ['from' => $this->_dateFilter, 'to' => $this->_dateFilter], - [], - $requestData - ); - $requestData = $inputFilter->getUnescaped(); - $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); - $params = new \Magento\Framework\DataObject(); - - foreach ($requestData as $key => $value) { - if (!empty($value)) { - $params->setData($key, $value); - } - } + $params = $this->initFilterData(); foreach ($blocks as $block) { if ($block) { @@ -147,7 +139,7 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) ->loadSelf(); $updatedAt = 'undefined'; if ($flag->hasData()) { - $updatedAt = $this->timezone->formatDate( + $updatedAt = $this->timezone->formatDate( $flag->getLastUpdate(), \IntlDateFormatter::MEDIUM, true @@ -155,17 +147,51 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) } $refreshStatsLink = $this->getUrl('reports/report_statistics'); - $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent', ['code' => $refreshCode]); + $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent'); $this->messageManager->addNotice( __( 'Last updated: %1. To refresh last day\'s <a href="%2">statistics</a>, ' . - 'click <a href="%3">here</a>.', + 'click <a href="#2" data-post="%3">here</a>.', $updatedAt, $refreshStatsLink, - $directRefreshLink + str_replace( + '"', + '"', + json_encode(['action' => $directRefreshLink, 'data' => ['code' => $refreshCode]]) + ) ) ); return $this; } + + /** + * Init filter data + * + * @return \Magento\Framework\DataObject + */ + private function initFilterData(): \Magento\Framework\DataObject + { + $requestData = $this->backendHelper + ->prepareFilterString( + $this->getRequest()->getParam('filter') + ); + + $filterRules = ['from' => $this->_dateFilter, 'to' => $this->_dateFilter]; + $inputFilter = new \Zend_Filter_Input($filterRules, [], $requestData); + + $requestData = $inputFilter->getUnescaped(); + $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); + $requestData['group'] = $this->getRequest()->getParam('group'); + $requestData['website'] = $this->getRequest()->getParam('website'); + + $params = new \Magento\Framework\DataObject(); + + foreach ($requestData as $key => $value) { + if (!empty($value)) { + $params->setData($key, $value); + } + } + return $params; + } } diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales.php index d77f30a004651..c77ff5c26b04e 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales.php @@ -56,9 +56,6 @@ protected function _isAllowed() case 'coupons': return $this->_authorization->isAllowed('Magento_Reports::coupons'); break; - case 'shipping': - return $this->_authorization->isAllowed('Magento_Reports::shipping'); - break; case 'bestsellers': return $this->_authorization->isAllowed('Magento_Reports::bestsellers'); break; diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php index 1f0f6e8e40535..957b1160d0281 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php @@ -6,15 +6,22 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; +use Magento\Framework\Exception\NotFoundException; + class RefreshRecent extends \Magento\Reports\Controller\Adminhtml\Report\Statistics { /** * Refresh statistics for last 25 hours * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + try { $collectionsNames = $this->_getCollectionNames(); /** @var \DateTime $currentDate */ diff --git a/app/code/Magento/Reports/Helper/Data.php b/app/code/Magento/Reports/Helper/Data.php index 411c7dde14c9d..5b5799807fc1b 100644 --- a/app/code/Magento/Reports/Helper/Data.php +++ b/app/code/Magento/Reports/Helper/Data.php @@ -63,22 +63,27 @@ public function getIntervals($from, $to, $period = self::REPORT_PERIOD_TYPE_DAY) $dateStart = new \DateTime($from); $dateEnd = new \DateTime($to); + $dateFormat = 'Y-m-d'; + $dateInterval = new \DateInterval('P1D'); + switch ($period) { + case self::REPORT_PERIOD_TYPE_MONTH: + $dateFormat = 'Y-m'; + $dateInterval = new \DateInterval('P1M'); + break; + case self::REPORT_PERIOD_TYPE_YEAR: + $dateFormat = 'Y'; + $dateInterval = new \DateInterval('P1Y'); + break; + } while ($dateStart->diff($dateEnd)->invert == 0) { - switch ($period) { - case self::REPORT_PERIOD_TYPE_DAY: - $intervals[] = $dateStart->format('Y-m-d'); - $dateStart->add(new \DateInterval('P1D')); - break; - case self::REPORT_PERIOD_TYPE_MONTH: - $intervals[] = $dateStart->format('Y-m'); - $dateStart->add(new \DateInterval('P1M')); - break; - case self::REPORT_PERIOD_TYPE_YEAR: - $intervals[] = $dateStart->format('Y'); - $dateStart->add(new \DateInterval('P1Y')); - break; - } + $intervals[] = $dateStart->format($dateFormat); + $dateStart->add($dateInterval); + } + + if (!in_array($dateEnd->format($dateFormat), $intervals)) { + $intervals[] = $dateEnd->format($dateFormat); } + return $intervals; } diff --git a/app/code/Magento/Reports/Model/ReportStatus.php b/app/code/Magento/Reports/Model/ReportStatus.php new file mode 100644 index 0000000000000..ec0c32d9af1ec --- /dev/null +++ b/app/code/Magento/Reports/Model/ReportStatus.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; + +/** + * Is report for specified event type is enabled in system configuration + */ +class ReportStatus +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Is report for specified event type is enabled in system configuration + * + * @param string $reportEventType + * @return bool + * @throws InputException + */ + public function isReportEnabled(string $reportEventType): bool + { + return (bool)$this->scopeConfig->getValue('reports/options/enabled') + && (bool)$this->scopeConfig->getValue($this->getConfigPathByEventType($reportEventType)); + } + + /** + * @param string $reportEventType + * @return string + * @throws InputException + */ + private function getConfigPathByEventType(string $reportEventType): string + { + $typeToPathMap = [ + Event::EVENT_PRODUCT_VIEW => 'reports/options/product_view_enabled', + Event::EVENT_PRODUCT_SEND => 'reports/options/product_send_enabled', + Event::EVENT_PRODUCT_COMPARE => 'reports/options/product_compare_enabled', + Event::EVENT_PRODUCT_TO_CART => 'reports/options/product_to_cart_enabled', + Event::EVENT_PRODUCT_TO_WISHLIST => 'reports/options/product_to_wishlist_enabled', + Event::EVENT_WISHLIST_SHARE => 'reports/options/wishlist_share_enabled', + ]; + + if (!isset($typeToPathMap[$reportEventType])) { + throw new InputException( + __('System configuration is not found for report event type "%1"', $reportEventType) + ); + } + + return $typeToPathMap[$reportEventType]; + } +} diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 82ebc74a0468e..d89a118bff94b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Reports\Model\ResourceModel\Order; use Magento\Framework\DB\Select; @@ -81,7 +80,7 @@ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\Order\Config $orderConfig * @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -446,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; @@ -770,11 +769,12 @@ public function addOrdersCount() */ public function addRevenueToSelect($convertCurrency = false) { - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( !$convertCurrency, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns(['revenue' => $expr]); @@ -792,11 +792,12 @@ public function addSumAvgTotals($storeId = 0) /** * calculate average and total amount */ - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( $storeId, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns( @@ -809,13 +810,15 @@ public function addSumAvgTotals($storeId = 0) } /** - * Get SQL expression for totals + * Get SQL expression for totals. * * @param int $storeId * @param string $baseSubtotalRefunded * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string + * @deprecated + * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( $storeId, @@ -825,11 +828,41 @@ protected function getTotalsExpression( ) { $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' - : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) - %3$s) ' - . ' * main_table.base_to_global_rate)'; + : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' + . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } + /** + * Get SQL expression for totals with discount refunded. + * + * @param int $storeId + * @param string $baseSubtotalRefunded + * @param string $baseSubtotalCanceled + * @param string $baseDiscountRefunded + * @param string $baseDiscountCanceled + * @return string + */ + private function getTotalsExpressionWithDiscountRefunded( + $storeId, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ) { + $template = ($storeId != 0) + ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))' + : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) ' + . ' * main_table.base_to_global_rate)'; + return sprintf( + $template, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ); + } + /** * Sort order by total amount * diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index 1985db0b90e2a..2009cd3ff9d92 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ +namespace Magento\Reports\Model\ResourceModel\Product\Downloads; + /** * Product Downloads Report collection * * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\Reports\Model\ResourceModel\Product\Downloads; - -/** + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -97,4 +97,14 @@ public function addFieldToFilter($field, $condition = null) } return $this; } + + /** + * @inheritDoc + */ + public function getSelectCountSql() + { + $countSelect = parent::getSelectCountSql(); + $countSelect->reset(\Zend\Db\Sql\Select::GROUP); + return $countSelect; + } } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Sold/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Sold/Collection.php index 61dc77d188438..35a14e09e314f 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Sold/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Sold/Collection.php @@ -14,6 +14,8 @@ use Magento\Framework\DB\Select; /** + * Data collection. + * * @SuppressWarnings(PHPMD.DepthOfInheritance) * @api * @since 100.0.2 @@ -21,7 +23,7 @@ class Collection extends \Magento\Reports\Model\ResourceModel\Order\Collection { /** - * Set Date range to collection + * Set Date range to collection. * * @param int $from * @param int $to @@ -79,6 +81,10 @@ public function addOrderedQty($from = '', $to = '') )->having( 'order_items.qty_ordered > ?', 0 + )->columns( + 'SUM(order_items.qty_ordered) as ordered_qty' + )->group( + 'order_items.product_id' ); return $this; } @@ -116,6 +122,8 @@ public function setOrder($attribute, $dir = self::SORT_ORDER_DESC) } /** + * @inheritdoc + * * @return Select * @since 100.2.0 */ diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php index 671acc9701012..7b3e165687bac 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php @@ -5,7 +5,11 @@ */ namespace Magento\Reports\Model\ResourceModel\Quote; +use Magento\Store\Model\Store; + /** + * Collection of abandoned quotes with reports join. + * * @api * @since 100.0.2 */ @@ -48,6 +52,24 @@ public function __construct( $this->customerResource = $customerResource; } + /** + * Filter collections by stores. + * + * @param array $storeIds + * @param bool $withAdmin + * @return $this + */ + public function addStoreFilter(array $storeIds, bool $withAdmin = true) + { + if ($withAdmin) { + $storeIds[] = Store::DEFAULT_STORE_ID; + } + + $this->addFieldToFilter('store_id', ['in' => $storeIds]); + + return $this; + } + /** * Prepare for abandoned report * diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php index f3f14ef8c3543..d219aefe81d45 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php @@ -220,8 +220,10 @@ protected function _afterLoad() $orderData = $this->getOrdersData($productIds); foreach ($items as $item) { $item->setId($item->getProductId()); - $item->setPrice($productData[$item->getProductId()]['price'] * $item->getBaseToGlobalRate()); - $item->setName($productData[$item->getProductId()]['name']); + if (isset($productData[$item->getProductId()])) { + $item->setPrice($productData[$item->getProductId()]['price'] * $item->getBaseToGlobalRate()); + $item->setName($productData[$item->getProductId()]['name']); + } $item->setOrders(0); if (isset($orderData[$item->getProductId()])) { $item->setOrders($orderData[$item->getProductId()]['orders']); diff --git a/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php index d0fed6e1a0654..65ab8b78774e4 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php @@ -20,14 +20,14 @@ class Collection extends \Magento\Framework\Data\Collection /** * From value * - * @var \DateTime + * @var \DateTimeInterface */ protected $_from; /** * To value * - * @var \DateTime + * @var \DateTimeInterface */ protected $_to; @@ -121,8 +121,8 @@ public function setPeriod($period) */ public function setInterval(\DateTimeInterface $fromDate, \DateTimeInterface $toDate) { - $this->_from = new \DateTime($fromDate->format('Y-m-d'), $fromDate->getTimezone()); - $this->_to = new \DateTime($toDate->format('Y-m-d'), $toDate->getTimezone()); + $this->_from = $fromDate; + $this->_to = $toDate; return $this; } @@ -139,8 +139,8 @@ protected function _getIntervals() if (!$this->_from && !$this->_to) { return $this->_intervals; } - $dateStart = $this->_from; - $dateEnd = $this->_to; + $dateStart = new \DateTime($this->_from->format('Y-m-d'), $this->_from->getTimezone()); + $dateEnd = new \DateTime($this->_to->format('Y-m-d'), $this->_to->getTimezone()); $firstInterval = true; while ($dateStart <= $dateEnd) { @@ -175,11 +175,7 @@ protected function _getIntervals() protected function _getDayInterval(\DateTime $dateStart) { $interval = [ - 'period' => $this->_localeDate->formatDateTime( - $dateStart, - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE - ), + 'period' => $this->_localeDate->formatDate($dateStart, \IntlDateFormatter::SHORT), 'start' => $this->_localeDate->convertConfigTimeToUtc($dateStart->format('Y-m-d 00:00:00')), 'end' => $this->_localeDate->convertConfigTimeToUtc($dateStart->format('Y-m-d 23:59:59')), ]; @@ -215,8 +211,11 @@ protected function _getMonthInterval(\DateTime $dateStart, \DateTime $dateEnd, $ ) ); } else { + // Transform the start date to UTC whilst preserving the date. This is required as getTimestamp() + // is in UTC which may result in a different month from the original start date due to time zones. + $dateStartUtc = (new \DateTime())->createFromFormat('d-m-Y g:i:s', $dateStart->format('d-m-Y 00:00:00')); $interval['end'] = $this->_localeDate->convertConfigTimeToUtc( - $dateStart->format('Y-m-' . date('t', $dateStart->getTimestamp()) . ' 23:59:59') + $dateStart->format('Y-m-' . date('t', $dateStartUtc->getTimestamp()) . ' 23:59:59') ); } diff --git a/app/code/Magento/Reports/Observer/CatalogProductCompareAddProductObserver.php b/app/code/Magento/Reports/Observer/CatalogProductCompareAddProductObserver.php index c7da558180a85..0bdc3ed3f124a 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductCompareAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductCompareAddProductObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -32,22 +33,30 @@ class CatalogProductCompareAddProductObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Visitor $customerVisitor * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Visitor $customerVisitor, - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->_productCompFactory = $productCompFactory; $this->_customerSession = $customerSession; $this->_customerVisitor = $customerVisitor; $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** @@ -60,6 +69,9 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_PRODUCT_COMPARE)) { + return; + } $productId = $observer->getEvent()->getProduct()->getId(); $viewData = ['product_id' => $productId]; if ($this->_customerSession->isLoggedIn()) { @@ -69,6 +81,6 @@ public function execute(\Magento\Framework\Event\Observer $observer) } $this->_productCompFactory->create()->setData($viewData)->save()->calculate(); - $this->eventSaver->save(\Magento\Reports\Model\Event::EVENT_PRODUCT_COMPARE, $productId); + $this->eventSaver->save(Event::EVENT_PRODUCT_COMPARE, $productId); } } diff --git a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php index daa52d9daa22d..bbe431aeeef9c 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php @@ -17,13 +17,20 @@ class CatalogProductCompareClearObserver implements ObserverInterface */ protected $_productCompFactory; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory */ public function __construct( - \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory + \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->_productCompFactory = $productCompFactory; + $this->reportStatus = $reportStatus; } /** @@ -37,8 +44,10 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { - $this->_productCompFactory->create()->calculate(); + if (!$this->reportStatus->isReportEnabled(\Magento\Reports\Model\Event::EVENT_PRODUCT_VIEW)) { + return; + } - return $this; + $this->_productCompFactory->create()->calculate(); } } diff --git a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php index 4c4476ed7c673..7797dda8eabfb 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -37,6 +38,11 @@ class CatalogProductViewObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Reports\Model\Product\Index\ViewedFactory $productIndxFactory @@ -49,13 +55,15 @@ public function __construct( \Magento\Reports\Model\Product\Index\ViewedFactory $productIndxFactory, \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Visitor $customerVisitor, - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->_storeManager = $storeManager; $this->_productIndxFactory = $productIndxFactory; $this->_customerSession = $customerSession; $this->_customerVisitor = $customerVisitor; $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** @@ -66,6 +74,10 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_PRODUCT_VIEW)) { + return; + } + $productId = $observer->getEvent()->getProduct()->getId(); $viewData['product_id'] = $productId; @@ -78,6 +90,6 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->_productIndxFactory->create()->setData($viewData)->save()->calculate(); - $this->eventSaver->save(\Magento\Reports\Model\Event::EVENT_PRODUCT_VIEW, $productId); + $this->eventSaver->save(Event::EVENT_PRODUCT_VIEW, $productId); } } diff --git a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php index 1e44b3c4fbec2..718cc02349ce5 100644 --- a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -17,29 +18,38 @@ class CheckoutCartAddProductObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param EventSaver $eventSaver */ public function __construct( - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** * Add product to shopping cart action * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_PRODUCT_TO_CART)) { + return; + } + $quoteItem = $observer->getEvent()->getItem(); if (!$quoteItem->getId() && !$quoteItem->getParentItem()) { $productId = $quoteItem->getProductId(); - $this->eventSaver->save(\Magento\Reports\Model\Event::EVENT_PRODUCT_TO_CART, $productId); + $this->eventSaver->save(Event::EVENT_PRODUCT_TO_CART, $productId); } - - return $this; } } diff --git a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php index b8ae653043362..0583b45d2d05f 100644 --- a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php +++ b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -17,13 +18,20 @@ class SendfriendProductObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param EventSaver $eventSaver */ public function __construct( - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** @@ -34,8 +42,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_PRODUCT_SEND)) { + return; + } + $this->eventSaver->save( - \Magento\Reports\Model\Event::EVENT_PRODUCT_SEND, + Event::EVENT_PRODUCT_SEND, $observer->getEvent()->getProduct()->getId() ); } diff --git a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php index 1bbd4a1666ba6..3fd868abbd968 100644 --- a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -17,13 +18,20 @@ class WishlistAddProductObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param EventSaver $eventSaver */ public function __construct( - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** @@ -34,8 +42,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_PRODUCT_TO_WISHLIST)) { + return; + } + $this->eventSaver->save( - \Magento\Reports\Model\Event::EVENT_PRODUCT_TO_WISHLIST, + Event::EVENT_PRODUCT_TO_WISHLIST, $observer->getEvent()->getProduct()->getId() ); } diff --git a/app/code/Magento/Reports/Observer/WishlistShareObserver.php b/app/code/Magento/Reports/Observer/WishlistShareObserver.php index 832429cf6a3a0..2c4926ac12a16 100644 --- a/app/code/Magento/Reports/Observer/WishlistShareObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistShareObserver.php @@ -6,6 +6,7 @@ namespace Magento\Reports\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Reports\Model\Event; /** * Reports Event observer model @@ -17,13 +18,20 @@ class WishlistShareObserver implements ObserverInterface */ protected $eventSaver; + /** + * @var \Magento\Reports\Model\ReportStatus + */ + private $reportStatus; + /** * @param EventSaver $eventSaver */ public function __construct( - EventSaver $eventSaver + EventSaver $eventSaver, + \Magento\Reports\Model\ReportStatus $reportStatus ) { $this->eventSaver = $eventSaver; + $this->reportStatus = $reportStatus; } /** @@ -34,8 +42,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + if (!$this->reportStatus->isReportEnabled(Event::EVENT_WISHLIST_SHARE)) { + return; + } + $this->eventSaver->save( - \Magento\Reports\Model\Event::EVENT_WISHLIST_SHARE, + Event::EVENT_WISHLIST_SHARE, $observer->getEvent()->getWishlist()->getId() ); } diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.xml new file mode 100644 index 0000000000000..90ea72ce9dda9 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminReviewOrderActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{AdminMenuSection.reports}}" stepKey="openReports"/> + <waitForPageLoad time="5" stepKey="waitForReports"/> + <click selector="{{AdminMenuSection.ordered}}" stepKey="openOrdered"/> + <waitForPageLoad time="5" stepKey="waitForOrdersPage"/> + <click selector="{{AdminOrderedProductsSection.refresh}}" stepKey="refresh"/> + <scrollTo selector="{{AdminOrderedProductsSection.total}}" stepKey="scrollTo"/> + <see userInput="{{productName}}" stepKey="seeOrderedProduct"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml new file mode 100644 index 0000000000000..8ddfd4092645f --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="GenerateOrderReportActionGroup"> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + </arguments> + <click selector="{{AdminOrderReportMainActionsSection.refreshStatistics}}" stepKey="refreshStatistics"/> + <fillField selector="{{AdminOrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{AdminOrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{AdminOrderReportFilterSection.orderStatus}}" userInput="Any" stepKey="selectAnyOption"/> + <click selector="{{AdminOrderReportMainActionsSection.showReport}}" stepKey="showReport"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/LICENSE.txt b/app/code/Magento/Reports/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Reports/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 <insert your license name here>" 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/app/code/Magento/Reports/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Reports/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Reports/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 <insert your license name here>" 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/app/code/Magento/Reports/Test/Mftf/Page/AdminOrderedProductsPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrderedProductsPage.xml new file mode 100644 index 0000000000000..fc9784498cbb3 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrderedProductsPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderedProductsPage" url="admin/reports/report_product/sold" module="Magento_Reports" area="admin"> + <section name="AdminOrderedProductsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml new file mode 100644 index 0000000000000..f0b51f6e39357 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrdersReportPage" url="reports/report_sales/sales/" area="admin" module="Reports"> + <section name="AdminOrderReportFilterSection"/> + <section name="AdminOrderReportMainActionsSection"/> + <section name="AdminOrderReportTableSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml new file mode 100644 index 0000000000000..ff90be1ced389 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminReportSalesTaxPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminReportSalesTaxPage" url="reports/report_sales/tax/" module="Magento_Reports" area="admin"> + <section name="AdminReportMainActionSection"/> + <section name="AdminReportTaxSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/README.md b/app/code/Magento/Reports/Test/Mftf/README.md new file mode 100644 index 0000000000000..3617914a36783 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Reports Functional Tests + +The Functional Test Module for **Magento Reports** module. diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 0000000000000..b8f0f0750bbf4 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMenuSection"> + <element name="reports" type="button" selector="#menu-magento-reports-report"/> + <element name="ordered" type="button" selector=".item-report-products-sold"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml new file mode 100644 index 0000000000000..33527e1262020 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderReportFilterSection"> + <element name="dateFrom" type="input" selector="#sales_report_from"/> + <element name="dateTo" type="input" selector="#sales_report_to"/> + <element name="orderStatus" type="select" selector="#sales_report_show_order_statuses"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml new file mode 100644 index 0000000000000..3b05c19cd59e9 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderReportMainActionsSection"> + <element name="showReport" type="button" timeout="30" selector="#filter_form_submit"/> + <element name="refreshStatistics" type="text" timeout="30" selector="//a[contains(text(), 'here')]"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml new file mode 100644 index 0000000000000..e920d28bcf386 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderReportTableSection"> + <element name="ordersCount" type="text" selector=".totals .col-orders.col-orders_count.col-number"/> + <element name="canceledOrders" type="text" selector=".totals .col-canceled.col-total_canceled_amount.a-right"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderedProductsSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderedProductsSection.xml new file mode 100644 index 0000000000000..dd4e78f4ee3f9 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderedProductsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderedProductsSection"> + <element name="refresh" type="button" selector="button[title='Refresh']" timeout="30"/> + <element name="total" type="text" selector="#gridProductsSold_table tfoot th.col-period"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml new file mode 100644 index 0000000000000..207e508b6b020 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportMainActionSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminReportMainActionSection"> + <element name="showReport" type="button" selector="#filter_form_submit" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml new file mode 100644 index 0000000000000..a9583d8772688 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminReportTaxSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminReportTaxSection"> + <element name="refreshStatistics" type="button" selector="//*[@id='messages']//a[text()='here']" timeout="30"/> + <element name="dataPickerFrom" type="button" selector="#sales_report_from + button.ui-datepicker-trigger"/> + <element name="dataPickerTo" type="button" selector="#sales_report_to + button.ui-datepicker-trigger"/> + <element name="goTodayButton" type="button" selector="#ui-datepicker-div [data-handler='today']"/> + <element name="closeButton" type="button" selector="#ui-datepicker-div [data-handler='hide']"/> + <element name="row" type="button" selector="//td[contains(text(),'{{var1}}')]/..//td[last()]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml new file mode 100644 index 0000000000000..073b4ce6fb90c --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminTaxReportGridTest.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTaxReportGridTest"> + <annotations> + <stories value="MAGETWO-86649: Reports / Sales / Tax report show incorrect amount"/> + <title value="Checking Tax Report grid"/> + <description value="Checking Tax Report grid with tax rates and same zip code"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97018"/> + <group value="tax_report"/> + <group value="reports"/> + </annotations> + <before> + <!--Log in as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Create Product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Create product Tax Class and Tax Rates--> + <createData entity="ProductTaxClass" stepKey="createProductTaxClass"/> + <createData entity="TexasTaxRate" stepKey="createStateTaxRate"/> + <createData entity="AustinTaxRate" stepKey="createCityTaxRate"/> + <!--Get Data from product Tax Class--> + <getData entity="ProductTaxClassGetter" stepKey="getProductTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + </before> + <after> + <!--Delete Tax Rules--> + <actionGroup ref="DeleteTaxRule" stepKey="deleteCityTaxRule"> + <argument name="taxRuleName" value="CityTaxRule"/> + </actionGroup> + <actionGroup ref="DeleteTaxRule" stepKey="deleteStateTaxRule"> + <argument name="taxRuleName" value="StateTaxRule"/> + </actionGroup> + <!--Delete Customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Delete Product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <!--Delete Tax Rates and product Tax Class--> + <deleteData createDataKey="createStateTaxRate" stepKey="deleteTaxClass1"/> + <deleteData createDataKey="createCityTaxRate" stepKey="deleteTaxClass2"/> + <deleteData createDataKey="createProductTaxClass" stepKey="deleteProductTaxClass"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Adding Tax Rule with all (*) zip code--> + <actionGroup ref="AddTaxRule" stepKey="addStateTaxRule"> + <argument name="taxRuleName" value="StateTaxRule"/> + <argument name="taxRate" value="$$createStateTaxRate$$"/> + <argument name="productTaxClass" value="$$getProductTaxClass$$"/> + </actionGroup> + <!--Adding Tax Rule with zip code--> + <actionGroup ref="AddTaxRule" stepKey="addCityTaxRule"> + <argument name="taxRuleName" value="CityTaxRule"/> + <argument name="taxRate" value="$$createCityTaxRate$$"/> + <argument name="productTaxClass" value="$$getProductTaxClass$$"/> + </actionGroup> + <!-- Open Product Edit --> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="$$getProductTaxClass.class_name$$" stepKey="selectCustomTaxClass"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Create new order with existing Customer --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Select shipping--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeOrderSuccessMessage"/> + <!--Create and submit Invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Create and submit Shipment --> + <actionGroup ref="StartCreateShipmentFromOrderPage" stepKey="createShipment"/> + <actionGroup ref="SubmitShipment" stepKey="submitShipment"/> + <!--Go to Report -> Tax --> + <amOnPage url="{{AdminReportSalesTaxPage.url}}" stepKey="goToReportTax"/> + <click selector="{{AdminReportTaxSection.refreshStatistics}}" stepKey="clickRefreshStatistics"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Recent statistics have been updated." stepKey="seeReportSuccessMessage"/> + <!--Select Date From--> + <click selector="{{AdminReportTaxSection.dataPickerFrom}}" stepKey="clickOnDatePickerFrom"/> + <waitForElementVisible selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="waitForGoTodayButton1"/> + <click selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="selectToday1"/> + <click selector="{{AdminReportTaxSection.closeButton}}" stepKey="selectClose1"/> + <!--Select Date To--> + <click selector="{{AdminReportTaxSection.dataPickerTo}}" stepKey="clickOnDatePickerTo"/> + <waitForElementVisible selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="waitForGoTodayButton2"/> + <click selector="{{AdminReportTaxSection.goTodayButton}}" stepKey="selectToday2"/> + <click selector="{{AdminReportTaxSection.closeButton}}" stepKey="selectClose2"/> + <click selector="{{AdminReportMainActionSection.showReport}}" stepKey="clickOnShowReportButton"/> + <!--Assert taxes--> + <grabTextFrom selector="{{AdminReportTaxSection.row($$createStateTaxRate.code$$)}}" stepKey="grabStateTax"/> + <assertEquals stepKey="checkStateTaxForProduct"> + <expectedResult type="string">$2.00</expectedResult> + <actualResult type="variable">$grabStateTax</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminReportTaxSection.row($$createCityTaxRate.code$$)}}" stepKey="grabCityTax"/> + <assertEquals stepKey="checkCityTaxForProduct"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabCityTax</actualResult> + </assertEquals> + <!--Assert total--> + <grabTextFrom selector="{{AdminReportTaxSection.row('Subtotal')}}" stepKey="grabTotalTax"/> + <assertEquals stepKey="checkTotalTaxForProduct"> + <expectedResult type="string">$12.00</expectedResult> + <actualResult type="variable">$grabTotalTax</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml new file mode 100644 index 0000000000000..009e4b8e5f6f1 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CancelOrdersInOrderSalesReportTest"> + <annotations> + <features value="Reports"/> + <stories value="Order Sales Report"/> + <group value="reports"/> + <title value="Canceled orders in order sales report"/> + <description value="Verify canceling of orders in order sales report"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13838"/> + <useCaseId value="MAGETWO-95463"/> + </annotations> + <before> + <!-- create new product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--login to Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrderWithUserDefinedQty"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Submit Order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage"/> + <!--Create order invoice--> + <comment userInput="Admin creates invoice for order" stepKey="adminCreateInvoiceComment" /> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Ship Order--> + <comment userInput="Admin creates shipment" stepKey="adminCreatesShipmentComment"/> + <actionGroup ref="StartCreateShipmentFromOrderPage" stepKey="createShipment"/> + <actionGroup ref="SubmitShipment" stepKey="submitShipment"/> + + <!--Create new order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment1"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder1"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrderWithUserDefinedQty1"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping1"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment1"/> + <!--Submit Order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage1"/> + <!-- Cancel order --> + <actionGroup ref="cancelPendingOrder" stepKey="cancelOrder"/> + + <!-- Generate Order report --> + <amOnPage url="{{AdminOrdersReportPage.url}}" stepKey="goToAdminOrdersReportPage"/> + <!-- Get date --> + <generateDate date="+0 day" format="m/d/Y" stepKey="generateEndDate"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="generateStartDate"/> + <actionGroup ref="GenerateOrderReportActionGroup" stepKey="generateReportAfterCancelOrder"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + </actionGroup> + <waitForElement selector="{{AdminOrderReportTableSection.ordersCount}}" stepKey="waitForOrdersCount"/> + <see selector="{{AdminOrderReportTableSection.canceledOrders}}" userInput="$0.00" stepKey="seeCanceledOrderPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Sales/Coupons/GridTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Sales/Coupons/GridTest.php new file mode 100644 index 0000000000000..9e97af428de90 --- /dev/null +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Sales/Coupons/GridTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Unit\Block\Adminhtml\Sales\Coupons; + +/** + * Test for class \Magento\Reports\Block\Adminhtml\Sales\Coupons\Grid + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Reports\Block\Adminhtml\Sales\Coupons\Grid + */ + private $model; + + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var \Magento\Reports\Model\ResourceModel\Report\Collection\Factory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceFactoryMock; + + protected function setUp() + { + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMock(); + $this->resourceFactoryMock = $this + ->getMockBuilder(\Magento\Reports\Model\ResourceModel\Report\Collection\Factory::class) + ->disableOriginalConstructor() + ->getMock(); + $aggregatedColumns = [1 => 'SUM(value)']; + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManager->getObject( + \Magento\Reports\Block\Adminhtml\Sales\Coupons\Grid::class, + [ + '_storeManager' => $this->storeManagerMock, + '_aggregatedColumns' => $aggregatedColumns, + 'resourceFactory' => $this->resourceFactoryMock, + ] + ); + } + + /** + * @dataProvider getCountTotalsDataProvider + * + * @param string $reportType + * @param int $priceRuleType + * @param int $collectionSize + * @param bool $expectedCountTotals + */ + public function testGetCountTotals( + string $reportType, + int $priceRuleType, + int $collectionSize, + bool $expectedCountTotals + ) { + $filterData = new \Magento\Framework\DataObject(); + $filterData->setData('report_type', $reportType); + $filterData->setData('period_type', 'day'); + $filterData->setData('from', '2000-01-01'); + $filterData->setData('to', '2000-01-30'); + $filterData->setData('store_ids', '1'); + $filterData->setData('price_rule_type', $priceRuleType); + if ($priceRuleType) { + $filterData->setData('rules_list', ['0,1']); + } + $filterData->setData('order_statuses', 'statuses'); + $this->model->setFilterData($filterData); + + $resourceCollectionName = $this->model->getResourceCollectionName(); + $collectionMock = $this->buildBaseCollectionMock($filterData, $resourceCollectionName, $collectionSize); + + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->getMock(); + $this->storeManagerMock->method('getStores') + ->willReturn([1 => $store]); + $this->resourceFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + + $this->assertEquals($expectedCountTotals, $this->model->getCountTotals()); + } + + /** + * @return array + */ + public function getCountTotalsDataProvider(): array + { + return [ + ['created_at_shipment', 0, 0, false], + ['created_at_shipment', 0, 1, true], + ['updated_at_order', 0, 1, true], + ['updated_at_order', 1, 1, true], + ]; + } + + /** + * @param \Magento\Framework\DataObject $filterData + * @param string $resourceCollectionName + * @param int $collectionSize + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function buildBaseCollectionMock( + \Magento\Framework\DataObject $filterData, + string $resourceCollectionName, + int $collectionSize + ): \PHPUnit_Framework_MockObject_MockObject { + $collectionMock = $this->getMockBuilder($resourceCollectionName) + ->disableOriginalConstructor() + ->getMock(); + $collectionMock->expects($this->once()) + ->method('setPeriod') + ->with($filterData->getData('period_type')) + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('setDateRange') + ->with($filterData->getData('from'), $filterData->getData('to')) + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('addStoreFilter') + ->with(\explode(',', $filterData->getData('store_ids'))) + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('setAggregatedColumns') + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('isTotals') + ->with(true) + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('addOrderStatusFilter') + ->with($filterData->getData('order_statuses')) + ->willReturnSelf(); + + if ($filterData->getData('price_rule_type')) { + $collectionMock->expects($this->once()) + ->method('addRuleFilter') + ->with(\explode(',', $filterData->getData('rules_list')[0])) + ->willReturnSelf(); + } + + $collectionMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('getSize') + ->willReturn($collectionSize); + if ($collectionSize) { + $itemMock = $this->getMockBuilder(\Magento\Reports\Model\Item::class) + ->disableOriginalConstructor() + ->getMock(); + $collectionMock->expects($this->once()) + ->method('getItems') + ->willReturn([$itemMock]); + } + + return $collectionMock; + } +} diff --git a/app/code/Magento/Reports/Test/Unit/Helper/DataTest.php b/app/code/Magento/Reports/Test/Unit/Helper/DataTest.php index 25981c9d3cfcd..3aee96720c7cb 100644 --- a/app/code/Magento/Reports/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Reports/Test/Unit/Helper/DataTest.php @@ -6,34 +6,38 @@ namespace Magento\Reports\Test\Unit\Helper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Data\Collection; use Magento\Reports\Helper\Data; +use Magento\Reports\Model\Item; +use Magento\Reports\Model\ItemFactory; class DataTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Reports\Helper\Data + * @var Data */ protected $data; /** - * @var \Magento\Framework\App\Helper\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|\PHPUnit_Framework_MockObject_MockObject */ protected $contextMock; /** - * @var \Magento\Reports\Model\ItemFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $itemFactoryMock; /** - * {@inheritDoc} + * @inheritdoc */ protected function setUp() { - $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Helper\Context::class) + $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); - $this->itemFactoryMock = $this->getMockBuilder(\Magento\Reports\Model\ItemFactory::class) + $this->itemFactoryMock = $this->getMockBuilder(ItemFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); @@ -52,7 +56,7 @@ protected function setUp() * @dataProvider intervalsDataProvider * @return void */ - public function testGetIntervals($from, $to, $period, $results) + public function testGetIntervals(string $from, string $to, string $period, array $results) { $this->assertEquals($this->data->getIntervals($from, $to, $period), $results); } @@ -65,14 +69,14 @@ public function testGetIntervals($from, $to, $period, $results) * @dataProvider intervalsDataProvider * @return void */ - public function testPrepareIntervalsCollection($from, $to, $period, $results) + public function testPrepareIntervalsCollection(string $from, string $to, string $period, array $results) { - $collection = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) + $collection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->setMethods(['addItem']) ->getMock(); - $item = $this->getMockBuilder(\Magento\Reports\Model\Item::class) + $item = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods(['setPeriod', 'setIsEmpty']) ->getMock(); @@ -97,50 +101,56 @@ public function testPrepareIntervalsCollection($from, $to, $period, $results) /** * @return array */ - public function intervalsDataProvider() + public function intervalsDataProvider(): array { return [ [ 'from' => '2000-01-15 10:00:00', 'to' => '2000-01-15 11:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_DAY, - 'results' => ['2000-01-15'] + 'period' => Data::REPORT_PERIOD_TYPE_DAY, + 'results' => ['2000-01-15'], ], [ 'from' => '2000-01-15 10:00:00', 'to' => '2000-01-17 10:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_MONTH, - 'results' => ['2000-01'] + 'period' => Data::REPORT_PERIOD_TYPE_MONTH, + 'results' => ['2000-01'], ], [ 'from' => '2000-01-15 10:00:00', 'to' => '2000-02-15 10:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_YEAR, - 'results' => ['2000'] + 'period' => Data::REPORT_PERIOD_TYPE_YEAR, + 'results' => ['2000'], ], [ 'from' => '2000-01-15 10:00:00', 'to' => '2000-01-16 11:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_DAY, - 'results' => ['2000-01-15', '2000-01-16'] + 'period' => Data::REPORT_PERIOD_TYPE_DAY, + 'results' => ['2000-01-15', '2000-01-16'], ], [ 'from' => '2000-01-15 10:00:00', 'to' => '2000-02-17 10:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_MONTH, - 'results' => ['2000-01', '2000-02'] + 'period' => Data::REPORT_PERIOD_TYPE_MONTH, + 'results' => ['2000-01', '2000-02'], ], [ 'from' => '2000-01-15 10:00:00', 'to' => '2003-02-15 10:00:00', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_YEAR, - 'results' => ['2000', '2001', '2002', '2003'] + 'period' => Data::REPORT_PERIOD_TYPE_YEAR, + 'results' => ['2000', '2001', '2002', '2003'], + ], + [ + 'from' => '2000-12-31 10:00:00', + 'to' => '2001-01-01 10:00:00', + 'period' => Data::REPORT_PERIOD_TYPE_YEAR, + 'results' => ['2000', '2001'], ], [ 'from' => '', 'to' => '', - 'period' => \Magento\Reports\Helper\Data::REPORT_PERIOD_TYPE_YEAR, - 'results' => [] + 'period' => Data::REPORT_PERIOD_TYPE_YEAR, + 'results' => [], ] ]; } diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/CollectionTest.php index 51d890dd56df9..41b4745594571 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/CollectionTest.php @@ -6,49 +6,49 @@ namespace Magento\Reports\Test\Unit\Model\ResourceModel\Report; +use Magento\Framework\Data\Collection\EntityFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Reports\Model\ResourceModel\Report\Collection; +use Magento\Reports\Model\ResourceModel\Report\Collection\Factory as ReportCollectionFactory; +/** + * Class CollectionTest + * + * @covers \Magento\Reports\Model\ResourceModel\Report\Collection + */ class CollectionTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Reports\Model\ResourceModel\Report\Collection + * @var Collection */ protected $collection; /** - * @var \Magento\Framework\Data\Collection\EntityFactory|\PHPUnit_Framework_MockObject_MockObject + * @var EntityFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $entityFactoryMock; /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $timezoneMock; /** - * @var \Magento\Reports\Model\ResourceModel\Report\Collection\Factory|\PHPUnit_Framework_MockObject_MockObject + * @var ReportCollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $factoryMock; /** - * {@inheritDoc} + * @inheritDoc */ protected function setUp() { - $this->entityFactoryMock = $this->getMockBuilder(\Magento\Framework\Data\Collection\EntityFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->timezoneMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) - ->getMock(); - $this->factoryMock = $this->getMockBuilder( - \Magento\Reports\Model\ResourceModel\Report\Collection\Factory::class - )->disableOriginalConstructor() - ->getMock(); - - $this->timezoneMock - ->expects($this->any()) - ->method('formatDateTime') - ->will($this->returnCallback([$this, 'formatDateTime'])); + $this->entityFactoryMock = $this->createMock(EntityFactory::class); + $this->timezoneMock = $this->createMock(TimezoneInterface::class); + $this->factoryMock = $this->createMock(ReportCollectionFactory::class); + + $this->timezoneMock->method('formatDate') + ->will($this->returnCallback([$this, 'formatDate'])); $this->collection = new Collection( $this->entityFactoryMock, @@ -131,7 +131,7 @@ public function testGetReports($period, $fromDate, $toDate, $size) public function testLoadData() { $this->assertInstanceOf( - \Magento\Reports\Model\ResourceModel\Report\Collection::class, + Collection::class, $this->collection->loadData() ); } @@ -182,14 +182,11 @@ public function intervalsDataProvider() } /** + * @param \DateTimeInterface $dateStart * @return string */ - public function formatDateTime() + public function formatDate(\DateTimeInterface $dateStart): string { - $args = func_get_args(); - - $dateStart = $args[0]; - $formatter = new \IntlDateFormatter( "en_US", \IntlDateFormatter::SHORT, diff --git a/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductCompareAddProductObserverTest.php b/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductCompareAddProductObserverTest.php index 959f9ddc442f8..a1bb9722f6ade 100644 --- a/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductCompareAddProductObserverTest.php +++ b/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductCompareAddProductObserverTest.php @@ -50,6 +50,11 @@ class CatalogProductCompareAddProductObserverTest extends \PHPUnit\Framework\Tes */ protected $productCompModelMock; + /** + * @var \Magento\Reports\Model\ReportStatus|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportStatusMock; + /** * {@inheritDoc} */ @@ -98,13 +103,19 @@ protected function setUp() ->setMethods(['save']) ->getMock(); + $this->reportStatusMock = $this->getMockBuilder(\Magento\Reports\Model\ReportStatus::class) + ->disableOriginalConstructor() + ->setMethods(['isReportEnabled']) + ->getMock(); + $this->observer = $objectManager->getObject( \Magento\Reports\Observer\CatalogProductCompareAddProductObserver::class, [ 'productCompFactory' => $this->productCompFactoryMock, 'customerSession' => $this->customerSessionMock, 'customerVisitor' => $this->customerVisitorMock, - 'eventSaver' => $this->eventSaverMock + 'eventSaver' => $this->eventSaverMock, + 'reportStatus' => $this->reportStatusMock ] ); } @@ -127,6 +138,7 @@ public function testCatalogProductCompareAddProduct($isLoggedIn, $userKey, $user ]; $observerMock = $this->getObserverMock($productId); + $this->reportStatusMock->expects($this->once())->method('isReportEnabled')->willReturn(true); $this->customerSessionMock->expects($this->any())->method('isLoggedIn')->willReturn($isLoggedIn); $this->customerSessionMock->expects($this->any())->method('getCustomerId')->willReturn($customerId); diff --git a/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductViewObserverTest.php b/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductViewObserverTest.php index d881778e3a86a..0a43dde0b8253 100644 --- a/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductViewObserverTest.php +++ b/app/code/Magento/Reports/Test/Unit/Observer/CatalogProductViewObserverTest.php @@ -60,6 +60,11 @@ class CatalogProductViewObserverTest extends \PHPUnit\Framework\TestCase */ protected $productIndexFactoryMock; + /** + * @var \Magento\Reports\Model\ReportStatus|\PHPUnit_Framework_MockObject_MockObject + */ + private $reportStatusMock; + /** * {@inheritDoc} */ @@ -127,6 +132,11 @@ protected function setUp() ->setMethods(['save']) ->getMock(); + $this->reportStatusMock = $this->getMockBuilder(\Magento\Reports\Model\ReportStatus::class) + ->disableOriginalConstructor() + ->setMethods(['isReportEnabled']) + ->getMock(); + $this->observer = $objectManager->getObject( \Magento\Reports\Observer\CatalogProductViewObserver::class, [ @@ -134,7 +144,8 @@ protected function setUp() 'productIndxFactory' => $this->productIndexFactoryMock, 'customerSession' => $this->customerSessionMock, 'customerVisitor' => $this->customerVisitorMock, - 'eventSaver' => $this->eventSaverMock + 'eventSaver' => $this->eventSaverMock, + 'reportStatus' => $this->reportStatusMock ] ); } @@ -161,6 +172,7 @@ public function testCatalogProductViewCustomer() 'store_id' => $storeId, ]; + $this->reportStatusMock->expects($this->once())->method('isReportEnabled')->willReturn(true); $this->storeMock->expects($this->any())->method('getId')->willReturn($storeId); $this->customerSessionMock->expects($this->any())->method('isLoggedIn')->willReturn(true); @@ -197,6 +209,7 @@ public function testCatalogProductViewVisitor() 'store_id' => $storeId, ]; + $this->reportStatusMock->expects($this->once())->method('isReportEnabled')->willReturn(true); $this->storeMock->expects($this->any())->method('getId')->willReturn($storeId); $this->customerSessionMock->expects($this->any())->method('isLoggedIn')->willReturn(false); diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json index 45514175aa9f1..258bdf59cf222 100644 --- a/app/code/Magento/Reports/composer.json +++ b/app/code/Magento/Reports/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-reports", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", @@ -22,7 +22,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Reports/etc/adminhtml/system.xml b/app/code/Magento/Reports/etc/adminhtml/system.xml index a16dfb4c68aee..5a5524d41d473 100644 --- a/app/code/Magento/Reports/etc/adminhtml/system.xml +++ b/app/code/Magento/Reports/etc/adminhtml/system.xml @@ -39,6 +39,62 @@ <comment>Select day of the month.</comment> </field> </group> + <group id="options" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>General Options</label> + <field id="enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable Reports</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>If disabled, all report events will be disabled</comment> + </field> + <field id="product_view_enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Product View" Report</label> + <comment>If enabled, will collect statistic of viewed product pages</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="product_send_enabled" translate="label comment" type="select" sortOrder="2" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Send Product Link To Friend" Report</label> + <comment>If enabled, will collect statistic of product links sent to friend</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="product_compare_enabled" translate="label comment" type="select" sortOrder="3" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Add Product To Compare List" Report</label> + <comment>If enabled, will collect statistic of products added to Compare List</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="product_to_cart_enabled" translate="label comment" type="select" sortOrder="4" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Product Added To Cart" Report</label> + <comment>If enabled, will collect statistic of products added to Cart</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="product_to_wishlist_enabled" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Product Added To WishList" Report</label> + <comment>If enabled, will collect statistic of products added to WishList</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="wishlist_share_enabled" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable "Share WishList" Report</label> + <comment>If enabled, will collect statistic of shared WishLists</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + </group> </section> </system> </config> diff --git a/app/code/Magento/Reports/etc/config.xml b/app/code/Magento/Reports/etc/config.xml index ecf9569ad1979..ffd2299eb6884 100644 --- a/app/code/Magento/Reports/etc/config.xml +++ b/app/code/Magento/Reports/etc/config.xml @@ -19,6 +19,15 @@ <ytd_start>1,1</ytd_start> <mtd_start>1</mtd_start> </dashboard> + <options> + <enabled>1</enabled> + <product_view_enabled>1</product_view_enabled> + <product_send_enabled>1</product_send_enabled> + <product_compare_enabled>1</product_compare_enabled> + <product_to_cart_enabled>1</product_to_cart_enabled> + <product_to_wishlist_enabled>1</product_to_wishlist_enabled> + <wishlist_share_enabled>1</wishlist_share_enabled> + </options> </reports> </default> </config> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml index cb267ce29dd34..81453a5a17ad2 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml @@ -3,59 +3,55 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Reports\Block\Adminhtml\Grid */ -$numColumns = sizeof($block->getColumns()); ?> -<?php if ($block->getCollection()): ?> - <?php if ($block->canDisplayContainer()): ?> - <div id="<?= /* @escapeNotVerified */ $block->getId() ?>"> - <?php else: ?> +<?php if ($block->getCollection()) : ?> + <?php if ($block->canDisplayContainer()) : ?> + <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> + <?php else : ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> - <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()): ?> + <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()) : ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> <div class="admin__data-grid-header-row"> - <?php if ($block->getDateFilterVisibility()): ?> - <div class="admin__filter-actions" data-role="filter-form" id="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_range') ?>"> + <?php if ($block->getDateFilterVisibility()) : ?> + <div class="admin__filter-actions" data-role="filter-form" id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_range')) ?>"> <span class="field-row"> - <label for="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from') ?>" + <label for="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" class="admin__control-support-text"> - <span><?= /* @escapeNotVerified */ __('From') ?>:</span> + <span><?= $block->escapeHtml(__('From')) ?>:</span> </label> <input class="input-text no-changes required-entry admin__control-text" type="text" - id="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from') ?>" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" name="report_from" - value="<?= /* @escapeNotVerified */ $block->getFilter('report_from') ?>"> - <span id="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from_advice') ?>"></span> + value="<?= $block->escapeHtmlAttr($block->getFilter('report_from')) ?>"> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from_advice')) ?>"></span> </span> <span class="field-row"> - <label for="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to') ?>" + <label for="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to')) ?>" class="admin__control-support-text"> - <span><?= /* @escapeNotVerified */ __('To') ?>:</span> + <span><?= $block->escapeHtml(__('To')) ?>:</span> </label> <input class="input-text no-changes required-entry admin__control-text" type="text" - id="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to') ?>" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to')) ?>" name="report_to" - value="<?= /* @escapeNotVerified */ $block->getFilter('report_to') ?>"/> - <span id="<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to_advice') ?>"></span> + value="<?= $block->escapeHtmlAttr($block->getFilter('report_to')) ?>"/> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to_advice')) ?>"></span> </span> <span class="field-row admin__control-filter"> - <label for="<?= /* @escapeNotVerified */ $block->getSuffixId('report_period') ?>" + <label for="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" class="admin__control-support-text"> - <span><?= /* @escapeNotVerified */ __('Show By') ?>:</span> + <span><?= $block->escapeHtml(__('Show By')) ?>:</span> </label> - <select name="report_period" id="<?= /* @escapeNotVerified */ $block->getSuffixId('report_period') ?>" class="admin__control-select"> - <?php foreach ($block->getPeriods() as $_value => $_label): ?> - <option value="<?= /* @escapeNotVerified */ $_value ?>" <?php if ($block->getFilter('report_period') == $_value): ?> selected<?php endif; ?>><?= /* @escapeNotVerified */ $_label ?></option> + <select name="report_period" id="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" class="admin__control-select"> + <?php foreach ($block->getPeriods() as $_value => $_label) : ?> + <option value="<?= $block->escapeHtmlAttr($_value) ?>" <?php if ($block->getFilter('report_period') == $_value) : ?> selected<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> <?php endforeach; ?> </select> <?= $block->getRefreshButtonHtml() ?> @@ -66,33 +62,33 @@ $numColumns = sizeof($block->getColumns()); "mage/calendar" ], function($){ - $("#<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_range') ?>").dateRange({ - dateFormat:"<?= /* @escapeNotVerified */ $block->getDateFormat() ?>", - buttonText:"<?= /* @escapeNotVerified */ __('Select Date') ?>", + $("#<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_range'))) ?>").dateRange({ + dateFormat:"<?= $block->escapeJs($block->escapeHtml($block->getDateFormat())) ?>", + buttonText:"<?= $block->escapeJs($block->escapeHtml(__('Select Date'))) ?>", from:{ - id:"<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from') ?>" + id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>" }, to:{ - id:"<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to') ?>" + id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>" } }); }); </script> </div> <?php endif; ?> - <?php if ($block->getChildBlock('grid.export')): ?> + <?php if ($block->getChildBlock('grid.export')) : ?> <?= $block->getChildHtml('grid.export') ?> <?php endif; ?> </div> </div> <?php endif; ?> <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> - <table class="data-grid" id="<?= /* @escapeNotVerified */ $block->getId() ?>_table"> + <table class="data-grid" id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> <?= $block->getChildHtml('grid.columnSet') ?> </table> </div> </div> - <?php if ($block->canDisplayContainer()): ?> + <?php if ($block->canDisplayContainer()) : ?> <script> require([ "jquery", @@ -102,29 +98,31 @@ $numColumns = sizeof($block->getColumns()); ], function(jQuery){ //<![CDATA[ - <?= /* @escapeNotVerified */ $block->getJsObjectName() ?> = new varienGrid('<?= /* @escapeNotVerified */ $block->getId() ?>', '<?= /* @escapeNotVerified */ $block->getGridUrl() ?>', '<?= /* @escapeNotVerified */ $block->getVarNamePage() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameSort() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameDir() ?>', '<?= /* @escapeNotVerified */ $block->getVarNameFilter() ?>'); - <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.useAjax = '<?php if ($block->getUseAjax()): /* @escapeNotVerified */ echo $block->getUseAjax(); endif; ?>'; - <?php if ($block->getDateFilterVisibility()): ?> - <?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.doFilterCallback = validateFilterDate; - var period_date_from = $('<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from') ?>'); - var period_date_to = $('<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to') ?>'); - period_date_from.adviceContainer = $('<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from_advice') ?>'); - period_date_to.adviceContainer = $('<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to_advice') ?>'); + <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?> = new varienGrid('<?= $block->escapeJs($block->escapeHtml($block->getId())) ?>', '<?= $block->escapeJs($block->escapeUrl($block->getGridUrl())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNamePage())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameSort())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameDir())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameFilter())) ?>'); + <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.useAjax = '<?php if ($block->getUseAjax()) : + echo $block->escapeJs($block->escapeHtml($block->getUseAjax())); + endif; ?>'; + <?php if ($block->getDateFilterVisibility()) : ?> + <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.doFilterCallback = validateFilterDate; + var period_date_from = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>'); + var period_date_to = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>'); + period_date_from.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from_advice'))) ?>'); + period_date_to.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to_advice'))) ?>'); - var validateFilterDate = function() { - if (period_date_from && period_date_to) { - var valid = true; - jQuery(period_date_from).add(period_date_to).each(function() { - valid = Validation.validate(this) && valid; - }); - return valid; - } - else { - return true; + var validateFilterDate = function() { + if (period_date_from && period_date_to) { + var valid = true; + jQuery(period_date_from).add(period_date_to).each(function() { + valid = Validation.validate(this) && valid; + }); + return valid; + } + else { + return true; + } } - } <?php endif;?> - <?php if ($block->getStoreSwitcherVisibility()): ?> + <?php if ($block->getStoreSwitcherVisibility()) : ?> /* Overwrite function from switcher.phtml widget*/ switchStore = function(obj) { if (obj.options[obj.selectedIndex].getAttribute('website') == 'true') { @@ -138,9 +136,9 @@ $numColumns = sizeof($block->getColumns()); if (obj.switchParams) { storeParam += obj.switchParams; } - var formParam = new Array('<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_from') ?>', '<?= /* @escapeNotVerified */ $block->getSuffixId('period_date_to') ?>', '<?= /* @escapeNotVerified */ $block->getSuffixId('report_period') ?>'); + var formParam = new Array('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('report_period'))) ?>'); var paramURL = ''; - var switchURL = '<?= /* @escapeNotVerified */ $block->getAbsoluteGridUrl(['_current' => false]) ?>'.replace(/(store|group|website)\/\d+\//, ''); + var switchURL = '<?= $block->escapeUrl($block->getAbsoluteGridUrl(['_current' => false])) ?>'.replace(/(store|group|website)\/\d+\//, ''); for (var i = 0; i < formParam.length; i++) { if ($(formParam[i]).value && $(formParam[i]).name) { diff --git a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml index 6f93cd157a4f0..089327f4de99b 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml @@ -32,7 +32,7 @@ require([ } if (jQuery('#filter_form').valid()) { - setLocation('<?= /* @escapeNotVerified */ $block->getFilterUrl() ?>filter/'+ + setLocation('<?= $block->escapeJs($block->escapeUrl($block->getFilterUrl())) ?>filter/'+ Base64.encode(Form.serializeElements(elements))+'/' ); } diff --git a/app/code/Magento/Reports/view/adminhtml/templates/report/refresh/statistics.phtml b/app/code/Magento/Reports/view/adminhtml/templates/report/refresh/statistics.phtml index 1c448f966375a..4ce64f0bd0036 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/report/refresh/statistics.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/report/refresh/statistics.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getButtonsHtml() ?></div> <?= $block->getChildHtml('grid') ?> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/report/wishlist.phtml b/app/code/Magento/Reports/view/adminhtml/templates/report/wishlist.phtml index 16b99744c42e4..9183145a2cab1 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/report/wishlist.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/report/wishlist.phtml @@ -4,27 +4,20 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ ?> <?= $block->getChildHtml('grid') ?> <div class="switcher f-left" style="margin: 10px 10px 10px 0px; padding:15px;"> -<?php - /* @escapeNotVerified */ echo __('Customers that have Wish List: %1', $block->getCustomerWithWishlist()) -?> +<?= $block->escapeHtml(__('Customers that have Wish List: %1', $block->getCustomerWithWishlist())) ?> </div> <div class="switcher" style="float: right; margin: 10px 0px 10px 10px; padding:15px;"> - <?php - /* @escapeNotVerified */ echo __('Number of Wish Lists: %1', $block->getWishlistsCount()) - ?><br /> - <?php - /* @escapeNotVerified */ echo __('Number of items bought from a Wish List: %1', $block->getItemsBought()) - ?><br /> - <?php /* @escapeNotVerified */ echo __('Number of times Wish Lists have been shared (emailed): %1', $block->getSharedCount()) - ?><br /> - <?php /* @escapeNotVerified */ echo __('Number of Wish List referrals: %1', $block->getReferralsCount()) - ?><br /> - <?php /* @escapeNotVerified */ echo __('Number of Wish List conversions: %1', $block->getConversionsCount()) - ?><br /> + <?= $block->escapeHtml(__('Number of Wish Lists: %1', $block->getWishlistsCount())) ?><br /> + <?= $block->escapeHtml(__('Number of items bought from a Wish List: %1', $block->getItemsBought())) ?><br /> + <?= $block->escapeHtml(__('Number of times Wish Lists have been shared (emailed): %1', $block->getSharedCount())) ?><br /> + <?= $block->escapeHtml(__('Number of Wish List referrals: %1', $block->getReferralsCount())) ?><br /> + <?= $block->escapeHtml(__('Number of Wish List conversions: %1', $block->getConversionsCount())) ?><br /> </div> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/store/switcher.phtml b/app/code/Magento/Reports/view/adminhtml/templates/store/switcher.phtml index 0a1dc5dacd99d..090bda48a351c 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/store/switcher.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/store/switcher.phtml @@ -4,40 +4,40 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @see \Magento\Backend\Block\Store\Switcher */ ?> -<?php if ($block->isShow()): ?> +<?php if ($block->isShow()) : ?> <div class="field field-store-switcher"> - <label class="label" for="store_switcher"><?= /* @escapeNotVerified */ __('Show Report For:') ?></label> + <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Show Report For:')) ?></label> <div class="control"> <select id="store_switcher" class="admin__control-select" name="store_switcher" onchange="return switchStore(this);"> - <option value=""><?= /* @escapeNotVerified */ __('All Websites') ?></option> - <?php foreach ($block->getWebsiteCollection() as $_website): ?> + <option value=""><?= $block->escapeHtml(__('All Websites')) ?></option> + <?php foreach ($block->getWebsiteCollection() as $_website) : ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> - <?php if ($showWebsite == false): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> + <?php if ($showWebsite == false) : ?> <?php $showWebsite = true; ?> - <option website="true" value="<?= /* @escapeNotVerified */ $_website->getId() ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()): ?> selected<?php endif; ?>> - <?= /* @escapeNotVerified */ $_website->getName() ?> + <option website="true" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()) : ?> selected<?php endif; ?>> + <?= $block->escapeHtml($_website->getName()) ?> </option> <?php endif; ?> - <?php if ($showGroup == false): ?> + <?php if ($showGroup == false) : ?> <?php $showGroup = true; ?> - <option group="true" value="<?= /* @escapeNotVerified */ $_group->getId() ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()): ?> selected<?php endif; ?>>   <?= /* @escapeNotVerified */ $_group->getName() ?></option> + <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()) : ?> selected<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> <?php endif; ?> - <option value="<?= /* @escapeNotVerified */ $_store->getId() ?>"<?php if ($block->getStoreId() == $_store->getId()): ?> selected<?php endif; ?>>      <?= /* @escapeNotVerified */ $_store->getName() ?></option> + <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php if ($block->getStoreId() == $_store->getId()) : ?> selected<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> <?php endforeach; ?> <?php endforeach; ?> <?php endforeach; ?> @@ -60,7 +60,7 @@ require(['prototype'], function(){ if(obj.switchParams){ storeParam+= obj.switchParams; } - setLocation('<?= /* @escapeNotVerified */ $block->getSwitchUrl() ?>'+storeParam); + setLocation('<?= $block->escapeUrl($block->getSwitchUrl()) ?>'+storeParam); } }); diff --git a/app/code/Magento/Reports/view/adminhtml/templates/store/switcher/enhanced.phtml b/app/code/Magento/Reports/view/adminhtml/templates/store/switcher/enhanced.phtml index 7edeee628be4a..e7a712e83e036 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/store/switcher/enhanced.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/store/switcher/enhanced.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @see \Magento\Backend\Block\Store\Switcher */ @@ -17,32 +17,32 @@ */ ?> -<?php if ($block->isShow()): ?> +<?php if ($block->isShow()) : ?> <div class="field field-store-switcher"> - <label class="label" for="store_switcher"><?= /* @escapeNotVerified */ __('Show Report For:') ?></label> + <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Show Report For:')) ?></label> <div class="control"> <select id="store_switcher" class="admin__control-select" name="store_switcher" onchange="return switchStore(this);"> - <option value=""><?= /* @escapeNotVerified */ __('All Websites') ?></option> - <?php foreach ($block->getWebsiteCollection() as $_website): ?> + <option value=""><?= $block->escapeHtml(__('All Websites')) ?></option> + <?php foreach ($block->getWebsiteCollection() as $_website) : ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> - <?php if ($showWebsite == false): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> + <?php if ($showWebsite == false) : ?> <?php $showWebsite = true; ?> - <option website="true" value="<?= /* @escapeNotVerified */ implode(',', $_website->getStoreIds()) ?>"<?php if ($block->getRequest()->getParam('store_ids') == implode(',', $_website->getStoreIds())): ?> selected<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?></option> + <option website="true" value="<?= $block->escapeHtmlAttr(implode(',', $_website->getStoreIds())) ?>"<?php if ($block->getRequest()->getParam('store_ids') == implode(',', $_website->getStoreIds())) : ?> selected<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?></option> <?php endif; ?> - <?php if ($showGroup == false): ?> + <?php if ($showGroup == false) : ?> <?php $showGroup = true; ?> - <option group="true" value="<?= /* @escapeNotVerified */ implode(',', $_group->getStoreIds()) ?>"<?php if ($block->getRequest()->getParam('store_ids') == implode(',', $_group->getStoreIds())): ?> selected<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> + <option group="true" value="<?= $block->escapeHtmlAttr(implode(',', $_group->getStoreIds())) ?>"<?php if ($block->getRequest()->getParam('store_ids') == implode(',', $_group->getStoreIds())) : ?> selected<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> <?php endif; ?> - <option value="<?= /* @escapeNotVerified */ $_store->getId() ?>"<?php if ($block->getStoreId() == $_store->getId()): ?> selected<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php if ($block->getStoreId() == $_store->getId()) : ?> selected<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> <?php endforeach; ?> - <?php if ($showGroup): ?> + <?php if ($showGroup) : ?> </optgroup> <?php endif; ?> <?php endforeach; ?> @@ -59,7 +59,7 @@ require(['prototype'], function(){ if(obj.switchParams){ storeParam+= obj.switchParams; } - setLocation('<?= /* @escapeNotVerified */ $block->getSwitchUrl() ?>'+storeParam); + setLocation('<?= $block->escapeUrl($block->getSwitchUrl()) ?>'+storeParam); } }); diff --git a/app/code/Magento/Reports/view/frontend/templates/js/components.phtml b/app/code/Magento/Reports/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Reports/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed.phtml b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed.phtml index a6e2f3dc72261..055a26b3dc308 100644 --- a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed.phtml @@ -4,7 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ /* @var $block \Magento\Reports\Block\Product\Widget\Viewed */ ?> @@ -15,12 +17,12 @@ $type = $block->getType() . '-' . $mode; $class = 'widget viewed' . ' ' . $mode; $title = __('Recently Viewed'); ?> -<div class="block <?= /* @escapeNotVerified */ $class ?>" id="recently_viewed_container" data-mage-init='{"recentlyViewedProducts":{}}' data-count="<?= /* @escapeNotVerified */ $block->getCount() ?>" style="display: none;"> +<div class="block <?= $block->escapeHtmlAttr($class) ?>" id="recently_viewed_container" data-mage-init='{"recentlyViewedProducts":{}}' data-count="<?= $block->escapeHtmlAttr($block->getCount()) ?>" style="display: none;"> <div class="title"> - <strong><?= /* @escapeNotVerified */ $title ?></strong> + <strong><?= $block->escapeHtml($title) ?></strong> </div> <div class="content"> - <ol class="products list items <?= /* @escapeNotVerified */ $type ?>"> + <ol class="products list items <?= $block->escapeHtmlAttr($type) ?>"> </ol> </div> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml index 579239d63e11e..562c9a2b63a99 100644 --- a/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/product/widget/viewed/item.phtml @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ /* @var $block \Magento\Reports\Block\Product\Widget\Viewed\Item */ -?> -<?php $type = $block->getType() . '-' . $block->getViewMode(); $item = $block->getProduct(); @@ -17,19 +17,19 @@ $item = $block->getProduct(); $image = $block->getImageType(); $rating = 'short'; ?> -<div class="block" id="widget_viewed_item" data-sku="<?= /* @escapeNotVerified */ $item->getSku() ?>" style="display: none;"> +<div class="block" id="widget_viewed_item" data-sku="<?= $block->escapeHtmlAttr($item->getSku()) ?>" style="display: none;"> <li class="item product"> <div class="product"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($item) ?>" class="product photo"> + <?= '<!-- ' . $block->escapeHtml($image) . '-->' ?> + <a href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" class="product photo"> <?= $block->getImage($item, $image)->toHtml() ?> </a> <div class="product details"> - <strong class="product name"><a title="<?= $block->escapeHtml($item->getName()) ?>" href="<?= /* @escapeNotVerified */ $block->getProductUrl($item) ?>"> + <strong class="product name"><a title="<?= $block->escapeHtmlAttr($item->getName()) ?>" href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>"> <?= $block->escapeHtml($item->getName()) ?></a> </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( + <?= /* @noEscape */ $block->getProductPriceHtml( $item, \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, @@ -38,51 +38,51 @@ $rating = 'short'; ] ) ?> - <?php if ($rating): ?> + <?php if ($rating) : ?> <?= $block->getReviewsSummaryHtml($item, $rating) ?> <?php endif; ?> <div class="product actions"> <div class="primary"> - <?php if ($item->isSaleable()): ?> - <?php if ($item->getTypeInstance()->hasRequiredOptions($item)): ?> - <button class="action tocart" data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($item) ?>"}}' type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + <?php if ($item->isSaleable()) : ?> + <?php if ($item->getTypeInstance()->hasRequiredOptions($item)) : ?> + <button class="action tocart" data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> - <?php else: ?> - <?php $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); + <?php else : ?> + <?php $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); $postData = $postDataHelper->getPostData($block->getAddToCartUrl($item), ['product' => $item->getEntityId()]) ?> <button class="action tocart" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> <?php endif; ?> - <?php else: ?> - <?php if ($item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php else : ?> + <?php if ($item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> <div class="secondary-addto-links" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow()): ?> - <a href="#" data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow()) : ?> + <a href="#" data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> <?php endif; ?> - <?php if ($block->getAddToCompareUrl()): ?> + <?php if ($block->getAddToCompareUrl()) : ?> <?php - $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare'); + $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class); ?> <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($item) ?>' + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($item) ?>' data-role="add-to-links" - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> </a> <?php endif; ?> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml index 999452ebb7cbd..8007cf645df1a 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_default_list.phtml @@ -4,8 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** + * @deprecated + */ ?> <?php if ($exist = $block->getRecentlyComparedProducts()) { @@ -25,69 +26,68 @@ if ($exist = $block->getRecentlyComparedProducts()) { $description = false; } ?> -<?php if ($exist): ?> -<div class="block widget block-compared-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> +<div class="block widget block-compared-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ $title ?></strong> + <strong><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol class="product-items" id="widget-compared-<?= /* @escapeNotVerified */ $suffix ?>"> - <?php $iterator = 1; ?> - <?php foreach ($items as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> - <?= $block->getImage($_product, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>)"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_product, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-widget-compared-' . $suffix - ] - ) ?> - <div class="product-item-actions"> - <?php if ($_product->isSaleable()): ?> - <div class="actions-primary"> - <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_product) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]) - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> + <ol class="product-items" id="widget-compared-<?= $block->escapeHtmlAttr($suffix) ?>"> + <?php foreach ($items as $_product) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>"> + <?= $block->getImage($_product, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>)"> + <?= $block->escapeHtml($this->helper(\Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name')) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_product, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-widget-compared-' . $suffix + ] + ) ?> + <div class="product-item-actions"> + <?php if ($_product->isSaleable()) : ?> + <div class="actions-primary"> + <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_product)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]) + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + </div> + <?php else : ?> + <?php if ($_product->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> </div> - <?php else: ?> - <?php if ($_product->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> - <?php endif; ?> - <?php endif; ?> </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach; ?> </ol> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_images_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_images_list.phtml index e125467b71f77..c6b6ae73ab039 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_images_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_images_list.phtml @@ -4,8 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** + * @deprecated + */ ?> <?php if ($exist = $block->getRecentlyComparedProducts()) { @@ -22,18 +23,18 @@ if ($exist = $block->getRecentlyComparedProducts()) { $description = false; } ?> -<?php if ($exist): ?> +<?php if ($exist) : ?> <div class="block widget block-compared-products-images"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ $title ?></strong> + <strong><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-compared-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-images"> - <?php $i = 0; foreach ($items as $_product): ?> + <ol id="widget-compared-<?= $block->escapeHtmlAttr($suffix) ?>" class="product-items product-items-images"> + <?php $i = 0; foreach ($items as $_product) : ?> <li class="product-item"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>"> <?= $block->getImage($_product, $image)->toHtml() ?> </a> </li> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_names_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_names_list.phtml index ee24db7d693bd..117e7b9eb8185 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_names_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/column/compared_names_list.phtml @@ -4,22 +4,23 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** + * @deprecated + */ ?> -<?php if ($_products = $block->getRecentlyComparedProducts()): ?> +<?php if ($_products = $block->getRecentlyComparedProducts()) : ?> <div class="block widget block-compared-products-names"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Recently Compared') ?></span></strong> + <strong><?= $block->escapeHtml(__('Recently Compared')) ?></span></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-compared-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-names"> - <?php $i = 0; foreach ($_products as $_product): ?> + <ol id="widget-compared-<?= $block->escapeHtmlAttr($suffix) ?>" class="product-items product-items-names"> + <?php $i = 0; foreach ($_products as $_product) : ?> <li class="product-item"> <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product-item-link"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" class="product-item-link"> + <?= $block->escapeHtml($this->helper(\Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name')) ?> </a> </strong> </li> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml index f56448b11db0f..2fbc054047f14 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_grid.phtml @@ -4,11 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> +/** + * @deprecated + */ -<?php /** @var \Magento\Catalog\Block\Product\Compare\ListCompare $block */ if ($exist = $block->getRecentlyComparedProducts()) { $type = 'widget-compared'; @@ -28,98 +27,97 @@ if ($exist = $block->getRecentlyComparedProducts()) { } ?> -<?php if ($exist):?> - <div class="block widget block-compared-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> + <div class="block widget block-compared-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> - <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_item, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-' . $type - ] - ) ?> - <?php if ($rating): ?> - <?= $block->getReviewsSummaryHtml($_item, $rating) ?> - <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?= '<!-- ' . $block->escapeHtml($image) . '-->' ?> + <div class="products-<?= $block->escapeHtmlAttr($mode) ?> <?= $block->escapeHtmlAttr($mode) ?>"> + <ol class="product-items <?= $block->escapeHtmlAttr($type)?>"> + <?php foreach ($items as $_item) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> + <?= $block->getImage($_item, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> + <?= $block->escapeHtml($_item->getName()) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_item, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-' . $type + ] + ) ?> + <?php if ($rating) : ?> + <?= $block->getReviewsSummaryHtml($_item, $rating) ?> + <?php endif; ?> + <?php if ($showWishlist || $showCompare || $showCart) : ?> + <div class="product-item-actions"> + <?php if ($showCart) : ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) : ?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + <?php else : ?> + <?php if ($_item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' - data-action="add-to-wishlist" - class="action towishlist" - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare'); ?> - <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> - </div> - <?php endif; ?> + <?php if ($showWishlist || $showCompare) : ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' + data-action="add-to-wishlist" + class="action towishlist" + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> + <?php endif; ?> + <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> + <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class); ?> + <a href="#" class="action tocompare" + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> + <?php endif; ?> + </div> + <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach ?> </ol> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml index 48463f3baaa5c..ae713a4fa12c1 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/compared/content/compared_list.phtml @@ -4,11 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -?> +/** + * @deprecated + */ -<?php /** @var \Magento\Catalog\Block\Product\Compare\ListCompare $block */ if ($exist = $block->getRecentlyComparedProducts()) { $type = 'widget-compared'; @@ -19,7 +18,7 @@ if ($exist = $block->getRecentlyComparedProducts()) { $image = 'recently_compared_products_list_content_widget'; $title = __('Recently Compared'); $items = $exist; - $_helper = $this->helper('Magento\Catalog\Helper\Output'); + $_helper = $this->helper(\Magento\Catalog\Helper\Output::class); $showWishlist = true; $showCompare = true; @@ -29,106 +28,105 @@ if ($exist = $block->getRecentlyComparedProducts()) { } ?> -<?php if ($exist):?> - <div class="block widget block-compared-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> + <div class="block widget block-compared-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> - <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_item, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-' . $type - ] - ) ?> - <?php if ($rating): ?> - <?= $block->getReviewsSummaryHtml($_item, $rating) ?> - <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?= '<!-- ' . $block->escapeHtml($image) . '-->' ?> + <div class="products-<?= $block->escapeHtmlAttr($mode) ?> <?= $block->escapeHtmlAttr($mode) ?>"> + <ol class="product-items <?= $block->escapeHtmlAttr($type) ?>"> + <?php foreach ($items as $_item) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> + <?= $block->getImage($_item, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> + <?= $block->escapeHtml($_item->getName()) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_item, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-' . $type + ] + ) ?> + <?php if ($rating) : ?> + <?= $block->getReviewsSummaryHtml($_item, $rating) ?> + <?php endif; ?> + <?php if ($showWishlist || $showCompare || $showCart) : ?> + <div class="product-item-actions"> + <?php if ($showCart) : ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) : ?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeHtmlAttr($block->escapeUrl($block->getAddToCartUrl($_item))) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + <?php else : ?> + <?php if ($_item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' - data-action="add-to-wishlist" - class="action towishlist" - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($description):?> - <div class="product-item-description"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description') ?> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" - class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> - </div> - <?php endif; ?> + <?php if ($showWishlist || $showCompare) : ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' + data-action="add-to-wishlist" + class="action towishlist" + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> + <?php endif; ?> + <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> + <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class);?> + <a href="#" class="action tocompare" + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> + <?php endif; ?> + </div> + <?php endif; ?> + </div> + <?php endif; ?> + <?php if ($description) : ?> + <div class="product-item-description"> + <?= $block->escapeHtml($_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description')) ?> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" + class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> + </div> + <?php endif; ?> + </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach ?> </ol> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml index e26dc7ea31761..3acf6daeef253 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_default_list.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @var $block \Magento\Reports\Block\Product\Viewed */ @@ -30,68 +30,68 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr $description = false; } ?> -<?php if ($exist): ?> -<div class="block widget block-viewed-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> +<div class="block widget block-viewed-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ $title ?></strong> + <strong><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol class="product-items" id="widget-viewed-<?= /* @escapeNotVerified */ $suffix ?>"> + <ol class="product-items" id="widget-viewed-<?= $block->escapeHtmlAttr($suffix) ?>"> <?php $iterator = 1; ?> - <?php foreach ($items as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> - <?= $block->getImage($_product, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" - title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>" - class="product-item-link"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_product, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-widget-viewed-' . $suffix - ] - ) ?> - <div class="product-item-actions"> - <?php if ($_product->isSaleable()): ?> - <div class="actions-primary"> - <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_product) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]); - ?> - <button type="button" class="action tocart primary" data-post='<?= /* @escapeNotVerified */ $postData ?>'> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> + <?php foreach ($items as $_product) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>"> + <?= $block->getImage($_product, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" + title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>" + class="product-item-link"> + <?= $block->escapeHtml($this->helper(\Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name')) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_product, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-widget-viewed-' . $suffix + ] + ) ?> + <div class="product-item-actions"> + <?php if ($_product->isSaleable()) : ?> + <div class="actions-primary"> + <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_product)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_product), ['product' => $_product->getEntityId()]); + ?> + <button type="button" class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>'> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + </div> + <?php else : ?> + <?php if ($_product->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> </div> - <?php else: ?> - <?php if ($_product->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> - <?php endif; ?> - <?php endif; ?> </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach; ?> </ol> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_images_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_images_list.phtml index 8a1daebda3049..d5d41f059d0dd 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_images_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_images_list.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @var $block \Magento\Reports\Block\Product\Viewed */ @@ -27,22 +27,22 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr $description = false; } ?> -<?php if ($exist): ?> +<?php if ($exist) : ?> <div class="block widget block-viewed-products-images"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ $title ?></strong> + <strong><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-viewed-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-images"> + <ol id="widget-viewed-<?= $block->escapeHtmlAttr($suffix) ?>" class="product-items product-items-images"> <?php $iterator = 1; ?> - <?php foreach ($items as $_product): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <a class="product-item-photo" href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" title="<?= /* @escapeNotVerified */ $block->stripTags($_product->getName(), null, true) ?>"> + <?php foreach ($items as $_product) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <a class="product-item-photo" href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" title="<?= $block->escapeHtmlAttr($block->stripTags($_product->getName(), null, true)) ?>"> <?= $block->getImage($_product, $image)->toHtml() ?> </a> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> - <?php endforeach; ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> + <?php endforeach; ?> </ol> </div> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_names_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_names_list.phtml index 25cc6c3c921d7..d2ea09ebdbfeb 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_names_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/column/viewed_names_list.phtml @@ -4,27 +4,27 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @var $block \Magento\Reports\Block\Product\Viewed */ ?> -<?php if (($_products = $block->getRecentlyViewedProducts()) && $_products->getSize()): ?> +<?php if (($_products = $block->getRecentlyViewedProducts()) && $_products->getSize()) : ?> <div class="block widget block-viewed-products-names"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Recently Viewed') ?></strong> + <strong><?= $block->escapeHtml(__('Recently Viewed')) ?></strong> </div> <div class="block-content"> <?php $suffix = $block->getNameInLayout(); ?> - <ol id="widget-viewed-<?= /* @escapeNotVerified */ $suffix ?>" class="product-items product-items-names"> - <?php foreach ($_products as $_product): ?> + <ol id="widget-viewed-<?= $block->escapeHtmlAttr($suffix) ?>" class="product-items product-items-names"> + <?php foreach ($_products as $_product) : ?> <li class="product-item"> <strong class="product-item-name"> - <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>" class="product-item-link"> - <?= /* @escapeNotVerified */ $this->helper('Magento\Catalog\Helper\Output')->productAttribute($_product, $_product->getName(), 'name') ?> + <a href="<?= $block->escapeUrl($_product->getProductUrl()) ?>" class="product-item-link"> + <?= $block->escapeHtml($this->helper(\Magento\Catalog\Helper\Output::class)->productAttribute($_product, $_product->getName(), 'name')) ?> </a> </strong> </li> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml index de1145a26fe2b..a560ffc857ffd 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_grid.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @var $block \Magento\Reports\Block\Product\Viewed */ @@ -30,96 +30,95 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr $description = ($mode == 'list') ? true : false; } ?> -<?php if ($exist):?> - <div class="block widget block-viewed-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> + <div class="block widget block-viewed-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> - <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_item, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-' . $type - ] - ) ?> - <?php if ($rating): ?> - <?= $block->getReviewsSummaryHtml($_item, $rating) ?> - <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> + <?= '<!-- ' . $block->escapeHtml($image) . '-->' ?> + <div class="products-<?= $block->escapeHtmlAttr($mode) ?> <?= $block->escapeHtmlAttr($mode) ?>"> + <ol class="product-items <?= $block->escapeHtmlAttr($type) ?>"> + <?php foreach ($items as $_item) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> + <?= $block->getImage($_item, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> + <?= $block->escapeHtml($_item->getName()) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_item, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-' . $type + ] + ) ?> + <?php if ($rating) : ?> + <?= $block->getReviewsSummaryHtml($_item, $rating) ?> + <?php endif; ?> + <?php if ($showWishlist || $showCompare || $showCart) : ?> + <div class="product-item-actions"> + <?php if ($showCart) : ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) : ?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + <?php else : ?> + <?php if ($_item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> + <?php endif; ?> + </div> + <?php endif; ?> + <?php if ($showWishlist || $showCompare) : ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> + <a href="#" + class="action towishlist" data-action="add-to-wishlist" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> + <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> + <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class);?> + <a href="#" class="action tocompare" + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - class="action towishlist" data-action="add-to-wishlist" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach ?> </ol> </div> diff --git a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml index 076c715dc4049..3b23802fda9c6 100644 --- a/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml +++ b/app/code/Magento/Reports/view/frontend/templates/widget/viewed/content/viewed_list.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** + * @deprecated + */ -?> -<?php /** * @var $block \Magento\Reports\Block\Product\Viewed */ @@ -22,7 +22,7 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr $image = 'recently_viewed_products_list_content_widget'; $title = __('Recently Viewed'); $items = $block->getRecentlyViewedProducts(); - $_helper = $this->helper('Magento\Catalog\Helper\Output'); + $_helper = $this->helper(\Magento\Catalog\Helper\Output::class); $showWishlist = true; $showCompare = true; @@ -32,105 +32,105 @@ if ($exist = ($block->getRecentlyViewedProducts() && $block->getRecentlyViewedPr } ?> -<?php if ($exist):?> - <div class="block widget block-viewed-products-<?= /* @escapeNotVerified */ $mode ?>"> +<?php if ($exist) : ?> + <div class="block widget block-viewed-products-<?= $block->escapeHtmlAttr($mode) ?>"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ $title ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml($title) ?></strong> </div> <div class="block-content"> - <?= /* @escapeNotVerified */ '<!-- ' . $image . '-->' ?> - <div class="products-<?= /* @escapeNotVerified */ $mode ?> <?= /* @escapeNotVerified */ $mode ?>"> - <ol class="product-items <?= /* @escapeNotVerified */ $type ?>"> + <?= '<!-- ' . $block->escapeHtml($image) . '-->' ?> + <div class="products-<?= $block->escapeHtmlAttr($mode) ?> <?= $block->escapeHtmlAttr($mode) ?>"> + <ol class="product-items <?= $block->escapeHtmlAttr($type) ?>"> <?php $iterator = 1; ?> - <?php foreach ($items as $_item): ?> - <?= /* @escapeNotVerified */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> - <div class="product-item-info"> - <a href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-photo"> - <?= $block->getImage($_item, $image)->toHtml() ?> - </a> - <div class="product-item-details"> - <strong class="product-item-name"> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" class="product-item-link"> - <?= $block->escapeHtml($_item->getName()) ?> - </a> - </strong> - <?php /* @escapeNotVerified */ echo $block->getProductPriceHtml( - $_item, - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, - [ - 'price_id_suffix' => '-' . $type - ] - ) ?> - <?php if ($rating): ?> - <?= $block->getReviewsSummaryHtml($_item, $rating) ?> - <?php endif; ?> - <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" - data-mage-init='{"redirectUrl": {"url": "<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]); - ?> - <button class="action tocart primary" - data-post='<?= /* @escapeNotVerified */ $postData ?>' - type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> - </button> + <?php foreach ($items as $_item) : ?> + <?= /* @noEscape */ ($iterator++ == 1) ? '<li class="product-item">' : '</li><li class="product-item">' ?> + <div class="product-item-info"> + <a href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-photo"> + <?= $block->getImage($_item, $image)->toHtml() ?> + </a> + <div class="product-item-details"> + <strong class="product-item-name"> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" class="product-item-link"> + <?= $block->escapeHtml($_item->getName()) ?> + </a> + </strong> + <?= /* @noEscape */ $block->getProductPriceHtml( + $_item, + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + \Magento\Framework\Pricing\Render::ZONE_ITEM_LIST, + [ + 'price_id_suffix' => '-' . $type + ] + ) ?> + <?php if ($rating) : ?> + <?= $block->getReviewsSummaryHtml($_item, $rating) ?> + <?php endif; ?> + <?php if ($showWishlist || $showCompare || $showCart) : ?> + <div class="product-item-actions"> + <?php if ($showCart) : ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()) : ?> + <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)) : ?> + <button class="action tocart primary" + data-mage-init='{"redirectUrl": {"url": "<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php else : ?> + <?php + $postDataHelper = $this->helper(\Magento\Framework\Data\Helper\PostHelper::class); + $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]); + ?> + <button class="action tocart primary" + data-post='<?= /* @noEscape */ $postData ?>' + type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + </button> + <?php endif; ?> + <?php else : ?> + <?php if ($_item->getIsSalable()) : ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else : ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= /* @escapeNotVerified */ __('In stock') ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= /* @escapeNotVerified */ __('Out of stock') ?></span></div> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - class="action towishlist" data-action="add-to-wishlist" - data-post='<?= /* @escapeNotVerified */ $block->getAddToWishlistParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Wish List') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Wish List') ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" - data-post='<?= /* @escapeNotVerified */ $compareHelper->getPostDataParams($_item) ?>' - title="<?= /* @escapeNotVerified */ __('Add to Compare') ?>"> - <span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($description):?> - <div class="product-item-description"> - <?= /* @escapeNotVerified */ $_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description') ?> - <a title="<?= $block->escapeHtml($_item->getName()) ?>" - href="<?= /* @escapeNotVerified */ $block->getProductUrl($_item) ?>" - class="action more"><?= /* @escapeNotVerified */ __('Learn More') ?></a> - </div> - <?php endif; ?> + <?php if ($showWishlist || $showCompare) : ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper(\Magento\Wishlist\Helper\Data::class)->isAllow() && $showWishlist) : ?> + <a href="#" + class="action towishlist" data-action="add-to-wishlist" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> + <?php endif; ?> + <?php if ($block->getAddToCompareUrl() && $showCompare) : ?> + <?php $compareHelper = $this->helper(\Magento\Catalog\Helper\Product\Compare::class);?> + <a href="#" class="action tocompare" + data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' + title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> + <?php endif; ?> + </div> + <?php endif; ?> + </div> + <?php endif; ?> + <?php if ($description) : ?> + <div class="product-item-description"> + <?= $block->escapeHtml($_helper->productAttribute($_item, $_item->getShortDescription(), 'short_description')) ?> + <a title="<?= $block->escapeHtmlAttr($_item->getName()) ?>" + href="<?= $block->escapeUrl($block->getProductUrl($_item)) ?>" + class="action more"><?= $block->escapeHtml(__('Learn More')) ?></a> + </div> + <?php endif; ?> + </div> </div> - </div> - <?= ($iterator == count($items)+1) ? '</li>' : '' ?> + <?= /* @noEscape */ ($iterator == count($items)+1) ? '</li>' : '' ?> <?php endforeach ?> </ol> </div> diff --git a/app/code/Magento/RequireJs/Model/FileManager.php b/app/code/Magento/RequireJs/Model/FileManager.php index 019c2cbedb75c..ec41c4238967f 100644 --- a/app/code/Magento/RequireJs/Model/FileManager.php +++ b/app/code/Magento/RequireJs/Model/FileManager.php @@ -183,6 +183,9 @@ public function createBundleJsPool() } foreach ($libDir->read($bundleDir) as $bundleFile) { + if (pathinfo($bundleFile, PATHINFO_EXTENSION) !== 'js') { + continue; + } $relPath = $libDir->getRelativePath($bundleFile); $bundles[] = $this->assetRepo->createArbitrary($relPath, ''); } diff --git a/app/code/Magento/RequireJs/Test/Mftf/LICENSE.txt b/app/code/Magento/RequireJs/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/RequireJs/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 <insert your license name here>" 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/app/code/Magento/RequireJs/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/RequireJs/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/RequireJs/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 <insert your license name here>" 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/app/code/Magento/RequireJs/Test/Mftf/README.md b/app/code/Magento/RequireJs/Test/Mftf/README.md new file mode 100644 index 0000000000000..152b706f7be1e --- /dev/null +++ b/app/code/Magento/RequireJs/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Require Js Functional Tests + +The Functional Test Module for **Magento Require Js** module. diff --git a/app/code/Magento/RequireJs/Test/Unit/Model/FileManagerTest.php b/app/code/Magento/RequireJs/Test/Unit/Model/FileManagerTest.php index 6b6d709cbb608..834ee5b68485e 100644 --- a/app/code/Magento/RequireJs/Test/Unit/Model/FileManagerTest.php +++ b/app/code/Magento/RequireJs/Test/Unit/Model/FileManagerTest.php @@ -153,7 +153,7 @@ public function testCreateBundleJsPool() ->expects($this->once()) ->method('read') ->with('path/to/bundle/dir/js/bundle') - ->willReturn(['bundle1.js', 'bundle2.js']); + ->willReturn(['bundle1.js', 'bundle2.js', 'some_file.not_js']); $dirRead ->expects($this->exactly(2)) ->method('getRelativePath') diff --git a/app/code/Magento/RequireJs/composer.json b/app/code/Magento/RequireJs/composer.json index 04757e61586c2..31741718e22b0 100644 --- a/app/code/Magento/RequireJs/composer.json +++ b/app/code/Magento/RequireJs/composer.json @@ -2,11 +2,11 @@ "name": "magento/module-require-js", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 26dff821308d6..d17470d51669d 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Review\Block\Adminhtml; /** @@ -41,6 +39,7 @@ protected function _construct() }); '; + // @codingStandardsIgnoreStart $this->_formInitScripts[] = ' require(["jquery","prototype"], function(jQuery){ window.review = function() { @@ -94,13 +93,14 @@ protected function _construct() if( response.error ) { alert(response.message); } else if( response.id ){ + var productName = response.name; $("product_id").value = response.id; $("product_name").innerHTML = \'<a href="' . $this->getUrl( 'catalog/product/edit' ) . - 'id/\' + response.id + \'" target="_blank">\' + response.name + \'</a>\'; + 'id/\' + response.id + \'" target="_blank">\' + productName.escapeHTML() + \'</a>\'; } else if ( response.message ) { alert(response.message); } @@ -115,6 +115,7 @@ protected function _construct() }); //]]> '; + // @codingStandardsIgnoreEnd } /** diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php index 2d619725ae201..04e6343eb43ca 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php @@ -142,11 +142,6 @@ protected function _prepareForm() $fieldset->addField('product_id', 'hidden', ['name' => 'product_id']); - /*$gridFieldset = $form->addFieldset('add_review_grid', array('legend' => __('Please select a product'))); - $gridFieldset->addField('products_grid', 'note', array( - 'text' => $this->getLayout()->createBlock(\Magento\Review\Block\Adminhtml\Product\Grid::class)->toHtml(), - ));*/ - $form->setMethod('post'); $form->setUseContainer(true); $form->setId('edit_form'); diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcbc..5efead952280e 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Adminhtml; /** - * Review edit form + * Review edit form. */ class Edit extends \Magento\Backend\Block\Widget\Form\Container { @@ -56,6 +57,7 @@ public function __construct( * * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -77,7 +79,13 @@ protected function _construct() 'previous', [ 'label' => __('Previous'), - 'onclick' => 'setLocation(\'' . $this->getUrl('review/*/*', ['id' => $prevId]) . '\')' + 'onclick' => 'setLocation(\'' . $this->getUrl( + 'review/*/*', + [ + 'id' => $prevId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 10 @@ -93,7 +101,10 @@ protected function _construct() 'button' => [ 'event' => 'save', 'target' => '#edit_form', - 'eventData' => ['action' => ['args' => ['next_item' => $prevId]]], + 'eventData' => ['action' => ['args' => [ + 'next_item' => $prevId, + 'ret' => $this->getRequest()->getParam('ret'), + ]]], ], ], ] @@ -113,7 +124,10 @@ protected function _construct() 'button' => [ 'event' => 'save', 'target' => '#edit_form', - 'eventData' => ['action' => ['args' => ['next_item' => $nextId]]], + 'eventData' => ['action' => ['args' => [ + 'next_item' => $nextId, + 'ret' => $this->getRequest()->getParam('ret'), + ]]], ], ], ] @@ -126,7 +140,13 @@ protected function _construct() 'next', [ 'label' => __('Next'), - 'onclick' => 'setLocation(\'' . $this->getUrl('review/*/*', ['id' => $nextId]) . '\')' + 'onclick' => 'setLocation(\'' . $this->getUrl( + 'review/*/*', + [ + 'id' => $nextId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 105 @@ -159,16 +179,16 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] - ) . '\'' . ')' + ) . '\'' . ', {data: {}})' ); $this->_coreRegistry->register('ret', 'pending'); } diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php index 8a8395de72b62..c2cbce09a6930 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ + /** * Adminhtml Review Edit Form */ @@ -69,12 +70,24 @@ public function __construct( * * @return $this * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _prepareForm() { $review = $this->_coreRegistry->registry('review_data'); $product = $this->_productFactory->create()->load($review->getEntityPkValue()); + $formActionParams =[ + 'id' => $this->getRequest()->getParam('id'), + 'ret' => $this->_coreRegistry->registry('ret') + ]; + if ($this->getRequest()->getParam('productId')) { + $formActionParams['productId'] = $this->getRequest()->getParam('productId'); + } + if ($this->getRequest()->getParam('customerId')) { + $formActionParams['customerId'] = $this->getRequest()->getParam('customerId'); + } + /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create( [ @@ -82,10 +95,7 @@ protected function _prepareForm() 'id' => 'edit_form', 'action' => $this->getUrl( 'review/*/save', - [ - 'id' => $this->getRequest()->getParam('id'), - 'ret' => $this->_coreRegistry->registry('ret') - ] + $formActionParams ), 'method' => 'post', ], diff --git a/app/code/Magento/Review/Block/Adminhtml/Grid.php b/app/code/Magento/Review/Block/Adminhtml/Grid.php index 75ac201270152..66c767182619f 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Review/Block/Adminhtml/Grid.php @@ -3,22 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile +namespace Magento\Review\Block\Adminhtml; /** * Adminhtml reviews grid * - * @method int getProductId() getProductId() - * @method \Magento\Review\Block\Adminhtml\Grid setProductId() setProductId(int $productId) - * @method int getCustomerId() getCustomerId() - * @method \Magento\Review\Block\Adminhtml\Grid setCustomerId() setCustomerId(int $customerId) - * @method \Magento\Review\Block\Adminhtml\Grid setMassactionIdFieldOnlyIndexValue() setMassactionIdFieldOnlyIndexValue(bool $onlyIndex) + * @method int getProductId() + * @method \Magento\Review\Block\Adminhtml\Grid setProductId(int $productId) + * @method int getCustomerId() + * @method \Magento\Review\Block\Adminhtml\Grid setCustomerId(int $customerId) + * @method bool getMassactionIdFieldOnlyIndexValue() + * @method \Magento\Review\Block\Adminhtml\Grid setMassactionIdFieldOnlyIndexValue(bool $onlyIndex) * - * @author Magento Core Team <core@magentocommerce.com> + * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Review\Block\Adminhtml; - class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { /** @@ -115,6 +113,8 @@ protected function _afterLoadCollection() * Prepare collection * * @return \Magento\Review\Block\Adminhtml\Grid + * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _prepareCollection() { @@ -350,6 +350,18 @@ protected function _prepareMassaction() ); } + /** + * @inheritdoc + */ + protected function _prepareMassactionColumn() + { + parent::_prepareMassactionColumn(); + /** needs for correct work of mass action select functionality */ + $this->setMassactionIdField('rt.review_id'); + + return $this; + } + /** * Get row url * diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php index adad931da5a69..a02c998f856bd 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php @@ -121,9 +121,9 @@ public function getRating() )->setStoreFilter( $stores )->setPositionOrder()->load()->addOptionToItems(); - if (intval($this->getRequest()->getParam('id'))) { + if ((int)$this->getRequest()->getParam('id')) { $this->_voteCollection = $this->_votesFactory->create()->setReviewFilter( - intval($this->getRequest()->getParam('id')) + (int)$this->getRequest()->getParam('id') )->addOptionInfo()->load()->addRatingOptions(); } } diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php index 0841388252905..dbf0a79bc42ff 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php @@ -17,7 +17,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic /** * @var string */ - protected $_template = 'rating/form.phtml'; + protected $_template = 'Magento_Review::rating/form.phtml'; /** * Session diff --git a/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php b/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php index 5d2ec9fc186ca..def0e896fc95f 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php @@ -16,7 +16,7 @@ class Link extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rss/grid/link.phtml'; + protected $_template = 'Magento_Review::rss/grid/link.phtml'; /** * @var \Magento\Framework\App\Rss\UrlBuilderInterface diff --git a/app/code/Magento/Review/Block/Customer/ListCustomer.php b/app/code/Magento/Review/Block/Customer/ListCustomer.php index 8377cc73646b9..eb67af5780ddb 100644 --- a/app/code/Magento/Review/Block/Customer/ListCustomer.php +++ b/app/code/Magento/Review/Block/Customer/ListCustomer.php @@ -154,13 +154,13 @@ public function getProductLink() /** * Get product URL * - * @param \Magento\Review\Model\Review $review + * @param \Magento\Catalog\Model\Product $product * @return string * @since 100.2.0 */ - public function getProductUrl($review) + public function getProductUrl($product) { - return $this->getUrl('catalog/product/view', ['id' => $review->getEntityPkValue()]); + return $product->getProductUrl(); } /** diff --git a/app/code/Magento/Review/Block/Customer/Recent.php b/app/code/Magento/Review/Block/Customer/Recent.php index 8f593f5695812..5c7f1ec2c0dad 100644 --- a/app/code/Magento/Review/Block/Customer/Recent.php +++ b/app/code/Magento/Review/Block/Customer/Recent.php @@ -20,7 +20,7 @@ class Recent extends \Magento\Framework\View\Element\Template * * @var string */ - protected $_template = 'customer/list.phtml'; + protected $_template = 'Magento_Review::customer/list.phtml'; /** * Product reviews collection diff --git a/app/code/Magento/Review/Block/Customer/View.php b/app/code/Magento/Review/Block/Customer/View.php index b7dfd4b969a9d..06c4f7d735523 100644 --- a/app/code/Magento/Review/Block/Customer/View.php +++ b/app/code/Magento/Review/Block/Customer/View.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Customer; use Magento\Catalog\Model\Product; @@ -23,7 +24,7 @@ class View extends \Magento\Catalog\Block\Product\AbstractProduct * * @var string */ - protected $_template = 'customer/view.phtml'; + protected $_template = 'Magento_Review::customer/view.phtml'; /** * Catalog product model @@ -91,6 +92,7 @@ public function __construct( * Initialize review id * * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -160,6 +162,7 @@ public function getRating() /** * Get rating summary * + * @deprecated * @return array */ public function getRatingSummary() @@ -201,26 +204,7 @@ public function dateFormat($date) } /** - * Get product reviews summary - * - * @param \Magento\Catalog\Model\Product $product - * @param bool $templateType - * @param bool $displayIfNoReviews - * @return string - */ - public function getReviewsSummaryHtml( - \Magento\Catalog\Model\Product $product, - $templateType = false, - $displayIfNoReviews = false - ) { - if (!$product->getRatingSummary()) { - $this->_reviewFactory->create()->getEntitySummary($product, $this->_storeManager->getStore()->getId()); - } - return parent::getReviewsSummaryHtml($product, $templateType, $displayIfNoReviews); - } - - /** - * @return string + * @inheritDoc */ protected function _toHtml() { diff --git a/app/code/Magento/Review/Block/Form.php b/app/code/Magento/Review/Block/Form.php index 440e13deb5839..f6a579e844386 100644 --- a/app/code/Magento/Review/Block/Form.php +++ b/app/code/Magento/Review/Block/Form.php @@ -5,7 +5,6 @@ */ namespace Magento\Review\Block; -use Magento\Catalog\Model\Product; use Magento\Customer\Model\Context; use Magento\Customer\Model\Url; use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; diff --git a/app/code/Magento/Review/Block/Product/Compare/ListCompare/Plugin/Review.php b/app/code/Magento/Review/Block/Product/Compare/ListCompare/Plugin/Review.php deleted file mode 100644 index 8393f87f8d45d..0000000000000 --- a/app/code/Magento/Review/Block/Product/Compare/ListCompare/Plugin/Review.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Review\Block\Product\Compare\ListCompare\Plugin; - -class Review -{ - /** - * Review model - * - * @var \Magento\Review\Model\ReviewFactory - */ - protected $reviewFactory; - - /** - * Store manager - * - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $storeManager; - - /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Review\Model\ReviewFactory $reviewFactory - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Review\Model\ReviewFactory $reviewFactory - ) { - $this->storeManager = $storeManager; - $this->reviewFactory = $reviewFactory; - } - - /** - * Initialize product review - * - * @param \Magento\Catalog\Block\Product\Compare\ListCompare $subject - * @param \Magento\Catalog\Model\Product $product - * @param bool $templateType - * @param bool $displayIfNoReviews - * - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeGetReviewsSummaryHtml( - \Magento\Catalog\Block\Product\Compare\ListCompare $subject, - \Magento\Catalog\Model\Product $product, - $templateType = false, - $displayIfNoReviews = false - ) { - if (!$product->getRatingSummary()) { - $this->reviewFactory->create()->getEntitySummary($product, $this->storeManager->getStore()->getId()); - } - } -} diff --git a/app/code/Magento/Review/Block/Product/ReviewRenderer.php b/app/code/Magento/Review/Block/Product/ReviewRenderer.php index 8aa10d2437cbb..0fd6327e1f777 100644 --- a/app/code/Magento/Review/Block/Product/ReviewRenderer.php +++ b/app/code/Magento/Review/Block/Product/ReviewRenderer.php @@ -5,11 +5,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Product; use Magento\Catalog\Block\Product\ReviewRendererInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Review\Model\ReviewSummaryFactory; +use Magento\Review\Observer\PredispatchReviewObserver; +/** + * Class ReviewRenderer + */ class ReviewRenderer extends \Magento\Framework\View\Element\Template implements ReviewRendererInterface { /** @@ -18,8 +25,8 @@ class ReviewRenderer extends \Magento\Framework\View\Element\Template implements * @var array */ protected $_availableTemplates = [ - self::FULL_VIEW => 'helper/summary.phtml', - self::SHORT_VIEW => 'helper/summary_short.phtml', + self::FULL_VIEW => 'Magento_Review::helper/summary.phtml', + self::SHORT_VIEW => 'Magento_Review::helper/summary_short.phtml', ]; /** @@ -29,20 +36,42 @@ class ReviewRenderer extends \Magento\Framework\View\Element\Template implements */ protected $_reviewFactory; + /** + * @var ReviewSummaryFactory + */ + private $reviewSummaryFactory; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Review\Model\ReviewFactory $reviewFactory * @param array $data + * @param ReviewSummaryFactory $reviewSummaryFactory */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Review\Model\ReviewFactory $reviewFactory, - array $data = [] + array $data = [], + ReviewSummaryFactory $reviewSummaryFactory = null ) { $this->_reviewFactory = $reviewFactory; + $this->reviewSummaryFactory = $reviewSummaryFactory ?? + ObjectManager::getInstance()->get(ReviewSummaryFactory::class); parent::__construct($context, $data); } + /** + * Review module availability + * + * @return string + */ + public function isReviewEnabled(): string + { + return $this->_scopeConfig->getValue( + PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + /** * Get review summary html * @@ -51,17 +80,22 @@ public function __construct( * @param bool $displayIfNoReviews * * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getReviewsSummaryHtml( \Magento\Catalog\Model\Product $product, $templateType = self::DEFAULT_VIEW, $displayIfNoReviews = false ) { - if (!$product->getRatingSummary()) { - $this->_reviewFactory->create()->getEntitySummary($product, $this->_storeManager->getStore()->getId()); + if ($product->getRatingSummary() === null) { + $this->reviewSummaryFactory->create()->appendSummaryDataToObject( + $product, + $this->_storeManager->getStore()->getId() + ); } - if (!$product->getRatingSummary() && !$displayIfNoReviews) { + if (null === $product->getRatingSummary() && !$displayIfNoReviews) { return ''; } // pick template among available @@ -84,7 +118,7 @@ public function getReviewsSummaryHtml( */ public function getRatingSummary() { - return $this->getProduct()->getRatingSummary()->getRatingSummary(); + return $this->getProduct()->getRatingSummary(); } /** @@ -94,7 +128,7 @@ public function getRatingSummary() */ public function getReviewsCount() { - return $this->getProduct()->getRatingSummary()->getReviewsCount(); + return $this->getProduct()->getReviewsCount(); } /** diff --git a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php index de871d9061428..72cc921f530bb 100644 --- a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php +++ b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php @@ -15,7 +15,7 @@ class Detailed extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'detailed.phtml'; + protected $_template = 'Magento_Review::detailed.phtml'; /** * @var \Magento\Review\Model\RatingFactory @@ -42,7 +42,7 @@ public function __construct( protected function _toHtml() { $entityId = $this->_request->getParam('id'); - if (intval($entityId) <= 0) { + if ((int)$entityId <= 0) { return ''; } diff --git a/app/code/Magento/Review/Block/View.php b/app/code/Magento/Review/Block/View.php index e2d0355671688..82a5f37f9b6bf 100644 --- a/app/code/Magento/Review/Block/View.php +++ b/app/code/Magento/Review/Block/View.php @@ -19,7 +19,7 @@ class View extends \Magento\Catalog\Block\Product\AbstractProduct * * @var string */ - protected $_template = 'view.phtml'; + protected $_template = 'Magento_Review::view.phtml'; /** * Rating option model @@ -119,6 +119,7 @@ public function getRating() /** * Retrieve rating summary for current product * + * @deprecated * @return string */ public function getRatingSummary() @@ -160,23 +161,4 @@ public function dateFormat($date) { return $this->formatDate($date, \IntlDateFormatter::LONG); } - - /** - * Get product reviews summary - * - * @param \Magento\Catalog\Model\Product $product - * @param bool $templateType - * @param bool $displayIfNoReviews - * @return string - */ - public function getReviewsSummaryHtml( - \Magento\Catalog\Model\Product $product, - $templateType = false, - $displayIfNoReviews = false - ) { - if (!$product->getRatingSummary()) { - $this->_reviewFactory->create()->getEntitySummary($product, $this->_storeManager->getStore()->getId()); - } - return parent::getReviewsSummaryHtml($product, $templateType, $displayIfNoReviews); - } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product.php b/app/code/Magento/Review/Controller/Adminhtml/Product.php index 8c4503d8f023e..e694fe0edec6c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product.php @@ -12,10 +12,15 @@ use Magento\Review\Model\RatingFactory; /** - * Reviews admin controller + * Reviews admin controller. */ abstract class Product extends Action { + /** + * Authorization resource + */ + const ADMIN_RESOURCE = 'Magento_Review::reviews_all'; + /** * Array of actions which can be processed without secret key validation * @@ -61,19 +66,4 @@ public function __construct( $this->ratingFactory = $ratingFactory; parent::__construct($context); } - - /** - * @return bool - */ - protected function _isAllowed() - { - switch ($this->getRequest()->getActionName()) { - case 'pending': - return $this->_authorization->isAllowed('Magento_Review::pending'); - break; - default: - return $this->_authorization->isAllowed('Magento_Review::reviews_all'); - break; - } - } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php index 75015d65e1a18..1289299b51eb5 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php @@ -7,10 +7,21 @@ use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; +use Magento\Review\Model\Review; +/** + * Delete action. + */ class Delete extends ProductController { /** + * @var Review + */ + private $model; + + /** + * Execute action. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -18,22 +29,67 @@ public function execute() /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $reviewId = $this->getRequest()->getParam('id', false); - try { - $this->reviewFactory->create()->setId($reviewId)->aggregate()->delete(); - - $this->messageManager->addSuccess(__('The review has been deleted.')); - if ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('review/*/pending'); - } else { - $resultRedirect->setPath('review/*/'); + if ($this->getRequest()->isPost()) { + try { + $this->getModel()->aggregate()->delete(); + + $this->messageManager->addSuccess(__('The review has been deleted.')); + if ($this->getRequest()->getParam('ret') == 'pending') { + $resultRedirect->setPath('review/*/pending'); + } else { + $resultRedirect->setPath('review/*/'); + } + + return $resultRedirect; + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addError($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addException($e, __('Something went wrong deleting this review.')); } - return $resultRedirect; - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong deleting this review.')); } return $resultRedirect->setPath('review/*/edit/', ['id' => $reviewId]); } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + if (parent::_isAllowed()) { + return true; + } + + if (!$this->_authorization->isAllowed('Magento_Review::pending')) { + return false; + } + + if ($this->getModel()->getStatusId() != Review::STATUS_PENDING) { + $this->messageManager->addErrorMessage( + __( + 'You don’t have permission to perform this operation.' + . ' The selected review must be in Pending Status.' + ) + ); + + return false; + } + + return true; + } + + /** + * Returns requested model. + * + * @return Review + */ + private function getModel(): Review + { + if ($this->model === null) { + $this->model = $this->reviewFactory->create() + ->load($this->getRequest()->getParam('id', false)); + } + + return $this->model; + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php index a39b22ec080c6..86964fe38e952 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php @@ -7,10 +7,21 @@ use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; +use Magento\Review\Model\Review; +/** + * Edit action. + */ class Edit extends ProductController { /** + * @var Review + */ + private $review; + + /** + * Execute action. + * * @return \Magento\Backend\Model\View\Result\Page */ public function execute() @@ -23,4 +34,46 @@ public function execute() $resultPage->addContent($resultPage->getLayout()->createBlock(\Magento\Review\Block\Adminhtml\Edit::class)); return $resultPage; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + if (parent::_isAllowed()) { + return true; + } + + if (!$this->_authorization->isAllowed('Magento_Review::pending')) { + return false; + } + + if ($this->getModel()->getStatusId() != Review::STATUS_PENDING) { + $this->messageManager->addErrorMessage( + __( + 'You don’t have permission to perform this operation.' + . ' The selected review must be in Pending Status.' + ) + ); + + return false; + } + + return true; + } + + /** + * Returns requested model. + * + * @return Review + */ + private function getModel(): Review + { + if ($this->review === null) { + $this->review = $this->reviewFactory->create() + ->load($this->getRequest()->getParam('id', false)); + } + + return $this->review; + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php b/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php index c6e9cc81d5814..42fc6bf734ab9 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php @@ -46,7 +46,7 @@ public function execute() { $response = new DataObject(); $id = $this->getRequest()->getParam('id'); - if (intval($id) > 0) { + if ((int) $id > 0) { $product = $this->productRepository->getById($id); $response->setId($id); $response->addData($product->getData()); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php index c792540000233..c9d0f5f0259b1 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php @@ -5,24 +5,68 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Registry; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory; +use Magento\Review\Model\ReviewFactory; +/** + * Mass Delete action. + */ class MassDelete extends ProductController { /** + * @var Collection + */ + private $collection; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param ReviewFactory $reviewFactory + * @param RatingFactory $ratingFactory + * @param CollectionFactory $collectionFactory + */ + public function __construct( + Context $context, + Registry $coreRegistry, + ReviewFactory $reviewFactory, + RatingFactory $ratingFactory, + CollectionFactory $collectionFactory + ) { + parent::__construct($context, $coreRegistry, $reviewFactory, $ratingFactory); + $this->collectionFactory = $collectionFactory; + } + + /** + * Execute action. + * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); } else { try { - foreach ($reviewsIds as $reviewId) { - $model = $this->reviewFactory->create()->load($reviewId); + foreach ($this->getCollection() as $model) { $model->delete(); } $this->messageManager->addSuccess( @@ -39,4 +83,54 @@ public function execute() $resultRedirect->setPath('review/*/' . $this->getRequest()->getParam('ret', 'index')); return $resultRedirect; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + if (parent::_isAllowed()) { + return true; + } + + if (!$this->_authorization->isAllowed('Magento_Review::pending')) { + return false; + } + + foreach ($this->getCollection() as $model) { + if ($model->getStatusId() != Review::STATUS_PENDING) { + $this->messageManager->addErrorMessage( + __( + 'You don’t have permission to perform this operation.' + . ' Selected reviews must be in Pending Status only.' + ) + ); + + return false; + } + } + + return true; + } + + /** + * Returns requested collection. + * + * @return Collection + */ + private function getCollection(): Collection + { + if ($this->collection === null) { + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter( + 'main_table.' . $collection->getResource() + ->getIdFieldName(), + $this->getRequest()->getParam('reviews') + ); + + $this->collection = $collection; + } + + return $this->collection; + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php index 2769a35ba9a48..edcef8c8d6653 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php @@ -5,25 +5,69 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Registry; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory; +use Magento\Review\Model\ReviewFactory; +/** + * Mass Update Status action. + */ class MassUpdateStatus extends ProductController { /** + * @var Collection + */ + private $collection; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param ReviewFactory $reviewFactory + * @param RatingFactory $ratingFactory + * @param CollectionFactory $collectionFactory + */ + public function __construct( + Context $context, + Registry $coreRegistry, + ReviewFactory $reviewFactory, + RatingFactory $ratingFactory, + CollectionFactory $collectionFactory + ) { + parent::__construct($context, $coreRegistry, $reviewFactory, $ratingFactory); + $this->collectionFactory = $collectionFactory; + } + + /** + * Execute action. + * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); } else { try { $status = $this->getRequest()->getParam('status'); - foreach ($reviewsIds as $reviewId) { - $model = $this->reviewFactory->create()->load($reviewId); + foreach ($this->getCollection() as $model) { $model->setStatusId($status)->save()->aggregate(); } $this->messageManager->addSuccess( @@ -43,4 +87,54 @@ public function execute() $resultRedirect->setPath('review/*/' . $this->getRequest()->getParam('ret', 'index')); return $resultRedirect; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + if (parent::_isAllowed()) { + return true; + } + + if (!$this->_authorization->isAllowed('Magento_Review::pending')) { + return false; + } + + foreach ($this->getCollection() as $model) { + if ($model->getStatusId() != Review::STATUS_PENDING) { + $this->messageManager->addErrorMessage( + __( + 'You don’t have permission to perform this operation. ' + . 'Selected reviews must be in Pending Status only.' + ) + ); + + return false; + } + } + + return true; + } + + /** + * Returns requested collection. + * + * @return Collection + */ + private function getCollection(): Collection + { + if ($this->collection === null) { + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter( + 'main_table.' . $collection->getResource() + ->getIdFieldName(), + $this->getRequest()->getParam('reviews') + ); + + $this->collection = $collection; + } + + return $this->collection; + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php index eca37d3fe24da..759ec36b9e834 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php @@ -13,9 +13,14 @@ class MassVisibleIn extends ProductController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Pending.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Pending.php index 75eaaaeeef24e..9c54f22814c15 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Pending.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Pending.php @@ -8,9 +8,14 @@ use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; +/** + * Pending reviews grid. + */ class Pending extends ProductController { /** + * Execute action. + * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() @@ -30,4 +35,13 @@ public function execute() $resultPage->addContent($resultPage->getLayout()->createBlock(\Magento\Review\Block\Adminhtml\Main::class)); return $resultPage; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Review::reviews_all') + || $this->_authorization->isAllowed('Magento_Review::pending'); + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/ReviewGrid.php b/app/code/Magento/Review/Controller/Adminhtml/Product/ReviewGrid.php index 017fbc95a8b9a..8c2324f35b09c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/ReviewGrid.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/ReviewGrid.php @@ -13,6 +13,9 @@ use Magento\Framework\View\LayoutFactory; use Magento\Framework\Controller\ResultFactory; +/** + * Review grid. + */ class ReviewGrid extends ProductController { /** @@ -39,6 +42,8 @@ public function __construct( } /** + * Execute action. + * * @return \Magento\Framework\Controller\Result\Raw */ public function execute() @@ -49,4 +54,13 @@ public function execute() $resultRaw->setContents($layout->createBlock(\Magento\Review\Block\Adminhtml\Grid::class)->toHtml()); return $resultRaw; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Review::reviews_all') + || $this->_authorization->isAllowed('Magento_Review::pending'); + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 7159b1825dc4d..44a6aba172b15 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -8,10 +8,21 @@ use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; +use Magento\Review\Model\Review; +/** + * Save Review action. + */ class Save extends ProductController { /** + * @var Review + */ + private $review; + + /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -20,7 +31,7 @@ public function execute() /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (($data = $this->getRequest()->getPostValue()) && ($reviewId = $this->getRequest()->getParam('id'))) { - $review = $this->reviewFactory->create()->load($reviewId); + $review = $this->getModel(); if (!$review->getId()) { $this->messageManager->addError(__('The review was removed by another user or does not exist.')); } else { @@ -61,15 +72,72 @@ public function execute() $nextId = (int)$this->getRequest()->getParam('next_item'); if ($nextId) { - $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); + $resultRedirect->setPath( + 'review/*/edit', + [ + 'id' => $nextId, + 'ret' => $this->getRequest() + ->getParam('ret'), + ] + ); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } + $productId = $this->getRequest()->getParam('productId'); + if ($productId) { + $resultRedirect->setPath("catalog/product/edit/id/$productId"); + } + $customerId = (int)$this->getRequest()->getParam('customerId'); + if ($customerId) { + $resultRedirect->setPath("customer/index/edit/id/$customerId"); + } return $resultRedirect; } $resultRedirect->setPath('review/*/'); return $resultRedirect; } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + if (parent::_isAllowed()) { + return true; + } + + if (!$this->_authorization->isAllowed('Magento_Review::pending')) { + return false; + } + + if ($this->getModel()->getStatusId() != Review::STATUS_PENDING) { + $this->messageManager->addErrorMessage( + __( + 'You don’t have permission to perform this operation.' + . ' The selected review must be in Pending Status.' + ) + ); + + return false; + } + + return true; + } + + /** + * Returns requested model. + * + * @return Review + */ + private function getModel(): Review + { + if (!$this->review) { + $this->review = $this->reviewFactory->create() + ->load($this->getRequest()->getParam('id', false)); + } + + return $this->review; + } } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php index 5535c3de26e43..c5610d135222a 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php @@ -5,6 +5,7 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\Exception\NotFoundException; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; @@ -12,19 +13,25 @@ class Delete extends RatingController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if ($this->getRequest()->getParam('id') > 0) { + $ratingId = (int)$this->getRequest()->getParam('id'); + if ($ratingId) { try { /** @var \Magento\Review\Model\Rating $model */ $model = $this->_objectManager->create(\Magento\Review\Model\Rating::class); - $model->load($this->getRequest()->getParam('id'))->delete(); - $this->messageManager->addSuccess(__('You deleted the rating.')); + $model->load($ratingId)->delete(); + $this->messageManager->addSuccessMessage(__('You deleted the rating.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('review/rating/edit', ['id' => $this->getRequest()->getParam('id')]); return $resultRedirect; } diff --git a/app/code/Magento/Review/Controller/Product.php b/app/code/Magento/Review/Controller/Product.php index fea94dae23bcc..80f8e931c1d33 100644 --- a/app/code/Magento/Review/Controller/Product.php +++ b/app/code/Magento/Review/Controller/Product.php @@ -219,6 +219,11 @@ protected function loadProduct($productId) try { $product = $this->productRepository->getById($productId); + + if (!in_array($this->storeManager->getStore()->getWebsiteId(), $product->getWebsiteIds())) { + throw new NoSuchEntityException(); + } + if (!$product->isVisibleInCatalog() || !$product->isVisibleInSiteVisibility()) { throw new NoSuchEntityException(); } diff --git a/app/code/Magento/Review/Controller/Product/ListAction.php b/app/code/Magento/Review/Controller/Product/ListAction.php index dd8b272867c55..26344d125172a 100644 --- a/app/code/Magento/Review/Controller/Product/ListAction.php +++ b/app/code/Magento/Review/Controller/Product/ListAction.php @@ -26,8 +26,8 @@ protected function getProductPage($product) $resultPage->getConfig()->setPageLayout($product->getPageLayout()); } $urlSafeSku = rawurlencode($product->getSku()); - $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]); $resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], null, false); + $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]); $resultPage->addUpdate($product->getCustomLayoutUpdate()); return $resultPage; } diff --git a/app/code/Magento/Review/Controller/Product/ListAjax.php b/app/code/Magento/Review/Controller/Product/ListAjax.php index 32d608704c241..d6aa26f72c577 100644 --- a/app/code/Magento/Review/Controller/Product/ListAjax.php +++ b/app/code/Magento/Review/Controller/Product/ListAjax.php @@ -14,17 +14,16 @@ class ListAjax extends ProductController /** * Show list of product's reviews * - * @return \Magento\Framework\View\Result\Layout + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|\Magento\Framework\View\Result\Layout */ public function execute() { if (!$this->initProduct()) { - throw new LocalizedException(__('Cannot initialize product')); - } else { - /** @var \Magento\Framework\View\Result\Layout $resultLayout */ - $resultLayout = $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); + /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ + $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); + return $resultForward->forward('noroute'); } - return $resultLayout; + return $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); } } diff --git a/app/code/Magento/Review/Controller/Product/Post.php b/app/code/Magento/Review/Controller/Product/Post.php index be18f8fe25bbe..67c38f25d7ce3 100644 --- a/app/code/Magento/Review/Controller/Product/Post.php +++ b/app/code/Magento/Review/Controller/Product/Post.php @@ -22,7 +22,7 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->formKeyValidator->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->formKeyValidator->validate($this->getRequest())) { $resultRedirect->setUrl($this->_redirect->getRefererUrl()); return $resultRedirect; } diff --git a/app/code/Magento/Review/Helper/Action/Pager.php b/app/code/Magento/Review/Helper/Action/Pager.php index 6f374157b3761..39c4f3c09d0b7 100644 --- a/app/code/Magento/Review/Helper/Action/Pager.php +++ b/app/code/Magento/Review/Helper/Action/Pager.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Review\Helper\Action; use Magento\Framework\Exception\LocalizedException; @@ -45,8 +43,10 @@ class Pager extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Backend\Model\Session $backendSession */ - public function __construct(\Magento\Framework\App\Helper\Context $context, \Magento\Backend\Model\Session $backendSession) - { + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Backend\Model\Session $backendSession + ) { $this->_backendSession = $backendSession; parent::__construct($context); } @@ -83,7 +83,7 @@ public function setItems(array $items) */ protected function _loadItems() { - if (is_null($this->_items)) { + if ($this->_items === null) { $this->_items = (array)$this->_backendSession->getData($this->_getStorageKey()); } } diff --git a/app/code/Magento/Review/Helper/Data.php b/app/code/Magento/Review/Helper/Data.php index 4d195ba65d1cd..195293cde63f3 100644 --- a/app/code/Magento/Review/Helper/Data.php +++ b/app/code/Magento/Review/Helper/Data.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Review\Helper; /** @@ -77,7 +75,10 @@ public function getDetailHtml($origDetail) */ public function getIsGuestAllowToWrite() { - return $this->scopeConfig->isSetFlag(self::XML_REVIEW_GUETS_ALLOW, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return $this->scopeConfig->isSetFlag( + self::XML_REVIEW_GUETS_ALLOW, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php index 3f54c17f6ff7c..5567c21ba12ee 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php @@ -5,6 +5,9 @@ */ namespace Magento\Review\Model\ResourceModel; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; + /** * Rating resource model * @@ -12,6 +15,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -34,13 +38,19 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $_logger; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary + * @param Review\Summary $reviewSummary * @param string $connectionName + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -48,12 +58,14 @@ public function __construct( \Magento\Framework\Module\Manager $moduleManager, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary, - $connectionName = null + $connectionName = null, + ScopeConfigInterface $scopeConfig = null ) { $this->moduleManager = $moduleManager; $this->_storeManager = $storeManager; $this->_logger = $logger; $this->_reviewSummary = $reviewSummary; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct($context, $connectionName); } @@ -178,6 +190,8 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) } /** + * Process rating codes. + * * @param \Magento\Framework\Model\AbstractModel $object * @return $this */ @@ -201,6 +215,8 @@ protected function processRatingCodes(\Magento\Framework\Model\AbstractModel $ob } /** + * Process rating stores. + * * @param \Magento\Framework\Model\AbstractModel $object * @return $this */ @@ -224,6 +240,8 @@ protected function processRatingStores(\Magento\Framework\Model\AbstractModel $o } /** + * Delete rating data. + * * @param int $ratingId * @param string $table * @param array $storeIds @@ -247,6 +265,8 @@ protected function deleteRatingData($ratingId, $table, array $storeIds) } /** + * Insert rating data. + * * @param string $table * @param array $data * @return void @@ -269,6 +289,7 @@ protected function insertRatingData($table, array $data) /** * Perform actions after object delete + * * Prepare rating data for reaggregate all data for reviews * * @param \Magento\Framework\Model\AbstractModel $object @@ -277,7 +298,12 @@ protected function insertRatingData($table, array $data) protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object) { parent::_afterDelete($object); - if (!$this->moduleManager->isEnabled('Magento_Review')) { + if (!$this->moduleManager->isEnabled('Magento_Review') && + !$this->scopeConfig->getValue( + \Magento\Review\Observer\PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ) { return $this; } $data = $this->_getEntitySummaryData($object); @@ -425,9 +451,11 @@ public function getReviewSummary($object, $onlyForCurrentStore = true) $data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]); + $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null; + if ($onlyForCurrentStore) { foreach ($data as $row) { - if ($row['store_id'] == $this->_storeManager->getStore()->getId()) { + if ($row['store_id'] !== $currentStore) { $object->addData($row); } } diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php index cbbe17a47c0ad..0dcb9da6a8c75 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php @@ -141,7 +141,6 @@ public function setStoreFilter($storeId) 'main_table.rating_id = store.rating_id', [] ); - // ->group('main_table.rating_id') $this->_isStoreJoined = true; } $inCondition = $connection->prepareSqlCondition('store.store_id', ['in' => $storeId]); diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Option.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Option.php index 6f68000a1efff..ef4acb6c90cb8 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Option.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Option.php @@ -154,7 +154,7 @@ public function addVote($option) } $connection->commit(); } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); throw new \Exception($e->getMessage()); } return $this; diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 830354796907f..4e55484e5dd94 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Model\ResourceModel\Review\Product; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; @@ -403,12 +404,20 @@ public function getAllIds($limit = null, $offset = null) public function getResultingIds() { $idsSelect = clone $this->getSelect(); - $idsSelect->reset(Select::LIMIT_COUNT); - $idsSelect->reset(Select::LIMIT_OFFSET); - $idsSelect->reset(Select::COLUMNS); - $idsSelect->reset(Select::ORDER); - $idsSelect->columns('rt.review_id'); - return $this->getConnection()->fetchCol($idsSelect); + $data = $this->getConnection() + ->fetchAll( + $idsSelect + ->reset(Select::LIMIT_COUNT) + ->reset(Select::LIMIT_OFFSET) + ->columns('rt.review_id') + ); + + return array_map( + function ($value) { + return $value['review_id']; + }, + $data + ); } /** @@ -540,6 +549,16 @@ protected function _afterLoad() return $this; } + /** + * Not add store ids to items + * + * @return $this + */ + protected function prepareStoreId() + { + return $this; + } + /** * Add store data * diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php index b69065fbaf6cd..f18bc2094930a 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Model\ResourceModel\Review; use Magento\Framework\Model\AbstractModel; @@ -73,4 +74,46 @@ public function reAggregate($summary) } return $this; } + + /** + * Append review summary fields to product collection + * + * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection + * @param string $storeId + * @param string $entityCode + * @return Summary + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function appendSummaryFieldsToCollection( + \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection, + string $storeId, + string $entityCode + ) { + if (!$productCollection->isLoaded()) { + $summaryEntitySubSelect = $this->getConnection()->select(); + $summaryEntitySubSelect + ->from( + ['review_entity' => $this->getTable('review_entity')], + ['entity_id'] + )->where( + 'entity_code = ?', + $entityCode + ); + $joinCond = new \Zend_Db_Expr( + "e.entity_id = review_summary.entity_pk_value AND review_summary.store_id = {$storeId}" + . " AND review_summary.entity_type = ({$summaryEntitySubSelect})" + ); + $productCollection->getSelect() + ->joinLeft( + ['review_summary' => $this->getMainTable()], + $joinCond, + [ + 'reviews_count' => new \Zend_Db_Expr("IFNULL(review_summary.reviews_count, 0)"), + 'rating_summary' => new \Zend_Db_Expr("IFNULL(review_summary.rating_summary, 0)") + ] + ); + } + + return $this; + } } diff --git a/app/code/Magento/Review/Model/Review.php b/app/code/Magento/Review/Model/Review.php index c00af3fc61407..0c581f570ef0c 100644 --- a/app/code/Magento/Review/Model/Review.php +++ b/app/code/Magento/Review/Model/Review.php @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Model; +use Magento\Framework\DataObject; use Magento\Catalog\Model\Product; use Magento\Framework\DataObject\IdentityInterface; use Magento\Review\Model\ResourceModel\Review\Product\Collection as ProductCollection; @@ -99,6 +101,7 @@ class Review extends \Magento\Framework\Model\AbstractModel implements IdentityI /** * Review model summary * + * @deprecated Summary factory injected as separate property * @var \Magento\Review\Model\Review\Summary */ protected $_reviewSummary; @@ -213,6 +216,7 @@ public function aggregate() /** * Get entity summary * + * @deprecated * @param Product $product * @param int $storeId * @return void @@ -300,10 +304,12 @@ public function afterDeleteCommit() } /** - * Append review summary to product collection + * Append review summary data object to product collection * + * @deprecated * @param ProductCollection $collection * @return $this + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function appendSummary($collection) { @@ -312,7 +318,7 @@ public function appendSummary($collection) $entityIds[] = $item->getEntityId(); } - if (sizeof($entityIds) == 0) { + if (count($entityIds) === 0) { return $this; } @@ -327,6 +333,9 @@ public function appendSummary($collection) $item->setRatingSummary($summary); } } + if (!$item->getRatingSummary()) { + $item->setRatingSummary(new DataObject()); + } } return $this; @@ -352,7 +361,7 @@ public function isAvailableOnStore($store = null) { $store = $this->_storeManager->getStore($store); if ($store) { - return in_array($store->getId(), (array) $this->getStores()); + return in_array($store->getId(), (array)$this->getStores()); } return false; } diff --git a/app/code/Magento/Review/Model/ReviewSummary.php b/app/code/Magento/Review/Model/ReviewSummary.php new file mode 100644 index 0000000000000..6e9568ecf354c --- /dev/null +++ b/app/code/Magento/Review/Model/ReviewSummary.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Model; + +use Magento\Framework\Model\AbstractModel; +use Magento\Review\Model\ResourceModel\Review\Summary\CollectionFactory as SummaryCollectionFactory; + +/** + * ReviewSummary model. + */ +class ReviewSummary +{ + /** + * @var SummaryCollectionFactory + */ + private $summaryCollectionFactory; + + /** + * @param SummaryCollectionFactory $sumColFactory + */ + public function __construct( + SummaryCollectionFactory $sumColFactory + ) { + $this->summaryCollectionFactory = $sumColFactory; + } + + /** + * Append review summary data to product + * + * @param AbstractModel $object + * @param int $storeId + * @param int $entityType + */ + public function appendSummaryDataToObject(AbstractModel $object, int $storeId, int $entityType = 1) + { + $summary = $this->summaryCollectionFactory->create() + ->addEntityFilter($object->getId(), $entityType) + ->addStoreFilter($storeId) + ->getFirstItem(); + $object->addData( + [ + 'reviews_count' => $summary->getData('reviews_count'), + 'rating_summary' => $summary->getData('rating_summary') + ] + ); + } +} diff --git a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php deleted file mode 100644 index 6256194cef53b..0000000000000 --- a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Review\Observer; - -use Magento\Framework\Event\ObserverInterface; - -class CatalogBlockProductCollectionBeforeToHtmlObserver implements ObserverInterface -{ - /** - * Review model - * - * @var \Magento\Review\Model\ReviewFactory - */ - protected $_reviewFactory; - - /** - * @param \Magento\Review\Model\ReviewFactory $reviewFactory - */ - public function __construct( - \Magento\Review\Model\ReviewFactory $reviewFactory - ) { - $this->_reviewFactory = $reviewFactory; - } - - /** - * Append review summary before rendering html - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - $productCollection = $observer->getEvent()->getCollection(); - if ($productCollection instanceof \Magento\Framework\Data\Collection) { - $productCollection->load(); - $this->_reviewFactory->create()->appendSummary($productCollection); - } - - return $this; - } -} diff --git a/app/code/Magento/Review/Observer/CatalogProductListCollectionAppendSummaryFieldsObserver.php b/app/code/Magento/Review/Observer/CatalogProductListCollectionAppendSummaryFieldsObserver.php new file mode 100644 index 0000000000000..bb69284b5f0b8 --- /dev/null +++ b/app/code/Magento/Review/Observer/CatalogProductListCollectionAppendSummaryFieldsObserver.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Observer; + +use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; +use Magento\Review\Model\ResourceModel\Review\SummaryFactory; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Append review summary to product list collection. + */ +class CatalogProductListCollectionAppendSummaryFieldsObserver implements ObserverInterface +{ + /** + * Review model + * + * @var Summary + */ + private $sumResourceFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param SummaryFactory $sumResourceFactory + * @param StoreManagerInterface $storeManager + */ + public function __construct( + SummaryFactory $sumResourceFactory, + StoreManagerInterface $storeManager + ) { + $this->sumResourceFactory = $sumResourceFactory; + $this->storeManager = $storeManager; + } + + /** + * Append review summary to collection + * + * @param EventObserver $observer + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(EventObserver $observer) + { + $productCollection = $observer->getEvent()->getCollection(); + $this->sumResourceFactory->create()->appendSummaryFieldsToCollection( + $productCollection, + $this->storeManager->getStore()->getId(), + \Magento\Review\Model\Review::ENTITY_PRODUCT_CODE + ); + + return $this; + } +} diff --git a/app/code/Magento/Review/Observer/PredispatchReviewObserver.php b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php new file mode 100644 index 0000000000000..bdca0f5ecb1ec --- /dev/null +++ b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Review\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\UrlInterface; +use Magento\Review\Block\Product\ReviewRenderer; +use Magento\Store\Model\ScopeInterface; + +/** + * Class PredispatchReviewObserver + */ +class PredispatchReviewObserver implements ObserverInterface +{ + /** + * Configuration path to review active setting + */ + const XML_PATH_REVIEW_ACTIVE = 'catalog/review/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var UrlInterface + */ + private $url; + + /** + * PredispatchReviewObserver constructor. + * + * @param ScopeConfigInterface $scopeConfig + * @param UrlInterface $url + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + UrlInterface $url + ) { + $this->scopeConfig = $scopeConfig; + $this->url = $url; + } + /** + * Redirect review routes to 404 when review module is disabled. + * + * @param Observer $observer + */ + public function execute(Observer $observer) + { + if (!$this->scopeConfig->getValue( + self::XML_PATH_REVIEW_ACTIVE, + ScopeInterface::SCOPE_STORE + ) + ) { + $defaultNoRouteUrl = $this->scopeConfig->getValue( + 'web/default/no_route', + ScopeInterface::SCOPE_STORE + ); + $redirectUrl = $this->url->getUrl($defaultNoRouteUrl); + $observer->getControllerAction() + ->getResponse() + ->setRedirect($redirectUrl); + } + } +} diff --git a/app/code/Magento/Review/Observer/TagProductCollectionLoadAfterObserver.php b/app/code/Magento/Review/Observer/TagProductCollectionLoadAfterObserver.php deleted file mode 100644 index 52d6f09a08557..0000000000000 --- a/app/code/Magento/Review/Observer/TagProductCollectionLoadAfterObserver.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Review\Observer; - -use Magento\Framework\Event\ObserverInterface; - -class TagProductCollectionLoadAfterObserver implements ObserverInterface -{ - /** - * Review model - * - * @var \Magento\Review\Model\ReviewFactory - */ - protected $_reviewFactory; - - /** - * @param \Magento\Review\Model\ReviewFactory $reviewFactory - */ - public function __construct( - \Magento\Review\Model\ReviewFactory $reviewFactory - ) { - $this->_reviewFactory = $reviewFactory; - } - - /** - * Add review summary info for tagged product collection - * - * @param \Magento\Framework\Event\Observer $observer - * @return $this - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - $collection = $observer->getEvent()->getCollection(); - $this->_reviewFactory->create()->appendSummary($collection); - - return $this; - } -} diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminChangeReviewStatusActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminChangeReviewStatusActionGroup.xml new file mode 100644 index 0000000000000..b8292f6aa3820 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminChangeReviewStatusActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeReviewStatusActionGroup"> + <arguments> + <argument name="status" type="string" defaultValue="1"/> + </arguments> + <selectOption selector="{{AdminReviewEditSection.status}}" userInput="{{status}}" stepKey="changeReviewStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminDeleteReviewsByUserNicknameActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminDeleteReviewsByUserNicknameActionGroup.xml new file mode 100644 index 0000000000000..fcc2b238d6275 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminDeleteReviewsByUserNicknameActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteReviewsByUserNicknameActionGroup"> + <arguments> + <argument name="nickname" type="string" defaultValue="{{SimpleProductReview.nickname}}"/> + </arguments> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilters"/> + <fillField selector="{{AdminReviewGridSection.nicknameColumnFilter}}" userInput="{{nickname}}" stepKey="fillNickname"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <selectOption selector="{{AdminReviewGridSection.massActions}}" userInput="selectAll" stepKey="selectAll"/> + <selectOption selector="{{AdminReviewGridSection.massActionsSelect}}" userInput="delete" stepKey="clickDeleteActionDropdown"/> + <click selector="{{AdminReviewGridSection.submit}}" stepKey="clickSubmit"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModalPopUp"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="record(s) have been deleted." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenPendingReviewsPageActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenPendingReviewsPageActionGroup.xml new file mode 100644 index 0000000000000..960af943e8436 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenPendingReviewsPageActionGroup.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenPendingReviewsPageActionGroup"> + <amOnPage url="{{AdminPendingReviewsPage.url}}" stepKey="openReviewsPageActionGroup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewByUserNicknameActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewByUserNicknameActionGroup.xml new file mode 100644 index 0000000000000..6d0d0ee9f7b44 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewByUserNicknameActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenReviewByUserNicknameActionGroup"> + <arguments> + <argument name="nickname" type="string" defaultValue="{{SimpleProductReview.nickname}}"/> + </arguments> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilters"/> + <fillField selector="{{AdminReviewGridSection.nicknameColumnFilter}}" userInput="{{nickname}}" stepKey="fillNickname"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForEditReviewPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewsPageActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewsPageActionGroup.xml new file mode 100644 index 0000000000000..3865c8f20a980 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminOpenReviewsPageActionGroup.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenReviewsPageActionGroup"> + <amOnPage url="{{AdminReviewsPage.url}}" stepKey="openReviewsPageActionGroup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml new file mode 100644 index 0000000000000..2a9aca207c9c6 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/AdminSaveReviewActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveReviewActionGroup"> + <click selector="{{AdminReviewEditSection.saveReview}}" stepKey="saveReview"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the review." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAddProductReviewActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAddProductReviewActionGroup.xml new file mode 100644 index 0000000000000..c1e324d46a6de --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAddProductReviewActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddProductReviewActionGroup"> + <arguments> + <argument name="review" type="entity" defaultValue="SimpleProductReview"/> + </arguments> + <click selector="{{StorefrontProductReviewsSection.reviewsTab}}" stepKey="openReviewTab"/> + <fillField selector="{{StorefrontReviewFormSection.nicknameField}}" userInput="{{review.nickname}}" stepKey="fillNicknameField"/> + <fillField selector="{{StorefrontReviewFormSection.summaryField}}" userInput="{{review.title}}" stepKey="fillSummaryField"/> + <fillField selector="{{StorefrontReviewFormSection.reviewField}}" userInput="{{review.detail}}" stepKey="fillReviewField"/> + <click selector="{{StorefrontReviewFormSection.submitReview}}" stepKey="clickSubmitReview"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="You submitted your review for moderation." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAssertReviewAtProductPageActionGroup.xml b/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAssertReviewAtProductPageActionGroup.xml new file mode 100644 index 0000000000000..a0422c9f4795d --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/ActionGroup/StorefrontAssertReviewAtProductPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertReviewAtProductPageActionGroup"> + <arguments> + <argument name="review" type="entity" defaultValue="SimpleProductReview"/> + <argument name="rowIndex" type="string"/> + </arguments> + <see selector="{{StorefrontProductReviewsSection.reviewTitle(rowIndex)}}" userInput="{{review.title}}" stepKey="seeReviewTitle"/> + <see selector="{{StorefrontProductReviewsSection.reviewContent(rowIndex)}}" userInput="{{review.detail}}" stepKey="seeReviewContent"/> + <see selector="{{StorefrontProductReviewsSection.reviewAuthor(rowIndex)}}" userInput="{{review.nickname}}" stepKey="seeAuthorReview"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Review/Test/Mftf/Data/ProductReviewData.xml b/app/code/Magento/Review/Test/Mftf/Data/ProductReviewData.xml new file mode 100644 index 0000000000000..e0b825f488a48 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Data/ProductReviewData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SimpleProductReview"> + <data key="nickname" unique="suffix">nickname</data> + <data key="title">Review title</data> + <data key="detail">Simple product review</data> + </entity> +</entities> diff --git a/app/code/Magento/Review/Test/Mftf/LICENSE.txt b/app/code/Magento/Review/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Review/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 <insert your license name here>" 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/app/code/Magento/Review/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Review/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Review/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 <insert your license name here>" 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/app/code/Magento/Review/Test/Mftf/Page/AdminPendingReviewsPage.xml b/app/code/Magento/Review/Test/Mftf/Page/AdminPendingReviewsPage.xml new file mode 100644 index 0000000000000..aa12db99630e3 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Page/AdminPendingReviewsPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminPendingReviewsPage" url="review/product/pending/" area="admin" module="Magento_Review"> + <section name="AdminReviewEditSection"/> + <section name="AdminReviewGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Review/Test/Mftf/Page/AdminReviewsPage.xml b/app/code/Magento/Review/Test/Mftf/Page/AdminReviewsPage.xml new file mode 100644 index 0000000000000..c5c712765f2d7 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Page/AdminReviewsPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminReviewsPage" url="review/product/index/" area="admin" module="Magento_Review"> + <section name="AdminReviewEditSection"/> + <section name="AdminReviewGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Review/Test/Mftf/README.md b/app/code/Magento/Review/Test/Mftf/README.md new file mode 100644 index 0000000000000..86af37286a7d6 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Review Functional Tests + +The Functional Test Module for **Magento Review** module. diff --git a/app/code/Magento/Review/Test/Mftf/Section/AdminReviewEditSection.xml b/app/code/Magento/Review/Test/Mftf/Section/AdminReviewEditSection.xml new file mode 100644 index 0000000000000..b2d789f4b6b8e --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Section/AdminReviewEditSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminReviewEditSection"> + <element name="status" type="select" selector="#status_id"/> + <element name="saveReview" type="button" timeout="30" selector="#save_button"/> + </section> +</sections> diff --git a/app/code/Magento/Review/Test/Mftf/Section/AdminReviewGridSection.xml b/app/code/Magento/Review/Test/Mftf/Section/AdminReviewGridSection.xml new file mode 100644 index 0000000000000..c097464a08fc4 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Section/AdminReviewGridSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminReviewGridSection"> + <element name="nicknameColumnFilter" type="input" selector="#reviwGrid_filter_nickname"/> + <element name="massActions" type="button" selector="#reviwGrid_massaction-mass-select"/> + <element name="massActionsSelect" type="button" selector="#reviwGrid_massaction-select"/> + <element name="submit" type="button" timeout="30" selector="#reviwGrid_massaction button[title='Submit']"/> + </section> +</sections> diff --git a/app/code/Magento/Review/Test/Mftf/Section/StorefrontProductReviewsSection.xml b/app/code/Magento/Review/Test/Mftf/Section/StorefrontProductReviewsSection.xml new file mode 100644 index 0000000000000..21820c6551b10 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Section/StorefrontProductReviewsSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductReviewsSection"> + <element name="reviewsTab" type="button" timeout="30" selector="#tab-label-reviews-title"/> + <element name="reviewsBlock" type="block" selector="#customer-reviews"/> + <element name="reviewTitle" type="text" selector=".item.review-item:nth-of-type({{row}}) .review-title" parameterized="true"/> + <element name="reviewContent" type="text" selector=".item.review-item:nth-of-type({{row}}) .review-content" parameterized="true"/> + <element name="reviewAuthor" type="text" selector=".item.review-item:nth-of-type({{row}}) .review-author .review-details-value" parameterized="true"/> + <!-- The tab transform to an accordion when window resize --> + <element name="reviewsSectionToggleState" type="button" selector="div#tab-label-reviews[aria-expanded='{{expanded}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Review/Test/Mftf/Section/StorefrontReviewFormSection.xml b/app/code/Magento/Review/Test/Mftf/Section/StorefrontReviewFormSection.xml new file mode 100644 index 0000000000000..839a5fec98a0f --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Section/StorefrontReviewFormSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontReviewFormSection"> + <element name="nicknameField" type="text" selector="#review-form [name='nickname']"/> + <element name="summaryField" type="text" selector="#review-form [name='title']"/> + <element name="reviewField" type="textarea" selector="#review-form [name='detail']"/> + <element name="submitReview" type="button" timeout="30" selector="#review-form button[type=submit]"/> + </section> +</sections> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml new file mode 100644 index 0000000000000..d573850a57c51 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest"> + <annotations> + <features value="Review"/> + <stories value="Frontend review representation"/> + <title value="Ensure that accordion anchor is visible on viewport once clicked"/> + <description value="Ensure that accordion anchor is visible on viewport once clicked"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-18013"/> + <group value="review"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Create product with description --> + <createData entity="ApiProductWithDescription" stepKey="createProduct"/> + + <!-- Create 4 product attributes visible on frontend --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createFirstAttribute"/> + <createData entity="productAttributeOption1" stepKey="createOption"> + <requiredEntity createDataKey="createFirstAttribute"/> + </createData> + <createData entity="ProductAttributeText" stepKey="createSecondAttribute"/> + <createData entity="ProductAttributeText" stepKey="createThirdAttribute"/> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createFourthAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstProductOption"> + <requiredEntity createDataKey="createFourthAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createSecondProductOption"> + <requiredEntity createDataKey="createFourthAttribute"/> + </createData> + + <!-- Add all created attributes to Default Attribute Set --> + <createData entity="AddToDefaultSet" stepKey="addFirstAttributeToAttributeSet"> + <requiredEntity createDataKey="createFirstAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addSecondAttributeToAttributeSet"> + <requiredEntity createDataKey="createSecondAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addThirdAttributeToAttributeSet"> + <requiredEntity createDataKey="createThirdAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addFourthAttributeToAttributeSet"> + <requiredEntity createDataKey="createFourthAttribute"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstAttribute" stepKey="deleteFirstAttribute"/> + <deleteData createDataKey="createSecondAttribute" stepKey="deleteSecondAttribute"/> + <deleteData createDataKey="createThirdAttribute" stepKey="deleteThirdAttribute"/> + <deleteData createDataKey="createFourthAttribute" stepKey="deleteFourthAttribute"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!--Logout from customer account--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + <!-- Delete customer --> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="CustomerEntityOne.email"/> + </actionGroup> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearCustomersFilters"/> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!-- Edit the product and set those attributes values --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="findCreatedProductInGrid"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToEditProductPage"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customAttributeDropdownField($createFirstAttribute.attribute[attribute_code]$)}}" userInput="$createOption.option[store_labels][0][label]$" stepKey="setFirstAttributeValue"/> + <fillField selector="{{AdminProductFormSection.customAttributeInputField($createSecondAttribute.attribute[attribute_code]$)}}" userInput="{{colorProductAttribute1.name}}" stepKey="setSecondAttributeValue"/> + <fillField selector="{{AdminProductFormSection.customAttributeInputField($createThirdAttribute.attribute[attribute_code]$)}}" userInput="{{colorProductAttribute2.name}}" stepKey="setThirdAttributeValue"/> + <selectOption selector="{{AdminProductFormSection.customAttributeDropdownField($createFourthAttribute.attribute[attribute_code]$)}}" userInput="$createSecondProductOption.option[store_labels][0][label]$" stepKey="setFourthAttributeValue"/> + + <!-- Save product form --> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + + <!-- Go to frontend and make a user account and login with it --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + + <!-- Go to the product view page --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openCreatedProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Click on reviews and add 2 reviews with current user --> + <actionGroup ref="StorefrontAddProductReviewActionGroup" stepKey="addFirstReview"/> + <actionGroup ref="StorefrontAddProductReviewActionGroup" stepKey="addSecondReview"/> + + <!-- Go to Pending reviews page and clear filters --> + <actionGroup ref="AdminOpenPendingReviewsPageActionGroup" stepKey="openReviewsPage"/> + + <!-- Moderate first product reviews: change review status from pending to approved, save --> + <actionGroup ref="AdminOpenReviewByUserNicknameActionGroup" stepKey="openFirstCustomerReviews"/> + <actionGroup ref="AdminChangeReviewStatusActionGroup" stepKey="changeFirstReviewStatus"/> + <actionGroup ref="AdminSaveReviewActionGroup" stepKey="saveModeratedFirstReview"/> + + <!-- Moderate second product reviews: change review status from pending to approved, save --> + <actionGroup ref="AdminOpenReviewByUserNicknameActionGroup" stepKey="openSecondCustomerReviews"/> + <actionGroup ref="AdminChangeReviewStatusActionGroup" stepKey="changeSecondReviewStatus"/> + <actionGroup ref="AdminSaveReviewActionGroup" stepKey="saveModeratedSecondReview"/> + + <!-- Assert that product page has the description --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <click selector="{{StorefrontProductInfoDetailsSection.detailsTab}}" stepKey="clickDetailsTab"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="waitProductDescription"/> + <see selector="{{StorefrontProductInfoMainSection.productDescription}}" userInput="$createProduct.custom_attributes[description]$" stepKey="assertProductDescription"/> + + <!-- Assert that product page has added reviews --> + <click selector="{{StorefrontProductReviewsSection.reviewsTab}}" stepKey="clickReviewTab"/> + <waitForElementVisible selector="{{StorefrontProductReviewsSection.reviewsBlock}}" stepKey="seeAllReviews"/> + <actionGroup ref="StorefrontAssertReviewAtProductPageActionGroup" stepKey="assertFirstReview"> + <argument name="rowIndex" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAssertReviewAtProductPageActionGroup" stepKey="assertSecondReview"> + <argument name="rowIndex" value="2"/> + </actionGroup> + + <!-- Assert that product page has all product attributes in More Info tab --> + <actionGroup ref="CheckAttributeInAdditionalInformationTabActionGroup" stepKey="checkFirstAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="$createFirstAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeValue" value="$createOption.option[store_labels][0][label]$"/> + </actionGroup> + <actionGroup ref="CheckAttributeInAdditionalInformationTabActionGroup" stepKey="checkSecondAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="$createSecondAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeValue" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + <actionGroup ref="CheckAttributeInAdditionalInformationTabActionGroup" stepKey="checkThirdAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="$createThirdAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeValue" value="{{colorProductAttribute2.name}}"/> + </actionGroup> + <actionGroup ref="CheckAttributeInAdditionalInformationTabActionGroup" stepKey="checkFourthAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="$createFourthAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeValue" value="$createSecondProductOption.option[store_labels][0][label]$"/> + </actionGroup> + + <!-- Collapse the view of the page to minimum width so that mobile view becomes visible --> + <resizeWindow width="400" height="590" stepKey="resizeWindowToMobileView"/> + + <!-- Assert that Details tab on product page become accordion --> + <click selector="{{StorefrontProductInfoDetailsSection.detailsTab}}" stepKey="clickDetails"/> + <seeElement selector="{{StorefrontProductInfoDetailsSection.detailsSectionToggleState('true')}}" stepKey="seeOpenDetailsTab"/> + <seeElement selector="{{StorefrontProductAdditionalInformationSection.moreInformationSectionToggleState('false')}}" stepKey="seeClosedMoreInformationTab"/> + <seeElement selector="{{StorefrontProductReviewsSection.reviewsSectionToggleState('false')}}" stepKey="seeClosedReviewTab"/> + + <!-- Scroll so that the description is visible and More info tab is on the upper middle of the page --> + <scrollTo selector="{{StorefrontProductInfoDetailsSection.detailsTab}}" stepKey="scrollToMoreInfoTab"/> + <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml new file mode 100644 index 0000000000000..2c460f45ec88e --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectReview"> + <annotations> + <features value="Review"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Review Pages"/> + <description value="Verify that the Secure URL configuration applies to the Review pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15713"/> + <group value="review"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/review/customer" stepKey="goToUnsecureReviewURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/review/customer" stepKey="seeSecureReviewURL"/> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Unit/Block/FormTest.php b/app/code/Magento/Review/Test/Unit/Block/FormTest.php index 2184385035c8d..1fd38551702ab 100644 --- a/app/code/Magento/Review/Test/Unit/Block/FormTest.php +++ b/app/code/Magento/Review/Test/Unit/Block/FormTest.php @@ -136,6 +136,9 @@ public function testGetAction($isSecure, $actionUrl, $productId) $this->assertEquals($actionUrl . '/id/' . $productId, $this->object->getAction()); } + /** + * @return array + */ public function getActionDataProvider() { return [ diff --git a/app/code/Magento/Review/Test/Unit/Block/Product/ReviewTest.php b/app/code/Magento/Review/Test/Unit/Block/Product/ReviewTest.php index 243b4e8389923..01868242d0e0c 100644 --- a/app/code/Magento/Review/Test/Unit/Block/Product/ReviewTest.php +++ b/app/code/Magento/Review/Test/Unit/Block/Product/ReviewTest.php @@ -204,6 +204,9 @@ public function testGetProductReviewUrl($isSecure, $actionUrl, $productId) $this->assertEquals($actionUrl . '/id/' . $productId, $this->block->getProductReviewUrl()); } + /** + * @return array + */ public function getProductReviewUrlDataProvider() { return [ diff --git a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php index 3186c0fcc3c57..73e85a7cdc179 100644 --- a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php +++ b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php @@ -105,7 +105,8 @@ class PostTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam']); + $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam', 'isPost']); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->response = $this->createPartialMock(\Magento\Framework\App\Response\Http::class, ['setRedirect']); $this->formKeyValidator = $this->createPartialMock( \Magento\Framework\Data\Form\FormKey\Validator::class, @@ -147,7 +148,11 @@ protected function setUp() $ratingFactory->expects($this->once())->method('create')->willReturn($this->rating); $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId']); + $this->store = $this->createPartialMock( + \Magento\Store\Model\Store::class, + ['getId', 'getWebsiteId'] + ); + $storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); $storeManager->expects($this->any())->method('getStore')->willReturn($this->store); @@ -211,15 +216,15 @@ public function testExecute() $this->reviewSession->expects($this->any())->method('getFormData') ->with(true) ->willReturn($reviewData); - $this->request->expects($this->at(0))->method('getParam') - ->with('category', false) - ->willReturn(false); - $this->request->expects($this->at(1))->method('getParam') - ->with('id') - ->willReturn(1); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['category', false, false], + ['id', null, 1], + ] + ); $product = $this->createPartialMock( \Magento\Catalog\Model\Product::class, - ['__wakeup', 'isVisibleInCatalog', 'isVisibleInSiteVisibility', 'getId'] + ['__wakeup', 'isVisibleInCatalog', 'isVisibleInSiteVisibility', 'getId', 'getWebsiteIds'] ); $product->expects($this->once()) ->method('isVisibleInCatalog') @@ -227,6 +232,15 @@ public function testExecute() $product->expects($this->once()) ->method('isVisibleInSiteVisibility') ->willReturn(true); + + $product->expects($this->once()) + ->method('getWebsiteIds') + ->willReturn([1]); + + $this->store->expects($this->once()) + ->method('getWebsiteId') + ->willReturn(1); + $this->productRepository->expects($this->any())->method('getById') ->with(1) ->willReturn($product); diff --git a/app/code/Magento/Review/Test/Unit/Model/ResourceModel/Review/CollectionTest.php b/app/code/Magento/Review/Test/Unit/Model/ResourceModel/Review/CollectionTest.php index b3d2cec648dc6..36cbe455fa890 100644 --- a/app/code/Magento/Review/Test/Unit/Model/ResourceModel/Review/CollectionTest.php +++ b/app/code/Magento/Review/Test/Unit/Model/ResourceModel/Review/CollectionTest.php @@ -147,6 +147,9 @@ public function testAddEntityFilter( $this->model->addEntityFilter($entity, $pkValue); } + /** + * @return array + */ public function addEntityFilterDataProvider() { return [ diff --git a/app/code/Magento/Review/Test/Unit/Model/ReviewSummaryTest.php b/app/code/Magento/Review/Test/Unit/Model/ReviewSummaryTest.php new file mode 100644 index 0000000000000..9723ece0c4904 --- /dev/null +++ b/app/code/Magento/Review/Test/Unit/Model/ReviewSummaryTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Test for Magento\Review\Model\ReviewSummary class. + */ +class ReviewSummaryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var MockObject + */ + private $reviewSummaryCollectionFactoryMock; + + /** + * @var \Magento\Review\Model\ReviewSummary | MockObject + */ + private $reviewSummary; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + protected function setUp() + { + $this->reviewSummaryCollectionFactoryMock = $this->createPartialMock( + \Magento\Review\Model\ResourceModel\Review\Summary\CollectionFactory::class, + ['create'] + ); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->reviewSummary = $this->objectManagerHelper->getObject( + \Magento\Review\Model\ReviewSummary::class, + [ + 'sumColFactory' => $this->reviewSummaryCollectionFactoryMock + ] + ); + } + + public function testAppendSummaryDataToObject() + { + $productId = 6; + $storeId = 4; + $testSummaryData = [ + 'reviews_count' => 2, + 'rating_summary' => 80 + ]; + $product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getId', 'addData', '__wakeup'] + ); + $product->expects($this->once())->method('getId')->will($this->returnValue($productId)); + $product->expects($this->once())->method('addData') + ->with($testSummaryData) + ->will($this->returnSelf()); + + $summaryData = $this->createPartialMock( + \Magento\Review\Model\Review\Summary::class, + ['getData', '__wakeup'] + ); + $summaryData->expects($this->atLeastOnce())->method('getData')->will( + $this->returnValueMap( + [ + ['reviews_count', null, $testSummaryData['reviews_count']], + ['rating_summary', null, $testSummaryData['rating_summary']] + ] + ) + ); + $summaryCollection = $this->createPartialMock( + \Magento\Review\Model\ResourceModel\Review\Summary\Collection::class, + ['addEntityFilter', 'addStoreFilter', 'getFirstItem', '__wakeup'] + ); + $summaryCollection->expects($this->once())->method('addEntityFilter') + ->will($this->returnSelf()); + $summaryCollection->expects($this->once())->method('addStoreFilter') + ->will($this->returnSelf()); + $summaryCollection->expects($this->once())->method('getFirstItem') + ->will($this->returnValue($summaryData)); + + $this->reviewSummaryCollectionFactoryMock->expects($this->once())->method('create') + ->will($this->returnValue($summaryCollection)); + + $this->assertNull($this->reviewSummary->appendSummaryDataToObject($product, $storeId)); + } +} diff --git a/app/code/Magento/Review/Test/Unit/Model/ReviewTest.php b/app/code/Magento/Review/Test/Unit/Model/ReviewTest.php index 9f57b289fa749..3302ba7e6a036 100644 --- a/app/code/Magento/Review/Test/Unit/Model/ReviewTest.php +++ b/app/code/Magento/Review/Test/Unit/Model/ReviewTest.php @@ -51,7 +51,7 @@ class ReviewTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Review\Model\ResourceModel\Review|\PHPUnit_Framework_MockObject_MockObject */ protected $resource; - /** @var int */ + /** @var int */ protected $reviewId = 8; protected function setUp() @@ -135,6 +135,9 @@ public function testAggregate() $this->assertSame($this->review, $this->review->aggregate()); } + /** + * @deprecated + */ public function testGetEntitySummary() { $productId = 6; diff --git a/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php new file mode 100644 index 0000000000000..2a8f8d8e38a64 --- /dev/null +++ b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Review\Test\Unit\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\UrlInterface; +use Magento\Review\Observer\PredispatchReviewObserver; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\TestCase; + +/** + * Test class for \Magento\Review\Observer\PredispatchReviewObserver + */ +class PredispatchReviewObserverTest extends TestCase +{ + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $mockObject; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlMock; + + /** + * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlMock = $this->getMockBuilder(UrlInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setRedirect']) + ->getMockForAbstractClass(); + $this->redirectMock = $this->getMockBuilder(RedirectInterface::class) + ->getMock(); + $this->objectManager = new ObjectManager($this); + $this->mockObject = $this->objectManager->getObject( + PredispatchReviewObserver::class, + [ + 'scopeConfig' => $this->configMock, + 'url' => $this->urlMock + ] + ); + } + + /** + * Test with enabled review active config. + */ + public function testReviewEnabled() + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getResponse', 'getData', 'setRedirect']) + ->getMockForAbstractClass(); + + $this->configMock->method('getValue') + ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(true); + $observerMock->expects($this->never()) + ->method('getData') + ->with('controller_action') + ->willReturnSelf(); + + $observerMock->expects($this->never()) + ->method('getResponse') + ->willReturnSelf(); + + $this->assertNull($this->mockObject->execute($observerMock)); + } + + /** + * Test with disabled review active config. + */ + public function testReviewDisabled() + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getControllerAction', 'getResponse']) + ->getMockForAbstractClass(); + + $this->configMock->expects($this->at(0)) + ->method('getValue') + ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(false); + + $expectedRedirectUrl = 'https://test.com/index'; + + $this->configMock->expects($this->at(1)) + ->method('getValue') + ->with('web/default/no_route', ScopeInterface::SCOPE_STORE) + ->willReturn($expectedRedirectUrl); + + $this->urlMock->expects($this->once()) + ->method('getUrl') + ->willReturn($expectedRedirectUrl); + + $observerMock->expects($this->once()) + ->method('getControllerAction') + ->willReturnSelf(); + + $observerMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with($expectedRedirectUrl); + + $this->assertNull($this->mockObject->execute($observerMock)); + } +} diff --git a/app/code/Magento/Review/Test/Unit/Observer/ProcessProductAfterDeleteEventObserverTest.php b/app/code/Magento/Review/Test/Unit/Observer/ProcessProductAfterDeleteEventObserverTest.php new file mode 100644 index 0000000000000..1a8490a921716 --- /dev/null +++ b/app/code/Magento/Review/Test/Unit/Observer/ProcessProductAfterDeleteEventObserverTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Test\Unit\Observer; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Review\Model\ResourceModel\Rating; +use Magento\Review\Model\ResourceModel\Review; +use Magento\Review\Observer\ProcessProductAfterDeleteEventObserver; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Class ProcessProductAfterDeleteEventObserverTest + */ +class ProcessProductAfterDeleteEventObserverTest extends TestCase +{ + /** + * Testable Object + * + * @var ProcessProductAfterDeleteEventObserver + */ + private $observer; + + /** + * @var Review|PHPUnit_Framework_MockObject_MockObject + */ + private $_resourceReviewMock; + + /** + * @var Rating|PHPUnit_Framework_MockObject_MockObject + */ + private $_resourceRatingMock; + + /** + * Set up + */ + protected function setUp() + { + $this->_resourceReviewMock = $this->createMock(Review::class); + $this->_resourceRatingMock = $this->createMock(Rating::class); + + $this->observer = new ProcessProductAfterDeleteEventObserver( + $this->_resourceReviewMock, + $this->_resourceRatingMock + ); + } + + /** + * Test cleanup product reviews after product delete + * + * @return void + */ + public function testCleanupProductReviewsWithProduct() + { + $productId = 1; + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMock(); + + $productMock->expects(self::exactly(3)) + ->method('getId') + ->willReturn($productId); + $eventMock->expects($this->once()) + ->method('getProduct') + ->willReturn($productMock); + $observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($eventMock); + $this->_resourceReviewMock->expects($this->once()) + ->method('deleteReviewsByProductId') + ->willReturnSelf(); + $this->_resourceRatingMock->expects($this->once()) + ->method('deleteAggregatedRatingsByProductId') + ->willReturnSelf(); + + $this->observer->execute($observerMock); + } + + /** + * Test with no event product + * + * @return void + */ + public function testCleanupProductReviewsWithoutProduct() + { + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + + $eventMock->expects($this->once()) + ->method('getProduct') + ->willReturn(null); + $observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($eventMock); + $this->_resourceReviewMock->expects($this->never()) + ->method('deleteReviewsByProductId') + ->willReturnSelf(); + $this->_resourceRatingMock->expects($this->never()) + ->method('deleteAggregatedRatingsByProductId') + ->willReturnSelf(); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json index 608dd51273d2e..135c553d117cd 100644 --- a/app/code/Magento/Review/composer.json +++ b/app/code/Magento/Review/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-review", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-customer": "101.0.*", @@ -18,7 +18,7 @@ "magento/module-review-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d6..09b80750da14d 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -17,7 +17,7 @@ <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index 2ed92a48ad421..8b56f36bce68e 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -9,7 +9,8 @@ <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> - <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="15" action="review/product/pending" resource="Magento_Review::pending"/> + <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> </menu> diff --git a/app/code/Magento/Review/etc/adminhtml/system.xml b/app/code/Magento/Review/etc/adminhtml/system.xml index c0574e9491782..a24ed29dc2c23 100644 --- a/app/code/Magento/Review/etc/adminhtml/system.xml +++ b/app/code/Magento/Review/etc/adminhtml/system.xml @@ -10,7 +10,11 @@ <section id="catalog"> <group id="review" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Product Reviews</label> - <field id="allow_guest" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + <field id="allow_guest" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Allow Guests to Write Reviews</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Review/etc/config.xml b/app/code/Magento/Review/etc/config.xml index 78dc87960f090..9fd9443be67ef 100644 --- a/app/code/Magento/Review/etc/config.xml +++ b/app/code/Magento/Review/etc/config.xml @@ -9,6 +9,7 @@ <default> <catalog> <review> + <active>1</active> <allow_guest>1</allow_guest> </review> </catalog> diff --git a/app/code/Magento/Review/etc/frontend/di.xml b/app/code/Magento/Review/etc/frontend/di.xml index e6efb36e88d56..4ea0f4449cdd8 100644 --- a/app/code/Magento/Review/etc/frontend/di.xml +++ b/app/code/Magento/Review/etc/frontend/di.xml @@ -40,7 +40,4 @@ </argument> </arguments> </type> - <type name="Magento\Catalog\Block\Product\Compare\ListCompare"> - <plugin name="reviewInitializer" type="Magento\Review\Block\Product\Compare\ListCompare\Plugin\Review" /> - </type> </config> diff --git a/app/code/Magento/Review/etc/frontend/events.xml b/app/code/Magento/Review/etc/frontend/events.xml index bc94277d69709..44cc888fb323f 100644 --- a/app/code/Magento/Review/etc/frontend/events.xml +++ b/app/code/Magento/Review/etc/frontend/events.xml @@ -6,10 +6,10 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="tag_tag_product_collection_load_after"> - <observer name="review" instance="Magento\Review\Observer\TagProductCollectionLoadAfterObserver" shared="false" /> - </event> <event name="catalog_block_product_list_collection"> - <observer name="review" instance="Magento\Review\Observer\CatalogBlockProductCollectionBeforeToHtmlObserver" shared="false" /> + <observer name="review" instance="Magento\Review\Observer\CatalogProductListCollectionAppendSummaryFieldsObserver" shared="false" /> + </event> + <event name="controller_action_predispatch_review"> + <observer name="catalog_review_enabled" instance="Magento\Review\Observer\PredispatchReviewObserver" /> </event> </config> diff --git a/app/code/Magento/Review/i18n/en_US.csv b/app/code/Magento/Review/i18n/en_US.csv index cb5452f2f0c39..07b7e8c13bd1f 100644 --- a/app/code/Magento/Review/i18n/en_US.csv +++ b/app/code/Magento/Review/i18n/en_US.csv @@ -133,3 +133,7 @@ Summary,Summary Active,Active Inactive,Inactive "Please select one of each of the ratings above.","Please select one of each of the ratings above." +star,star +stars,stars +"You don’t have permission to perform this operation. Selected reviews must be in Pending Status only.","You don’t have permission to perform this operation. Selected reviews must be in Pending Status only." +"You don’t have permission to perform this operation. The selected review must be in Pending Status.","You don’t have permission to perform this operation. The selected review must be in Pending Status." diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml index 013c5d87a88bb..bf0cab4c621f5 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml @@ -4,22 +4,20 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Adminhtml\Rating\Detailed $block */ ?> -<?php if ($block->getRating() && $block->getRating()->getSize()): ?> - <?php foreach ($block->getRating() as $_rating): ?> +<?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php foreach ($block->getRating() as $_rating) : ?> <div class="admin__field admin__field-rating"> <label class="admin__field-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></label> <?php $_iterator = 1; ?> <?php $_options = ($_rating->getRatingOptions()) ? $_rating->getRatingOptions() : $_rating->getOptions() ?> <div class="admin__field-control" data-widget="ratingControl"> - <?php foreach (array_reverse($_options) as $_option): ?> - <input type="radio" name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : $_rating->getId()) ?>]" id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" <?php if ($block->isSelected($_option, $_rating)): ?>checked="checked"<?php endif; ?> /> + <?php foreach (array_reverse($_options) as $_option) : ?> + <input type="radio" name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : $_rating->getId()) ?>]" id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" <?php if ($block->isSelected($_option, $_rating)) : ?>checked="checked"<?php endif; ?> /> <label for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>">★</label> - <?php $_iterator++ ?> + <?php $_iterator++ ?> <?php endforeach; ?> </div> </div> @@ -36,6 +34,6 @@ require([ $('[data-widget=ratingControl]').ratingControl(); }); </script> -<?php else: ?> +<?php else : ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/form.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/form.phtml index 0779d0fcbea3e..3d49d3c31e34f 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/form.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/form.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Adminhtml\Rating\Edit\Tab\Form $block */ ?> <div class="messages"> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/options.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/options.phtml index 779ee425e26f1..6e1dae94298c0 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/options.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/options.phtml @@ -4,22 +4,21 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile // @deprecated ?> <div class="entry-edit-head"> <h4 class="icon-head head-edit-form fieldset-legend"><?= $block->escapeHtml(__('Assigned Options')) ?></h4> </div> <fieldset id="options_form"> -<?php if (!$options): ?> - <?php for ($_i = 1; $_i <= 5; $_i++): ?> +<?php if (!$options) : ?> + <?php for ($_i = 1; $_i <= 5; $_i++) : ?> <span class="field-row"> <label for="option_<?= /* @noEscape */ $_i ?>"><?= $block->escapeHtml(__('Option Title:')) ?></label> <input id="option_<?= /* @noEscape */ $_i ?>" name="option[<?= /* @noEscape */ $_i ?>][code]" value="<?= /* @noEscape */ $_i ?>" class="input-text" type="text" /> </span> <?php endfor; ?> -<?php elseif ($options->getSize() > 0): ?> - <?php foreach ($options->getItems() as $_item): ?> +<?php elseif ($options->getSize() > 0) : ?> + <?php foreach ($options->getItems() as $_item) : ?> <span class="field-row"> <label for="option_<?= $block->escapeHtmlAttr($_item->getId()) ?>"><?= $block->escapeHtml(__('Option Title:')) ?></label> <input id="option_<?= $block->escapeHtmlAttr($_item->getId()) ?>" name="option[<?= $block->escapeHtmlAttr($_item->getId()) ?>][code]" value="<?= $block->escapeHtmlAttr($_item->getCode()) ?>" class="input-text" type="text" /> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/detailed.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/detailed.phtml index e36344997c806..085c445cae1f7 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/detailed.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/detailed.phtml @@ -4,22 +4,21 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile // @deprecated ?> -<?php if ($block->getRating() && $block->getRating()->getSize()): ?> +<?php if ($block->getRating() && $block->getRating()->getSize()) : ?> <div class="ratings-container"> - <?php foreach ($block->getRating() as $_rating): ?> - <?php if ($_rating->getPercent()): ?> - <div class="ratings"> - <?= $block->escapeHtml($_rating->getRatingCode()) ?> - <div class="rating-box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getPercent()) ?>%;"></div> + <?php foreach ($block->getRating() as $_rating) : ?> + <?php if ($_rating->getPercent()) : ?> + <div class="ratings"> + <?= $block->escapeHtml($_rating->getRatingCode()) ?> + <div class="rating-box"> + <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getPercent()) ?>%;"></div> + </div> </div> - </div> - <?php endif; ?> + <?php endif; ?> <?php endforeach; ?> </div> -<?php else: ?> +<?php else : ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml index 56f5186b92524..1f27db795f8c9 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Adminhtml\Rating\Summary $block */ ?> -<?php if ($block->getRatingSummary()->getCount()): ?> +<?php if ($block->getRatingSummary()->getCount()) : ?> <div class="rating-box"> <div class="rating" style="width:<?= /* @noEscape */ ceil($block->getRatingSummary()->getSum() / ($block->getRatingSummary()->getCount())) ?>%;"></div> </div> -<?php else: ?> +<?php else : ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rss/grid/link.phtml b/app/code/Magento/Review/view/adminhtml/templates/rss/grid/link.phtml index 375e983b267e4..b50d618601fc3 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rss/grid/link.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rss/grid/link.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Review\Block\Adminhtml\Grid\Rss\Link */ ?> -<?php if ($block->isRssAllowed() && $block->getLink()): ?> +<?php if ($block->isRssAllowed() && $block->getLink()) : ?> <a href="<?= $block->escapeUrl($block->getLink()) ?>" class="link-feed"><?= $block->escapeHtml($block->getLabel()) ?></a> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/web/js/rating.js b/app/code/Magento/Review/view/adminhtml/web/js/rating.js index cc72d386dc053..b8d1b1b241b8f 100644 --- a/app/code/Magento/Review/view/adminhtml/web/js/rating.js +++ b/app/code/Magento/Review/view/adminhtml/web/js/rating.js @@ -27,7 +27,7 @@ define([ _bind: function () { this._labels.on({ click: $.proxy(function (e) { - $('[id="' + $(e.currentTarget).attr('for') + '"]').prop('checked', true); + $(e.currentTarget).prev().prop('checked', true); this._updateRating(); }, this), diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index d7c5c19d4d813..6fcf5b0c82b4f 100644 --- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml @@ -9,7 +9,7 @@ <update handle="review_product_form_component"/> <body> <referenceContainer name="content"> - <block class="Magento\Cookie\Block\RequireCookie" name="require-cookie" template="Magento_Cookie::require_cookie.phtml"> + <block class="Magento\Cookie\Block\RequireCookie" name="require-cookie" template="Magento_Cookie::require_cookie.phtml" ifconfig="catalog/review/active"> <arguments> <argument name="triggers" xsi:type="array"> <item name="submitReviewButton" xsi:type="string">.review .action.submit</item> @@ -18,8 +18,8 @@ </block> </referenceContainer> <referenceBlock name="product.info.details"> - <block class="Magento\Review\Block\Product\Review" name="reviews.tab" as="reviews" template="Magento_Review::review.phtml" group="detailed_info"> - <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Product\Review" name="reviews.tab" as="reviews" template="Magento_Review::review.phtml" group="detailed_info" ifconfig="catalog/review/active"> + <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before"/> </block> </block> diff --git a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml index 0a7ddd8b8903d..8a853cdd2e409 100644 --- a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml @@ -9,7 +9,7 @@ <update handle="catalog_product_view"/> <body> <referenceBlock name="reviews.tab"> - <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account.xml b/app/code/Magento/Review/view/frontend/layout/customer_account.xml index 54d171cbf1322..9f759dba41782 100644 --- a/app/code/Magento/Review/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Review/view/frontend/layout/customer_account.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="customer_account_navigation"> - <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-product-reviews-link"> + <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-product-reviews-link" ifconfig="catalog/review/active"> <arguments> <argument name="path" xsi:type="string">review/customer</argument> <argument name="label" xsi:type="string" translate="true">My Product Reviews</argument> diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml index 73174f0570e28..2e898a539a954 100644 --- a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml +++ b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\Recent" name="customer_account_dashboard_info1" template="Magento_Review::customer/recent.phtml" after="customer_account_dashboard_address" cacheable="false"/> + <block class="Magento\Review\Block\Customer\Recent" name="customer_account_dashboard_info1" template="Magento_Review::customer/recent.phtml" after="customer_account_dashboard_address" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml index 2857e859aa06c..b5f7562963314 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml @@ -9,7 +9,7 @@ <update handle="customer_account"/> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\ListCustomer" name="review_customer_list" template="Magento_Review::customer/list.phtml" cacheable="false"/> + <block class="Magento\Review\Block\Customer\ListCustomer" name="review_customer_list" template="Magento_Review::customer/list.phtml" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml index d51c89a1abe1a..d3adbd7950cf9 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml @@ -9,7 +9,7 @@ <update handle="customer_account"/> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\View" name="customers_review" cacheable="false"/> + <block class="Magento\Review\Block\Customer\View" name="customers_review" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml index c83cfe95d7964..8c5c1297cdda3 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml @@ -9,15 +9,15 @@ <update handle="catalog_product_view"/> <body> <referenceContainer name="product.info.main"> - <block class="Magento\Review\Block\Product\View\Other" name="product.info.other" as="other" template="Magento_Review::product/view/other.phtml" before="product.info.addto"/> + <block class="Magento\Review\Block\Product\View\Other" name="product.info.other" as="other" template="Magento_Review::product/view/other.phtml" before="product.info.addto" ifconfig="catalog/review/active"/> </referenceContainer> <referenceContainer name="content"> <container name="product.info.details" htmlTag="div" htmlClass="product info detailed" after="product.info.media"> - <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before" htmlTag="div" htmlClass="rewards"/> </block> - <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml"/> - <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar"/> + <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" ifconfig="catalog/review/active"/> + <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar" ifconfig="catalog/review/active"/> </container> </referenceContainer> </body> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml index af8d2dc2f506f..36fa71ea5125a 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml @@ -7,8 +7,8 @@ --> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root"> - <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" /> - <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar"> + <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" ifconfig="catalog/review/active"/> + <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar" ifconfig="catalog/review/active"> <arguments> <argument name="show_per_page" xsi:type="boolean">false</argument> <argument name="show_amounts" xsi:type="boolean">false</argument> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_view.xml b/app/code/Magento/Review/view/frontend/layout/review_product_view.xml index b70aec3f00b68..3bfc98cad9736 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_view.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\View" name="review_view"/> + <block class="Magento\Review\Block\View" name="review_view" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml index 0a7ddd8b8903d..8a853cdd2e409 100644 --- a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml @@ -9,7 +9,7 @@ <update handle="catalog_product_view"/> <body> <referenceBlock name="reviews.tab"> - <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml index 725df702bb465..11ea987b74cec 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Customer\ListCustomer $block */ ?> -<?php if ($block->getReviews() && count($block->getReviews())): ?> +<?php if ($block->getReviews() && count($block->getReviews())) : ?> <div class="table-wrapper reviews"> <table class="data table table-reviews" id="my-reviews-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Reviews')) ?></caption> @@ -22,29 +20,29 @@ </tr> </thead> <tbody> - <?php foreach ($block->getReviews() as $_review): ?> + <?php foreach ($block->getReviews() as $review) : ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Created')) ?>" class="col date"><?= $block->escapeHtml($block->dateFormat($_review->getReviewCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtml(__('Created')) ?>" class="col date"><?= $block->escapeHtml($block->dateFormat($review->getReviewCreatedAt())) ?></td> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-name"> - <a href="<?= $block->escapeUrl($block->getProductUrl($_review)) ?>"><?= $block->escapeHtml($_review->getName()) ?></a> + <a href="<?= $block->escapeUrl($block->getProductUrl($review)) ?>"><?= $block->escapeHtml($review->getName()) ?></a> </strong> </td> <td data-th="<?= $block->escapeHtml(__('Rating')) ?>" class="col summary"> - <?php if ($_review->getSum()): ?> + <?php if ($review->getSum()) : ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= /* @noEscape */ ((int)$_review->getSum() / (int)$_review->getCount()) ?>%"> - <span style="width:<?= /* @noEscape */ ((int)$_review->getSum() / (int)$_review->getCount()) ?>%;"><span><?= /* @noEscape */ ((int)$_review->getSum() / (int)$_review->getCount()) ?>%</span></span> + <div class="rating-result" title="<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%"> + <span style="width:<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%;"><span><?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%</span></span> </div> </div> <?php endif; ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Review')) ?>" class="col description"> - <?= $this->helper('Magento\Review\Helper\Data')->getDetailHtml($_review->getDetail()) ?> + <?= $this->helper(\Magento\Review\Helper\Data::class)->getDetailHtml($review->getDetail()) ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Actions')) ?>" class="col actions"> - <a href="<?= $block->escapeUrl($block->getReviewUrl($_review)) ?>" class="action more"> + <a href="<?= $block->escapeUrl($block->getReviewUrl($review)) ?>" class="action more"> <span><?= $block->escapeHtml(__('See Details')) ?></span> </a> </td> @@ -53,12 +51,12 @@ </tbody> </table> </div> - <?php if ($block->getToolbarHtml()): ?> + <?php if ($block->getToolbarHtml()) : ?> <div class="toolbar products-reviews-toolbar bottom"> <?= $block->getToolbarHtml() ?> </div> <?php endif; ?> -<?php else: ?> +<?php else : ?> <div class="message info empty"><span><?= $block->escapeHtml(__('You have submitted no reviews.')) ?></span></div> <?php endif; ?> <div class="actions-toolbar"> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml index ed58e3832577b..5cd81a2f17cbc 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Customer\Recent $block */ ?> -<?php if ($block->getReviews() && count($block->getReviews())): ?> +<?php if ($block->getReviews() && count($block->getReviews())) : ?> <div class="block block-reviews-dashboard"> <div class="block-title"> <strong><?= $block->escapeHtml(__('My Recent Reviews')) ?></strong> @@ -16,11 +14,11 @@ </div> <div class="block-content"> <ol class="items"> - <?php foreach ($block->getReviews() as $_review): ?> + <?php foreach ($block->getReviews() as $_review) : ?> <li class="item"> <strong class="product-name"><a href="<?= $block->escapeUrl($block->getReviewUrl($_review->getReviewId())) ?>"><?= $block->escapeHtml($_review->getName()) ?></a></strong> - <?php if ($_review->getSum()): ?> - <?php $rating = $_review->getSum() / $_review->getCount() ?> + <?php if ($_review->getSum()) : ?> + <?php $rating = $_review->getSum() / $_review->getCount() ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml index 4903b154d9f13..f92282848b1b7 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Customer\View $block */ $product = $block->getProductData(); ?> -<?php if ($product->getId()): ?> +<?php if ($product->getId()) : ?> <div class="customer-review view"> <div class="product-details"> <div class="product-media"> @@ -21,7 +19,7 @@ $product = $block->getProductData(); </div> <div class="product-info"> <h2 class="product-name"><?= $block->escapeHtml($product->getName()) ?></h2> - <?php if ($block->getRating() && $block->getRating()->getSize()): ?> + <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> <span class="rating-average-label"><?= $block->escapeHtml(__('Average Customer Rating:')) ?></span> <?= $block->getReviewsSummaryHtml($product) ?> <?php endif; ?> @@ -29,29 +27,29 @@ $product = $block->getProductData(); </div> <div class="review-details"> - <?php if ($block->getRating() && $block->getRating()->getSize()): ?> + <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> <div class="title"> <strong><?= $block->escapeHtml(__('Your Review')) ?></strong> </div> <div class="customer-review-rating"> - <?php foreach ($block->getRating() as $_rating): ?> - <?php if ($_rating->getPercent()): ?> - <?php $rating = ceil($_rating->getPercent()) ?> - <div class="rating-summary item"> - <span class="rating-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></span> - <div class="rating-result" title="<?= /* @noEscape */ $rating ?>%"> - <span style="width:<?= /* @noEscape */ $rating ?>%"> - <span><?= /* @noEscape */ $rating ?>%</span> - </span> + <?php foreach ($block->getRating() as $_rating) : ?> + <?php if ($_rating->getPercent()) : ?> + <?php $rating = ceil($_rating->getPercent()) ?> + <div class="rating-summary item"> + <span class="rating-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></span> + <div class="rating-result" title="<?= /* @noEscape */ $rating ?>%"> + <span style="width:<?= /* @noEscape */ $rating ?>%"> + <span><?= /* @noEscape */ $rating ?>%</span> + </span> + </div> </div> - </div> - <?php endif; ?> + <?php endif; ?> <?php endforeach; ?> </div> <?php endif; ?> <div class="review-title"><?= $block->escapeHtml($block->getReviewData()->getTitle()) ?></div> - <div class="review-content"><?= nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></div> + <div class="review-content"><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></div> <div class="review-date"> <?= $block->escapeHtml(__('Submitted on %1', '<time class="date">' . $block->dateFormat($block->getReviewData()->getCreatedAt()) . '</time>'), ['time']) ?> </div> diff --git a/app/code/Magento/Review/view/frontend/templates/detailed.phtml b/app/code/Magento/Review/view/frontend/templates/detailed.phtml index df14955a29ea6..7b3b0e2dd6d02 100644 --- a/app/code/Magento/Review/view/frontend/templates/detailed.phtml +++ b/app/code/Magento/Review/view/frontend/templates/detailed.phtml @@ -4,17 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Rating\Entity\Detailed $block */ ?> -<?php if (!empty($collection) && $collection->getSize()): ?> +<?php if (!empty($collection) && $collection->getSize()) : ?> <div class="table-wrapper"> <table class="data table ratings review summary"> <caption class="table-caption"><?= $block->escapeHtml(__('Ratings Review Summary')) ?></caption> <tbody> - <?php foreach ($collection as $_rating): ?> - <?php if ($_rating->getSummary()): ?> + <?php foreach ($collection as $_rating) : ?> + <?php if ($_rating->getSummary()) : ?> <tr> <th class="label" scope="row"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></th> <td class="value"> diff --git a/app/code/Magento/Review/view/frontend/templates/empty.phtml b/app/code/Magento/Review/view/frontend/templates/empty.phtml index 4328a135a98f3..9efdd34081429 100644 --- a/app/code/Magento/Review/view/frontend/templates/empty.phtml +++ b/app/code/Magento/Review/view/frontend/templates/empty.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Rating\Entity\Detailed $block */ ?> <p class="no-rating"><a href="#review-form"><?= $block->escapeHtml(__('Be the first to review this product')) ?></a></p> diff --git a/app/code/Magento/Review/view/frontend/templates/form.phtml b/app/code/Magento/Review/view/frontend/templates/form.phtml index 3594062a73524..2234425bbf989 100644 --- a/app/code/Magento/Review/view/frontend/templates/form.phtml +++ b/app/code/Magento/Review/view/frontend/templates/form.phtml @@ -4,47 +4,45 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Form $block */ ?> <div class="block review-add"> <div class="block-title"><strong><?= $block->escapeHtml(__('Write Your Own Review')) ?></strong></div> <div class="block-content"> -<?php if ($block->getAllowWriteReviewFlag()): ?> +<?php if ($block->getAllowWriteReviewFlag()) : ?> <form action="<?= $block->escapeUrl($block->getAction()) ?>" class="review-form" method="post" id="review-form" data-role="product-review-form" data-bind="scope: 'review-form'"> <?= $block->getBlockHtml('formkey') ?> <?= $block->getChildHtml('form_fields_before') ?> <fieldset class="fieldset review-fieldset" data-hasrequired="<?= $block->escapeHtmlAttr(__('* Required Fields')) ?>"> <legend class="legend review-legend"><span><?= $block->escapeHtml(__("You're reviewing:")) ?></span><strong><?= $block->escapeHtml($block->getProductInfo()->getName()) ?></strong></legend><br /> - <?php if ($block->getRatings() && $block->getRatings()->getSize()): ?> + <?php if ($block->getRatings() && $block->getRatings()->getSize()) : ?> <span id="input-message-box"></span> <fieldset class="field required review-field-ratings"> <legend class="label"><span><?= $block->escapeHtml(__('Your Rating')) ?></span></legend><br/> <div class="control"> <div class="nested" id="product-review-table"> - <?php foreach ($block->getRatings() as $_rating): ?> + <?php foreach ($block->getRatings() as $_rating) : ?> <div class="field choice review-field-rating"> - <label class="label" id="<?= $block->escapeHtml($_rating->getRatingCode()) ?>_rating_label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></label> + <label class="label" id="<?= $block->escapeHtml(str_replace(' ', '_', $_rating->getRatingCode())) ?>_rating_label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></label> <div class="control review-control-vote"> <?php $options = $_rating->getOptions();?> - <?php $iterator = 1; foreach ($options as $_option): ?> + <?php $iterator = 1; foreach ($options as $_option) : ?> <input type="radio" name="ratings[<?= $block->escapeHtmlAttr($_rating->getId()) ?>]" - id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" + id="<?= $block->escapeHtmlAttr(str_replace(' ', '_', $_rating->getRatingCode())) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" class="radio" data-validate="{ 'rating-required':true}" - aria-labelledby="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_rating_label <?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>_label" /> + aria-labelledby="<?= $block->escapeHtmlAttr(str_replace(' ', '_', $_rating->getRatingCode())) ?>_rating_label <?= $block->escapeHtmlAttr(str_replace(' ', '_', $_rating->getRatingCode())) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>_label" /> <label class="rating-<?= $block->escapeHtmlAttr($iterator) ?>" - for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" - title="<?= $block->escapeHtmlAttr(__('%1 %2', $iterator, $iterator > 1 ? 'stars' : 'star')) ?>" - id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>_label"> - <span><?= $block->escapeHtml(__('%1 %2', $iterator, $iterator > 1 ? 'stars' : 'star')) ?></span> + for="<?= $block->escapeHtmlAttr(str_replace(' ', '_', $_rating->getRatingCode())) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" + title="<?= $block->escapeHtmlAttr(__('%1 %2', $iterator, $iterator > 1 ? __('stars') : __('star'))) ?>" + id="<?= $block->escapeHtmlAttr(str_replace(' ', '_', $_rating->getRatingCode())) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>_label"> + <span><?= $block->escapeHtml(__('%1 %2', $iterator, $iterator > 1 ? __('stars') : __('star'))) ?></span> </label> - <?php $iterator++; ?> + <?php $iterator++; ?> <?php endforeach; ?> </div> </div> @@ -90,7 +88,7 @@ } } </script> -<?php else: ?> +<?php else : ?> <div class="message info notlogged" id="review-form"> <div> <?= $block->escapeHtml(__('Only registered users can write reviews. Please <a href="%1">Sign in</a> or <a href="%2">create an account</a>', $block->getLoginLink(), $block->getRegisterUrl()), ['a']) ?> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml index da689960dfe54..94120b7ff1f98 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml @@ -4,27 +4,25 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Product\ReviewRenderer $block */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->getReviewsCount()): ?> -<?php $rating = $block->getRatingSummary(); ?> -<div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"> - <?php if ($rating):?> - <div class="rating-summary"> - <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating); ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating); ?>%"> - <span> - <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?></span>% of <span itemprop="bestRating">100</span> - </span> - </span> - </div> - </div> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> + <?php $rating = $block->getRatingSummary(); ?> + <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"> + <?php if ($rating) : ?> + <div class="rating-summary"> + <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> + <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating); ?>%"> + <span style="width:<?= $block->escapeHtmlAttr($rating); ?>%"> + <span> + <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?></span>% of <span itemprop="bestRating">100</span> + </span> + </span> + </div> + </div> <?php endif;?> <div class="reviews-actions"> <a class="action view" @@ -35,12 +33,12 @@ $urlForm = $block->getReviewsUrl() . '#review-form'; <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"><?= $block->escapeHtml(__('Add Your Review')) ?></a> </div> </div> -<?php elseif ($block->getDisplayIfEmpty()): ?> -<div class="product-reviews-summary empty"> - <div class="reviews-actions"> - <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> - <?= $block->escapeHtml(__('Be the first to review this product')) ?> - </a> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> + <div class="product-reviews-summary empty"> + <div class="reviews-actions"> + <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> + <?= $block->escapeHtml(__('Be the first to review this product')) ?> + </a> + </div> </div> -</div> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml index c3eb11f03fd7d..ca2ac1e50702a 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml @@ -4,34 +4,32 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Product\ReviewRenderer $block */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->getReviewsCount()): ?> -<?php $rating = $block->getRatingSummary(); ?> -<div class="product-reviews-summary short<?= !$rating ? ' no-rating' : '' ?>"> - <?php if ($rating):?> - <div class="rating-summary"> - <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> + <?php $rating = $block->getRatingSummary(); ?> + <div class="product-reviews-summary short<?= !$rating ? ' no-rating' : '' ?>"> + <?php if ($rating) : ?> + <div class="rating-summary"> + <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> + <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> + <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> + </div> + </div> + <?php endif;?> + <div class="reviews-actions"> + <a class="action view" href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span></a> </div> </div> - <?php endif;?> - <div class="reviews-actions"> - <a class="action view" href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span></a> - </div> -</div> -<?php elseif ($block->getDisplayIfEmpty()): ?> -<div class="product-reviews-summary short empty"> - <div class="reviews-actions"> - <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> - <?= $block->escapeHtml(__('Be the first to review this product')) ?> - </a> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> + <div class="product-reviews-summary short empty"> + <div class="reviews-actions"> + <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> + <?= $block->escapeHtml(__('Be the first to review this product')) ?> + </a> + </div> </div> -</div> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/count.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/count.phtml index bff1c48508e11..cd98ab3c4cc78 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/count.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/count.phtml @@ -4,9 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile // @deprecated ?> -<?php if (!empty($count)):?> +<?php if (!empty($count)) : ?> <a href="#customer-reviews" class="nobr"><?= $block->escapeHtml(__('%1 Review(s)', $count)) ?></a> <?php endif;?> diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml index dc02853e77b18..347686d5c2ba4 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml @@ -4,14 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var Magento\Review\Block\Product\View\ListView $block */ $_items = $block->getReviewsCollection()->getItems(); $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; ?> -<?php if (count($_items)):?> +<?php if (count($_items)) : ?> <div class="block review-list" id="customer-reviews"> <div class="block-title"> <strong><?= $block->escapeHtml(__('Customer Reviews')) ?></strong> @@ -21,17 +19,17 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; <?= $block->getChildHtml('toolbar') ?> </div> <ol class="items review-items"> - <?php foreach ($_items as $_review):?> + <?php foreach ($_items as $_review) : ?> <li class="item review-item" itemscope itemprop="review" itemtype="http://schema.org/Review"> <div class="review-title" itemprop="name"><?= $block->escapeHtml($_review->getTitle()) ?></div> - <?php if (count($_review->getRatingVotes())): ?> + <?php if (count($_review->getRatingVotes())) : ?> <div class="review-ratings"> - <?php foreach ($_review->getRatingVotes() as $_vote): ?> + <?php foreach ($_review->getRatingVotes() as $_vote) : ?> <div class="rating-summary item" itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating"> <span class="label rating-label"><span><?= $block->escapeHtml($_vote->getRatingCode()) ?></span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> - <meta itemprop="worstRating" content = "1"/> - <meta itemprop="bestRating" content = "100"/> + <meta itemprop="worstRating" content = "1"/> + <meta itemprop="bestRating" content = "100"/> <span style="width:<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> <span itemprop="ratingValue"><?= $block->escapeHtml($_vote->getPercent()) ?>%</span> </span> @@ -41,7 +39,7 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; </div> <?php endif; ?> <div class="review-content" itemprop="description"> - <?= nl2br($block->escapeHtml($_review->getDetail())) ?> + <?= /* @noEscape */ nl2br($block->escapeHtml($_review->getDetail())) ?> </div> <div class="review-details"> <p class="review-author"> diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/other.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/other.phtml index 48d668ca85a0d..686bd8b117b6e 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/other.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/other.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\Product\View\Other $block */ ?> <?php $_product = $block->getProduct(); ?> diff --git a/app/code/Magento/Review/view/frontend/templates/redirect.phtml b/app/code/Magento/Review/view/frontend/templates/redirect.phtml index fc74cadacb319..c56aa4c0d7689 100644 --- a/app/code/Magento/Review/view/frontend/templates/redirect.phtml +++ b/app/code/Magento/Review/view/frontend/templates/redirect.phtml @@ -4,13 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?php -/* if(isset($GET['limit'])) { - $limit = $GET['limit'] - }*/ header("Location:{$block->getProduct()->getProductUrl()}#info-product_reviews"); exit; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/review.phtml b/app/code/Magento/Review/view/frontend/templates/review.phtml index ea4b4bc42a1ed..04782080fc775 100644 --- a/app/code/Magento/Review/view/frontend/templates/review.phtml +++ b/app/code/Magento/Review/view/frontend/templates/review.phtml @@ -13,7 +13,7 @@ { "*": { "Magento_Review/js/process-reviews": { - "productReviewUrl": "<?= $block->escapeJs($block->escapeUrl($block->getProductReviewUrl())) ?>", + "productReviewUrl": "<?= $block->escapeJs($block->getProductReviewUrl()) ?>", "reviewsTabSelector": "#tab-label-reviews" } } diff --git a/app/code/Magento/Review/view/frontend/templates/view.phtml b/app/code/Magento/Review/view/frontend/templates/view.phtml index 564a6e1a7c537..1c3d1942dd2e7 100644 --- a/app/code/Magento/Review/view/frontend/templates/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/view.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Review\Block\View $block */ ?> -<?php if ($block->getProductData()->getId()): ?> +<?php if ($block->getProductData()->getId()) : ?> <div class="product-review"> <div class="page-title-wrapper"> <h1><?= $block->escapeHtml(__('Review Details')) ?></h1> @@ -17,20 +15,20 @@ <a href="<?= $block->escapeUrl($block->getProductData()->getProductUrl()) ?>"> <?= $block->getImage($block->getProductData(), 'product_base_image', ['class' => 'product-image'])->toHtml() ?> </a> - <?php if ($block->getRating() && $block->getRating()->getSize()): ?> - <p><?= $block->escapeHtml(__('Average Customer Rating')) ?>:</p> - <?= $block->getReviewsSummaryHtml($block->getProductData()) ?> + <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <p><?= $block->escapeHtml(__('Average Customer Rating')) ?>:</p> + <?= $block->getReviewsSummaryHtml($block->getProductData()) ?> <?php endif; ?> </div> <div class="details"> <h3 class="product-name"><?= $block->escapeHtml($block->getProductData()->getName()) ?></h3> - <?php if ($block->getRating() && $block->getRating()->getSize()): ?> + <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> <h4><?= $block->escapeHtml(__('Product Rating:')) ?></h4> <div class="table-wrapper"> <table class="data-table review-summary-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Rating')) ?></caption> - <?php foreach ($block->getRating() as $_rating): ?> - <?php if ($_rating->getPercent()): ?> + <?php foreach ($block->getRating() as $_rating) : ?> + <?php if ($_rating->getPercent()) : ?> <tr> <td class="label"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></td> <td class="value"> @@ -44,12 +42,12 @@ </div> <?php endif; ?> <p class="date"><?= $block->escapeHtml(__('Product Review (submitted on %1):', $block->dateFormat($block->getReviewData()->getCreatedAt()))) ?></p> - <p><?= nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></p> + <p><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></p> </div> <div class="actions"> <div class="secondary"> <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"> - <span><?= $block->escapeHtml(__('Back to Product Reviews')) ?></a></span> + <span><?= $block->escapeHtml(__('Back to Product Reviews')) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index 08464e3da6f42..88c61fa38af34 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -20,7 +20,7 @@ define([ showLoader: false, loaderContext: $('.product.data.items') }).done(function (data) { - $('#product-review-container').html(data); + $('#product-review-container').html(data).trigger('contentUpdated'); $('[data-role="product-review"] .pages a').each(function (index, element) { $(element).click(function (event) { //eslint-disable-line max-nested-callbacks processReviews($(element).attr('href'), true); @@ -50,15 +50,15 @@ define([ $(function () { $('.product-info-main .reviews-actions a').click(function (event) { - var acnchor; + var anchor; event.preventDefault(); - acnchor = $(this).attr('href').replace(/^.*?(#|$)/, ''); + anchor = $(this).attr('href').replace(/^.*?(#|$)/, ''); $('.product.data.items [data-role="content"]').each(function (index) { //eslint-disable-line if (this.id == 'reviews') { //eslint-disable-line eqeqeq $('.product.data.items').tabs('activate', index); $('html, body').animate({ - scrollTop: $('#' + acnchor).offset().top - 50 + scrollTop: $('#' + anchor).offset().top - 50 }, 300); } }); diff --git a/app/code/Magento/ReviewAnalytics/Test/Mftf/LICENSE.txt b/app/code/Magento/ReviewAnalytics/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/ReviewAnalytics/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 <insert your license name here>" 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/app/code/Magento/ReviewAnalytics/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/ReviewAnalytics/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/ReviewAnalytics/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 <insert your license name here>" 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/app/code/Magento/ReviewAnalytics/Test/Mftf/README.md b/app/code/Magento/ReviewAnalytics/Test/Mftf/README.md new file mode 100644 index 0000000000000..9175fe01002bc --- /dev/null +++ b/app/code/Magento/ReviewAnalytics/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Review Analytics Functional Tests + +The Functional Test Module for **Magento Review Analytics** module. diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json index 2c982969a391f..8dc5f92589ddd 100644 --- a/app/code/Magento/ReviewAnalytics/composer.json +++ b/app/code/Magento/ReviewAnalytics/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-review-analytics", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-review": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Robots/Controller/Index/Index.php b/app/code/Magento/Robots/Controller/Index/Index.php index b94626e93432d..679066d723dce 100644 --- a/app/code/Magento/Robots/Controller/Index/Index.php +++ b/app/code/Magento/Robots/Controller/Index/Index.php @@ -43,6 +43,7 @@ public function execute() /** @var Page $resultPage */ $resultPage = $this->resultPageFactory->create(true); $resultPage->addHandle('robots_index_index'); + $resultPage->setHeader('Content-Type', 'text/plain'); return $resultPage; } } diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 83c21d6602fca..4c80588d814f6 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Robots\Model\Config; use Magento\Framework\App\Cache\TypeListInterface; @@ -30,12 +31,11 @@ class Value extends ConfigValue implements IdentityInterface const CACHE_TAG = 'robots'; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [self::CACHE_TAG]; /** * @var StoreResolver diff --git a/app/code/Magento/Robots/Test/Mftf/LICENSE.txt b/app/code/Magento/Robots/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Robots/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 <insert your license name here>" 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/app/code/Magento/Robots/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Robots/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Robots/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 <insert your license name here>" 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/app/code/Magento/Robots/Test/Mftf/README.md b/app/code/Magento/Robots/Test/Mftf/README.md new file mode 100644 index 0000000000000..e10f7c3e78419 --- /dev/null +++ b/app/code/Magento/Robots/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Robots Functional Tests + +The Functional Test Module for **Magento Robots** module. diff --git a/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php index 22a69cc13bd52..d3a7a97c7ea80 100644 --- a/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php +++ b/app/code/Magento/Robots/Test/Unit/Controller/Index/IndexTest.php @@ -51,6 +51,9 @@ public function testExecute() $resultPageMock->expects($this->once()) ->method('addHandle') ->with('robots_index_index'); + $resultPageMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'text/plain'); $this->resultPageFactory->expects($this->any()) ->method('create') diff --git a/app/code/Magento/Robots/composer.json b/app/code/Magento/Robots/composer.json index 38c143e2547d7..f550597960cd4 100644 --- a/app/code/Magento/Robots/composer.json +++ b/app/code/Magento/Robots/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-robots", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-store": "100.2.*" }, @@ -10,7 +10,7 @@ "magento/module-theme": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Robots/view/frontend/templates/robots.phtml b/app/code/Magento/Robots/view/frontend/templates/robots.phtml index 644445bf10640..e4423b9fc9c3c 100644 --- a/app/code/Magento/Robots/view/frontend/templates/robots.phtml +++ b/app/code/Magento/Robots/view/frontend/templates/robots.phtml @@ -3,4 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?><?= $layoutContent; + +// phpcs:disable PSR2.Files.ClosingTag +?><?= /* @noEscape */ $layoutContent; ?> diff --git a/app/code/Magento/Rss/Block/Feeds.php b/app/code/Magento/Rss/Block/Feeds.php index 2e88d25c02891..86998f87f5c17 100644 --- a/app/code/Magento/Rss/Block/Feeds.php +++ b/app/code/Magento/Rss/Block/Feeds.php @@ -16,7 +16,7 @@ class Feeds extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'feeds.phtml'; + protected $_template = 'Magento_Rss::feeds.phtml'; /** * @var \Magento\Framework\App\Rss\RssManagerInterface diff --git a/app/code/Magento/Rss/Controller/Feed.php b/app/code/Magento/Rss/Controller/Feed.php index 8fbe7addb560d..4c5acf97bde8c 100644 --- a/app/code/Magento/Rss/Controller/Feed.php +++ b/app/code/Magento/Rss/Controller/Feed.php @@ -76,6 +76,8 @@ public function __construct( } /** + * Authenticate not logged in customer. + * * @return bool */ protected function auth() @@ -85,7 +87,6 @@ protected function auth() try { $customer = $this->customerAccountManagement->authenticate($login, $password); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $this->customerSession->regenerateId(); } catch (\Exception $e) { $this->logger->critical($e); } diff --git a/app/code/Magento/Rss/Model/Rss.php b/app/code/Magento/Rss/Model/Rss.php index 7461c780fb230..c46c2240173f6 100644 --- a/app/code/Magento/Rss/Model/Rss.php +++ b/app/code/Magento/Rss/Model/Rss.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Rss\DataProviderInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\App\FeedFactoryInterface; /** * Provides functionality to work with RSS feeds @@ -27,6 +28,11 @@ class Rss */ protected $cache; + /** + * @var \Magento\Framework\App\FeedFactoryInterface + */ + private $feedFactory; + /** * @var SerializerInterface */ @@ -37,13 +43,16 @@ class Rss * * @param \Magento\Framework\App\CacheInterface $cache * @param SerializerInterface|null $serializer + * @param FeedFactoryInterface|null $feedFactory */ public function __construct( \Magento\Framework\App\CacheInterface $cache, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + FeedFactoryInterface $feedFactory = null ) { $this->cache = $cache; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); + $this->feedFactory = $feedFactory ?: ObjectManager::getInstance()->get(FeedFactoryInterface::class); } /** @@ -89,10 +98,12 @@ public function setDataProvider(DataProviderInterface $dataProvider) /** * @return string + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\RuntimeException */ public function createRssXml() { - $rssFeedFromArray = \Zend_Feed::importArray($this->getFeeds(), 'rss'); - return $rssFeedFromArray->saveXML(); + $feed = $this->feedFactory->create($this->getFeeds(), FeedFactoryInterface::FORMAT_RSS); + return $feed->getFormattedContent(); } } diff --git a/app/code/Magento/Rss/Test/Mftf/LICENSE.txt b/app/code/Magento/Rss/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Rss/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 <insert your license name here>" 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/app/code/Magento/Rss/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Rss/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Rss/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 <insert your license name here>" 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/app/code/Magento/Rss/Test/Mftf/README.md b/app/code/Magento/Rss/Test/Mftf/README.md new file mode 100644 index 0000000000000..f0dfa5b1df52f --- /dev/null +++ b/app/code/Magento/Rss/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Rss Functional Tests + +The Functional Test Module for **Magento Rss** module. diff --git a/app/code/Magento/Rss/Test/Unit/Controller/Adminhtml/Feed/IndexTest.php b/app/code/Magento/Rss/Test/Unit/Controller/Adminhtml/Feed/IndexTest.php index 32aab6ffb92bc..a601f8fb2d1d7 100644 --- a/app/code/Magento/Rss/Test/Unit/Controller/Adminhtml/Feed/IndexTest.php +++ b/app/code/Magento/Rss/Test/Unit/Controller/Adminhtml/Feed/IndexTest.php @@ -6,6 +6,7 @@ namespace Magento\Rss\Test\Unit\Controller\Adminhtml\Feed; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Zend\Feed\Writer\Exception\InvalidArgumentException; /** * Class IndexTest @@ -103,14 +104,22 @@ public function testExecuteWithException() $dataProvider = $this->createMock(\Magento\Framework\App\Rss\DataProviderInterface::class); $dataProvider->expects($this->once())->method('isAllowed')->will($this->returnValue(true)); - $rssModel = $this->createPartialMock(\Magento\Rss\Model\Rss::class, ['setDataProvider']); + $rssModel = $this->createPartialMock(\Magento\Rss\Model\Rss::class, ['setDataProvider', 'createRssXml']); $rssModel->expects($this->once())->method('setDataProvider')->will($this->returnSelf()); + $exceptionMock = new \Magento\Framework\Exception\RuntimeException( + new \Magento\Framework\Phrase('Any message') + ); + + $rssModel->expects($this->once())->method('createRssXml')->will( + $this->throwException($exceptionMock) + ); + $this->response->expects($this->once())->method('setHeader')->will($this->returnSelf()); $this->rssFactory->expects($this->once())->method('create')->will($this->returnValue($rssModel)); $this->rssManager->expects($this->once())->method('getProvider')->will($this->returnValue($dataProvider)); - $this->expectException('\Zend_Feed_Builder_Exception'); + $this->expectException(\Magento\Framework\Exception\RuntimeException::class); $this->controller->execute(); } } diff --git a/app/code/Magento/Rss/Test/Unit/Controller/Feed/IndexTest.php b/app/code/Magento/Rss/Test/Unit/Controller/Feed/IndexTest.php index 71802deee0a8d..30415155d5f6e 100644 --- a/app/code/Magento/Rss/Test/Unit/Controller/Feed/IndexTest.php +++ b/app/code/Magento/Rss/Test/Unit/Controller/Feed/IndexTest.php @@ -6,6 +6,7 @@ namespace Magento\Rss\Test\Unit\Controller\Feed; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Zend\Feed\Writer\Exception\InvalidArgumentException; /** * Class IndexTest @@ -52,6 +53,7 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $objectManagerHelper = new ObjectManagerHelper($this); + $this->controller = $objectManagerHelper->getObject( \Magento\Rss\Controller\Feed\Index::class, [ @@ -90,14 +92,22 @@ public function testExecuteWithException() $dataProvider = $this->createMock(\Magento\Framework\App\Rss\DataProviderInterface::class); $dataProvider->expects($this->once())->method('isAllowed')->will($this->returnValue(true)); - $rssModel = $this->createPartialMock(\Magento\Rss\Model\Rss::class, ['setDataProvider']); + $rssModel = $this->createPartialMock(\Magento\Rss\Model\Rss::class, ['setDataProvider', 'createRssXml']); $rssModel->expects($this->once())->method('setDataProvider')->will($this->returnSelf()); + $exceptionMock = new \Magento\Framework\Exception\RuntimeException( + new \Magento\Framework\Phrase('Any message') + ); + + $rssModel->expects($this->once())->method('createRssXml')->will( + $this->throwException($exceptionMock) + ); + $this->response->expects($this->once())->method('setHeader')->will($this->returnSelf()); $this->rssFactory->expects($this->once())->method('create')->will($this->returnValue($rssModel)); $this->rssManager->expects($this->once())->method('getProvider')->will($this->returnValue($dataProvider)); - $this->expectException('\Zend_Feed_Builder_Exception'); + $this->expectException(\Magento\Framework\Exception\RuntimeException::class); $this->controller->execute(); } } diff --git a/app/code/Magento/Rss/Test/Unit/Model/RssManagerTest.php b/app/code/Magento/Rss/Test/Unit/Model/RssManagerTest.php index 6583e772589c7..b31fd676a6885 100644 --- a/app/code/Magento/Rss/Test/Unit/Model/RssManagerTest.php +++ b/app/code/Magento/Rss/Test/Unit/Model/RssManagerTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Rss\Test\Unit\Model; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -45,8 +43,8 @@ public function testGetProvider() $this->objectManager->expects($this->once())->method('get')->will($this->returnValue($dataProvider)); $this->assertInstanceOf( - \Magento\Framework\App\Rss\DataProviderInterface::class, - $this->rssManager->getProvider('rss_feed') + \Magento\Framework\App\Rss\DataProviderInterface::class, + $this->rssManager->getProvider('rss_feed') ); } diff --git a/app/code/Magento/Rss/Test/Unit/Model/RssTest.php b/app/code/Magento/Rss/Test/Unit/Model/RssTest.php index 6f98b9f202e30..f2888e4296b40 100644 --- a/app/code/Magento/Rss/Test/Unit/Model/RssTest.php +++ b/app/code/Magento/Rss/Test/Unit/Model/RssTest.php @@ -19,7 +19,7 @@ class RssTest extends \PHPUnit\Framework\TestCase /** * @var array */ - protected $feedData = [ + private $feedData = [ 'title' => 'Feed Title', 'link' => 'http://magento.com/rss/link', 'description' => 'Feed Description', @@ -33,6 +33,27 @@ class RssTest extends \PHPUnit\Framework\TestCase ], ]; + /** + * @var string + */ + private $feedXml = '<?xml version="1.0" encoding="UTF-8"?> +<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"> + <channel> + <title><![CDATA[Feed Title]]> + http://magento.com/rss/link + + Sat, 22 Apr 2017 13:21:12 +0200 + Zend\Feed + http://blogs.law.harvard.edu/tech/rss + + <![CDATA[Feed 1 Title]]> + http://magento.com/rss/link/id/1 + + Sat, 22 Apr 2017 13:21:12 +0200 + + +'; + /** * @var ObjectManagerHelper */ @@ -43,6 +64,16 @@ class RssTest extends \PHPUnit\Framework\TestCase */ private $cacheMock; + /** + * @var \Magento\Framework\App\FeedFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $feedFactoryMock; + + /** + * @var \Magento\Framework\App\FeedInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $feedMock; + /** * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -52,11 +83,15 @@ protected function setUp() { $this->cacheMock = $this->createMock(\Magento\Framework\App\CacheInterface::class); $this->serializerMock = $this->createMock(SerializerInterface::class); + $this->feedFactoryMock = $this->createMock(\Magento\Framework\App\FeedFactoryInterface::class); + $this->feedMock = $this->createMock(\Magento\Framework\App\FeedInterface::class); + $this->objectManagerHelper = new ObjectManagerHelper($this); $this->rss = $this->objectManagerHelper->getObject( \Magento\Rss\Model\Rss::class, [ 'cache' => $this->cacheMock, + 'feedFactory' => $this->feedFactoryMock, 'serializer' => $this->serializerMock ] ); @@ -116,14 +151,16 @@ public function testCreateRssXml() $dataProvider->expects($this->any())->method('getCacheLifetime')->will($this->returnValue(100)); $dataProvider->expects($this->any())->method('getRssData')->will($this->returnValue($this->feedData)); + $this->feedMock->expects($this->once()) + ->method('getFormattedContent') + ->willReturn($this->feedXml); + + $this->feedFactoryMock->expects($this->once()) + ->method('create') + ->with($this->feedData, \Magento\Framework\App\FeedFactoryInterface::FORMAT_RSS) + ->will($this->returnValue($this->feedMock)); + $this->rss->setDataProvider($dataProvider); - $result = $this->rss->createRssXml(); - $this->assertContains('', $result); - $this->assertContains('<![CDATA[Feed Title]]>', $result); - $this->assertContains('<![CDATA[Feed 1 Title]]>', $result); - $this->assertContains('http://magento.com/rss/link', $result); - $this->assertContains('http://magento.com/rss/link/id/1', $result); - $this->assertContains('', $result); - $this->assertContains('', $result); + $this->assertNotNull($this->rss->createRssXml()); } } diff --git a/app/code/Magento/Rss/Test/Unit/Model/UrlBuilderTest.php b/app/code/Magento/Rss/Test/Unit/Model/UrlBuilderTest.php index df3ff3649d1ab..fcb1eddaef1f0 100644 --- a/app/code/Magento/Rss/Test/Unit/Model/UrlBuilderTest.php +++ b/app/code/Magento/Rss/Test/Unit/Model/UrlBuilderTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Rss\Test\Unit\Model; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -64,6 +62,7 @@ public function testGetUrl() ->will($this->returnValue('http://magento.com/rss/feed/index/type/rss_feed')); $this->assertEquals( 'http://magento.com/rss/feed/index/type/rss_feed', - $this->urlBuilder->getUrl(['type' => 'rss_feed'])); + $this->urlBuilder->getUrl(['type' => 'rss_feed']) + ); } } diff --git a/app/code/Magento/Rss/composer.json b/app/code/Magento/Rss/composer.json index e462497581b14..3277fe52ca1eb 100644 --- a/app/code/Magento/Rss/composer.json +++ b/app/code/Magento/Rss/composer.json @@ -2,14 +2,14 @@ "name": "magento/module-rss", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-backend": "100.2.*", "magento/framework": "101.0.*", "magento/module-customer": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Rss/etc/di.xml b/app/code/Magento/Rss/etc/di.xml index 3b00e1893efaf..d9e930482578e 100644 --- a/app/code/Magento/Rss/etc/di.xml +++ b/app/code/Magento/Rss/etc/di.xml @@ -8,4 +8,5 @@ + diff --git a/app/code/Magento/Rss/view/frontend/templates/feeds.phtml b/app/code/Magento/Rss/view/frontend/templates/feeds.phtml index 22328e9188ea5..267973f51c7a4 100644 --- a/app/code/Magento/Rss/view/frontend/templates/feeds.phtml +++ b/app/code/Magento/Rss/view/frontend/templates/feeds.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Rss\Block\Feeds $block */ ?> @@ -13,15 +11,15 @@
    - getFeeds() as $feed): ?> - + getFeeds() as $feed) : ?> + - + diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 67e4671236ea0..095e3382b2d31 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\AbstractBlock; /** + * Renderer for Editable sales rules. + * * @api * @since 100.0.2 */ @@ -52,15 +54,15 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele if ($element->getShowAsText()) { $html = ' ' . - htmlspecialchars( + $this->escapeHtml( $valueName ) . ' '; } else { diff --git a/app/code/Magento/Rule/Model/Action/AbstractAction.php b/app/code/Magento/Rule/Model/Action/AbstractAction.php index fb15edf8a4893..4d56f6cc56edc 100644 --- a/app/code/Magento/Rule/Model/Action/AbstractAction.php +++ b/app/code/Magento/Rule/Model/Action/AbstractAction.php @@ -49,13 +49,16 @@ public function __construct( $this->loadAttributeOptions()->loadOperatorOptions()->loadValueOptions(); - foreach (array_keys($this->getAttributeOption()) as $attr) { - $this->setAttribute($attr); - break; + $attributes = $this->getAttributeOption(); + if ($attributes) { + reset($attributes); + $this->setAttribute(key($attributes)); } - foreach (array_keys($this->getOperatorOption()) as $operator) { - $this->setOperator($operator); - break; + + $operators = $this->getOperatorOption(); + if ($operators) { + reset($operators); + $this->setOperator(key($operators)); } } diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index 01e715f06a27f..dcf742ee91103 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -24,7 +24,6 @@ abstract class AbstractCondition extends \Magento\Framework\DataObject implement { /** * Defines which operators will be available for this condition - * * @var string */ protected $_inputType = null; @@ -84,17 +83,13 @@ public function __construct(Context $context, array $data = []) $options = $this->getAttributeOptions(); if ($options) { - foreach (array_keys($options) as $attr) { - $this->setAttribute($attr); - break; - } + reset($options); + $this->setAttribute(key($options)); } $options = $this->getOperatorOptions(); if ($options) { - foreach (array_keys($options) as $operator) { - $this->setOperator($operator); - break; - } + reset($options); + $this->setOperator(key($options)); } } @@ -160,14 +155,13 @@ public function getForm() */ public function asArray(array $arrAttributes = []) { - $out = [ + return [ 'type' => $this->getType(), 'attribute' => $this->getAttribute(), 'operator' => $this->getOperator(), 'value' => $this->getValue(), 'is_value_processed' => $this->getIsValueParsed(), ]; - return $out; } /** @@ -205,7 +199,7 @@ public function getMappedSqlField() */ public function asXml() { - $xml = "" . + return "" . $this->getType() . "" . "" . @@ -217,7 +211,6 @@ public function asXml() "" . $this->getValue() . ""; - return $xml; } /** @@ -244,8 +237,7 @@ public function loadXml($xml) if (is_string($xml)) { $xml = simplexml_load_string($xml); } - $arr = (array)$xml; - $this->loadArray($arr); + $this->loadArray((array)$xml); return $this; } @@ -304,10 +296,7 @@ public function loadOperatorOptions() */ public function getInputType() { - if (null === $this->_inputType) { - return 'string'; - } - return $this->_inputType; + return null === $this->_inputType ? 'string' : $this->_inputType; } /** @@ -348,12 +337,11 @@ public function loadValueOptions() */ public function getValueSelectOptions() { - $valueOption = $opt = []; + $opt = []; if ($this->hasValueOption()) { - $valueOption = (array)$this->getValueOption(); - } - foreach ($valueOption as $key => $value) { - $opt[] = ['value' => $key, 'label' => $value]; + foreach ((array)$this->getValueOption() as $key => $value) { + $opt[] = ['value' => $key, 'label' => $value]; + } } return $opt; } @@ -367,10 +355,10 @@ public function getValueParsed() { if (!$this->hasValueParsed()) { $value = $this->getData('value'); - if (is_array($value) && isset($value[0]) && is_string($value[0])) { - $value = $value[0]; + if (is_array($value) && count($value) === 1) { + $value = reset($value); } - if ($this->isArrayOperatorType() && $value) { + if (!is_array($value) && $this->isArrayOperatorType() && $value) { $value = preg_split('#\s*[,;]\s*#', $value, null, PREG_SPLIT_NO_EMPTY); } $this->setValueParsed($value); @@ -392,7 +380,7 @@ public function isArrayOperatorType() } /** - * @return array + * @return mixed */ public function getValue() { @@ -470,13 +458,12 @@ public function getNewChildName() */ public function asHtml() { - $html = $this->getTypeElementHtml() . + return $this->getTypeElementHtml() . $this->getAttributeElementHtml() . $this->getOperatorElementHtml() . $this->getValueElementHtml() . $this->getRemoveLinkHtml() . $this->getChooserContainerHtml(); - return $html; } /** @@ -484,8 +471,7 @@ public function asHtml() */ public function asHtmlRecursive() { - $html = $this->asHtml(); - return $html; + return $this->asHtml(); } /** @@ -520,9 +506,10 @@ public function getTypeElementHtml() public function getAttributeElement() { if (null === $this->getAttribute()) { - foreach (array_keys($this->getAttributeOption()) as $option) { - $this->setAttribute($option); - break; + $options = $this->getAttributeOption(); + if ($options) { + reset($options); + $this->setAttribute(key($options)); } } return $this->getForm()->addField( @@ -558,10 +545,8 @@ public function getOperatorElement() { $options = $this->getOperatorSelectOptions(); if ($this->getOperator() === null) { - foreach ($options as $option) { - $this->setOperator($option['value']); - break; - } + $option = reset($options); + $this->setOperator($option['value']); } $elementId = sprintf('%s__%s__operator', $this->getPrefix(), $this->getId()); @@ -630,6 +615,9 @@ public function getValueElement() // date format intentionally hard-coded $elementParams['input_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; $elementParams['date_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['placeholder'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['autocomplete'] = 'off'; + $elementParams['readonly'] = 'true'; } return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__value', @@ -654,8 +642,7 @@ public function getValueElementHtml() public function getAddLinkHtml() { $src = $this->_assetRepo->getUrl('images/rule_component_add.gif'); - $html = ''; - return $html; + return ''; } /** @@ -676,11 +663,7 @@ public function getRemoveLinkHtml() public function getChooserContainerHtml() { $url = $this->getValueElementChooserUrl(); - $html = ''; - if ($url) { - $html = '
    '; - } - return $html; + return $url ? '
    ' : ''; } /** @@ -690,8 +673,7 @@ public function getChooserContainerHtml() */ public function asString($format = '') { - $str = $this->getAttributeName() . ' ' . $this->getOperatorName() . ' ' . $this->getValueName(); - return $str; + return $this->getAttributeName() . ' ' . $this->getOperatorName() . ' ' . $this->getValueName(); } /** @@ -700,8 +682,7 @@ public function asString($format = '') */ public function asStringRecursive($level = 0) { - $str = str_pad('', $level * 3, ' ', STR_PAD_LEFT) . $this->asString(); - return $str; + return str_pad('', $level * 3, ' ', STR_PAD_LEFT) . $this->asString(); } /** @@ -740,12 +721,10 @@ public function validateAttribute($validatedValue) case '==': case '!=': if (is_array($value)) { - if (is_array($validatedValue)) { - $result = array_intersect($value, $validatedValue); - $result = !empty($result); - } else { + if (!is_array($validatedValue)) { return false; } + $result = !empty(array_intersect($value, $validatedValue)); } else { if (is_array($validatedValue)) { $result = count($validatedValue) == 1 && array_shift($validatedValue) == $value; @@ -759,18 +738,16 @@ public function validateAttribute($validatedValue) case '>': if (!is_scalar($validatedValue)) { return false; - } else { - $result = $validatedValue <= $value; } + $result = $validatedValue <= $value; break; case '>=': case '<': if (!is_scalar($validatedValue)) { return false; - } else { - $result = $validatedValue >= $value; } + $result = $validatedValue >= $value; break; case '{}': @@ -783,12 +760,11 @@ public function validateAttribute($validatedValue) } } } elseif (is_array($value)) { - if (is_array($validatedValue)) { - $result = array_intersect($value, $validatedValue); - $result = !empty($result); - } else { + if (!is_array($validatedValue)) { return false; } + $result = array_intersect($value, $validatedValue); + $result = !empty($result); } else { if (is_array($validatedValue)) { $result = in_array($value, $validatedValue); @@ -833,13 +809,13 @@ protected function _compareValues($validatedValue, $value, $strict = true) { if ($strict && is_numeric($validatedValue) && is_numeric($value)) { return $validatedValue == $value; - } else { - $validatePattern = preg_quote($validatedValue, '~'); - if ($strict) { - $validatePattern = '^' . $validatePattern . '$'; - } - return (bool)preg_match('~' . $validatePattern . '~iu', $value); } + + $validatePattern = preg_quote($validatedValue, '~'); + if ($strict) { + $validatePattern = '^' . $validatePattern . '$'; + } + return (bool)preg_match('~' . $validatePattern . '~iu', $value); } /** diff --git a/app/code/Magento/Rule/Model/Condition/Combine.php b/app/code/Magento/Rule/Model/Condition/Combine.php index 24ed1cb497472..48873aec66295 100644 --- a/app/code/Magento/Rule/Model/Condition/Combine.php +++ b/app/code/Magento/Rule/Model/Condition/Combine.php @@ -46,10 +46,8 @@ public function __construct(Context $context, array $data = []) $this->loadAggregatorOptions(); $options = $this->getAggregatorOptions(); if ($options) { - foreach (array_keys($options) as $aggregator) { - $this->setAggregator($aggregator); - break; - } + reset($options); + $this->setAggregator(key($options)); } } @@ -90,9 +88,10 @@ public function getAggregatorName() public function getAggregatorElement() { if ($this->getAggregator() === null) { - foreach (array_keys($this->getAggregatorOption()) as $key) { - $this->setAggregator($key); - break; + $options = $this->getAggregatorOption(); + if ($options) { + reset($options); + $this->setAggregator(key($options)); } } return $this->getForm()->addField( diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index 5ab1379b96cf6..53ba319d47ef0 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -95,8 +95,8 @@ abstract class AbstractProduct extends \Magento\Rule\Model\Condition\AbstractCon * @param \Magento\Catalog\Model\ResourceModel\Product $productResource * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection * @param \Magento\Framework\Locale\FormatInterface $localeFormat - * @param ProductCategoryList|null $categoryList * @param array $data + * @param ProductCategoryList|null $categoryList * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -241,7 +241,9 @@ protected function _prepareValueOptions() } else { $addEmptyOption = true; } - $selectOptions = $attributeObject->getSource()->getAllOptions($addEmptyOption); + $selectOptions = $this->removeTagsFromLabel( + $attributeObject->getSource()->getAllOptions($addEmptyOption) + ); } } @@ -514,6 +516,10 @@ public function loadArray($arr) ) ? $this->_localeFormat->getNumber( $arr['is_value_parsed'] ) : false; + } elseif (!empty($arr['operator']) && $arr['operator'] == '()') { + if (isset($arr['value'])) { + $arr['value'] = preg_replace('/\s*,\s*/', ',', $arr['value']); + } } return parent::loadArray($arr); @@ -605,6 +611,7 @@ public function getBindArgumentValue() )->__toString() ); } + return parent::getBindArgumentValue(); } @@ -695,6 +702,7 @@ protected function _getAttributeSetId($productId) /** * Correct '==' and '!=' operators + * * Categories can't be equal because product is included categories selected by administrator and in their parents * * @return string @@ -702,7 +710,7 @@ protected function _getAttributeSetId($productId) public function getOperatorForValidate() { $operator = $this->getOperator(); - if ($this->getInputType() == 'category') { + if ('category' === $this->getInputType()) { if ($operator == '==') { $operator = '{}'; } elseif ($operator == '!=') { @@ -734,4 +742,21 @@ protected function getEavAttributeTableAlias() return 'at_' . $attribute->getAttributeCode(); } + + /** + * Remove html tags from attribute labels. + * + * @param array $selectOptions + * @return array + */ + private function removeTagsFromLabel(array $selectOptions) + { + foreach ($selectOptions as &$option) { + if (isset($option['label'])) { + $option['label'] = strip_tags($option['label']); + } + } + + return $selectOptions; + } } diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 41a55f4c25166..dff582220e084 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -6,9 +6,14 @@ namespace Magento\Rule\Model\Condition\Sql; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Rule\Model\Condition\AbstractCondition; use Magento\Rule\Model\Condition\Combine; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Entity\Collection\AbstractCollection; /** * Class SQL Builder @@ -27,9 +32,13 @@ class Builder '==' => ':field = ?', '!=' => ':field <> ?', '>=' => ':field >= ?', + '>=' => ':field >= ?', '>' => ':field > ?', + '>' => ':field > ?', '<=' => ':field <= ?', + '<=' => ':field <= ?', '<' => ':field < ?', + '<' => ':field < ?', '{}' => ':field IN (?)', '!{}' => ':field NOT IN (?)', '()' => ':field IN (?)', @@ -41,12 +50,22 @@ class Builder */ protected $_expressionFactory; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + /** * @param ExpressionFactory $expressionFactory + * @param AttributeRepositoryInterface|null $attributeRepository */ - public function __construct(ExpressionFactory $expressionFactory) - { + public function __construct( + ExpressionFactory $expressionFactory, + AttributeRepositoryInterface $attributeRepository = null + ) { $this->_expressionFactory = $expressionFactory; + $this->attributeRepository = $attributeRepository ?: + ObjectManager::getInstance()->get(AttributeRepositoryInterface::class); } /** @@ -88,12 +107,12 @@ protected function _getChildCombineTablesToJoin(Combine $combine, $tables = []) /** * Join tables from conditions combination to collection * - * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @param AbstractCollection $collection * @param Combine $combine * @return $this */ protected function _joinTablesToCollection( - \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection, + AbstractCollection $collection, Combine $combine ) { foreach ($this->_getCombineTablesToJoin($combine) as $alias => $joinTable) { @@ -112,16 +131,20 @@ protected function _joinTablesToCollection( * * @param AbstractCondition $condition * @param string $value + * @param bool $isDefaultStoreUsed no longer used because caused an issue about not existing table alias * @return string * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function _getMappedSqlCondition(AbstractCondition $condition, $value = '') + protected function _getMappedSqlCondition(AbstractCondition $condition, $value = '', $isDefaultStoreUsed = true) { $argument = $condition->getMappedSqlField(); - // If rule hasn't valid argument - create negative expression to prevent incorrect rule behavior. + // If rule hasn't valid argument - prevent incorrect rule behavior. if (empty($argument)) { return $this->_expressionFactory->create(['expression' => '1 = -1']); + } elseif (preg_match('/[^a-z0-9\-_\.\`]/i', $argument) > 0) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid field')); } $conditionOperator = $condition->getOperatorForValidate(); @@ -130,24 +153,41 @@ protected function _getMappedSqlCondition(AbstractCondition $condition, $value = throw new \Magento\Framework\Exception\LocalizedException(__('Unknown condition operator')); } + $defaultValue = 0; $sql = str_replace( ':field', - $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), 0), + $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue), $this->_conditionOperatorMap[$conditionOperator] ); + $bindValue = $condition->getBindArgumentValue(); + $expression = $value . $this->_connection->quoteInto($sql, $bindValue); + + // values for multiselect attributes can be saved in comma separated format + // below is a solution for matching such conditions with selected values + if (in_array($conditionOperator, ['()', '{}']) && is_array($bindValue)) { + foreach ($bindValue as $item) { + $expression .= $this->_connection->quoteInto( + " OR (FIND_IN_SET (?, {$this->_connection->quoteIdentifier($argument)}) > 0)", + $item + ); + } + } + return $this->_expressionFactory->create( - ['expression' => $value . $this->_connection->quoteInto($sql, $condition->getBindArgumentValue())] + ['expression' => $expression] ); } /** * @param Combine $combine * @param string $value + * @param bool $isDefaultStoreUsed * @return string * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\LocalizedException */ - protected function _getMappedSqlCombination(Combine $combine, $value = '') + protected function _getMappedSqlCombination(Combine $combine, $value = '', $isDefaultStoreUsed = true) { $out = (!empty($value) ? $value : ''); $value = ($combine->getValue() ? '' : ' NOT '); @@ -158,9 +198,9 @@ protected function _getMappedSqlCombination(Combine $combine, $value = '') $con = ($getAggregator == 'any' ? Select::SQL_OR : Select::SQL_AND); $con = (isset($conditions[$key+1]) ? $con : ''); if ($condition instanceof Combine) { - $out .= $this->_getMappedSqlCombination($condition, $value); + $out .= $this->_getMappedSqlCombination($condition, $value, $isDefaultStoreUsed); } else { - $out .= $this->_getMappedSqlCondition($condition, $value); + $out .= $this->_getMappedSqlCondition($condition, $value, $isDefaultStoreUsed); } $out .= $out ? (' ' . $con) : ''; } @@ -170,13 +210,12 @@ protected function _getMappedSqlCombination(Combine $combine, $value = '') /** * Attach conditions filter to collection * - * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @param AbstractCollection $collection * @param Combine $combine - * * @return void */ public function attachConditionToCollection( - \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection, + AbstractCollection $collection, Combine $combine ) { $this->_connection = $collection->getResource()->getConnection(); diff --git a/app/code/Magento/Rule/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Rule/Model/ResourceModel/AbstractResource.php index 2fdb960521a97..6e685a9a9b978 100644 --- a/app/code/Magento/Rule/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Rule/Model/ResourceModel/AbstractResource.php @@ -81,7 +81,7 @@ public function bindRuleToEntity($ruleIds, $entityIds, $entityType) try { $this->_multiplyBunchInsert($ruleIds, $entityIds, $entityType); } catch (\Exception $e) { - $this->getConnection()->rollback(); + $this->getConnection()->rollBack(); throw $e; } diff --git a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php index b3d761b378d94..e0468f17e587a 100644 --- a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php +++ b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php @@ -83,11 +83,21 @@ public function addWebsiteFilter($websiteId) if ($website instanceof \Magento\Store\Model\Website) { $websiteIds[$index] = $website->getId(); } + $websiteIds[$index] = (int) $websiteIds[$index]; } + + $websiteSelect = $this->getConnection()->select(); + $websiteSelect->from( + $this->getTable($entityInfo['associations_table']), + [$entityInfo['rule_id_field']] + )->distinct( + true + )->where( + $this->getConnection()->quoteInto($entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) + ); $this->getSelect()->join( - ['website' => $this->getTable($entityInfo['associations_table'])], - $this->getConnection()->quoteInto('website.' . $entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) - . ' AND main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], + ['website' => $websiteSelect], + 'main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], [] ); } diff --git a/app/code/Magento/Rule/Test/Mftf/LICENSE.txt b/app/code/Magento/Rule/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Rule/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/app/code/Magento/Rule/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Rule/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Rule/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/app/code/Magento/Rule/Test/Mftf/README.md b/app/code/Magento/Rule/Test/Mftf/README.md new file mode 100644 index 0000000000000..46d592f4ab950 --- /dev/null +++ b/app/code/Magento/Rule/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Rule Functional Tests + +The Functional Test Module for **Magento Rule** module. diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php index 1201ba92464d9..52653197e3981 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php @@ -40,6 +40,9 @@ public function testGetMappedSqlField() $this->assertEquals('category_ids', $this->_condition->getMappedSqlField()); } + /** + * @return array + */ public function validateAttributeDataProvider() { return [ @@ -146,6 +149,9 @@ public function testValidate($existingValue, $operator, $valueForValidate, $expe ); } + /** + * @return array + */ public function validateAttributeArrayInputTypeDataProvider() { return [ diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php index f53098c4bb97e..7be94bf690e8b 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php @@ -35,7 +35,12 @@ public function testAttachConditionToCollection() { $collection = $this->createPartialMock( \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, - ['getResource', 'getSelect'] + [ + 'getResource', + 'getSelect', + 'getStoreId', + 'getDefaultStoreId', + ] ); $combine = $this->createPartialMock(\Magento\Rule\Model\Condition\Combine::class, ['getConditions']); $resource = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['getConnection']); @@ -53,7 +58,6 @@ public function testAttachConditionToCollection() $collection->expects($this->once()) ->method('getResource') ->will($this->returnValue($resource)); - $collection->expects($this->any()) ->method('getSelect') ->will($this->returnValue($select)); @@ -68,4 +72,69 @@ public function testAttachConditionToCollection() $this->_builder->attachConditionToCollection($collection, $combine); } + + /** + * Test for attach condition to collection with operator in html format. + * + * @covers \Magento\Rule\Model\Condition\Sql\Builder::attachConditionToCollection() + * @return void + */ + public function testAttachConditionAsHtmlToCollection() + { + $abstractCondition = $this->getMockForAbstractClass( + \Magento\Rule\Model\Condition\AbstractCondition::class, + [], + '', + false, + false, + true, + ['getOperatorForValidate', 'getMappedSqlField', 'getAttribute', 'getBindArgumentValue'] + ); + + $abstractCondition->expects($this->once())->method('getMappedSqlField')->will($this->returnValue('argument')); + $abstractCondition->expects($this->once())->method('getOperatorForValidate')->will($this->returnValue('>')); + $abstractCondition->expects($this->at(1))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->at(2))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->once())->method('getBindArgumentValue')->will($this->returnValue(10)); + + $conditions = [$abstractCondition]; + $collection = $this->createPartialMock( + \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, + [ + 'getResource', + 'getSelect', + ] + ); + $combine = $this->createPartialMock( + \Magento\Rule\Model\Condition\Combine::class, + [ + 'getConditions', + 'getValue', + 'getAggregator', + ] + ); + + $resource = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['getConnection']); + $select = $this->createPartialMock(\Magento\Framework\DB\Select::class, ['where']); + $select->expects($this->never())->method('where'); + + $connection = $this->getMockForAbstractClass( + \Magento\Framework\DB\Adapter\AdapterInterface::class, + ['quoteInto'], + '', + false + ); + + $connection->expects($this->once())->method('quoteInto')->with(' > ?', 10)->will($this->returnValue(' > 10')); + $collection->expects($this->once())->method('getResource')->will($this->returnValue($resource)); + $resource->expects($this->once())->method('getConnection')->will($this->returnValue($connection)); + $combine->expects($this->once())->method('getValue')->willReturn('attribute'); + $combine->expects($this->once())->method('getAggregator')->willReturn(' AND '); + $combine->expects($this->at(0))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(1))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(2))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(3))->method('getConditions')->will($this->returnValue($conditions)); + + $this->_builder->attachConditionToCollection($collection, $combine); + } } diff --git a/app/code/Magento/Rule/Test/Unit/Model/ConditionFactoryTest.php b/app/code/Magento/Rule/Test/Unit/Model/ConditionFactoryTest.php index d8c0cc470f55e..f78ee4f345d0d 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/ConditionFactoryTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/ConditionFactoryTest.php @@ -78,7 +78,8 @@ public function testCreateExceptionClass() ->expects($this->never()) ->method('create'); - $this->expectException(\InvalidArgumentException::class, 'Class does not exist'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Class does not exist'); $this->conditionFactory->create($type); } @@ -92,7 +93,8 @@ public function testCreateExceptionType() ->method('create') ->with($type) ->willReturn(new \stdClass()); - $this->expectException(\InvalidArgumentException::class, 'Class does not implement condition interface'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Class does not implement condition interface'); $this->conditionFactory->create($type); } } diff --git a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php b/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php deleted file mode 100644 index 644d10ad75fe6..0000000000000 --- a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php +++ /dev/null @@ -1,197 +0,0 @@ -_entityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactoryInterface::class); - $this->_loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->_fetchStrategyMock = $this->createMock( - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class - ); - $this->_managerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_db = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, - [], - '', - false, - false, - true, - ['__sleep', '__wakeup', 'getTable'] - ); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->abstractCollection = $this->getMockForAbstractClass( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - [ - 'entityFactory' => $this->_entityFactoryMock, - 'logger' => $this->_loggerMock, - 'fetchStrategy' => $this->_fetchStrategyMock, - 'eventManager' => $this->_managerMock, - null, - $this->_db - ], - '', - false, - false, - true, - ['__sleep', '__wakeup', '_getAssociatedEntityInfo', 'getConnection', 'getSelect', 'getTable'] - ); - } - - public function addWebsitesToResultDataProvider() - { - return [ - [null, true], - [true, true], - [false, false] - ]; - } - - /** - * @dataProvider addWebsitesToResultDataProvider - */ - public function testAddWebsitesToResult($flag, $expectedResult) - { - $this->abstractCollection->addWebsitesToResult($flag); - $this->assertEquals($expectedResult, $this->abstractCollection->getFlag('add_websites_to_result')); - } - - protected function _prepareAddFilterStubs() - { - $entityInfo = []; - $entityInfo['entity_id_field'] = 'entity_id'; - $entityInfo['rule_id_field'] = 'rule_id'; - $entityInfo['associations_table'] = 'assoc_table'; - - $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $select = $this->createMock(\Magento\Framework\DB\Select::class); - $collectionSelect = $this->createMock(\Magento\Framework\DB\Select::class); - - $connection->expects($this->any()) - ->method('select') - ->will($this->returnValue($select)); - - $select->expects($this->any()) - ->method('from') - ->will($this->returnSelf()); - - $select->expects($this->any()) - ->method('where') - ->will($this->returnSelf()); - - $this->abstractCollection->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connection)); - - $this->_db->expects($this->any()) - ->method('getTable') - ->will($this->returnArgument(0)); - - $this->abstractCollection->expects($this->any()) - ->method('getSelect') - ->will($this->returnValue($collectionSelect)); - - $this->abstractCollection->expects($this->any()) - ->method('_getAssociatedEntityInfo') - ->will($this->returnValue($entityInfo)); - } - - public function testAddWebsiteFilter() - { - $this->_prepareAddFilterStubs(); - $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getId', '__sleep', '__wakeup']); - - $website->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter($website) - ); - } - - public function testAddWebsiteFilterArray() - { - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->connectionMock->expects($this->atLeastOnce()) - ->method('quoteInto') - ->with($this->equalTo('website. IN (?)'), $this->equalTo(['2', '3'])) - ->willReturn(true); - - $this->abstractCollection->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock); - $this->abstractCollection->expects($this->atLeastOnce())->method('getConnection') - ->willReturn($this->connectionMock); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter(['2', '3']) - ); - } - - public function testAddFieldToFilter() - { - $this->_prepareAddFilterStubs(); - $result = $this->abstractCollection->addFieldToFilter('website_ids', []); - $this->assertNotNull($result); - } -} diff --git a/app/code/Magento/Rule/composer.json b/app/code/Magento/Rule/composer.json index d42f9dc60f08d..3a0ebbdd6c21d 100644 --- a/app/code/Magento/Rule/composer.json +++ b/app/code/Magento/Rule/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-rule", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-catalog": "102.0.*", @@ -11,7 +11,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Rule/view/adminhtml/web/rules.js b/app/code/Magento/Rule/view/adminhtml/web/rules.js index 5c4be367b9cb3..8e36562ebd7fe 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/rules.js +++ b/app/code/Magento/Rule/view/adminhtml/web/rules.js @@ -137,7 +137,7 @@ define([ }, onSuccess: function (transport) { if (this._processSuccess(transport)) { - $(chooser).update(transport.responseText); + jQuery(chooser).html(transport.responseText); this.showChooserLoaded(chooser, transport); jQuery(chooser).trigger('contentUpdated'); } @@ -220,6 +220,8 @@ define([ var elem = Element.down(elemContainer, 'input.input-text'); + jQuery(elem).trigger('contentUpdated'); + if (elem) { elem.focus(); diff --git a/app/code/Magento/Sales/Api/CreditmemoCommentRepositoryInterface.php b/app/code/Magento/Sales/Api/CreditmemoCommentRepositoryInterface.php index 6483b6b2293e7..05b9f164c3169 100644 --- a/app/code/Magento/Sales/Api/CreditmemoCommentRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/CreditmemoCommentRepositoryInterface.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Api; /** @@ -34,7 +32,8 @@ public function get($id); * Returns a credit memo comment search results interface. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. - * @return \Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterface Credit memo comment search results interface. + * @return \Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterface Credit memo comment + * search results interface. */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); diff --git a/app/code/Magento/Sales/Api/CreditmemoManagementInterface.php b/app/code/Magento/Sales/Api/CreditmemoManagementInterface.php index 4d0eaa045d16a..0222f3c2b7afa 100644 --- a/app/code/Magento/Sales/Api/CreditmemoManagementInterface.php +++ b/app/code/Magento/Sales/Api/CreditmemoManagementInterface.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Api; /** @@ -32,7 +30,8 @@ public function cancel($id); * Lists comments for a specified credit memo. * * @param int $id The credit memo ID. - * @return \Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterface Credit memo comment search results interface. + * @return \Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterface Credit memo comment + * search results interface. */ public function getCommentsList($id); diff --git a/app/code/Magento/Sales/Api/Data/CreditmemoInterface.php b/app/code/Magento/Sales/Api/Data/CreditmemoInterface.php index c3fad3e2fac57..66104a6531974 100644 --- a/app/code/Magento/Sales/Api/Data/CreditmemoInterface.php +++ b/app/code/Magento/Sales/Api/Data/CreditmemoInterface.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Api\Data; /** @@ -617,7 +615,8 @@ public function setItems($items); /** * Gets credit memo comments. * - * @return \Magento\Sales\Api\Data\CreditmemoCommentInterface[]|null Array of any credit memo comments. Otherwise, null. + * @return \Magento\Sales\Api\Data\CreditmemoCommentInterface[]|null Array of any credit memo comments. + * Otherwise, null. */ public function getComments(); diff --git a/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php b/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php new file mode 100644 index 0000000000000..8ed3d1caad6d7 --- /dev/null +++ b/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php @@ -0,0 +1,27 @@ +filterManager->truncate( + public function truncateString( + string $value, + int $length = 80, + string $etc = '...', + &$remainder = '', + bool $breakWords = true + ): string { + $this->truncateResult = $this->filterManager->truncateFilter( $value, - ['length' => $length, 'etc' => $etc, 'remainder' => $remainder, 'breakWords' => $breakWords] + ['length' => $length, 'etc' => $etc, 'breakWords' => $breakWords] ); + return $this->truncateResult->getValue(); } /** @@ -37,11 +53,14 @@ public function truncateString($value, $length = 80, $etc = '...', &$remainder = * @param string $value * @return array */ - public function getFormattedOption($value) + public function getFormattedOption(string $value): array { $remainder = ''; - $value = $this->truncateString($value, 55, '', $remainder); - $result = ['value' => nl2br($value), 'remainder' => nl2br($remainder)]; + $this->truncateString($value, 55, '', $remainder); + $result = [ + 'value' => nl2br($this->truncateResult->getValue()), + 'remainder' => nl2br($this->truncateResult->getRemainder()) + ]; return $result; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php index f2b454260dc22..2b5e1c5b6dc32 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php @@ -19,7 +19,7 @@ class Form extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address * * @var string */ - protected $_template = 'order/address/form.phtml'; + protected $_template = 'Magento_Sales::order/address/form.phtml'; /** * Core registry @@ -106,6 +106,14 @@ protected function _getAddress() */ protected function _prepareForm() { + $address = $this->_getAddress(); + if ($address !== null) { + $storeId = $this->_getAddress() + ->getOrder() + ->getStoreId(); + $this->_storeManager->setCurrentStore($storeId); + } + parent::_prepareForm(); $this->_form->setId('edit_form'); $this->_form->setMethod('post'); @@ -135,4 +143,20 @@ public function getFormValues() { return $this->_getAddress()->getData(); } + + /** + * @inheritdoc + */ + protected function processCountryOptions( + \Magento\Framework\Data\Form\Element\AbstractElement $countryElement, + $storeId = null + ) { + /** @var \Magento\Sales\Model\Order\Address $address */ + $address = $this->_getAddress(); + if ($address !== null) { + $storeId = $address->getOrder()->getStoreId(); + } + + parent::processCountryOptions($countryElement, $storeId); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php index 083055994a282..a26ddc2961765 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php @@ -81,7 +81,7 @@ protected function _construct() $this->buttonList->update( 'reset', 'onclick', - 'deleteConfirm(\'' . $confirm . '\', \'' . $this->getCancelUrl() . '\')' + 'deleteConfirm(\'' . $confirm . '\', \'' . $this->getCancelUrl() . '\', {data: {}})' ); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php index 5a29d058bf32f..2a32bd63d7076 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php @@ -164,6 +164,7 @@ public function getDataSelectorDisplay() public function getOrderDataJson() { $data = []; + $this->_storeManager->setCurrentStore($this->getStoreId()); if ($this->getCustomerId()) { $data['customer_id'] = $this->getCustomerId(); $data['addresses'] = []; @@ -189,6 +190,7 @@ public function getOrderDataJson() $data['shipping_method_reseted'] = !(bool)$this->getQuote()->getShippingAddress()->getShippingMethod(); $data['payment_method'] = $this->getQuote()->getPayment()->getMethod(); } + $data['quote_id'] = $this->_sessionQuote->getQuoteId(); return $this->_jsonEncoder->encode($data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php index d5aff28721534..c1b67e6dcf88a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php @@ -5,7 +5,6 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; -use Magento\Framework\Convert\ConvertArray; use Magento\Framework\Pricing\PriceCurrencyInterface; /** @@ -97,6 +96,11 @@ protected function _prepareLayout() public function getForm() { if ($this->_form === null) { + $storeId = $this->getCreateOrderModel() + ->getSession() + ->getStoreId(); + $this->_storeManager->setCurrentStore($storeId); + $this->_form = $this->_formFactory->create(); $this->_prepareForm(); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index f3647ca1bd5f6..c2a0eadf7b67a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Framework\Api\ExtensibleDataObjectConverter; @@ -134,7 +132,9 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - $this->_form->setValues($this->getFormValues()); + + $formValues = $this->extractValuesFromAttributes($attributes); + $this->_form->setValues($formValues); return $this; } @@ -149,7 +149,7 @@ protected function _addAdditionalFormElementData(AbstractElement $element) { switch ($element->getId()) { case 'email': - $element->setRequired(0); + $element->setRequired(1); $element->setClass('validate-email admin__control-text'); break; } @@ -168,7 +168,13 @@ public function getFormValues() } catch (\Exception $e) { /** If customer does not exist do nothing. */ } - $data = isset($customer) ? $this->_extensibleDataObjectConverter->toFlatArray($customer, [], \Magento\Customer\Api\Data\CustomerInterface::class) : []; + $data = isset($customer) + ? $this->_extensibleDataObjectConverter->toFlatArray( + $customer, + [], + \Magento\Customer\Api\Data\CustomerInterface::class + ) + : []; foreach ($this->getQuote()->getData() as $key => $value) { if (strpos($key, 'customer_') === 0) { $data[substr($key, 9)] = $value; @@ -181,4 +187,23 @@ public function getFormValues() return $data; } + + /** + * Extract the form values from attributes. + * + * @param array $attributes + * @return array + */ + private function extractValuesFromAttributes(array $attributes): array + { + $formValues = $this->getFormValues(); + foreach ($attributes as $code => $attribute) { + $defaultValue = $attribute->getDefaultValue(); + if (isset($defaultValue) && !isset($formValues[$code])) { + $formValues[$code] = $defaultValue; + } + } + + return $formValues; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index 5738e8ee33399..eb90a67ee9cf2 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -9,6 +9,8 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Eav\Model\AttributeDataFactory; /** * Order create address form @@ -190,17 +192,19 @@ public function getAddressCollectionJson() $emptyAddressForm = $this->_customerFormFactory->create( 'customer_address', 'adminhtml_customer_address', - [\Magento\Customer\Api\Data\AddressInterface::COUNTRY_ID => $defaultCountryId] + [AddressInterface::COUNTRY_ID => $defaultCountryId] ); - $data = [0 => $emptyAddressForm->outputData(\Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON)]; + $data = [0 => $emptyAddressForm->outputData(AttributeDataFactory::OUTPUT_FORMAT_JSON)]; foreach ($this->getAddressCollection() as $address) { $addressForm = $this->_customerFormFactory->create( 'customer_address', 'adminhtml_customer_address', - $this->addressMapper->toFlatArray($address) + $this->addressMapper->toFlatArray($address), + false, + false ); $data[$address->getId()] = $addressForm->outputData( - \Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON + AttributeDataFactory::OUTPUT_FORMAT_JSON ); } @@ -293,11 +297,17 @@ protected function _prepareForm() /** * @param \Magento\Framework\Data\Form\Element\AbstractElement $countryElement + * @param string|int $storeId + * * @return void */ - private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement) - { - $storeId = $this->getBackendQuoteSession()->getStoreId(); + protected function processCountryOptions( + \Magento\Framework\Data\Form\Element\AbstractElement $countryElement, + $storeId = null + ) { + if ($storeId === null) { + $storeId = $this->getBackendQuoteSession()->getStoreId(); + } $options = $this->getCountriesCollection() ->loadByStore($storeId) ->toOptionArray(); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 4bd2227d4bb1e..3be0df001b1b5 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,12 +5,17 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection + as ProductCollectionDataProvider; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml sales order create search products block * * @api * @author Magento Core Team * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -42,6 +47,11 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended */ protected $_productFactory; + /** + * @var ProductCollectionDataProvider $productCollectionProvider + */ + private $productCollectionProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper @@ -50,6 +60,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data + * @param ProductCollectionDataProvider|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +69,15 @@ public function __construct( \Magento\Catalog\Model\Config $catalogConfig, \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, - array $data = [] + array $data = [], + ProductCollectionDataProvider $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; + $this->productCollectionProvider = $productCollectionProvider + ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); parent::__construct($context, $backendHelper, $data); } @@ -71,6 +85,7 @@ public function __construct( * Constructor * * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -140,20 +155,18 @@ protected function _addColumnFilterToCollection($column) */ protected function _prepareCollection() { + $attributes = $this->_catalogConfig->getProductAttributes(); + $store = $this->getStore(); + /* @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ - $collection = $this->_productFactory->create()->getCollection(); - $collection->setStore( - $this->getStore() - )->addAttributeToSelect( + $collection = $this->productCollectionProvider->getCollectionForStore($store); + $collection->addAttributeToSelect( $attributes - )->addAttributeToSelect( - 'sku' - )->addStoreFilter()->addAttributeToFilter( + ); + $collection->addAttributeToFilter( 'type_id', $this->_salesConfig->getAvailableProductTypes() - )->addAttributeToSelect( - 'gift_message_available' ); $this->setCollection($collection); @@ -248,6 +261,7 @@ public function getGridUrl() * Get selected products * * @return mixed + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _getSelectedProducts() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php new file mode 100644 index 0000000000000..733791a2f9549 --- /dev/null +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php @@ -0,0 +1,55 @@ +collectionFactory = $collectionFactory; + } + + /** + * Provide products collection filtered with store + * + * @param Store $store + * @return Collection + */ + public function getCollectionForStore(Store $store):Collection + { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + $collection->setStore($store); + $collection->addAttributeToSelect( + 'gift_message_available' + ); + $collection->addAttributeToSelect( + 'sku' + ); + $collection->addStoreFilter(); + + return $collection; + } +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php index 1f0ee8304629a..19139350b509d 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php @@ -71,7 +71,7 @@ public function canDisplay() } /** - * Retrieve disply item qty availablity + * Retrieve display item qty availability * * @return false */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index 34d7a3f8ee25e..8179c0e8d282a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\FinalPrice; + /** * Adminhtml sales order create sidebar cart block * @@ -58,6 +63,17 @@ public function getItemCollection() return $collection; } + /** + * @inheritdoc + */ + public function getItemPrice(Product $product) + { + $customPrice = $this->getCartItemCustomPrice($product); + $price = $customPrice ?? $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + + return $this->convertPrice($price); + } + /** * Retrieve display item qty availability * @@ -111,4 +127,23 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + + /** + * Returns cart item custom price. + * + * @param Product $product + * @return float|null + */ + private function getCartItemCustomPrice(Product $product) + { + $items = $this->getItemCollection(); + foreach ($items as $item) { + $productItemId = $this->getProduct($item)->getId(); + if ($productItemId === $product->getId() && $item->getCustomPrice()) { + return (float)$item->getCustomPrice(); + } + } + + return null; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Discount.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Discount.php index 738f9a4fef999..b6b1d4f59715e 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Discount.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Discount.php @@ -15,9 +15,9 @@ */ class Discount extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\DefaultTotals { - //protected $_template = 'tax/checkout/subtotal.phtml'; - - //protected $_template = 'tax/checkout/subtotal.phtml'; + /** + * @var \Magento\Tax\Model\Config + */ protected $_taxConfig; /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php index eb437915ad668..cf9f8a44dee27 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php @@ -20,7 +20,7 @@ class Grandtotal extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defa * * @var string */ - protected $_template = 'order/create/totals/grandtotal.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/grandtotal.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php index 9225d8c2e5f68..34a9ed8070e26 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php @@ -20,7 +20,7 @@ class Shipping extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defaul * * @var string */ - protected $_template = 'order/create/totals/shipping.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/shipping.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php index 1807c587c6893..e4463cd612a2c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php @@ -20,7 +20,7 @@ class Subtotal extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defaul * * @var string */ - protected $_template = 'order/create/totals/subtotal.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/subtotal.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php index d3da37c3f1bf8..207a4eca60213 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php @@ -18,5 +18,5 @@ class Tax extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\DefaultTota * * @var string */ - protected $_template = 'order/create/totals/tax.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/tax.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index d73371d46dae1..3f38939296a04 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -109,4 +109,20 @@ public function getShippingLabel() } return $label; } + + /** + * Get update totals url + * + * @return string + */ + public function getUpdateTotalsUrl() + { + return $this->getUrl( + 'sales/*/updateQty', + [ + 'order_id' => $this->getSource()->getOrderId(), + 'invoice_id' => $this->getRequest()->getParam('invoice_id', null) + ] + ); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php index 65163f9ed5d82..da865cf3f541f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Items.php @@ -56,9 +56,14 @@ protected function _prepareLayout() $this->addChild( 'update_button', \Magento\Backend\Block\Widget\Button::class, - ['label' => __('Update Qty\'s'), 'class' => 'update-button', 'onclick' => $onclick] + ['label' => __('Update Qty\'s'), 'class' => 'update-button secondary', 'onclick' => $onclick] ); - + $this->addChild( + 'update_totals_button', + \Magento\Backend\Block\Widget\Button::class, + ['label' => __('Update Totals'), 'class' => 'update-totals-button secondary', 'onclick' => $onclick] + ); + if ($this->getCreditmemo()->canRefund()) { if ($this->getCreditmemo()->getInvoice() && $this->getCreditmemo()->getInvoice()->getTransactionId()) { $this->addChild( @@ -176,6 +181,16 @@ public function getUpdateButtonHtml() return $this->getChildHtml('update_button'); } + /** + * Get update totals button html + * + * @return string + */ + public function getUpdateTotalsButtonHtml() + { + return $this->getChildHtml('update_totals_button'); + } + /** * Get update url * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php index 5c3a7fce805cc..261f4b0cfd12a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php @@ -14,5 +14,5 @@ class Details extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/details.phtml'; + protected $_template = 'Magento_Sales::order/details.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php index 69c36d480dc01..7910736a2959a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Block\Adminhtml\Order\Invoice; /** @@ -306,8 +304,14 @@ public function updateBackButtonUrl($flag) 'setLocation(\'' . $this->getInvoice()->getBackUrl() . '\')' ); } - return $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('sales/invoice/') . '\')'); + + return $this->buttonList->update( + 'back', + 'onclick', + 'setLocation(\'' . $this->getUrl('sales/invoice/') . '\')' + ); } + return $this; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php index 648a281228908..c4ce48d162c2c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php @@ -8,6 +8,7 @@ /** * Adminhtml creditmemo bar * + * @deprecated * @api * @author Magento Core Team * @since 100.0.2 diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php index afd23b456dca6..2baf8e3dbab3f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php @@ -182,7 +182,7 @@ protected function _construct() 'class' => __('unhold'), 'id' => 'order-view-unhold-button', 'data_attribute' => [ - 'url' => $this->getUnHoldUrl() + 'url' => $this->getUnholdUrl() ] ] ); @@ -466,6 +466,8 @@ public function getReviewPaymentUrl($action) } /** + * Get edit message + * * @param \Magento\Sales\Model\Order $order * @return \Magento\Framework\Phrase */ @@ -486,6 +488,8 @@ protected function getEditMessage($order) } /** + * Get non editable types + * * @param \Magento\Sales\Model\Order $order * @return array */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php index 82c3effcab62d..6c06e9d624c81 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php @@ -17,5 +17,5 @@ class Form extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'order/view/form.phtml'; + protected $_template = 'Magento_Sales::order/view/form.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php index 1912655a9292d..10b80b6f4e527 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php @@ -88,7 +88,8 @@ public function getStatuses() */ public function canSendCommentEmail() { - return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()); + return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()) + && $this->_authorization->isAllowed('Magento_Sales::email'); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php index 5489a0b2e513f..30b0872dfa90d 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php @@ -18,7 +18,7 @@ class History extends \Magento\Backend\Block\Template implements \Magento\Backen * * @var string */ - protected $_template = 'order/view/tab/history.phtml'; + protected $_template = 'Magento_Sales::order/view/tab/history.phtml'; /** * Core registry @@ -303,11 +303,7 @@ public static function sortHistoryByTimestamp($a, $b) $createdAtA = $a['created_at']; $createdAtB = $b['created_at']; - /** @var $createdAtA \DateTime */ - if ($createdAtA->getTimestamp() == $createdAtB->getTimestamp()) { - return 0; - } - return $createdAtA->getTimestamp() < $createdAtB->getTimestamp() ? -1 : 1; + return $createdAtA->getTimestamp() <=> $createdAtB->getTimestamp(); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Reorder/Renderer/Action.php b/app/code/Magento/Sales/Block/Adminhtml/Reorder/Renderer/Action.php index e6e5d6ec3df3f..566ea1214d91f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Reorder/Renderer/Action.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Reorder/Renderer/Action.php @@ -41,6 +41,8 @@ public function __construct( } /** + * Render actions + * * @param \Magento\Framework\DataObject $row * @return string */ @@ -71,7 +73,8 @@ public function render(\Magento\Framework\DataObject $row) */ protected function _getEscapedValue($value) { - return addcslashes(htmlspecialchars($value), '\\\''); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return addcslashes($this->escapeHtml($value), '\\\''); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Report/Filter/Form/Order.php b/app/code/Magento/Sales/Block/Adminhtml/Report/Filter/Form/Order.php index 686f28aadf041..a94a7b9d3f557 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Report/Filter/Form/Order.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Report/Filter/Form/Order.php @@ -24,8 +24,6 @@ class Order extends \Magento\Sales\Block\Adminhtml\Report\Filter\Form protected function _prepareForm() { parent::_prepareForm(); - $form = $this->getForm(); - $htmlIdPrefix = $form->getHtmlIdPrefix(); /** @var \Magento\Framework\Data\Form\Element\Fieldset $fieldset */ $fieldset = $this->getForm()->getElement('base_fieldset'); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php index fbb78970a4de0..512539824da20 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php @@ -14,7 +14,7 @@ class Link extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rss/order/grid/link.phtml'; + protected $_template = 'Magento_Sales::rss/order/grid/link.phtml'; /** * @var \Magento\Framework\App\Rss\UrlBuilderInterface diff --git a/app/code/Magento/Sales/Block/Adminhtml/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Totals.php index 83b155293c2b9..8172a3c0db4ad 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Totals.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Block\Adminhtml; +use Magento\Sales\Model\Order; + +/** + * Adminhtml sales totals block + */ class Totals extends \Magento\Sales\Block\Order\Totals { /** @@ -67,12 +72,16 @@ protected function _initTotals() if (!$this->getSource()->getIsVirtual() && ((double)$this->getSource()->getShippingAmount() || $this->getSource()->getShippingDescription()) ) { + $shippingLabel = __('Shipping & Handling'); + if ($this->isFreeShipping($this->getOrder()) && $this->getSource()->getDiscountDescription()) { + $shippingLabel .= sprintf(' (%s)', $this->getSource()->getDiscountDescription()); + } $this->_totals['shipping'] = new \Magento\Framework\DataObject( [ 'code' => 'shipping', 'value' => $this->getSource()->getShippingAmount(), 'base_value' => $this->getSource()->getBaseShippingAmount(), - 'label' => __('Shipping & Handling'), + 'label' => $shippingLabel, ] ); } @@ -109,4 +118,23 @@ protected function _initTotals() return $this; } + + /** + * Availability of free shipping in at least one order item + * + * @param Order $order + * @return bool + */ + private function isFreeShipping(Order $order): bool + { + $isFreeShipping = false; + foreach ($order->getItems() as $orderItem) { + if ($orderItem->getFreeShipping() == '1') { + $isFreeShipping = true; + break; + } + } + + return $isFreeShipping; + } } diff --git a/app/code/Magento/Sales/Block/DataProviders/Email/Shipment/TrackingUrl.php b/app/code/Magento/Sales/Block/DataProviders/Email/Shipment/TrackingUrl.php new file mode 100644 index 0000000000000..cdc4ba2ce19d3 --- /dev/null +++ b/app/code/Magento/Sales/Block/DataProviders/Email/Shipment/TrackingUrl.php @@ -0,0 +1,42 @@ +helper = $helper; + } + + /** + * Get Shipping tracking URL + * + * @param Track $track + * @return string + */ + public function getUrl(Track $track): string + { + return $this->helper->getTrackingPopupUrlBySalesModel($track); + } +} diff --git a/app/code/Magento/Sales/Block/Order/Creditmemo.php b/app/code/Magento/Sales/Block/Order/Creditmemo.php index ae9a9a722291a..a32b2dbc74bde 100644 --- a/app/code/Magento/Sales/Block/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Block/Order/Creditmemo.php @@ -19,7 +19,7 @@ class Creditmemo extends \Magento\Sales\Block\Order\Creditmemo\Items /** * @var string */ - protected $_template = 'order/creditmemo.phtml'; + protected $_template = 'Magento_Sales::order/creditmemo.phtml'; /** * @var \Magento\Framework\App\Http\Context diff --git a/app/code/Magento/Sales/Block/Order/Creditmemo/Totals.php b/app/code/Magento/Sales/Block/Order/Creditmemo/Totals.php index 6ddd1989e2767..5ce0a83947ae7 100644 --- a/app/code/Magento/Sales/Block/Order/Creditmemo/Totals.php +++ b/app/code/Magento/Sales/Block/Order/Creditmemo/Totals.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Block\Order\Creditmemo; use Magento\Sales\Model\Order\Creditmemo; diff --git a/app/code/Magento/Sales/Block/Order/History.php b/app/code/Magento/Sales/Block/Order/History.php index 034df4edb5b49..80925f66fc83d 100644 --- a/app/code/Magento/Sales/Block/Order/History.php +++ b/app/code/Magento/Sales/Block/Order/History.php @@ -19,7 +19,7 @@ class History extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/history.phtml'; + protected $_template = 'Magento_Sales::order/history.phtml'; /** * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory diff --git a/app/code/Magento/Sales/Block/Order/Info.php b/app/code/Magento/Sales/Block/Order/Info.php index db3dbdbfde40b..689a55f06896c 100644 --- a/app/code/Magento/Sales/Block/Order/Info.php +++ b/app/code/Magento/Sales/Block/Order/Info.php @@ -23,7 +23,7 @@ class Info extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/info.phtml'; + protected $_template = 'Magento_Sales::order/info.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons.php b/app/code/Magento/Sales/Block/Order/Info/Buttons.php index a27b55cd8543f..18e79f6a76ecf 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons.php @@ -20,7 +20,7 @@ class Buttons extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/info/buttons.phtml'; + protected $_template = 'Magento_Sales::order/info/buttons.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php index 77e20eaa8d07b..689d02c8eefe2 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Order\Info\Buttons; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Rss\Signature; + /** * Block of links in Order view page * @@ -16,7 +19,7 @@ class Rss extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/info/buttons/rss.phtml'; + protected $_template = 'Magento_Sales::order/info/buttons/rss.phtml'; /** * @var \Magento\Sales\Model\OrderFactory @@ -28,24 +31,35 @@ class Rss extends \Magento\Framework\View\Element\Template */ protected $rssUrlBuilder; + /** + * @var Signature + */ + private $signature; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Framework\App\Rss\UrlBuilderInterface $rssUrlBuilder * @param array $data + * @param Signature|null $signature */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Framework\App\Rss\UrlBuilderInterface $rssUrlBuilder, - array $data = [] + array $data = [], + Signature $signature = null ) { $this->orderFactory = $orderFactory; $this->rssUrlBuilder = $rssUrlBuilder; + $this->signature = $signature ?: ObjectManager::getInstance()->get(Signature::class); + parent::__construct($context, $data); } /** + * Get link url. + * * @return string */ public function getLink() @@ -54,6 +68,8 @@ public function getLink() } /** + * Get translatable label for url. + * * @return \Magento\Framework\Phrase */ public function getLabel() @@ -91,15 +107,20 @@ protected function getUrlKey($order) } /** - * @return string + * Get type, secure and query params for link. + * + * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function getLinkParams() { $order = $this->orderFactory->create()->load($this->_request->getParam('order_id')); + $data = $this->getUrlKey($order); + return [ 'type' => 'order_status', '_secure' => true, - '_query' => ['data' => $this->getUrlKey($order)] + '_query' => ['data' => $data, 'signature' => $this->signature->signData($data)], ]; } } diff --git a/app/code/Magento/Sales/Block/Order/Invoice.php b/app/code/Magento/Sales/Block/Order/Invoice.php index 2d8448ea5bc98..24ddf4bac7696 100644 --- a/app/code/Magento/Sales/Block/Order/Invoice.php +++ b/app/code/Magento/Sales/Block/Order/Invoice.php @@ -18,7 +18,7 @@ class Invoice extends \Magento\Sales\Block\Order\Invoice\Items /** * @var string */ - protected $_template = 'order/invoice.phtml'; + protected $_template = 'Magento_Sales::order/invoice.phtml'; /** * @var \Magento\Framework\App\Http\Context diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index 4c6a2b586cfc4..ece2df1742204 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Block\Order\Item\Renderer; use Magento\Sales\Model\Order\CreditMemo\Item as CreditMemoItem; @@ -175,7 +173,8 @@ public function getFormatedOptionValue($optionValue) $result = ['value' => $truncatedValue]; if ($this->string->strlen($optionValue) > 55) { - $result['value'] = $result['value'] . ' ...'; + $result['value'] = $result['value'] + . ' ...'; $optionValue = nl2br($optionValue); $result = array_merge($result, ['full_view' => $optionValue]); } @@ -278,4 +277,21 @@ public function getItemRowTotalAfterDiscountHtml($item = null) $block->setItem($item); return $block->toHtml(); } + + /** + * Return the base total amount minus discount. + * + * @param OrderItem|InvoiceItem|CreditmemoItem $item + * @return float|null + */ + public function getBaseTotalAmount($item) + { + $baseTotalAmount = $item->getBaseRowTotal() + + $item->getBaseTaxAmount() + + $item->getBaseDiscountTaxCompensationAmount() + + $item->getBaseWeeeTaxAppliedAmount() + - $item->getBaseDiscountAmount(); + + return $baseTotalAmount; + } } diff --git a/app/code/Magento/Sales/Block/Order/Items.php b/app/code/Magento/Sales/Block/Order/Items.php index 028544cd56219..0b67e9a0746d3 100644 --- a/app/code/Magento/Sales/Block/Order/Items.php +++ b/app/code/Magento/Sales/Block/Order/Items.php @@ -71,7 +71,6 @@ protected function _prepareLayout() $this->itemCollection = $this->itemCollectionFactory->create(); $this->itemCollection->setOrderFilter($this->getOrder()); - $this->itemCollection->filterByParent(null); /** @var \Magento\Theme\Block\Html\Pager $pagerBlock */ $pagerBlock = $this->getChildBlock('sales_order_item_pager'); diff --git a/app/code/Magento/Sales/Block/Order/Recent.php b/app/code/Magento/Sales/Block/Order/Recent.php index e57aa1fe420a0..17436c50bd0e4 100644 --- a/app/code/Magento/Sales/Block/Order/Recent.php +++ b/app/code/Magento/Sales/Block/Order/Recent.php @@ -5,6 +5,13 @@ */ namespace Magento\Sales\Block\Order; +use Magento\Framework\View\Element\Template\Context; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\Customer\Model\Session; +use Magento\Sales\Model\Order\Config; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; + /** * Sales order history block * @@ -13,6 +20,11 @@ */ class Recent extends \Magento\Framework\View\Element\Template { + /** + * Limit of orders + */ + const ORDER_LIMIT = 5; + /** * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory */ @@ -28,25 +40,34 @@ class Recent extends \Magento\Framework\View\Element\Template */ protected $_orderConfig; + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Sales\Model\Order\Config $orderConfig * @param array $data + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory, - \Magento\Customer\Model\Session $customerSession, - \Magento\Sales\Model\Order\Config $orderConfig, - array $data = [] + Context $context, + CollectionFactory $orderCollectionFactory, + Session $customerSession, + Config $orderConfig, + array $data = [], + StoreManagerInterface $storeManager = null ) { $this->_orderCollectionFactory = $orderCollectionFactory; $this->_customerSession = $customerSession; $this->_orderConfig = $orderConfig; - parent::__construct($context, $data); $this->_isScopePrivate = true; + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManagerInterface::class); + parent::__construct($context, $data); } /** @@ -55,11 +76,22 @@ public function __construct( protected function _construct() { parent::_construct(); + $this->getRecentOrders(); + } + + /** + * Get recently placed orders. By default they will be limited by 5. + */ + protected function getRecentOrders() + { $orders = $this->_orderCollectionFactory->create()->addAttributeToSelect( '*' )->addAttributeToFilter( 'customer_id', $this->_customerSession->getCustomerId() + )->addAttributeToFilter( + 'store_id', + $this->storeManager->getStore()->getId() )->addAttributeToFilter( 'status', ['in' => $this->_orderConfig->getVisibleOnFrontStatuses()] @@ -67,7 +99,7 @@ protected function _construct() 'created_at', 'desc' )->setPageSize( - '5' + self::ORDER_LIMIT )->load(); $this->setOrders($orders); } diff --git a/app/code/Magento/Sales/Block/Order/Totals.php b/app/code/Magento/Sales/Block/Order/Totals.php index f910b654f4d8c..3720db76b5778 100644 --- a/app/code/Magento/Sales/Block/Order/Totals.php +++ b/app/code/Magento/Sales/Block/Order/Totals.php @@ -293,6 +293,12 @@ public function removeTotal($code) */ public function applySortOrder($order) { + \uksort( + $this->_totals, + function ($code1, $code2) use ($order) { + return ($order[$code1] ?? 0) <=> ($order[$code2] ?? 0); + } + ); return $this; } diff --git a/app/code/Magento/Sales/Block/Order/View.php b/app/code/Magento/Sales/Block/Order/View.php index 870e2e15ab7b3..03d1340e0f690 100644 --- a/app/code/Magento/Sales/Block/Order/View.php +++ b/app/code/Magento/Sales/Block/Order/View.php @@ -18,7 +18,7 @@ class View extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/view.phtml'; + protected $_template = 'Magento_Sales::order/view.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php index b413951d9d4f3..9df515a6c03ba 100644 --- a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php +++ b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php @@ -36,9 +36,16 @@ public function decorateAction($value, $row, $column, $isExport) $cell = ''; $state = $row->getState(); if (!empty($state)) { - $url = $this->getUrl('*/*/unassign', ['status' => $row->getStatus(), 'state' => $row->getState()]); + $url = $this->getUrl('*/*/unassign'); $label = __('Unassign'); - $cell = '' . $label . ''; + $cell = '' . $label . ''; } return $cell; } diff --git a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php index d7ab99377e1b5..619b0828ed33d 100644 --- a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php +++ b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php @@ -7,7 +7,11 @@ namespace Magento\Sales\Controller\AbstractController; use Magento\Framework\App\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Registry; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Controller\ResultFactory; abstract class Reorder extends Action\Action { @@ -21,18 +25,26 @@ abstract class Reorder extends Action\Action */ protected $_coreRegistry; + /** + * @var Validator + */ + private $formKeyValidator; + /** * @param Action\Context $context * @param OrderLoaderInterface $orderLoader * @param Registry $registry + * @param Validator|null $formKeyValidator */ public function __construct( Action\Context $context, OrderLoaderInterface $orderLoader, - Registry $registry + Registry $registry, + Validator $formKeyValidator = null ) { $this->orderLoader = $orderLoader; $this->_coreRegistry = $registry; + $this->formKeyValidator = $formKeyValidator ?: ObjectManager::getInstance()->create(Validator::class); parent::__construct($context); } @@ -43,6 +55,20 @@ public function __construct( */ public function execute() { + if ($this->getRequest()->isPost()) { + if (!$this->formKeyValidator->validate($this->getRequest())) { + $this->messageManager->addErrorMessage(__('Invalid Form Key. Please refresh the page.')); + + /** @var \Magento\Framework\Controller\Result\Redirect $redirect */ + $redirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $redirect->setPath('*/*/history'); + + return $redirect; + } + } else { + throw new NotFoundException(__('Page not found.')); + } + $result = $this->orderLoader->load($this->_request); if ($result instanceof \Magento\Framework\Controller\ResultInterface) { return $result; diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/Pdfcreditmemos.php b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/Pdfcreditmemos.php index 83486704984f7..94ca6a0375e7c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/Pdfcreditmemos.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/Pdfcreditmemos.php @@ -74,9 +74,12 @@ public function __construct( */ public function massAction(AbstractCollection $collection) { + $pdf = $this->pdfCreditmemo->getPdf($collection); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('creditmemo%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfCreditmemo->getPdf($collection)->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/PrintAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/PrintAction.php index d6c94feb57606..c5902aac33355 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/PrintAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/AbstractCreditmemo/PrintAction.php @@ -53,6 +53,7 @@ public function __construct( /** * @return ResponseInterface|\Magento\Backend\Model\View\Result\Forward + * @throws \Exception */ public function execute() { @@ -69,9 +70,11 @@ public function execute() $date = $this->_objectManager->get( \Magento\Framework\Stdlib\DateTime\DateTime::class )->date('Y-m-d_H-i-s'); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->_fileFactory->create( \creditmemo::class . $date . '.pdf', - $pdf->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/Pdfinvoices.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/Pdfinvoices.php index a949ceca53961..18cf397c0e3b9 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/Pdfinvoices.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/Pdfinvoices.php @@ -75,9 +75,12 @@ public function __construct( */ public function massAction(AbstractCollection $collection) { + $pdf = $this->pdfInvoice->getPdf($collection); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('invoice%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfInvoice->getPdf($collection)->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/PrintAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/PrintAction.php index 2421267aa753d..2caa8ec2dcb83 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/PrintAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/PrintAction.php @@ -45,6 +45,7 @@ public function __construct( /** * @return ResponseInterface|void + * @throws \Exception */ public function execute() { @@ -58,9 +59,11 @@ public function execute() $date = $this->_objectManager->get( \Magento\Framework\Stdlib\DateTime\DateTime::class )->date('Y-m-d_H-i-s'); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->_fileFactory->create( 'invoice' . $date . '.pdf', - $pdf->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php index 891bfeefc9f52..05066fe5b125e 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php @@ -70,7 +70,7 @@ public function execute() /** * Return component referrer url - * TODO: Technical dept referrer url should be implement as a part of Action configuration in in appropriate way + * TODO: Technical dept referrer url should be implement as a part of Action configuration in appropriate way * * @return null|string */ diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index 12038ee375059..88e05a80f3797 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -9,17 +9,27 @@ use Magento\Backend\App\Action; use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; +/** + * Controller to execute Adding Comments. + */ class AddComment extends \Magento\Sales\Controller\Adminhtml\Order { /** - * Authorization level of a basic admin session + * Authorization level of a basic admin session. * * @see _isAllowed() */ const ADMIN_RESOURCE = 'Magento_Sales::comment'; /** - * Add order comment action + * ACL resource needed to send comment email notification. + * + * @see _isAllowed() + */ + const ADMIN_SALES_EMAIL_RESOURCE = 'Magento_Sales::emails'; + + /** + * Add order comment action. * * @return \Magento\Framework\Controller\ResultInterface */ @@ -33,8 +43,12 @@ public function execute() throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a comment.')); } - $notify = isset($data['is_customer_notified']) ? $data['is_customer_notified'] : false; - $visible = isset($data['is_visible_on_front']) ? $data['is_visible_on_front'] : false; + $notify = $data['is_customer_notified'] ?? false; + $visible = $data['is_visible_on_front'] ?? false; + + if ($notify && !$this->_authorization->isAllowed(self::ADMIN_SALES_EMAIL_RESOURCE)) { + $notify = false; + } $history = $order->addStatusHistoryComment($data['comment'], $data['status']); $history->setIsVisibleOnFront($visible); @@ -59,9 +73,11 @@ public function execute() if (is_array($response)) { $resultJson = $this->resultJsonFactory->create(); $resultJson->setData($response); + return $resultJson; } } + return $this->resultRedirectFactory->create()->setPath('sales/*/'); } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddressSave.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddressSave.php index dc994e554b394..53563ccd70061 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddressSave.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddressSave.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Redirect; use Magento\Directory\Model\RegionFactory; +use Magento\Sales\Api\OrderAddressRepositoryInterface; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Api\Data\OrderAddressInterface; @@ -24,8 +25,11 @@ use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Action\HttpPostActionInterface; /** + * Sales address save + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddressSave extends Order @@ -54,6 +58,7 @@ class AddressSave extends Order * @param OrderRepositoryInterface $orderRepository * @param LoggerInterface $logger * @param RegionFactory|null $regionFactory + * @param OrderAddressRepositoryInterface|null $orderAddressRepository * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -69,9 +74,12 @@ public function __construct( OrderManagementInterface $orderManagement, OrderRepositoryInterface $orderRepository, LoggerInterface $logger, - RegionFactory $regionFactory = null + RegionFactory $regionFactory = null, + OrderAddressRepositoryInterface $orderAddressRepository = null ) { $this->regionFactory = $regionFactory ?: ObjectManager::getInstance()->get(RegionFactory::class); + $this->orderAddressRepository = $orderAddressRepository ?: ObjectManager::getInstance() + ->get(OrderAddressRepositoryInterface::class); parent::__construct( $context, $coreRegistry, @@ -87,6 +95,11 @@ public function __construct( ); } + /** + * @var OrderAddressRepositoryInterface + */ + private $orderAddressRepository; + /** * Save order address * @@ -105,7 +118,7 @@ public function execute() if ($data && $address->getId()) { $address->addData($data); try { - $address->save(); + $this->orderAddressRepository->save($address); $this->_eventManager->dispatch( 'admin_sales_order_address_update', [ diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php index de41c3c737968..7e41c7417b38d 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php @@ -24,18 +24,18 @@ public function execute() { $resultRedirect = $this->resultRedirectFactory->create(); if (!$this->isValidPostRequest()) { - $this->messageManager->addError(__('You have not canceled the item.')); + $this->messageManager->addErrorMessage(__('You have not canceled the item.')); return $resultRedirect->setPath('sales/*/'); } $order = $this->_initOrder(); if ($order) { try { $this->orderManagement->cancel($order->getEntityId()); - $this->messageManager->addSuccess(__('You canceled the order.')); + $this->messageManager->addSuccessMessage(__('You canceled the order.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('You have not canceled the item.')); + $this->messageManager->addErrorMessage(__('You have not canceled the item.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } return $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getId()]); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php index 1343b77bcd0ae..60229c3b93710 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php @@ -17,6 +17,10 @@ */ abstract class Create extends \Magento\Backend\App\Action { + /** + * Indicates how to process post data + */ + private static $actionSave = 'save'; /** * @var \Magento\Framework\Escaper */ @@ -206,7 +210,7 @@ protected function _processActionData($action = null) /** * Apply mass changes from sidebar */ - if ($data = $this->getRequest()->getPost('sidebar')) { + if (($data = $this->getRequest()->getPost('sidebar')) && $action !== self::$actionSave) { $this->_getOrderCreateModel()->applySidebarData($data); } @@ -222,7 +226,8 @@ protected function _processActionData($action = null) /** * Adding products to quote from special grid */ - if ($this->getRequest()->has('item') && !$this->getRequest()->getPost('update_items') && !($action == 'save') + if ($this->getRequest()->has('item') && !$this->getRequest()->getPost('update_items') + && $action !== self::$actionSave ) { $items = $this->getRequest()->getPost('item'); $items = $this->_processFiles($items); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php index ce3a36729de95..48d1201c910be 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php @@ -15,7 +15,7 @@ class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create public function execute() { $this->_initSession(); - + $this->_getOrderCreateModel()->initRuleData(); /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $resultPage->setActiveMenu('Magento_Sales::sales_order'); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php index 6a9d0a5dcb8ed..bc4eb3cfba423 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php @@ -55,10 +55,10 @@ public function execute() $this->_initSession()->_processData(); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_reloadQuote(); - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->_reloadQuote(); - $this->messageManager->addException($e, $e->getMessage()); + $this->messageManager->addExceptionMessage($e, $e->getMessage()); } $asJson = $request->getParam('json'); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php index 621705c7937cb..efe5ed5b7332a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Controller\Adminhtml\Order\Create; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Exception\PaymentException; class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create @@ -12,14 +14,20 @@ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create /** * Saving quote and create order * - * @return \Magento\Backend\Model\View\Result\Forward|\Magento\Backend\Model\View\Result\Redirect + * @return \Magento\Framework\Controller\ResultInterface + * @throws NotFoundException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); + $path = 'sales/*/'; + $pathParams = []; + + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { // check if the creation of a new customer is allowed if (!$this->_authorization->isAllowed('Magento_Customer::manage') @@ -49,31 +57,30 @@ public function execute() ->createOrder(); $this->_getSession()->clearStorage(); - $this->messageManager->addSuccess(__('You created the order.')); + $this->messageManager->addSuccessMessage(__('You created the order.')); if ($this->_authorization->isAllowed('Magento_Sales::actions_view')) { - $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getId()]); + $pathParams = ['order_id' => $order->getId()]; + $path = 'sales/order/view'; } else { - $resultRedirect->setPath('sales/order/index'); + $path = 'sales/order/index'; } } catch (PaymentException $e) { $this->_getOrderCreateModel()->saveQuote(); $message = $e->getMessage(); if (!empty($message)) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } - $resultRedirect->setPath('sales/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { // customer can be created before place order flow is completed and should be stored in current session - $this->_getSession()->setCustomerId($this->_getSession()->getQuote()->getCustomerId()); + $this->_getSession()->setCustomerId((int)$this->_getSession()->getQuote()->getCustomerId()); $message = $e->getMessage(); if (!empty($message)) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } - $resultRedirect->setPath('sales/*/'); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Order saving error: %1', $e->getMessage())); - $resultRedirect->setPath('sales/*/'); + $this->messageManager->addExceptionMessage($e, __('Order saving error: %1', $e->getMessage())); } - return $resultRedirect; + + return $this->resultRedirectFactory->create()->setPath($path, $pathParams); } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php index 826a2a2a8b6c1..f71f567467bff 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php @@ -6,7 +6,7 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; use Magento\Backend\App\Action; -use Magento\Sales\Model\Order; +use Magento\Framework\Exception\NotFoundException; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; class Save extends \Magento\Backend\App\Action @@ -56,12 +56,17 @@ public function __construct( * We can save only new creditmemo. Existing creditmemos are not editable * * @return \Magento\Backend\Model\View\Result\Redirect|\Magento\Backend\Model\View\Result\Forward + * @throws NotFoundException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $resultRedirect = $this->resultRedirectFactory->create(); $data = $this->getRequest()->getPost('creditmemo'); if (!empty($data['comment_text'])) { @@ -109,7 +114,7 @@ public function execute() $this->creditmemoSender->send($creditmemo); } - $this->messageManager->addSuccess(__('You created the credit memo.')); + $this->messageManager->addSuccessMessage(__('You created the credit memo.')); $this->_getSession()->getCommentText(true); $resultRedirect->setPath('sales/order/view', ['order_id' => $creditmemo->getOrderId()]); return $resultRedirect; @@ -119,11 +124,11 @@ public function execute() return $resultForward; } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_getSession()->setFormData($data); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError(__('We can\'t save the credit memo right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the credit memo right now.')); } $resultRedirect->setPath('sales/*/new', ['_current' => true]); return $resultRedirect; diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php index bfd95666e7c48..e445f98c5aa54 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php @@ -65,6 +65,10 @@ public function __construct( public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid request type.')); + } + $this->creditmemoLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->creditmemoLoader->setCreditmemoId($this->getRequest()->getParam('creditmemo_id')); $this->creditmemoLoader->setCreditmemo($this->getRequest()->getParam('creditmemo')); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index 63c266150384a..8e2f1e951606d 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -190,20 +190,7 @@ public function execute() } } $transactionSave->save(); - - if (isset($shippingResponse) && $shippingResponse->hasErrors()) { - $this->messageManager->addError( - __( - 'The invoice and the shipment have been created. ' . - 'The shipping label cannot be created now.' - ) - ); - } elseif (!empty($data['do_shipment'])) { - $this->messageManager->addSuccess(__('You created the invoice and shipment.')); - } else { - $this->messageManager->addSuccess(__('The invoice has been created.')); - } - + // send invoice/shipment emails try { if (!empty($data['send_email'])) { @@ -213,6 +200,7 @@ public function execute() $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $this->messageManager->addError(__('We can\'t send the invoice email right now.')); } + if ($shipment) { try { if (!empty($data['send_email'])) { @@ -223,6 +211,13 @@ public function execute() $this->messageManager->addError(__('We can\'t send the shipment right now.')); } } + + if (!empty($data['do_shipment'])) { + $this->messageManager->addSuccess(__('You created the invoice and shipment.')); + } else { + $this->messageManager->addSuccess(__('The invoice has been created.')); + } + $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); return $resultRedirect->setPath('sales/order/view', ['order_id' => $orderId]); } catch (LocalizedException $e) { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php index cdb4114f70976..de853bcb6f591 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php @@ -7,7 +7,6 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; use Magento\Framework\Exception\LocalizedException; -use Magento\Backend\App\Action; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; @@ -74,27 +73,27 @@ public function __construct( public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new LocalizedException(__('Invalid request type.')); + } + $orderId = $this->getRequest()->getParam('order_id'); $invoiceData = $this->getRequest()->getParam('invoice', []); $invoiceItems = isset($invoiceData['items']) ? $invoiceData['items'] : []; /** @var \Magento\Sales\Model\Order $order */ $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class)->load($orderId); if (!$order->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The order no longer exists.')); + throw new LocalizedException(__('The order no longer exists.')); } if (!$order->canInvoice()) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The order does not allow an invoice to be created.') - ); + throw new LocalizedException(__('The order does not allow an invoice to be created.')); } $invoice = $this->invoiceService->prepareInvoice($order, $invoiceItems); if (!$invoice->getTotalQty()) { - throw new \Magento\Framework\Exception\LocalizedException( - __('You can\'t create an invoice without products.') - ); + throw new LocalizedException(__('You can\'t create an invoice without products.')); } $this->registry->register('current_invoice', $invoice); // Save invoice comment text in current invoice object in order to display it in corresponding view diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php index 2ba8467ff6864..05a22245dc004 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php @@ -5,10 +5,13 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Sales\Api\OrderManagementInterface; class MassCancel extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassAction { @@ -16,16 +19,43 @@ class MassCancel extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassA * Authorization level of a basic admin session */ const ADMIN_RESOURCE = 'Magento_Sales::cancel'; + + /** + * @var OrderManagementInterface + */ + private $orderManagement; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory + * @param OrderManagementInterface|null $orderManagement */ - public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) - { + public function __construct( + Context $context, + Filter $filter, + CollectionFactory $collectionFactory, + OrderManagementInterface $orderManagement = null + ) { parent::__construct($context, $filter); $this->collectionFactory = $collectionFactory; + $this->orderManagement = $orderManagement ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Sales\Api\OrderManagementInterface::class + ); + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var HttpRequest $request */ + $request = $this->getRequest(); + if (!$request->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + + return parent::execute(); } /** @@ -38,11 +68,10 @@ protected function massAction(AbstractCollection $collection) { $countCancelOrder = 0; foreach ($collection->getItems() as $order) { - if (!$order->canCancel()) { + $isCanceled = $this->orderManagement->cancel($order->getEntityId()); + if ($isCanceled === false) { continue; } - $order->cancel(); - $order->save(); $countCancelOrder++; } $countNonCancelOrder = $collection->count() - $countCancelOrder; diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php index e894957dc8b6c..67263028d51a9 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php @@ -5,11 +5,13 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Sales\Api\OrderManagementInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * Class MassHold @@ -43,6 +45,20 @@ public function __construct( $this->orderManagement = $orderManagement; } + /** + * @inheritDoc + */ + public function execute() + { + /** @var HttpRequest $request */ + $request = $this->getRequest(); + if (!$request->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + + return parent::execute(); + } + /** * Hold selected orders * diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php index ebd6ff4a79b06..68090d7a75239 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php @@ -5,10 +5,13 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Framework\App\Request\Http as HttpRequest; class MassUnhold extends AbstractMassAction { @@ -16,16 +19,43 @@ class MassUnhold extends AbstractMassAction * Authorization level of a basic admin session */ const ADMIN_RESOURCE = 'Magento_Sales::unhold'; + + /** + * @var OrderManagementInterface + */ + private $orderManagement; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory + * @param OrderManagementInterface|null $orderManagement */ - public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) - { + public function __construct( + Context $context, + Filter $filter, + CollectionFactory $collectionFactory, + OrderManagementInterface $orderManagement = null + ) { parent::__construct($context, $filter); $this->collectionFactory = $collectionFactory; + $this->orderManagement = $orderManagement ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Sales\Api\OrderManagementInterface::class + ); + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var HttpRequest $request */ + $request = $this->getRequest(); + if (!$request->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + + return parent::execute(); } /** @@ -40,12 +70,10 @@ protected function massAction(AbstractCollection $collection) /** @var \Magento\Sales\Model\Order $order */ foreach ($collection->getItems() as $order) { - $order->load($order->getId()); if (!$order->canUnhold()) { continue; } - $order->unhold(); - $order->save(); + $this->orderManagement->unHold($order->getEntityId()); $countUnHoldOrder++; } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfcreditmemos.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfcreditmemos.php index f96e2fd09c2b0..b6e3c4998eade 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfcreditmemos.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfcreditmemos.php @@ -76,6 +76,7 @@ public function __construct( * * @param AbstractCollection $collection * @return ResponseInterface|ResultInterface + * @throws \Exception */ protected function massAction(AbstractCollection $collection) { @@ -85,9 +86,12 @@ protected function massAction(AbstractCollection $collection) $this->messageManager->addError(__('There are no printable documents related to selected orders.')); return $this->resultRedirectFactory->create()->setPath($this->getComponentRefererUrl()); } + $pdf = $this->pdfCreditmemo->getPdf($creditmemoCollection->getItems()); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('creditmemo%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfCreditmemo->getPdf($creditmemoCollection->getItems())->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfdocs.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfdocs.php index d68cfe696b0ef..90ffa2b75de22 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfdocs.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfdocs.php @@ -113,6 +113,7 @@ public function __construct( * @return ResponseInterface|\Magento\Backend\Model\View\Result\Redirect * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws \Exception */ protected function massAction(AbstractCollection $collection) { @@ -142,10 +143,11 @@ protected function massAction(AbstractCollection $collection) foreach ($documents as $document) { $pdf->pages = array_merge($pdf->pages, $document->pages); } + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; return $this->fileFactory->create( sprintf('docs%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $pdf->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfinvoices.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfinvoices.php index fee124a91410d..93cb5882ed5de 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfinvoices.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfinvoices.php @@ -75,6 +75,7 @@ public function __construct( * * @param AbstractCollection $collection * @return ResponseInterface|ResultInterface + * @throws \Exception */ protected function massAction(AbstractCollection $collection) { @@ -83,9 +84,12 @@ protected function massAction(AbstractCollection $collection) $this->messageManager->addError(__('There are no printable documents related to selected orders.')); return $this->resultRedirectFactory->create()->setPath($this->getComponentRefererUrl()); } + $pdf = $this->pdfInvoice->getPdf($invoicesCollection->getItems()); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('invoice%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfInvoice->getPdf($invoicesCollection->getItems())->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfshipments.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfshipments.php index 1aa5bfdb83878..d414ec99c2e6a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfshipments.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Pdfshipments.php @@ -77,6 +77,7 @@ public function __construct( * * @param AbstractCollection $collection * @return ResponseInterface|\Magento\Backend\Model\View\Result\Redirect + * @throws \Exception */ protected function massAction(AbstractCollection $collection) { @@ -87,9 +88,13 @@ protected function massAction(AbstractCollection $collection) $this->messageManager->addError(__('There are no printable documents related to selected orders.')); return $this->resultRedirectFactory->create()->setPath($this->getComponentRefererUrl()); } + + $pdf = $this->pdfShipment->getPdf($shipmentsCollection->getItems()); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('packingslip%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfShipment->getPdf($shipmentsCollection->getItems())->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php index 89820b41a68da..3b98d206d5f66 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php @@ -26,18 +26,18 @@ public function execute() if ($status && $status->getStatus()) { try { $status->assignState($state, $isDefault, $visibleOnFront); - $this->messageManager->addSuccess(__('You assigned the order status.')); + $this->messageManager->addSuccessMessage(__('You assigned the order status.')); return $resultRedirect->setPath('sales/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while assigning the order status.') ); } } else { - $this->messageManager->addError(__('We can\'t find this order status.')); + $this->messageManager->addErrorMessage(__('We can\'t find this order status.')); } return $resultRedirect->setPath('sales/*/assign'); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php index 849a7e2d0c817..06fa61c3263de 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php @@ -12,6 +12,7 @@ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Status * Save status form processing * * @return \Magento\Backend\Model\View\Result\Redirect + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { @@ -40,7 +41,9 @@ public function execute() $status = $this->_objectManager->create(\Magento\Sales\Model\Order\Status::class)->load($statusCode); // check if status exist if ($isNew && $status->getStatus()) { - $this->messageManager->addError(__('We found another order status with the same order status code.')); + $this->messageManager->addErrorMessage( + __('We found another order status with the same order status code.') + ); $this->_getSession()->setFormData($data); return $resultRedirect->setPath('sales/*/new'); } @@ -49,12 +52,12 @@ public function execute() try { $status->save(); - $this->messageManager->addSuccess(__('You saved the order status.')); + $this->messageManager->addSuccessMessage(__('You saved the order status.')); return $resultRedirect->setPath('sales/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('We can\'t add the order status right now.') ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php index 04db430e1ffa4..44238725ef65c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php @@ -6,29 +6,36 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; +use Magento\Framework\Exception\NotFoundException; + class Unassign extends \Magento\Sales\Controller\Adminhtml\Order\Status { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $state = $this->getRequest()->getParam('state'); $status = $this->_initStatus(); if ($status) { try { $status->unassignState($state); - $this->messageManager->addSuccess(__('You have unassigned the order status.')); + $this->messageManager->addSuccessMessage(__('You have unassigned the order status.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while unassigning the order.') ); } } else { - $this->messageManager->addError(__('We can\'t find this order status.')); + $this->messageManager->addErrorMessage(__('We can\'t find this order status.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php index 752ab088689c8..fa9676856a442 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php @@ -32,7 +32,7 @@ public function execute() if (!$order->canUnhold()) { throw new \Magento\Framework\Exception\LocalizedException(__('Can\'t unhold order.')); } - $this->orderManagement->unhold($order->getEntityId()); + $this->orderManagement->unHold($order->getEntityId()); $this->messageManager->addSuccess(__('You released the order from holding status.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($e->getMessage()); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/Pdfshipments.php b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/Pdfshipments.php index b10dca908fe6a..4a7c6e0533e33 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/Pdfshipments.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/Pdfshipments.php @@ -70,9 +70,12 @@ public function __construct( */ public function massAction(AbstractCollection $collection) { + $pdf = $this->pdfShipment->getPdf($collection); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->fileFactory->create( sprintf('packingslip%s.pdf', $this->dateTime->date('Y-m-d_H-i-s')), - $this->pdfShipment->getPdf($collection)->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/PrintAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/PrintAction.php index da8646a0c30b2..1e49fe7eff60a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/PrintAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/AbstractShipment/PrintAction.php @@ -48,6 +48,7 @@ public function __construct( /** * @return ResponseInterface|\Magento\Backend\Model\View\Result\Forward + * @throws \Exception */ public function execute() { @@ -63,9 +64,11 @@ public function execute() $date = $this->_objectManager->get( \Magento\Framework\Stdlib\DateTime\DateTime::class )->date('Y-m-d_H-i-s'); + $fileContent = ['type' => 'string', 'value' => $pdf->render(), 'rm' => true]; + return $this->_fileFactory->create( 'packingslip' . $date . '.pdf', - $pdf->render(), + $fileContent, DirectoryList::VAR_DIR, 'application/pdf' ); diff --git a/app/code/Magento/Sales/Controller/Guest/View.php b/app/code/Magento/Sales/Controller/Guest/View.php index 9e96a15ed2c12..cf74abd78a8aa 100644 --- a/app/code/Magento/Sales/Controller/Guest/View.php +++ b/app/code/Magento/Sales/Controller/Guest/View.php @@ -6,6 +6,8 @@ namespace Magento\Sales\Controller\Guest; use Magento\Framework\App\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Sales\Helper\Guest as GuestHelper; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\ResultInterface; @@ -22,26 +24,41 @@ class View extends Action\Action */ protected $resultPageFactory; + /** + * @var Validator + */ + private $formKeyValidator; + /** * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Sales\Helper\Guest $guestHelper - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param GuestHelper $guestHelper + * @param PageFactory $resultPageFactory + * @param Validator|null $formKeyValidator */ public function __construct( Action\Context $context, GuestHelper $guestHelper, - PageFactory $resultPageFactory + PageFactory $resultPageFactory, + Validator $formKeyValidator = null ) { $this->guestHelper = $guestHelper; $this->resultPageFactory = $resultPageFactory; + $this->formKeyValidator = $formKeyValidator ?? ObjectManager::getInstance()->get(Validator::class); parent::__construct($context); } /** * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if ($this->getRequest()->isPost()) { + if (!$this->formKeyValidator->validate($this->getRequest())) { + return $this->resultRedirectFactory->create()->setPath('*/*/form/'); + } + } + $result = $this->guestHelper->loadValidOrder($this->getRequest()); if ($result instanceof ResultInterface) { return $result; diff --git a/app/code/Magento/Sales/CustomerData/LastOrderedItems.php b/app/code/Magento/Sales/CustomerData/LastOrderedItems.php index 3cd3afbfa4d22..a6e3c5bb89043 100644 --- a/app/code/Magento/Sales/CustomerData/LastOrderedItems.php +++ b/app/code/Magento/Sales/CustomerData/LastOrderedItems.php @@ -6,11 +6,16 @@ namespace Magento\Sales\CustomerData; use Magento\Customer\CustomerData\SectionSourceInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Psr\Log\LoggerInterface; +use Magento\Framework\App\ObjectManager; /** * Returns information for "Recently Ordered" widget. * It contains list of 5 salable products from the last placed order. * Qty of products to display is limited by LastOrderedItems::SIDEBAR_ORDER_LIMIT constant. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class LastOrderedItems implements SectionSourceInterface { @@ -54,25 +59,41 @@ class LastOrderedItems implements SectionSourceInterface */ private $_storeManager; + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory * @param \Magento\Sales\Model\Order\Config $orderConfig * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param ProductRepositoryInterface $productRepository + * @param LoggerInterface|null $logger */ public function __construct( \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory, \Magento\Sales\Model\Order\Config $orderConfig, \Magento\Customer\Model\Session $customerSession, \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, - \Magento\Store\Model\StoreManagerInterface $storeManager + \Magento\Store\Model\StoreManagerInterface $storeManager, + ProductRepositoryInterface $productRepository, + LoggerInterface $logger = null ) { $this->_orderCollectionFactory = $orderCollectionFactory; $this->_orderConfig = $orderConfig; $this->_customerSession = $customerSession; $this->stockRegistry = $stockRegistry; $this->_storeManager = $storeManager; + $this->productRepository = $productRepository; + $this->logger = $logger ?? ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -108,11 +129,23 @@ protected function getItems() $website = $this->_storeManager->getStore()->getWebsiteId(); /** @var \Magento\Sales\Model\Order\Item $item */ foreach ($order->getParentItemsRandomCollection($limit) as $item) { - if ($item->hasData('product') && in_array($website, $item->getProduct()->getWebsiteIds())) { + /** @var \Magento\Catalog\Model\Product $product */ + try { + $product = $this->productRepository->getById( + $item->getProductId(), + false, + $this->_storeManager->getStore()->getId() + ); + } catch (NoSuchEntityException $noEntityException) { + $this->logger->critical($noEntityException); + continue; + } + if (isset($product) && in_array($website, $product->getWebsiteIds())) { + $url = $product->isVisibleInSiteVisibility() ? $product->getProductUrl() : null; $items[] = [ 'id' => $item->getId(), 'name' => $item->getName(), - 'url' => $item->getProduct()->getProductUrl(), + 'url' => $url, 'is_saleable' => $this->isItemAvailableForReorder($item), ]; } @@ -136,7 +169,7 @@ protected function isItemAvailableForReorder(\Magento\Sales\Model\Order\Item $or $orderItem->getStore()->getWebsiteId() ); return $stockItem->getIsInStock(); - } catch (\Magento\Framework\Exception\NoSuchEntityException $noEntityException) { + } catch (NoSuchEntityException $noEntityException) { return false; } } diff --git a/app/code/Magento/Sales/Helper/Admin.php b/app/code/Magento/Sales/Helper/Admin.php index 1b8d95441dfc5..2948c7b77304e 100644 --- a/app/code/Magento/Sales/Helper/Admin.php +++ b/app/code/Magento/Sales/Helper/Admin.php @@ -3,8 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Helper; +use Magento\Framework\App\ObjectManager; + +/** + * Sales admin helper. + */ class Admin extends \Magento\Framework\App\Helper\AbstractHelper { /** @@ -27,24 +33,33 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper */ protected $escaper; + /** + * @var \DOMDocumentFactory + */ + private $domDocumentFactory; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Sales\Model\Config $salesConfig * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param \Magento\Framework\Escaper $escaper + * @param \DOMDocumentFactory|null $domDocumentFactory */ public function __construct( \Magento\Framework\App\Helper\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Sales\Model\Config $salesConfig, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Escaper $escaper + \Magento\Framework\Escaper $escaper, + \DOMDocumentFactory $domDocumentFactory = null ) { $this->priceCurrency = $priceCurrency; $this->_storeManager = $storeManager; $this->_salesConfig = $salesConfig; $this->escaper = $escaper; + $this->domDocumentFactory = $domDocumentFactory + ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); parent::__construct($context); } @@ -84,7 +99,6 @@ public function displayPriceAttribute($dataObject, $code, $strong = false, $sepa */ public function displayPrices($dataObject, $basePrice, $price, $strong = false, $separator = '
    ') { - $order = false; if ($dataObject instanceof \Magento\Sales\Model\Order) { $order = $dataObject; } else { @@ -146,37 +160,66 @@ public function applySalableProductTypesFilter($collection) public function escapeHtmlWithLinks($data, $allowedTags = null) { if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { - $links = []; - $i = 1; - $data = str_replace('%', '%%', $data); - $regexp = "/]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU"; - while (preg_match($regexp, $data, $matches)) { - //Revert the sprintf escaping - $url = str_replace('%%', '%', $matches[2]); - $text = str_replace('%%', '%', $matches[3]); - //Check for an valid url - if ($url) { - $urlScheme = strtolower(parse_url($url, PHP_URL_SCHEME)); - if ($urlScheme !== 'http' && $urlScheme !== 'https') { - $url = null; - } + $wrapperElementId = uniqid(); + $domDocument = $this->domDocumentFactory->create(); + + $internalErrors = libxml_use_internal_errors(true); + + $data = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); + $domDocument->loadHTML( + '' . $data . '' + ); + + libxml_use_internal_errors($internalErrors); + + $linkTags = $domDocument->getElementsByTagName('a'); + + foreach ($linkTags as $linkNode) { + $linkAttributes = []; + foreach ($linkNode->attributes as $attribute) { + $linkAttributes[$attribute->name] = $attribute->value; } - //Use hash tag as fallback - if (!$url) { - $url = '#'; + + foreach ($linkAttributes as $attributeName => $attributeValue) { + if ($attributeName === 'href') { + $url = $this->filterUrl($attributeValue ?? ''); + $url = $this->escaper->escapeUrl($url); + $linkNode->setAttribute('href', $url); + } else { + $linkNode->removeAttribute($attributeName); + } } - //Recreate a minimalistic secure a tag - $links[] = sprintf( - '%s', - htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false), - $this->escaper->escapeHtml($text) - ); - $data = str_replace($matches[0], '%' . $i . '$s', $data); - ++$i; } - $data = $this->escaper->escapeHtml($data, $allowedTags); - return vsprintf($data, $links); + + $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); + preg_match('/(.+)<\/body><\/html>$/si', $result, $matches); + $data = !empty($matches) ? $matches[1] : ''; } + return $this->escaper->escapeHtml($data, $allowedTags); } + + /** + * Filter the URL for allowed protocols. + * + * @param string $url + * @return string + */ + private function filterUrl(string $url): string + { + if ($url) { + //Revert the sprintf escaping + $urlScheme = parse_url($url, PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { + $url = null; + } + } + + if (!$url) { + $url = '#'; + } + + return $url; + } } diff --git a/app/code/Magento/Sales/Helper/Guest.php b/app/code/Magento/Sales/Helper/Guest.php index dd8845008d79e..8407ce5a8d7cb 100644 --- a/app/code/Magento/Sales/Helper/Guest.php +++ b/app/code/Magento/Sales/Helper/Guest.php @@ -83,7 +83,7 @@ class Guest extends \Magento\Framework\App\Helper\AbstractHelper /** * @var \Magento\Store\Model\StoreManagerInterface */ - private $_storeManager; + private $storeManager; /** * @var string @@ -119,7 +119,7 @@ public function __construct( \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteria = null ) { $this->coreRegistry = $coreRegistry; - $this->_storeManager = $storeManager; + $this->storeManager = $storeManager; $this->customerSession = $customerSession; $this->cookieManager = $cookieManager; $this->cookieMetadataFactory = $cookieMetadataFactory; @@ -158,9 +158,10 @@ public function loadValidOrder(App\RequestInterface $request) // It is unique place in the class that process exception and only InputException. It is need because by // input data we found order and one more InputException could be throws deeper in stack trace try { - $order = (!empty($post) && isset($post['oar_order_id'], $post['oar_type'])) + $order = (!empty($post) + && isset($post['oar_order_id'], $post['oar_type']) + && !$this->hasPostDataEmptyFields($post)) ? $this->loadFromPost($post) : $this->loadFromCookie($fromCookie); - $this->validateOrderStoreId($order->getStoreId()); $this->coreRegistry->register('current_order', $order); return true; } catch (InputException $e) { @@ -186,7 +187,7 @@ public function getBreadcrumbs(\Magento\Framework\View\Result\Page $resultPage) [ 'label' => __('Home'), 'title' => __('Go to Home Page'), - 'link' => $this->_storeManager->getStore()->getBaseUrl() + 'link' => $this->storeManager->getStore()->getBaseUrl() ] ); $breadcrumbs->addCrumb( @@ -247,12 +248,9 @@ private function loadFromCookie($fromCookie) */ private function loadFromPost(array $postData) { - if ($this->hasPostDataEmptyFields($postData)) { - throw new InputException(); - } /** @var $order \Magento\Sales\Model\Order */ $order = $this->getOrderRecord($postData['oar_order_id']); - if (!$this->compareSoredBillingDataWithInput($order, $postData)) { + if (!$this->compareStoredBillingDataWithInput($order, $postData)) { throw new InputException(__('You entered incorrect data. Please try again.')); } $toCookie = base64_encode($order->getProtectCode() . ':' . $postData['oar_order_id']); @@ -267,7 +265,7 @@ private function loadFromPost(array $postData) * @param array $postData * @return bool */ - private function compareSoredBillingDataWithInput(Order $order, array $postData) + private function compareStoredBillingDataWithInput(Order $order, array $postData) { $type = $postData['oar_type']; $email = $postData['oar_email']; @@ -288,7 +286,7 @@ private function compareSoredBillingDataWithInput(Order $order, array $postData) private function hasPostDataEmptyFields(array $postData) { return empty($postData['oar_order_id']) || empty($postData['oar_billing_lastname']) || - empty($postData['oar_type']) || empty($this->_storeManager->getStore()->getId()) || + empty($postData['oar_type']) || empty($this->storeManager->getStore()->getId()) || !in_array($postData['oar_type'], ['email', 'zip'], true) || ('email' === $postData['oar_type'] && empty($postData['oar_email'])) || ('zip' === $postData['oar_type'] && empty($postData['oar_zip'])); @@ -306,26 +304,15 @@ private function getOrderRecord($incrementId) $records = $this->orderRepository->getList( $this->searchCriteriaBuilder ->addFilter('increment_id', $incrementId) + ->addFilter('store_id', $this->storeManager->getStore()->getId()) ->create() ); - if ($records->getTotalCount() < 1) { - throw new InputException(__($this->inputExceptionMessage)); - } - $items = $records->getItems(); - return array_shift($items); - } - /** - * Check that store_id from order are equals with system - * - * @param int $orderStoreId - * @return void - * @throws InputException - */ - private function validateOrderStoreId($orderStoreId) - { - if ($orderStoreId != $this->_storeManager->getStore()->getId()) { + $items = $records->getItems(); + if (empty($items)) { throw new InputException(__($this->inputExceptionMessage)); } + + return array_shift($items); } } diff --git a/app/code/Magento/Sales/Helper/Reorder.php b/app/code/Magento/Sales/Helper/Reorder.php index 6eaeb30102375..eae5122d8a85e 100644 --- a/app/code/Magento/Sales/Helper/Reorder.php +++ b/app/code/Magento/Sales/Helper/Reorder.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Helper; /** @@ -58,7 +56,11 @@ public function isAllow() */ public function isAllowed($store = null) { - if ($this->scopeConfig->getValue(self::XML_PATH_SALES_REORDER_ALLOW, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store)) { + if ($this->scopeConfig->getValue( + self::XML_PATH_SALES_REORDER_ALLOW, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + )) { return true; } return false; diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index c2f03ff5d9ac4..455e484d7fd0a 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -4,15 +4,17 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\AdminOrder; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Model\Metadata\Form as CustomerForm; +use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\App\ObjectManager; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Model\Order; +use Magento\Store\Model\StoreManagerInterface; /** * Order create model @@ -20,6 +22,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\Model\Cart\CartInterface @@ -235,6 +238,16 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ */ private $serializer; + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -265,6 +278,8 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ * @param \Magento\Quote\Model\QuoteFactory $quoteFactory * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param ExtensibleDataObjectConverter|null $dataObjectConverter + * @param StoreManagerInterface $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -296,7 +311,9 @@ public function __construct( \Magento\Sales\Api\OrderManagementInterface $orderManagement, \Magento\Quote\Model\QuoteFactory $quoteFactory, array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + ExtensibleDataObjectConverter $dataObjectConverter = null, + StoreManagerInterface $storeManager = null ) { $this->_objectManager = $objectManager; $this->_eventManager = $eventManager; @@ -328,6 +345,9 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); parent::__construct($data); + $this->dataObjectConverter = $dataObjectConverter ?: ObjectManager::getInstance() + ->get(ExtensibleDataObjectConverter::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -377,6 +397,7 @@ protected function _getQuoteItem($item) */ public function initRuleData() { + $this->_coreRegistry->unregister('rule_data'); $this->_coreRegistry->register( 'rule_data', new \Magento\Framework\DataObject( @@ -394,7 +415,8 @@ public function initRuleData() /** * Set collect totals flag for quote * - * @param bool $flag + * @param bool $flag + * * @return $this */ public function setRecollect($flag) @@ -405,7 +427,8 @@ public function setRecollect($flag) /** * Recollect totals for customer cart. - * Set recollect totals flag for quote + * + * Set recollect totals flag for quote. * * @return $this */ @@ -492,6 +515,9 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) /* Check if we edit guest order */ $session->setCustomerId($order->getCustomerId() ?: false); $session->setStoreId($order->getStoreId()); + if ($session->getData('reordered')) { + $this->getQuote()->setCustomerGroupId($order->getCustomerGroupId()); + } /* Initialize catalog rule data with new session values */ $this->initRuleData(); @@ -514,9 +540,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) $shippingAddress = $order->getShippingAddress(); if ($shippingAddress) { - $addressDiff = array_diff_assoc($shippingAddress->getData(), $order->getBillingAddress()->getData()); - unset($addressDiff['address_type'], $addressDiff['entity_id']); - $shippingAddress->setSameAsBilling(empty($addressDiff)); + $shippingAddress->setSameAsBilling($this->isAddressesAreEqual($order)); } $this->_initBillingAddressFromOrder($order); @@ -563,6 +587,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) } $quote->getShippingAddress()->unsCachedItemsAll(); + $quote->getBillingAddress()->unsCachedItemsAll(); $quote->setTotalsCollectedFlag(false); $this->quoteRepository->save($quote); @@ -671,7 +696,7 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q */ public function getCustomerWishlist($cacheReload = false) { - if (!is_null($this->_wishlist) && !$cacheReload) { + if (($this->_wishlist !== null) && !$cacheReload) { return $this->_wishlist; } @@ -698,16 +723,17 @@ public function getCustomerWishlist($cacheReload = false) */ public function getCustomerCart() { - if (!is_null($this->_cart)) { + if ($this->_cart !== null) { return $this->_cart; } $this->_cart = $this->quoteFactory->create(); $customerId = (int)$this->getSession()->getCustomerId(); + $storeId = (int)$this->getSession()->getStoreId(); if ($customerId) { try { - $this->_cart = $this->quoteRepository->getForCustomer($customerId); + $this->_cart = $this->quoteRepository->getForCustomer($customerId, [$storeId]); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { $this->_cart->setStore($this->getSession()->getStore()); $customerData = $this->customerRepository->getById($customerId); @@ -726,13 +752,14 @@ public function getCustomerCart() */ public function getCustomerCompareList() { - if (!is_null($this->_compareList)) { + if ($this->_compareList !== null) { return $this->_compareList; } $customerId = (int)$this->getSession()->getCustomerId(); if ($customerId) { $this->_compareList = $this->_objectManager->create( - \Magento\Catalog\Model\Product\Compare\ListCompare::class); + \Magento\Catalog\Model\Product\Compare\ListCompare::class + ); } else { $this->_compareList = false; } @@ -748,7 +775,7 @@ public function getCustomerCompareList() public function getCustomerGroupId() { $groupId = $this->getQuote()->getCustomerGroupId(); - if (!$groupId) { + if (!isset($groupId)) { $groupId = $this->getSession()->getCustomerGroupId(); } @@ -797,7 +824,7 @@ public function moveQuoteItem($item, $moveTo, $qty) break; case 'cart': $cart = $this->getCustomerCart(); - if ($cart && is_null($item->getOptionByCode('additional_options'))) { + if ($cart && ($item->getOptionByCode('additional_options') === null)) { //options and info buy request $product = $this->_objectManager->create( \Magento\Catalog\Model\Product::class @@ -843,8 +870,8 @@ public function moveQuoteItem($item, $moveTo, $qty) true ); } else { - $wishlist = $this->_objectManager->create( - \Magento\Wishlist\Model\Wishlist::class)->load($moveTo[1]); + $wishlist = $this->_objectManager->create(\Magento\Wishlist\Model\Wishlist::class) + ->load($moveTo[1]); if (!$wishlist->getId() || $wishlist->getCustomerId() != $this->getSession()->getCustomerId() ) { $wishlist = null; @@ -981,8 +1008,9 @@ public function removeItem($itemId, $from) } break; case 'compared': - $this->_objectManager->create( - \Magento\Catalog\Model\Product\Compare\Item::class)->load($itemId)->delete(); + $this->_objectManager->create(\Magento\Catalog\Model\Product\Compare\Item::class) + ->load($itemId) + ->delete(); break; } @@ -1104,6 +1132,7 @@ public function updateQuoteItems($items) } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->recollectCart(); throw $e; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $this->_logger->critical($e); } @@ -1137,11 +1166,15 @@ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additio if (strlen(trim($_additionalOption))) { try { if (strpos($_additionalOption, ':') === false) { - throw new \Magento\Framework\Exception\LocalizedException(__('There is an error in one of the option rows.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('There is an error in one of the option rows.') + ); } list($label, $value) = explode(':', $_additionalOption, 2); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('There is an error in one of the option rows.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('There is an error in one of the option rows.') + ); } $label = trim($label); $value = trim($value); @@ -1316,6 +1349,7 @@ protected function _createCustomerForm(\Magento\Customer\Api\Data\CustomerInterf /** * Set and validate Quote address + * * All errors added to _errors * * @param \Magento\Quote\Model\Quote\Address $address @@ -1519,6 +1553,8 @@ public function resetShippingMethod() */ public function collectShippingRates() { + $store = $this->getQuote()->getStore(); + $this->storeManager->setCurrentStore($store); $this->getQuote()->getShippingAddress()->setCollectShippingRates(true); $this->collectRates(); @@ -1604,7 +1640,8 @@ public function setAccountData($accountData) $customer = $this->customerFactory->create(); $this->dataObjectHelper->populateWithArray( $customer, - $data, \Magento\Customer\Api\Data\CustomerInterface::class + $data, + \Magento\Customer\Api\Data\CustomerInterface::class ); $this->getQuote()->updateCustomerData($customer); $data = []; @@ -1717,14 +1754,15 @@ protected function _validateCustomerData(\Magento\Customer\Api\Data\CustomerInte } $data = $form->restoreData($data); foreach ($data as $key => $value) { - if (!is_null($value)) { + if ($value !== null) { unset($data[$key]); } } $this->dataObjectHelper->populateWithArray( $customer, - $data, \Magento\Customer\Api\Data\CustomerInterface::class + $data, + \Magento\Customer\Api\Data\CustomerInterface::class ); return $customer; } @@ -1756,7 +1794,7 @@ public function _prepareCustomer() ->setWebsiteId($store->getWebsiteId()) ->setCreatedAt(null); $customer = $this->_validateCustomerData($customer); - } else if (!$customer->getId()) { + } elseif (!$customer->getId()) { /** Create new customer */ $customerBillingAddressDataObject = $this->getBillingAddress()->exportCustomerAddress(); $customer->setSuffix($customerBillingAddressDataObject->getSuffix()) @@ -1828,6 +1866,7 @@ protected function _prepareCustomerAddress($customer, $quoteCustomerAddress) } elseif ($addressType == \Magento\Quote\Model\Quote\Address::ADDRESS_TYPE_SHIPPING) { try { $billingAddressDataObject = $this->accountManagement->getDefaultBillingAddress($customer->getId()); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $e) { /** Billing address does not exist. */ } @@ -1841,12 +1880,12 @@ protected function _prepareCustomerAddress($customer, $quoteCustomerAddress) switch ($addressType) { case \Magento\Quote\Model\Quote\Address::ADDRESS_TYPE_BILLING: - if (is_null($customer->getDefaultBilling())) { + if ($customer->getDefaultBilling() === null) { $customerAddress->setIsDefaultBilling(true); } break; case \Magento\Quote\Model\Quote\Address::ADDRESS_TYPE_SHIPPING: - if (is_null($customer->getDefaultShipping())) { + if ($customer->getDefaultShipping() === null) { $customerAddress->setIsDefaultShipping(true); } break; @@ -1937,21 +1976,17 @@ public function createOrder() */ protected function _validate() { - $customerId = $this->getSession()->getCustomerId(); - if (is_null($customerId)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please select a customer')); - } - if (!$this->getSession()->getStore()->getId()) { throw new \Magento\Framework\Exception\LocalizedException(__('Please select a store')); } $items = $this->getQuote()->getAllItems(); - if (count($items) == 0) { + if (count($items) === 0) { $this->_errors[] = __('Please specify order items.'); } foreach ($items as $item) { + /** @var \Magento\Quote\Model\Quote\Item $item */ $messages = $item->getMessage(false); if ($item->getHasError() && is_array($messages) && !empty($messages)) { $this->_errors = array_merge($this->_errors, $messages); @@ -1973,6 +2008,7 @@ protected function _validate() } else { try { $method->validate(); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_errors[] = $e->getMessage(); } @@ -1980,7 +2016,7 @@ protected function _validate() } if (!empty($this->_errors)) { foreach ($this->_errors as $error) { - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); } throw new \Magento\Framework\Exception\LocalizedException(__('Validation is failed.')); } @@ -1989,25 +2025,34 @@ protected function _validate() } /** - * Retrieve or generate new customer email. + * Retrieve new customer email. * * @return string */ protected function _getNewCustomerEmail() { - $email = $this->getData('account/email'); - if (empty($email)) { - $host = $this->_scopeConfig->getValue( - self::XML_PATH_DEFAULT_EMAIL_DOMAIN, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $account = time(); - $email = $account . '@' . $host; - $account = $this->getData('account'); - $account['email'] = $email; - $this->setData('account', $account); - } + return $this->getData('account/email'); + } + + /** + * Checks id shipping and billing addresses are equal. + * + * @param Order $order + * @return bool + */ + private function isAddressesAreEqual(Order $order) + { + $shippingAddress = $order->getShippingAddress(); + $billingAddress = $order->getBillingAddress(); + $shippingData = $this->dataObjectConverter->toFlatArray($shippingAddress, [], OrderAddressInterface::class); + $billingData = $this->dataObjectConverter->toFlatArray($billingAddress, [], OrderAddressInterface::class); + unset( + $shippingData['address_type'], + $shippingData['entity_id'], + $billingData['address_type'], + $billingData['entity_id'] + ); - return $email; + return $shippingData == $billingData; } } diff --git a/app/code/Magento/Sales/Model/Config/Ordered.php b/app/code/Magento/Sales/Model/Config/Ordered.php index 8c5ddb8e07df7..bae6223ee7d5e 100644 --- a/app/code/Magento/Sales/Model/Config/Ordered.php +++ b/app/code/Magento/Sales/Model/Config/Ordered.php @@ -167,13 +167,8 @@ function ($a, $b) { if (!isset($a['sort_order']) || !isset($b['sort_order'])) { return 0; } - if ($a['sort_order'] > $b['sort_order']) { - return 1; - } elseif ($a['sort_order'] < $b['sort_order']) { - return -1; - } else { - return 0; - } + + return $a['sort_order'] <=> $b['sort_order']; } ); } diff --git a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php index 8a7bd0260df0f..999bb1786cf83 100644 --- a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php +++ b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\CronJob; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Store\Model\StoresConfig; use Magento\Sales\Model\Order; +/** + * Class that provides functionality of cleaning expired quotes by cron + */ class CleanExpiredOrders { /** @@ -16,20 +24,28 @@ class CleanExpiredOrders protected $storesConfig; /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory + * @var CollectionFactory */ protected $orderCollectionFactory; + /** + * @var OrderManagementInterface + */ + private $orderManagement; + /** * @param StoresConfig $storesConfig - * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + * @param CollectionFactory $collectionFactory + * @param OrderManagementInterface|null $orderManagement */ public function __construct( StoresConfig $storesConfig, - \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + OrderManagementInterface $orderManagement = null ) { $this->storesConfig = $storesConfig; $this->orderCollectionFactory = $collectionFactory; + $this->orderManagement = $orderManagement ?: ObjectManager::getInstance()->get(OrderManagementInterface::class); } /** @@ -48,8 +64,10 @@ public function execute() $orders->getSelect()->where( new \Zend_Db_Expr('TIME_TO_SEC(TIMEDIFF(CURRENT_TIMESTAMP, `updated_at`)) >= ' . $lifetime * 60) ); - $orders->walk('cancel'); - $orders->walk('save'); + + foreach ($orders->getAllIds() as $entityId) { + $this->orderManagement->cancel((int) $entityId); + } } } } diff --git a/app/code/Magento/Sales/Model/Download.php b/app/code/Magento/Sales/Model/Download.php index 6d3ad8491253a..14395bb9afedd 100644 --- a/app/code/Magento/Sales/Model/Download.php +++ b/app/code/Magento/Sales/Model/Download.php @@ -78,7 +78,8 @@ public function downloadFile($info) $this->_fileFactory->create( $info['title'], ['value' => $this->_rootDir->getRelativePath($relativePath), 'type' => 'filename'], - $this->rootDirBasePath + $this->rootDirBasePath, + $info['type'] ); } diff --git a/app/code/Magento/Sales/Model/EmailSenderHandler.php b/app/code/Magento/Sales/Model/EmailSenderHandler.php index 73d4eacdd1fc8..f52f3818068e9 100644 --- a/app/code/Magento/Sales/Model/EmailSenderHandler.php +++ b/app/code/Magento/Sales/Model/EmailSenderHandler.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Model; +use Magento\Sales\Model\Order\Email\Container\IdentityInterface; + /** * Sales emails sending * @@ -41,22 +43,41 @@ class EmailSenderHandler */ protected $globalConfig; + /** + * @var IdentityInterface + */ + private $identityContainer; + + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Sales\Model\Order\Email\Sender $emailSender * @param \Magento\Sales\Model\ResourceModel\EntityAbstract $entityResource * @param \Magento\Sales\Model\ResourceModel\Collection\AbstractCollection $entityCollection * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + * @param IdentityInterface|null $identityContainer + * @param \Magento\Store\Model\StoreManagerInterface $storeManager */ public function __construct( \Magento\Sales\Model\Order\Email\Sender $emailSender, \Magento\Sales\Model\ResourceModel\EntityAbstract $entityResource, \Magento\Sales\Model\ResourceModel\Collection\AbstractCollection $entityCollection, - \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig, + IdentityInterface $identityContainer = null, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { $this->emailSender = $emailSender; $this->entityResource = $entityResource; $this->entityCollection = $entityCollection; $this->globalConfig = $globalConfig; + + $this->identityContainer = $identityContainer ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\Order\Email\Container\NullIdentity::class); + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** @@ -68,15 +89,54 @@ public function sendEmails() if ($this->globalConfig->getValue('sales_email/general/async_sending')) { $this->entityCollection->addFieldToFilter('send_email', ['eq' => 1]); $this->entityCollection->addFieldToFilter('email_sent', ['null' => true]); + $this->entityCollection->setPageSize( + $this->globalConfig->getValue('sales_email/general/sending_limit') + ); - /** @var \Magento\Sales\Model\AbstractModel $item */ - foreach ($this->entityCollection->getItems() as $item) { - if ($this->emailSender->send($item, true)) { - $this->entityResource->save( - $item->setEmailSent(true) - ); + /** @var \Magento\Store\Api\Data\StoreInterface[] $stores */ + $stores = $this->getStores(clone $this->entityCollection); + + /** @var \Magento\Store\Model\Store $store */ + foreach ($stores as $store) { + $this->identityContainer->setStore($store); + if (!$this->identityContainer->isEnabled()) { + continue; + } + $entityCollection = clone $this->entityCollection; + $entityCollection->addFieldToFilter('store_id', $store->getId()); + + /** @var \Magento\Sales\Model\AbstractModel $item */ + foreach ($entityCollection->getItems() as $item) { + if ($this->emailSender->send($item, true)) { + $this->entityResource->save( + $item->setEmailSent(true) + ); + } } } } } + + /** + * Get stores for given entities. + * + * @param ResourceModel\Collection\AbstractCollection $entityCollection + * @return \Magento\Store\Api\Data\StoreInterface[] + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getStores( + \Magento\Sales\Model\ResourceModel\Collection\AbstractCollection $entityCollection + ): array { + $stores = []; + + $entityCollection->addAttributeToSelect('store_id')->getSelect()->group('store_id'); + /** @var \Magento\Sales\Model\EntityInterface $item */ + foreach ($entityCollection->getItems() as $item) { + /** @var \Magento\Store\Model\StoreManagerInterface $store */ + $store = $this->storeManager->getStore($item->getStoreId()); + $stores[$item->getStoreId()] = $store; + } + + return $stores; + } } diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 250aa510eafc6..f153e8362e6f9 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -7,14 +7,19 @@ use Magento\Directory\Model\Currency; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection as CreditmemoCollection; use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection as InvoiceCollection; -use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ImportCollection; +use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; use Magento\Sales\Model\ResourceModel\Order\Payment\Collection as PaymentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; @@ -267,6 +272,16 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ protected $timezone; + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * @var ProductOption + */ + private $productOption; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -295,7 +310,10 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ResolverInterface $localeResolver + * @param ProductOption|null $productOption * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -324,7 +342,9 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productListFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ResolverInterface $localeResolver = null, + ProductOption $productOption = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -335,7 +355,6 @@ public function __construct( $this->_productVisibility = $productVisibility; $this->invoiceManagement = $invoiceManagement; $this->_currencyFactory = $currencyFactory; - $this->_eavConfig = $eavConfig; $this->_orderHistoryFactory = $orderHistoryFactory; $this->_addressCollectionFactory = $addressCollectionFactory; $this->_paymentCollectionFactory = $paymentCollectionFactory; @@ -346,6 +365,9 @@ public function __construct( $this->_trackCollectionFactory = $trackCollectionFactory; $this->salesOrderCollectionFactory = $salesOrderCollectionFactory; $this->priceCurrency = $priceCurrency; + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(ResolverInterface::class); + $this->productOption = $productOption ?: ObjectManager::getInstance()->get(ProductOption::class); + parent::__construct( $context, $registry, @@ -530,7 +552,14 @@ public function canCancel() break; } } - if ($allInvoiced) { + + $allRefunded = true; + foreach ($this->getAllItems() as $orderItem) { + $allRefunded = $allRefunded + && ((float)$orderItem->getQtyRefunded() === (float)$orderItem->getQtyInvoiced()); + } + + if ($allInvoiced && !$allRefunded) { return false; } @@ -548,6 +577,7 @@ public function canCancel() /** * Getter whether the payment can be voided + * * @return bool */ public function canVoidPayment() @@ -604,11 +634,11 @@ public function canCreditmemo() return $this->getForcedCanCreditmemo(); } - if ($this->canUnhold() || $this->isPaymentReview()) { - return false; - } - - if ($this->isCanceled() || $this->getState() === self::STATE_CLOSED) { + if ($this->canUnhold() + || $this->isPaymentReview() + || $this->isCanceled() + || $this->getState() === self::STATE_CLOSED + ) { return false; } @@ -618,17 +648,55 @@ public function canCreditmemo() * TotalPaid - contains amount, that were not rounded. */ $totalRefunded = $this->priceCurrency->round($this->getTotalPaid()) - $this->getTotalRefunded(); - if (abs($totalRefunded) < .0001) { - return false; + if (abs($this->getGrandTotal()) < .0001) { + return $this->canCreditmemoForZeroTotal($totalRefunded); } + + return $this->canCreditmemoForZeroTotalRefunded($totalRefunded); + } + + /** + * Retrieve credit memo for zero total refunded availability. + * + * @param float $totalRefunded + * @return bool + */ + private function canCreditmemoForZeroTotalRefunded(float $totalRefunded): bool + { + $isRefundZero = abs($totalRefunded) < .0001; // Case when Adjustment Fee (adjustment_negative) has been used for first creditmemo - if (abs($totalRefunded - $this->getAdjustmentNegative()) < .0001) { + $hasAdjustmentFee = abs($totalRefunded - $this->getAdjustmentNegative()) < .0001; + $hasActionFlag = $this->getActionFlag(self::ACTION_FLAG_EDIT) === false; + if ($isRefundZero || $hasAdjustmentFee || $hasActionFlag) { return false; } - if ($this->getActionFlag(self::ACTION_FLAG_EDIT) === false) { + return true; + } + + /** + * Retrieve credit memo for zero total availability. + * + * @param float $totalRefunded + * @return bool + */ + private function canCreditmemoForZeroTotal(float $totalRefunded): bool + { + $totalPaid = $this->getTotalPaid(); + //check if total paid is less than grand total + $checkAmtTotalPaid = $totalPaid <= $this->getGrandTotal(); + //case when amount is due for invoice + $hasDueAmount = $this->canInvoice() && $checkAmtTotalPaid; + //case when paid amount is refunded and order has creditmemo created + $creditmemos = ($this->getCreditmemosCollection() === false) ? + true : (count($this->getCreditmemosCollection()) > 0); + $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; + if (($hasDueAmount || $paidAmtIsRefunded) + || (!$checkAmtTotalPaid && abs($totalRefunded - $this->getAdjustmentNegative()) < .0001) + ) { return false; } + return true; } @@ -683,7 +751,7 @@ public function canComment() } /** - * Retrieve order shipment availability + * Retrieve order shipment availability. * * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -703,13 +771,29 @@ public function canShip() } foreach ($this->getAllItems() as $item) { - if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip()) { + if ($item->getQtyToShip() > 0 + && !$item->getIsVirtual() + && !$item->getLockedDoShip() + && !$this->isRefunded($item) + ) { return true; } } + return false; } + /** + * Check if item is refunded. + * + * @param OrderItemInterface $item + * @return bool + */ + private function isRefunded(OrderItemInterface $item): bool + { + return $item->getQtyRefunded() == $item->getQtyOrdered(); + } + /** * Retrieve order edit availability * @@ -864,7 +948,7 @@ protected function _placePayment() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPayment() { @@ -966,10 +1050,21 @@ public function setState($state) return $this->setData(self::STATE, $state); } + /** + * Retrieve frontend label of order status + * + * @return string + */ + public function getFrontendStatusLabel() + { + return $this->getConfig()->getStatusFrontendLabel($this->getStatus()); + } + /** * Retrieve label of order status * * @return string + * @throws LocalizedException */ public function getStatusLabel() { @@ -997,8 +1092,24 @@ public function addStatusToHistory($status, $comment = '', $isCustomerNotified = * @param string $comment * @param bool|string $status * @return OrderStatusHistoryInterface + * @deprecated + * @see addCommentToStatusHistory */ public function addStatusHistoryComment($comment, $status = false) + { + return $this->addCommentToStatusHistory($comment, $status, false); + } + + /** + * Add a comment to order status history + * Different or default status may be specified + * + * @param string $comment + * @param bool|string $status + * @param bool $isVisibleOnFront + * @return OrderStatusHistoryInterface + */ + public function addCommentToStatusHistory($comment, $status = false, $isVisibleOnFront = false) { if (false === $status) { $status = $this->getStatus(); @@ -1013,6 +1124,8 @@ public function addStatusHistoryComment($comment, $status = false) $comment )->setEntityName( $this->entityType + )->setIsVisibleOnFront( + $isVisibleOnFront ); $this->addStatusHistory($history); return $history; @@ -1054,13 +1167,15 @@ public function place() } /** + * Hold order. + * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function hold() { if (!$this->canHold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('A hold action is not available.')); + throw new LocalizedException(__('A hold action is not available.')); } $this->setHoldBeforeState($this->getState()); $this->setHoldBeforeStatus($this->getStatus()); @@ -1073,12 +1188,12 @@ public function hold() * Attempt to unhold the order * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function unhold() { if (!$this->canUnhold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('You cannot remove the hold.')); + throw new LocalizedException(__('You cannot remove the hold.')); } $this->setState($this->getHoldBeforeState()) @@ -1122,7 +1237,7 @@ public function isFraudDetected() * @param string $comment * @param bool $graceful * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function registerCancellation($comment = '', $graceful = true) @@ -1161,7 +1276,7 @@ public function registerCancellation($comment = '', $graceful = true) $this->addStatusHistoryComment($comment, false); } } elseif (!$graceful) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot cancel this order.')); + throw new LocalizedException(__('We cannot cancel this order.')); } return $this; } @@ -1183,12 +1298,12 @@ public function getTrackingNumbers() * Retrieve shipping method * * @param bool $asObject return carrier code and shipping method data as object - * @return string|\Magento\Framework\DataObject + * @return string|null|\Magento\Framework\DataObject */ public function getShippingMethod($asObject = false) { $shippingMethod = parent::getShippingMethod(); - if (!$asObject) { + if (!$asObject || !$shippingMethod) { return $shippingMethod; } else { list($carrierCode, $method) = explode('_', $shippingMethod, 2); @@ -1199,6 +1314,8 @@ public function getShippingMethod($asObject = false) /*********************** ADDRESSES ***************************/ /** + * Get addresses collection. + * * @return Collection */ public function getAddressesCollection() @@ -1213,6 +1330,8 @@ public function getAddressesCollection() } /** + * Get address by id. + * * @param mixed $addressId * @return false */ @@ -1227,6 +1346,8 @@ public function getAddressById($addressId) } /** + * Add address. + * * @param \Magento\Sales\Model\Order\Address $address * @return $this */ @@ -1241,9 +1362,11 @@ public function addAddress(\Magento\Sales\Model\Order\Address $address) } /** + * Get items collection. + * * @param array $filterByTypes * @param bool $nonChildrenOnly - * @return ImportCollection + * @return ItemCollection */ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false) { @@ -1259,6 +1382,7 @@ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false if ($this->getId()) { foreach ($collection as $item) { $item->setOrder($this); + $this->productOption->add($item); } } return $collection; @@ -1268,7 +1392,7 @@ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false * Get random items collection without related children * * @param int $limit - * @return ImportCollection + * @return ItemCollection */ public function getParentItemsRandomCollection($limit = 1) { @@ -1276,15 +1400,18 @@ public function getParentItemsRandomCollection($limit = 1) } /** - * Get random items collection with or without related children + * Get random items collection with or without related children. * * @param int $limit * @param bool $nonChildrenOnly - * @return ImportCollection + * @return ItemCollection */ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) { - $collection = $this->_orderItemCollectionFactory->create()->setOrderFilter($this)->setRandomOrder(); + $collection = $this->_orderItemCollectionFactory->create() + ->setOrderFilter($this) + ->setRandomOrder() + ->setPageSize($limit); if ($nonChildrenOnly) { $collection->filterByParent(); @@ -1298,9 +1425,7 @@ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) $products )->setVisibility( $this->_productVisibility->getVisibleInSiteIds() - )->addPriceData()->setPageSize( - $limit - )->load(); + )->addPriceData()->load(); foreach ($collection as $item) { $product = $productsCollection->getItemById($item->getProductId()); @@ -1313,6 +1438,8 @@ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) } /** + * Get all items. + * * @return \Magento\Sales\Model\Order\Item[] */ public function getAllItems() @@ -1327,6 +1454,8 @@ public function getAllItems() } /** + * Get all visible items. + * * @return array */ public function getAllVisibleItems() @@ -1358,6 +1487,8 @@ public function getItemById($itemId) } /** + * Get item by quote item id. + * * @param mixed $quoteItemId * @return \Magento\Framework\DataObject|null */ @@ -1372,6 +1503,8 @@ public function getItemByQuoteItemId($quoteItemId) } /** + * Add item. + * * @param \Magento\Sales\Model\Order\Item $item * @return $this */ @@ -1387,6 +1520,8 @@ public function addItem(\Magento\Sales\Model\Order\Item $item) /*********************** PAYMENTS ***************************/ /** + * Get payments collection. + * * @return PaymentCollection */ public function getPaymentsCollection() @@ -1401,6 +1536,8 @@ public function getPaymentsCollection() } /** + * Get all payments. + * * @return array */ public function getAllPayments() @@ -1415,6 +1552,8 @@ public function getAllPayments() } /** + * Get payment by id. + * * @param mixed $paymentId * @return Payment|false */ @@ -1429,7 +1568,7 @@ public function getPaymentById($paymentId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPayment(\Magento\Sales\Api\Data\OrderPaymentInterface $payment = null) { @@ -1496,6 +1635,8 @@ public function getVisibleStatusHistory() } /** + * Get status history by id. + * * @param mixed $statusId * @return string|false */ @@ -1521,7 +1662,10 @@ public function getStatusHistoryById($statusId) public function addStatusHistory(\Magento\Sales\Model\Order\Status\History $history) { $history->setOrder($this); - $this->setStatus($history->getStatus()); + $status = $history->getStatus(); + if (null !== $status) { + $this->setStatus($status); + } if (!$history->getId()) { $this->setStatusHistories(array_merge($this->getStatusHistories(), [$history])); $this->setDataChanges(true); @@ -1530,6 +1674,8 @@ public function addStatusHistory(\Magento\Sales\Model\Order\Status\History $hist } /** + * Get real order id. + * * @return string */ public function getRealOrderId() @@ -1558,9 +1704,9 @@ public function getOrderCurrency() /** * Get formatted price value including order currency rate to order website currency * - * @param float $price - * @param bool $addBrackets - * @return string + * @param float $price + * @param bool $addBrackets + * @return string */ public function formatPrice($price, $addBrackets = false) { @@ -1568,6 +1714,8 @@ public function formatPrice($price, $addBrackets = false) } /** + * Format price precision. + * * @param float $price * @param int $precision * @param bool $addBrackets @@ -1581,8 +1729,8 @@ public function formatPricePrecision($price, $precision, $addBrackets = false) /** * Retrieve text formatted price value including order rate * - * @param float $price - * @return string + * @param float $price + * @return string */ public function formatPriceTxt($price) { @@ -1603,6 +1751,8 @@ public function getBaseCurrency() } /** + * Format base price. + * * @param float $price * @return string */ @@ -1612,6 +1762,8 @@ public function formatBasePrice($price) } /** + * Format Base Price Precision. + * * @param float $price * @param int $precision * @return string @@ -1622,6 +1774,8 @@ public function formatBasePricePrecision($price, $precision) } /** + * Is currency different. + * * @return bool */ public function isCurrencyDifferent() @@ -1654,6 +1808,8 @@ public function getBaseTotalDue() } /** + * Get data. + * * @param string $key * @param null|string|int $index * @return mixed @@ -1758,7 +1914,7 @@ public function getTracksCollection() */ public function hasInvoices() { - return $this->getInvoiceCollection()->count(); + return (bool)$this->getInvoiceCollection()->count(); } /** @@ -1768,7 +1924,7 @@ public function hasInvoices() */ public function hasShipments() { - return $this->getShipmentsCollection()->count(); + return (bool)$this->getShipmentsCollection()->count(); } /** @@ -1778,7 +1934,7 @@ public function hasShipments() */ public function hasCreditmemos() { - return $this->getCreditmemosCollection()->count(); + return (bool)$this->getCreditmemosCollection()->count(); } /** @@ -1794,6 +1950,8 @@ public function getRelatedObjects() } /** + * Get customer name. + * * @return string */ public function getCustomerName() @@ -1821,8 +1979,8 @@ public function addRelatedObject(\Magento\Framework\Model\AbstractModel $object) /** * Get formatted order created date in store timezone * - * @param string $format date format type (short|medium|long|full) - * @return string + * @param string $format date format type (short|medium|long|full) + * @return string */ public function getCreatedAtFormatted($format) { @@ -1830,12 +1988,14 @@ public function getCreatedAtFormatted($format) new \DateTime($this->getCreatedAt()), $format, $format, - null, + $this->localeResolver->getDefaultLocale(), $this->timezone->getConfigTimezone('store', $this->getStore()) ); } /** + * Get email customer note. + * * @return string */ public function getEmailCustomerNote() @@ -1847,6 +2007,8 @@ public function getEmailCustomerNote() } /** + * Get store group name. + * * @return string */ public function getStoreGroupName() @@ -1859,8 +2021,7 @@ public function getStoreGroupName() } /** - * Resets all data in object - * so after another load it will be complete new object + * Reset all data in object so after another load it will be complete new object. * * @return $this */ @@ -1884,6 +2045,8 @@ public function reset() } /** + * Get order is not virtual. + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -1914,7 +2077,7 @@ public function isCanceled() } /** - * Returns increment id + * Return increment id * * @codeCoverageIgnore * @@ -1926,6 +2089,8 @@ public function getIncrementId() } /** + * Get Items. + * * @return \Magento\Sales\Api\Data\OrderItemInterface[] */ public function getItems() @@ -1940,7 +2105,7 @@ public function getItems() } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function setItems($items) @@ -1949,6 +2114,8 @@ public function setItems($items) } /** + * Get addresses. + * * @return \Magento\Sales\Api\Data\OrderAddressInterface[] */ public function getAddresses() @@ -1963,6 +2130,8 @@ public function getAddresses() } /** + * Get status History. + * * @return \Magento\Sales\Api\Data\OrderStatusHistoryInterface[]|null */ public function getStatusHistories() @@ -1977,17 +2146,24 @@ public function getStatusHistories() } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Sales\Api\Data\OrderExtensionInterface|null + * @return \Magento\Sales\Api\Data\OrderExtensionInterface */ public function getExtensionAttributes() { - return $this->_getExtensionAttributes(); + $extensionAttributes = $this->_getExtensionAttributes(); + if (null === $extensionAttributes) { + /** @var \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes */ + $extensionAttributes = $this->extensionAttributesFactory->create(OrderInterface::class); + $this->setExtensionAttributes($extensionAttributes); + } + + return $extensionAttributes; } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes * @return $this @@ -2000,7 +2176,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderExtensionInt //@codeCoverageIgnoreStart /** - * Returns adjustment_negative + * Return adjustment_negative. * * @return float|null */ @@ -2010,7 +2186,7 @@ public function getAdjustmentNegative() } /** - * Returns adjustment_positive + * Return adjustment_positive. * * @return float|null */ @@ -2020,7 +2196,7 @@ public function getAdjustmentPositive() } /** - * Returns applied_rule_ids + * Return applied_rule_ids. * * @return string|null */ @@ -2030,7 +2206,7 @@ public function getAppliedRuleIds() } /** - * Returns base_adjustment_negative + * Return base_adjustment_negative. * * @return float|null */ @@ -2040,7 +2216,7 @@ public function getBaseAdjustmentNegative() } /** - * Returns base_adjustment_positive + * Return base_adjustment_positive. * * @return float|null */ @@ -2050,7 +2226,7 @@ public function getBaseAdjustmentPositive() } /** - * Returns base_currency_code + * Return base_currency_code. * * @return string|null */ @@ -2060,7 +2236,7 @@ public function getBaseCurrencyCode() } /** - * Returns base_discount_amount + * Return base_discount_amount. * * @return float|null */ @@ -2070,7 +2246,7 @@ public function getBaseDiscountAmount() } /** - * Returns base_discount_canceled + * Return base_discount_canceled. * * @return float|null */ @@ -2080,7 +2256,7 @@ public function getBaseDiscountCanceled() } /** - * Returns base_discount_invoiced + * Return base_discount_invoiced. * * @return float|null */ @@ -2090,7 +2266,7 @@ public function getBaseDiscountInvoiced() } /** - * Returns base_discount_refunded + * Return base_discount_refunded. * * @return float|null */ @@ -2100,7 +2276,7 @@ public function getBaseDiscountRefunded() } /** - * Returns base_grand_total + * Return base_grand_total. * * @return float */ @@ -2110,7 +2286,7 @@ public function getBaseGrandTotal() } /** - * Returns base_discount_tax_compensation_amount + * Return base_discount_tax_compensation_amount. * * @return float|null */ @@ -2120,7 +2296,7 @@ public function getBaseDiscountTaxCompensationAmount() } /** - * Returns base_discount_tax_compensation_invoiced + * Return base_discount_tax_compensation_invoiced. * * @return float|null */ @@ -2130,7 +2306,7 @@ public function getBaseDiscountTaxCompensationInvoiced() } /** - * Returns base_discount_tax_compensation_refunded + * Return base_discount_tax_compensation_refunded. * * @return float|null */ @@ -2140,7 +2316,7 @@ public function getBaseDiscountTaxCompensationRefunded() } /** - * Returns base_shipping_amount + * Return base_shipping_amount. * * @return float|null */ @@ -2150,7 +2326,7 @@ public function getBaseShippingAmount() } /** - * Returns base_shipping_canceled + * Return base_shipping_canceled. * * @return float|null */ @@ -2160,7 +2336,7 @@ public function getBaseShippingCanceled() } /** - * Returns base_shipping_discount_amount + * Return base_shipping_discount_amount. * * @return float|null */ @@ -2170,7 +2346,7 @@ public function getBaseShippingDiscountAmount() } /** - * Returns base_shipping_discount_tax_compensation_amnt + * Return base_shipping_discount_tax_compensation_amnt. * * @return float|null */ @@ -2180,7 +2356,7 @@ public function getBaseShippingDiscountTaxCompensationAmnt() } /** - * Returns base_shipping_incl_tax + * Return base_shipping_incl_tax. * * @return float|null */ @@ -2190,7 +2366,7 @@ public function getBaseShippingInclTax() } /** - * Returns base_shipping_invoiced + * Return base_shipping_invoiced. * * @return float|null */ @@ -2200,7 +2376,7 @@ public function getBaseShippingInvoiced() } /** - * Returns base_shipping_refunded + * Return base_shipping_refunded. * * @return float|null */ @@ -2210,7 +2386,7 @@ public function getBaseShippingRefunded() } /** - * Returns base_shipping_tax_amount + * Return base_shipping_tax_amount. * * @return float|null */ @@ -2220,7 +2396,7 @@ public function getBaseShippingTaxAmount() } /** - * Returns base_shipping_tax_refunded + * Return base_shipping_tax_refunded. * * @return float|null */ @@ -2230,7 +2406,7 @@ public function getBaseShippingTaxRefunded() } /** - * Returns base_subtotal + * Return base_subtotal. * * @return float|null */ @@ -2240,7 +2416,7 @@ public function getBaseSubtotal() } /** - * Returns base_subtotal_canceled + * Return base_subtotal_canceled. * * @return float|null */ @@ -2250,7 +2426,7 @@ public function getBaseSubtotalCanceled() } /** - * Returns base_subtotal_incl_tax + * Return base_subtotal_incl_tax. * * @return float|null */ @@ -2260,7 +2436,7 @@ public function getBaseSubtotalInclTax() } /** - * Returns base_subtotal_invoiced + * Return base_subtotal_invoiced. * * @return float|null */ @@ -2270,7 +2446,7 @@ public function getBaseSubtotalInvoiced() } /** - * Returns base_subtotal_refunded + * Return base_subtotal_refunded. * * @return float|null */ @@ -2280,7 +2456,7 @@ public function getBaseSubtotalRefunded() } /** - * Returns base_tax_amount + * Return base_tax_amount. * * @return float|null */ @@ -2290,7 +2466,7 @@ public function getBaseTaxAmount() } /** - * Returns base_tax_canceled + * Return base_tax_canceled. * * @return float|null */ @@ -2300,7 +2476,7 @@ public function getBaseTaxCanceled() } /** - * Returns base_tax_invoiced + * Return base_tax_invoiced. * * @return float|null */ @@ -2310,7 +2486,7 @@ public function getBaseTaxInvoiced() } /** - * Returns base_tax_refunded + * Return base_tax_refunded. * * @return float|null */ @@ -2320,7 +2496,7 @@ public function getBaseTaxRefunded() } /** - * Returns base_total_canceled + * Return base_total_canceled. * * @return float|null */ @@ -2330,7 +2506,7 @@ public function getBaseTotalCanceled() } /** - * Returns base_total_invoiced + * Return base_total_invoiced. * * @return float|null */ @@ -2340,7 +2516,7 @@ public function getBaseTotalInvoiced() } /** - * Returns base_total_invoiced_cost + * Return base_total_invoiced_cost. * * @return float|null */ @@ -2350,7 +2526,7 @@ public function getBaseTotalInvoicedCost() } /** - * Returns base_total_offline_refunded + * Return base_total_offline_refunded. * * @return float|null */ @@ -2360,7 +2536,7 @@ public function getBaseTotalOfflineRefunded() } /** - * Returns base_total_online_refunded + * Return base_total_online_refunded. * * @return float|null */ @@ -2370,7 +2546,7 @@ public function getBaseTotalOnlineRefunded() } /** - * Returns base_total_paid + * Return base_total_paid. * * @return float|null */ @@ -2380,7 +2556,7 @@ public function getBaseTotalPaid() } /** - * Returns base_total_qty_ordered + * Return base_total_qty_ordered. * * @return float|null */ @@ -2390,7 +2566,7 @@ public function getBaseTotalQtyOrdered() } /** - * Returns base_total_refunded + * Return base_total_refunded. * * @return float|null */ @@ -2400,7 +2576,7 @@ public function getBaseTotalRefunded() } /** - * Returns base_to_global_rate + * Return base_to_global_rate. * * @return float|null */ @@ -2410,7 +2586,7 @@ public function getBaseToGlobalRate() } /** - * Returns base_to_order_rate + * Return base_to_order_rate. * * @return float|null */ @@ -2420,7 +2596,7 @@ public function getBaseToOrderRate() } /** - * Returns billing_address_id + * Return billing_address_id. * * @return int|null */ @@ -2430,7 +2606,7 @@ public function getBillingAddressId() } /** - * Returns can_ship_partially + * Return can_ship_partially. * * @return int|null */ @@ -2440,7 +2616,7 @@ public function getCanShipPartially() } /** - * Returns can_ship_partially_item + * Return can_ship_partially_item. * * @return int|null */ @@ -2450,7 +2626,7 @@ public function getCanShipPartiallyItem() } /** - * Returns coupon_code + * Return coupon_code. * * @return string|null */ @@ -2460,7 +2636,7 @@ public function getCouponCode() } /** - * Returns created_at + * Return created_at. * * @return string|null */ @@ -2470,7 +2646,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -2478,7 +2654,7 @@ public function setCreatedAt($createdAt) } /** - * Returns customer_dob + * Return customer_dob. * * @return string|null */ @@ -2488,7 +2664,7 @@ public function getCustomerDob() } /** - * Returns customer_email + * Return customer_email. * * @return string */ @@ -2498,7 +2674,7 @@ public function getCustomerEmail() } /** - * Returns customer_firstname + * Return customer_firstname. * * @return string|null */ @@ -2508,7 +2684,7 @@ public function getCustomerFirstname() } /** - * Returns customer_gender + * Return customer_gender. * * @return int|null */ @@ -2518,7 +2694,7 @@ public function getCustomerGender() } /** - * Returns customer_group_id + * Return customer_group_id. * * @return int|null */ @@ -2528,7 +2704,7 @@ public function getCustomerGroupId() } /** - * Returns customer_id + * Return customer_id. * * @return int|null */ @@ -2538,7 +2714,7 @@ public function getCustomerId() } /** - * Returns customer_is_guest + * Return customer_is_guest. * * @return int|null */ @@ -2548,7 +2724,7 @@ public function getCustomerIsGuest() } /** - * Returns customer_lastname + * Return customer_lastname. * * @return string|null */ @@ -2558,7 +2734,7 @@ public function getCustomerLastname() } /** - * Returns customer_middlename + * Return customer_middlename. * * @return string|null */ @@ -2568,7 +2744,7 @@ public function getCustomerMiddlename() } /** - * Returns customer_note + * Return customer_note. * * @return string|null */ @@ -2578,7 +2754,7 @@ public function getCustomerNote() } /** - * Returns customer_note_notify + * Return customer_note_notify. * * @return int|null */ @@ -2588,7 +2764,7 @@ public function getCustomerNoteNotify() } /** - * Returns customer_prefix + * Return customer_prefix. * * @return string|null */ @@ -2598,7 +2774,7 @@ public function getCustomerPrefix() } /** - * Returns customer_suffix + * Return customer_suffix. * * @return string|null */ @@ -2608,7 +2784,7 @@ public function getCustomerSuffix() } /** - * Returns customer_taxvat + * Return customer_taxvat. * * @return string|null */ @@ -2618,7 +2794,7 @@ public function getCustomerTaxvat() } /** - * Returns discount_amount + * Return discount_amount. * * @return float|null */ @@ -2628,7 +2804,7 @@ public function getDiscountAmount() } /** - * Returns discount_canceled + * Return discount_canceled. * * @return float|null */ @@ -2638,7 +2814,7 @@ public function getDiscountCanceled() } /** - * Returns discount_description + * Return discount_description. * * @return string|null */ @@ -2648,7 +2824,7 @@ public function getDiscountDescription() } /** - * Returns discount_invoiced + * Return discount_invoiced. * * @return float|null */ @@ -2658,7 +2834,7 @@ public function getDiscountInvoiced() } /** - * Returns discount_refunded + * Return discount_refunded. * * @return float|null */ @@ -2668,7 +2844,7 @@ public function getDiscountRefunded() } /** - * Returns edit_increment + * Return edit_increment. * * @return int|null */ @@ -2678,7 +2854,7 @@ public function getEditIncrement() } /** - * Returns email_sent + * Return email_sent. * * @return int|null */ @@ -2688,7 +2864,7 @@ public function getEmailSent() } /** - * Returns ext_customer_id + * Return ext_customer_id. * * @return string|null */ @@ -2698,7 +2874,7 @@ public function getExtCustomerId() } /** - * Returns ext_order_id + * Return ext_order_id. * * @return string|null */ @@ -2708,7 +2884,7 @@ public function getExtOrderId() } /** - * Returns forced_shipment_with_invoice + * Return forced_shipment_with_invoice. * * @return int|null */ @@ -2718,7 +2894,7 @@ public function getForcedShipmentWithInvoice() } /** - * Returns global_currency_code + * Return global_currency_code. * * @return string|null */ @@ -2728,7 +2904,7 @@ public function getGlobalCurrencyCode() } /** - * Returns grand_total + * Return grand_total. * * @return float */ @@ -2738,7 +2914,7 @@ public function getGrandTotal() } /** - * Returns discount_tax_compensation_amount + * Return discount_tax_compensation_amount. * * @return float|null */ @@ -2748,7 +2924,7 @@ public function getDiscountTaxCompensationAmount() } /** - * Returns discount_tax_compensation_invoiced + * Return discount_tax_compensation_invoiced. * * @return float|null */ @@ -2758,7 +2934,7 @@ public function getDiscountTaxCompensationInvoiced() } /** - * Returns discount_tax_compensation_refunded + * Return discount_tax_compensation_refunded. * * @return float|null */ @@ -2768,7 +2944,7 @@ public function getDiscountTaxCompensationRefunded() } /** - * Returns hold_before_state + * Return hold_before_state. * * @return string|null */ @@ -2778,7 +2954,7 @@ public function getHoldBeforeState() } /** - * Returns hold_before_status + * Return hold_before_status. * * @return string|null */ @@ -2788,7 +2964,7 @@ public function getHoldBeforeStatus() } /** - * Returns is_virtual + * Return is_virtual. * * @return int|null */ @@ -2798,7 +2974,7 @@ public function getIsVirtual() } /** - * Returns order_currency_code + * Return order_currency_code. * * @return string|null */ @@ -2808,7 +2984,7 @@ public function getOrderCurrencyCode() } /** - * Returns original_increment_id + * Return original_increment_id. * * @return string|null */ @@ -2818,7 +2994,7 @@ public function getOriginalIncrementId() } /** - * Returns payment_authorization_amount + * Return payment_authorization_amount. * * @return float|null */ @@ -2828,7 +3004,7 @@ public function getPaymentAuthorizationAmount() } /** - * Returns payment_auth_expiration + * Return payment_auth_expiration. * * @return int|null */ @@ -2838,7 +3014,7 @@ public function getPaymentAuthExpiration() } /** - * Returns protect_code + * Return protect_code. * * @return string|null */ @@ -2848,7 +3024,7 @@ public function getProtectCode() } /** - * Returns quote_address_id + * Return quote_address_id. * * @return int|null */ @@ -2858,7 +3034,7 @@ public function getQuoteAddressId() } /** - * Returns quote_id + * Return quote_id. * * @return int|null */ @@ -2868,7 +3044,7 @@ public function getQuoteId() } /** - * Returns relation_child_id + * Return relation_child_id. * * @return string|null */ @@ -2878,7 +3054,7 @@ public function getRelationChildId() } /** - * Returns relation_child_real_id + * Return relation_child_real_id. * * @return string|null */ @@ -2888,7 +3064,7 @@ public function getRelationChildRealId() } /** - * Returns relation_parent_id + * Return relation_parent_id. * * @return string|null */ @@ -2898,7 +3074,7 @@ public function getRelationParentId() } /** - * Returns relation_parent_real_id + * Return relation_parent_real_id. * * @return string|null */ @@ -2908,7 +3084,7 @@ public function getRelationParentRealId() } /** - * Returns remote_ip + * Return remote_ip. * * @return string|null */ @@ -2918,7 +3094,7 @@ public function getRemoteIp() } /** - * Returns shipping_amount + * Return shipping_amount. * * @return float|null */ @@ -2928,7 +3104,7 @@ public function getShippingAmount() } /** - * Returns shipping_canceled + * Return shipping_canceled. * * @return float|null */ @@ -2938,7 +3114,7 @@ public function getShippingCanceled() } /** - * Returns shipping_description + * Return shipping_description. * * @return string|null */ @@ -2948,7 +3124,7 @@ public function getShippingDescription() } /** - * Returns shipping_discount_amount + * Return shipping_discount_amount. * * @return float|null */ @@ -2958,7 +3134,7 @@ public function getShippingDiscountAmount() } /** - * Returns shipping_discount_tax_compensation_amount + * Return shipping_discount_tax_compensation_amount. * * @return float|null */ @@ -2968,7 +3144,7 @@ public function getShippingDiscountTaxCompensationAmount() } /** - * Returns shipping_incl_tax + * Return shipping_incl_tax. * * @return float|null */ @@ -2978,7 +3154,7 @@ public function getShippingInclTax() } /** - * Returns shipping_invoiced + * Return shipping_invoiced. * * @return float|null */ @@ -2988,7 +3164,7 @@ public function getShippingInvoiced() } /** - * Returns shipping_refunded + * Return shipping_refunded. * * @return float|null */ @@ -2998,7 +3174,7 @@ public function getShippingRefunded() } /** - * Returns shipping_tax_amount + * Return shipping_tax_amount. * * @return float|null */ @@ -3008,7 +3184,7 @@ public function getShippingTaxAmount() } /** - * Returns shipping_tax_refunded + * Return shipping_tax_refunded. * * @return float|null */ @@ -3018,7 +3194,7 @@ public function getShippingTaxRefunded() } /** - * Returns state + * Return state. * * @return string|null */ @@ -3028,7 +3204,7 @@ public function getState() } /** - * Returns status + * Return status. * * @return string|null */ @@ -3038,7 +3214,7 @@ public function getStatus() } /** - * Returns store_currency_code + * Return store_currency_code. * * @return string|null */ @@ -3048,7 +3224,7 @@ public function getStoreCurrencyCode() } /** - * Returns store_id + * Return store_id. * * @return int|null */ @@ -3058,7 +3234,7 @@ public function getStoreId() } /** - * Returns store_name + * Return store_name. * * @return string|null */ @@ -3068,7 +3244,7 @@ public function getStoreName() } /** - * Returns store_to_base_rate + * Return store_to_base_rate. * * @return float|null */ @@ -3078,7 +3254,7 @@ public function getStoreToBaseRate() } /** - * Returns store_to_order_rate + * Return store_to_order_rate. * * @return float|null */ @@ -3088,7 +3264,7 @@ public function getStoreToOrderRate() } /** - * Returns subtotal + * Return subtotal. * * @return float|null */ @@ -3098,7 +3274,7 @@ public function getSubtotal() } /** - * Returns subtotal_canceled + * Return subtotal_canceled. * * @return float|null */ @@ -3108,7 +3284,7 @@ public function getSubtotalCanceled() } /** - * Returns subtotal_incl_tax + * Return subtotal_incl_tax. * * @return float|null */ @@ -3118,7 +3294,7 @@ public function getSubtotalInclTax() } /** - * Returns subtotal_invoiced + * Return subtotal_invoiced. * * @return float|null */ @@ -3128,7 +3304,7 @@ public function getSubtotalInvoiced() } /** - * Returns subtotal_refunded + * Return subtotal_refunded. * * @return float|null */ @@ -3138,7 +3314,7 @@ public function getSubtotalRefunded() } /** - * Returns tax_amount + * Return tax_amount. * * @return float|null */ @@ -3148,7 +3324,7 @@ public function getTaxAmount() } /** - * Returns tax_canceled + * Return tax_canceled. * * @return float|null */ @@ -3158,7 +3334,7 @@ public function getTaxCanceled() } /** - * Returns tax_invoiced + * Return tax_invoiced. * * @return float|null */ @@ -3168,7 +3344,7 @@ public function getTaxInvoiced() } /** - * Returns tax_refunded + * Return tax_refunded. * * @return float|null */ @@ -3178,7 +3354,7 @@ public function getTaxRefunded() } /** - * Returns total_canceled + * Return total_canceled. * * @return float|null */ @@ -3188,7 +3364,7 @@ public function getTotalCanceled() } /** - * Returns total_invoiced + * Return total_invoiced. * * @return float|null */ @@ -3198,7 +3374,7 @@ public function getTotalInvoiced() } /** - * Returns total_item_count + * Return total_item_count. * * @return int|null */ @@ -3208,7 +3384,7 @@ public function getTotalItemCount() } /** - * Returns total_offline_refunded + * Return total_offline_refunded. * * @return float|null */ @@ -3218,7 +3394,7 @@ public function getTotalOfflineRefunded() } /** - * Returns total_online_refunded + * Return total_online_refunded. * * @return float|null */ @@ -3228,7 +3404,7 @@ public function getTotalOnlineRefunded() } /** - * Returns total_paid + * Return total_paid. * * @return float|null */ @@ -3238,7 +3414,7 @@ public function getTotalPaid() } /** - * Returns total_qty_ordered + * Return total_qty_ordered. * * @return float|null */ @@ -3248,7 +3424,7 @@ public function getTotalQtyOrdered() } /** - * Returns total_refunded + * Return total_refunded. * * @return float|null */ @@ -3258,7 +3434,7 @@ public function getTotalRefunded() } /** - * Returns updated_at + * Return updated_at. * * @return string|null */ @@ -3268,7 +3444,7 @@ public function getUpdatedAt() } /** - * Returns weight + * Return weight. * * @return float|null */ @@ -3278,7 +3454,7 @@ public function getWeight() } /** - * Returns x_forwarded_for + * Return x_forwarded_for. * * @return string|null */ @@ -3288,7 +3464,7 @@ public function getXForwardedFor() } /** - * {@inheritdoc} + * @inheritdoc */ public function setStatusHistories(array $statusHistories = null) { @@ -3296,7 +3472,7 @@ public function setStatusHistories(array $statusHistories = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStatus($status) { @@ -3304,7 +3480,7 @@ public function setStatus($status) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCouponCode($code) { @@ -3312,7 +3488,7 @@ public function setCouponCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProtectCode($code) { @@ -3320,7 +3496,7 @@ public function setProtectCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDescription($description) { @@ -3328,7 +3504,7 @@ public function setShippingDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsVirtual($isVirtual) { @@ -3336,7 +3512,7 @@ public function setIsVirtual($isVirtual) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -3344,7 +3520,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerId($id) { @@ -3352,7 +3528,7 @@ public function setCustomerId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -3360,7 +3536,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountCanceled($baseDiscountCanceled) { @@ -3368,7 +3544,7 @@ public function setBaseDiscountCanceled($baseDiscountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountInvoiced($baseDiscountInvoiced) { @@ -3376,7 +3552,7 @@ public function setBaseDiscountInvoiced($baseDiscountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountRefunded($baseDiscountRefunded) { @@ -3384,7 +3560,7 @@ public function setBaseDiscountRefunded($baseDiscountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseGrandTotal($amount) { @@ -3392,7 +3568,7 @@ public function setBaseGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -3400,7 +3576,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingCanceled($baseShippingCanceled) { @@ -3408,7 +3584,7 @@ public function setBaseShippingCanceled($baseShippingCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInvoiced($baseShippingInvoiced) { @@ -3416,7 +3592,7 @@ public function setBaseShippingInvoiced($baseShippingInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingRefunded($baseShippingRefunded) { @@ -3424,7 +3600,7 @@ public function setBaseShippingRefunded($baseShippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxAmount($amount) { @@ -3432,7 +3608,7 @@ public function setBaseShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxRefunded($baseShippingTaxRefunded) { @@ -3440,7 +3616,7 @@ public function setBaseShippingTaxRefunded($baseShippingTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotal($amount) { @@ -3448,7 +3624,7 @@ public function setBaseSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalCanceled($baseSubtotalCanceled) { @@ -3456,7 +3632,7 @@ public function setBaseSubtotalCanceled($baseSubtotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInvoiced($baseSubtotalInvoiced) { @@ -3464,7 +3640,7 @@ public function setBaseSubtotalInvoiced($baseSubtotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalRefunded($baseSubtotalRefunded) { @@ -3472,7 +3648,7 @@ public function setBaseSubtotalRefunded($baseSubtotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -3480,7 +3656,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxCanceled($baseTaxCanceled) { @@ -3488,7 +3664,7 @@ public function setBaseTaxCanceled($baseTaxCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxInvoiced($baseTaxInvoiced) { @@ -3496,7 +3672,7 @@ public function setBaseTaxInvoiced($baseTaxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxRefunded($baseTaxRefunded) { @@ -3504,7 +3680,7 @@ public function setBaseTaxRefunded($baseTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToGlobalRate($rate) { @@ -3512,7 +3688,7 @@ public function setBaseToGlobalRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToOrderRate($rate) { @@ -3520,7 +3696,7 @@ public function setBaseToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalCanceled($baseTotalCanceled) { @@ -3528,7 +3704,7 @@ public function setBaseTotalCanceled($baseTotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalInvoiced($baseTotalInvoiced) { @@ -3536,7 +3712,7 @@ public function setBaseTotalInvoiced($baseTotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalInvoicedCost($baseTotalInvoicedCost) { @@ -3544,7 +3720,7 @@ public function setBaseTotalInvoicedCost($baseTotalInvoicedCost) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalOfflineRefunded($baseTotalOfflineRefunded) { @@ -3552,7 +3728,7 @@ public function setBaseTotalOfflineRefunded($baseTotalOfflineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalOnlineRefunded($baseTotalOnlineRefunded) { @@ -3560,7 +3736,7 @@ public function setBaseTotalOnlineRefunded($baseTotalOnlineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalPaid($baseTotalPaid) { @@ -3568,7 +3744,7 @@ public function setBaseTotalPaid($baseTotalPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalQtyOrdered($baseTotalQtyOrdered) { @@ -3576,7 +3752,7 @@ public function setBaseTotalQtyOrdered($baseTotalQtyOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalRefunded($baseTotalRefunded) { @@ -3584,7 +3760,7 @@ public function setBaseTotalRefunded($baseTotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -3592,7 +3768,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountCanceled($discountCanceled) { @@ -3600,7 +3776,7 @@ public function setDiscountCanceled($discountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountInvoiced($discountInvoiced) { @@ -3608,7 +3784,7 @@ public function setDiscountInvoiced($discountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountRefunded($discountRefunded) { @@ -3616,7 +3792,7 @@ public function setDiscountRefunded($discountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGrandTotal($amount) { @@ -3624,7 +3800,7 @@ public function setGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAmount($amount) { @@ -3632,7 +3808,7 @@ public function setShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingCanceled($shippingCanceled) { @@ -3640,7 +3816,7 @@ public function setShippingCanceled($shippingCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInvoiced($shippingInvoiced) { @@ -3648,7 +3824,7 @@ public function setShippingInvoiced($shippingInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingRefunded($shippingRefunded) { @@ -3656,7 +3832,7 @@ public function setShippingRefunded($shippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxAmount($amount) { @@ -3664,7 +3840,7 @@ public function setShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxRefunded($shippingTaxRefunded) { @@ -3672,7 +3848,7 @@ public function setShippingTaxRefunded($shippingTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToBaseRate($rate) { @@ -3680,7 +3856,7 @@ public function setStoreToBaseRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToOrderRate($rate) { @@ -3688,7 +3864,7 @@ public function setStoreToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotal($amount) { @@ -3696,7 +3872,7 @@ public function setSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalCanceled($subtotalCanceled) { @@ -3704,7 +3880,7 @@ public function setSubtotalCanceled($subtotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInvoiced($subtotalInvoiced) { @@ -3712,7 +3888,7 @@ public function setSubtotalInvoiced($subtotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalRefunded($subtotalRefunded) { @@ -3720,7 +3896,7 @@ public function setSubtotalRefunded($subtotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -3728,7 +3904,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxCanceled($taxCanceled) { @@ -3736,7 +3912,7 @@ public function setTaxCanceled($taxCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxInvoiced($taxInvoiced) { @@ -3744,7 +3920,7 @@ public function setTaxInvoiced($taxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxRefunded($taxRefunded) { @@ -3752,7 +3928,7 @@ public function setTaxRefunded($taxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalCanceled($totalCanceled) { @@ -3760,7 +3936,7 @@ public function setTotalCanceled($totalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalInvoiced($totalInvoiced) { @@ -3768,7 +3944,7 @@ public function setTotalInvoiced($totalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalOfflineRefunded($totalOfflineRefunded) { @@ -3776,7 +3952,7 @@ public function setTotalOfflineRefunded($totalOfflineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalOnlineRefunded($totalOnlineRefunded) { @@ -3784,7 +3960,7 @@ public function setTotalOnlineRefunded($totalOnlineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalPaid($totalPaid) { @@ -3792,7 +3968,7 @@ public function setTotalPaid($totalPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalQtyOrdered($totalQtyOrdered) { @@ -3800,7 +3976,7 @@ public function setTotalQtyOrdered($totalQtyOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalRefunded($totalRefunded) { @@ -3808,7 +3984,7 @@ public function setTotalRefunded($totalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCanShipPartially($flag) { @@ -3816,7 +3992,7 @@ public function setCanShipPartially($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCanShipPartiallyItem($flag) { @@ -3824,7 +4000,7 @@ public function setCanShipPartiallyItem($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerIsGuest($customerIsGuest) { @@ -3832,7 +4008,7 @@ public function setCustomerIsGuest($customerIsGuest) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerNoteNotify($customerNoteNotify) { @@ -3840,7 +4016,7 @@ public function setCustomerNoteNotify($customerNoteNotify) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBillingAddressId($id) { @@ -3848,7 +4024,7 @@ public function setBillingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerGroupId($id) { @@ -3856,7 +4032,7 @@ public function setCustomerGroupId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEditIncrement($editIncrement) { @@ -3864,7 +4040,7 @@ public function setEditIncrement($editIncrement) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmailSent($emailSent) { @@ -3872,7 +4048,7 @@ public function setEmailSent($emailSent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setForcedShipmentWithInvoice($forcedShipmentWithInvoice) { @@ -3880,7 +4056,7 @@ public function setForcedShipmentWithInvoice($forcedShipmentWithInvoice) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPaymentAuthExpiration($paymentAuthExpiration) { @@ -3888,7 +4064,7 @@ public function setPaymentAuthExpiration($paymentAuthExpiration) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuoteAddressId($id) { @@ -3896,7 +4072,7 @@ public function setQuoteAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuoteId($id) { @@ -3904,7 +4080,7 @@ public function setQuoteId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustmentNegative($adjustmentNegative) { @@ -3912,7 +4088,7 @@ public function setAdjustmentNegative($adjustmentNegative) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustmentPositive($adjustmentPositive) { @@ -3920,7 +4096,7 @@ public function setAdjustmentPositive($adjustmentPositive) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustmentNegative($baseAdjustmentNegative) { @@ -3928,7 +4104,7 @@ public function setBaseAdjustmentNegative($baseAdjustmentNegative) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustmentPositive($baseAdjustmentPositive) { @@ -3936,7 +4112,7 @@ public function setBaseAdjustmentPositive($baseAdjustmentPositive) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountAmount($amount) { @@ -3944,7 +4120,7 @@ public function setBaseShippingDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInclTax($amount) { @@ -3952,7 +4128,7 @@ public function setBaseSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalDue($baseTotalDue) { @@ -3960,7 +4136,7 @@ public function setBaseTotalDue($baseTotalDue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPaymentAuthorizationAmount($amount) { @@ -3968,7 +4144,7 @@ public function setPaymentAuthorizationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountAmount($amount) { @@ -3976,7 +4152,7 @@ public function setShippingDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInclTax($amount) { @@ -3984,7 +4160,7 @@ public function setSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalDue($totalDue) { @@ -3992,7 +4168,7 @@ public function setTotalDue($totalDue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -4000,7 +4176,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerDob($customerDob) { @@ -4008,7 +4184,7 @@ public function setCustomerDob($customerDob) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIncrementId($id) { @@ -4016,7 +4192,7 @@ public function setIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAppliedRuleIds($appliedRuleIds) { @@ -4024,7 +4200,7 @@ public function setAppliedRuleIds($appliedRuleIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCurrencyCode($code) { @@ -4032,7 +4208,7 @@ public function setBaseCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerEmail($customerEmail) { @@ -4040,7 +4216,7 @@ public function setCustomerEmail($customerEmail) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerFirstname($customerFirstname) { @@ -4048,7 +4224,7 @@ public function setCustomerFirstname($customerFirstname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerLastname($customerLastname) { @@ -4056,7 +4232,7 @@ public function setCustomerLastname($customerLastname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerMiddlename($customerMiddlename) { @@ -4064,7 +4240,7 @@ public function setCustomerMiddlename($customerMiddlename) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerPrefix($customerPrefix) { @@ -4072,7 +4248,7 @@ public function setCustomerPrefix($customerPrefix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerSuffix($customerSuffix) { @@ -4080,7 +4256,7 @@ public function setCustomerSuffix($customerSuffix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerTaxvat($customerTaxvat) { @@ -4088,7 +4264,7 @@ public function setCustomerTaxvat($customerTaxvat) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountDescription($description) { @@ -4096,7 +4272,7 @@ public function setDiscountDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtCustomerId($id) { @@ -4104,7 +4280,7 @@ public function setExtCustomerId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtOrderId($id) { @@ -4112,7 +4288,7 @@ public function setExtOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGlobalCurrencyCode($code) { @@ -4120,7 +4296,7 @@ public function setGlobalCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setHoldBeforeState($holdBeforeState) { @@ -4128,7 +4304,7 @@ public function setHoldBeforeState($holdBeforeState) } /** - * {@inheritdoc} + * @inheritdoc */ public function setHoldBeforeStatus($holdBeforeStatus) { @@ -4136,7 +4312,7 @@ public function setHoldBeforeStatus($holdBeforeStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderCurrencyCode($code) { @@ -4144,7 +4320,7 @@ public function setOrderCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOriginalIncrementId($id) { @@ -4152,7 +4328,7 @@ public function setOriginalIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationChildId($id) { @@ -4160,7 +4336,7 @@ public function setRelationChildId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationChildRealId($realId) { @@ -4168,7 +4344,7 @@ public function setRelationChildRealId($realId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationParentId($id) { @@ -4176,7 +4352,7 @@ public function setRelationParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationParentRealId($realId) { @@ -4184,7 +4360,7 @@ public function setRelationParentRealId($realId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRemoteIp($remoteIp) { @@ -4192,7 +4368,7 @@ public function setRemoteIp($remoteIp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCurrencyCode($code) { @@ -4200,7 +4376,7 @@ public function setStoreCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreName($storeName) { @@ -4208,7 +4384,7 @@ public function setStoreName($storeName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setXForwardedFor($xForwardedFor) { @@ -4216,7 +4392,7 @@ public function setXForwardedFor($xForwardedFor) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerNote($customerNote) { @@ -4224,7 +4400,7 @@ public function setCustomerNote($customerNote) } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -4232,7 +4408,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalItemCount($totalItemCount) { @@ -4240,7 +4416,7 @@ public function setTotalItemCount($totalItemCount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerGender($customerGender) { @@ -4248,7 +4424,7 @@ public function setCustomerGender($customerGender) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -4256,7 +4432,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -4264,7 +4440,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountTaxCompensationAmount($amount) { @@ -4272,7 +4448,7 @@ public function setShippingDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) { @@ -4280,7 +4456,7 @@ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoiced) { @@ -4288,7 +4464,7 @@ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoi } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensationInvoiced) { @@ -4299,7 +4475,7 @@ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefunded) { @@ -4310,7 +4486,7 @@ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefun } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensationRefunded) { @@ -4321,7 +4497,7 @@ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInclTax($amount) { @@ -4329,7 +4505,7 @@ public function setShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInclTax($amount) { diff --git a/app/code/Magento/Sales/Model/Order/Address.php b/app/code/Magento/Sales/Model/Order/Address.php index 77d8330a72550..1bd760c8434d2 100644 --- a/app/code/Magento/Sales/Model/Order/Address.php +++ b/app/code/Magento/Sales/Model/Order/Address.php @@ -600,7 +600,7 @@ public function setEmail($email) */ public function setTelephone($telephone) { - return $this->setData(OrderAddressInterface::TELEPHONE, $telephone); + return $this->setData(OrderAddressInterface::TELEPHONE, trim($telephone)); } /** diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index e6353f7f28899..e970cd66635f9 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -48,8 +48,8 @@ class Validator /** * @param DirectoryHelper $directoryHelper - * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param CountryFactory $countryFactory + * @param EavConfig $eavConfig */ public function __construct( DirectoryHelper $directoryHelper, @@ -60,6 +60,17 @@ public function __construct( $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + } + + /** + * Validate address. + * + * @param \Magento\Sales\Model\Order\Address $address + * @return array + */ + public function validate(Address $address) + { + $warnings = []; if ($this->isTelephoneRequired()) { $this->required['telephone'] = 'Phone Number'; @@ -72,16 +83,7 @@ public function __construct( if ($this->isFaxRequired()) { $this->required['fax'] = 'Fax'; } - } - /** - * - * @param \Magento\Sales\Model\Order\Address $address - * @return array - */ - public function validate(Address $address) - { - $warnings = []; foreach ($this->required as $code => $label) { if (!$address->hasData($code)) { $warnings[] = sprintf('%s is a required field', $label); @@ -194,7 +196,10 @@ protected function isStateRequired($countryId) } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -202,7 +207,10 @@ protected function isTelephoneRequired() } /** + * Check whether company is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -210,7 +218,10 @@ protected function isCompanyRequired() } /** + * Check whether fax is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Sales/Model/Order/AddressRepository.php b/app/code/Magento/Sales/Model/Order/AddressRepository.php index 96dc531a82bf4..7543a298c3a4a 100644 --- a/app/code/Magento/Sales/Model/Order/AddressRepository.php +++ b/app/code/Magento/Sales/Model/Order/AddressRepository.php @@ -5,7 +5,11 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Customer\Model\AttributeMetadataDataProvider; +use Magento\Customer\Model\ResourceModel\Form\Attribute\Collection as AttributeCollection; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\Data\OrderAddressInterface; use Magento\Sales\Model\ResourceModel\Metadata; use Magento\Sales\Api\Data\OrderAddressSearchResultInterfaceFactory as SearchResultFactory; use Magento\Framework\Exception\CouldNotDeleteException; @@ -40,20 +44,88 @@ class AddressRepository implements \Magento\Sales\Api\OrderAddressRepositoryInte */ private $collectionProcessor; + /** + * @var AttributeMetadataDataProvider + */ + private $attributeMetadataDataProvider; + + /** + * @var AttributeCollection|null + */ + private $attributesList = null; + /** * AddressRepository constructor. * @param Metadata $metadata * @param SearchResultFactory $searchResultFactory * @param CollectionProcessorInterface|null $collectionProcessor + * @param AttributeMetadataDataProvider $attributeMetadataDataProvider */ public function __construct( Metadata $metadata, SearchResultFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor = null, + AttributeMetadataDataProvider $attributeMetadataDataProvider = null ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->attributeMetadataDataProvider = $attributeMetadataDataProvider ?: ObjectManager::getInstance() + ->get(AttributeMetadataDataProvider::class); + } + + /** + * Format multiline and multiselect attributes + * + * @param OrderAddressInterface $orderAddress + * + * @return void + */ + private function formatCustomAddressAttributes(OrderAddressInterface $orderAddress) + { + $attributesList = $this->getAttributesList(); + + foreach ($attributesList as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + if (!$orderAddress->hasData($attributeCode)) { + continue; + } + $attributeValue = $orderAddress->getData($attributeCode); + if (is_array($attributeValue)) { + $glue = $attribute->getFrontendInput() === 'multiline' ? PHP_EOL : ','; + $attributeValue = trim(implode($glue, $attributeValue)); + } + $orderAddress->setData($attributeCode, $attributeValue); + } + } + + /** + * Get list of custom attributes. + * + * @return AttributeCollection|null + */ + private function getAttributesList() + { + if (!$this->attributesList) { + $attributesList = $this->attributeMetadataDataProvider->loadAttributesCollection( + 'customer_address', + 'customer_register_address' + ); + $attributesList->addFieldToFilter('is_user_defined', 1); + $attributesList->addFieldToFilter( + 'frontend_input', + [ + 'in' => [ + 'multiline', + 'multiselect', + ], + ] + ); + + $this->attributesList = $attributesList; + } + + return $this->attributesList; } /** @@ -95,7 +167,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $searchResult = $this->searchResultFactory->create(); $this->collectionProcessor->process($searchCriteria, $searchResult); $searchResult->setSearchCriteria($searchCriteria); - + return $searchResult; } @@ -141,6 +213,7 @@ public function deleteById($id) */ public function save(\Magento\Sales\Api\Data\OrderAddressInterface $entity) { + $this->formatCustomAddressAttributes($entity); try { $this->metadata->getMapper()->save($entity); $this->registry[$entity->getEntityId()] = $entity; diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index e00eda647dc8d..b972fc8c48eae 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Framework\Exception\LocalizedException; + /** * Order configuration model * @@ -85,7 +87,7 @@ protected function _getCollection() /** * @param string $state - * @return Status|null + * @return Status */ protected function _getState($state) { @@ -101,7 +103,7 @@ protected function _getState($state) * Retrieve default status for state * * @param string $state - * @return string + * @return string|null */ public function getStateDefaultStatus($state) { @@ -115,24 +117,48 @@ public function getStateDefaultStatus($state) } /** - * Retrieve status label + * Get status label for a specified area * - * @param string $code - * @return string + * @param string $code + * @param string $area + * @return string */ - public function getStatusLabel($code) + private function getStatusLabelForArea(string $code, string $area): string { - $area = $this->state->getAreaCode(); $code = $this->maskStatusForArea($area, $code); $status = $this->orderStatusFactory->create()->load($code); - if ($area == 'adminhtml') { + if ($area === 'adminhtml') { return $status->getLabel(); } return $status->getStoreLabel(); } + /** + * Retrieve status label for detected area + * + * @param string $code + * @return string + * @throws LocalizedException + */ + public function getStatusLabel($code) + { + $area = $this->state->getAreaCode() ?: \Magento\Framework\App\Area::AREA_FRONTEND; + return $this->getStatusLabelForArea($code, $area); + } + + /** + * Retrieve status label for area + * + * @param string $code + * @return string + */ + public function getStatusFrontendLabel(string $code): string + { + return $this->getStatusLabelForArea($code, \Magento\Framework\App\Area::AREA_FRONTEND); + } + /** * Mask status for order for specified area * @@ -151,7 +177,7 @@ protected function maskStatusForArea($area, $code) /** * State label getter * - * @param string $state + * @param string $state * @return \Magento\Framework\Phrase|string */ public function getStateLabel($state) @@ -183,7 +209,7 @@ public function getStates() { $states = []; foreach ($this->_getCollection() as $item) { - if ($item->getState()) { + if ($item->getState() && $item->getIsDefault()) { $states[$item->getState()] = __($item->getData('label')); } } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index 68339e7db9390..9bed2bfe2e1a9 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -4,15 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Api\Data\CreditmemoInterface; use Magento\Sales\Model\AbstractModel; use Magento\Sales\Model\EntityInterface; +use Magento\Sales\Model\Order\InvoiceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Order creditmemo model @@ -26,6 +27,7 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInterface @@ -40,6 +42,11 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt const REPORT_DATE_TYPE_REFUND_CREATED = 'refund_created'; + /** + * Allow Zero Grandtotal for Creditmemo path + */ + const XML_PATH_ALLOW_ZERO_GRANDTOTAL = 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal'; + /** * Identifier for order history item * @@ -114,6 +121,16 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt */ protected $priceCurrency; + /** + * @var InvoiceFactory + */ + private $invoiceFactory; + + /** + * @var ScopeConfigInterface; + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -130,6 +147,8 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param InvoiceFactory $invoiceFactory + * @param ScopeConfigInterface $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -147,7 +166,9 @@ public function __construct( PriceCurrencyInterface $priceCurrency, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + InvoiceFactory $invoiceFactory = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_creditmemoConfig = $creditmemoConfig; $this->_orderFactory = $orderFactory; @@ -157,6 +178,8 @@ public function __construct( $this->_commentFactory = $commentFactory; $this->_commentCollectionFactory = $commentCollectionFactory; $this->priceCurrency = $priceCurrency; + $this->invoiceFactory = $invoiceFactory ?: ObjectManager::getInstance()->get(InvoiceFactory::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct( $context, $registry, @@ -255,6 +278,8 @@ public function getShippingAddress() } /** + * Retrieve collection if items. + * * @return mixed */ public function getItemsCollection() @@ -270,6 +295,8 @@ public function getItemsCollection() } /** + * Retrieve all items. + * * @return \Magento\Sales\Model\Order\Creditmemo\Item[] */ public function getAllItems() @@ -284,6 +311,8 @@ public function getAllItems() } /** + * Retrieve item by id. + * * @param mixed $itemId * @return mixed */ @@ -314,6 +343,8 @@ public function getItemByOrderId($orderId) } /** + * Add an item to credit memo. + * * @param \Magento\Sales\Model\Order\Creditmemo\Item $item * @return $this */ @@ -359,6 +390,8 @@ public function roundPrice($price, $type = 'regular', $negative = false) } /** + * Check if credit memo can be refunded. + * * @return bool */ public function canRefund() @@ -379,6 +412,9 @@ public function canRefund() */ public function getInvoice() { + if (!$this->getData('invoice') instanceof \Magento\Sales\Api\Data\InvoiceInterface && $this->getInvoiceId()) { + $this->setInvoice($this->invoiceFactory->create()->load($this->getInvoiceId())); + } return $this->getData('invoice'); } @@ -412,23 +448,6 @@ public function canCancel() public function canVoid() { return false; - $canVoid = false; - if ($this->getState() == self::STATE_REFUNDED) { - $canVoid = $this->getCanVoidFlag(); - /** - * If we not retrieve negative answer from payment yet - */ - if (is_null($canVoid)) { - $canVoid = $this->getOrder()->getPayment()->canVoid(); - if ($canVoid === false) { - $this->setCanVoidFlag(false); - $this->_saveBeforeDestruct = true; - } - } else { - $canVoid = (bool)$canVoid; - } - } - return $canVoid; } /** @@ -438,7 +457,7 @@ public function canVoid() */ public static function getStates() { - if (is_null(static::$_states)) { + if (static::$_states === null) { static::$_states = [ self::STATE_OPEN => __('Pending'), self::STATE_REFUNDED => __('Refunded'), @@ -456,11 +475,11 @@ public static function getStates() */ public function getStateName($stateId = null) { - if (is_null($stateId)) { + if ($stateId === null) { $stateId = $this->getState(); } - if (is_null(static::$_states)) { + if (static::$_states === null) { static::getStates(); } if (isset(static::$_states[$stateId])) { @@ -470,22 +489,19 @@ public function getStateName($stateId = null) } /** + * Set shipping amount. + * * @param float $amount * @return $this */ public function setShippingAmount($amount) { - // base shipping amount calculated in total model - // $amount = $this->getStore()->round($amount); - // $this->setData('base_shipping_amount', $amount); - // - // $amount = $this->getStore()->round( - // $amount*$this->getOrder()->getStoreToOrderRate() - // ); return $this->setData(CreditmemoInterface::SHIPPING_AMOUNT, $amount); } /** + * Set adjustment positive amount. + * * @param string $amount * @return $this */ @@ -506,6 +522,8 @@ public function setAdjustmentPositive($amount) } /** + * Set adjustment negative amount. + * * @param string $amount * @return $this */ @@ -554,6 +572,8 @@ public function isLast() } /** + * Add comment to credit memo. + * * Adds comment to credit memo with additional possibility to send it to customer via email * and show it in customer account * @@ -580,6 +600,8 @@ public function addComment($comment, $notify = false, $visibleOnFront = false) } /** + * Retrieve collection of comments. + * * @param bool $reload * @return \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Comment\Collection * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -588,13 +610,6 @@ public function getCommentsCollection($reload = false) { $collection = $this->_commentCollectionFactory->create()->setCreditmemoFilter($this->getId()) ->setCreatedAtOrder(); -// -// $this->setComments($comments); -// /** -// * When credit memo created with adding comment, -// * comments collection must be loaded before we added this comment. -// */ -// $this->getComments()->load(); if ($this->getId()) { foreach ($collection as $comment) { @@ -626,11 +641,28 @@ public function getIncrementId() } /** + * Check if grand total is valid. + * * @return bool */ public function isValidGrandTotal() { - return !($this->getGrandTotal() <= 0 && !$this->getAllowZeroGrandTotal()); + return !($this->getGrandTotal() <= 0 && !$this->isAllowZeroGrandTotal()); + } + + /** + * Return Zero GrandTotal availability. + * + * @return bool + */ + private function isAllowZeroGrandTotal(): bool + { + $isAllowed = $this->scopeConfig->getValue( + self::XML_PATH_ALLOW_ZERO_GRANDTOTAL, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + return $isAllowed; } /** @@ -678,7 +710,7 @@ public function getDiscountDescription() } /** - * {@inheritdoc} + * @inheritdoc */ public function setItems($items) { @@ -918,7 +950,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -1177,7 +1209,7 @@ public function getUpdatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setComments($comments) { @@ -1185,7 +1217,7 @@ public function setComments($comments) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -1193,7 +1225,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxAmount($amount) { @@ -1201,7 +1233,7 @@ public function setBaseShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToOrderRate($rate) { @@ -1209,7 +1241,7 @@ public function setStoreToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -1217,7 +1249,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToOrderRate($rate) { @@ -1225,7 +1257,7 @@ public function setBaseToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGrandTotal($amount) { @@ -1233,7 +1265,7 @@ public function setGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInclTax($amount) { @@ -1241,7 +1273,7 @@ public function setBaseSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInclTax($amount) { @@ -1249,7 +1281,7 @@ public function setSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -1257,7 +1289,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToBaseRate($rate) { @@ -1265,7 +1297,7 @@ public function setStoreToBaseRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToGlobalRate($rate) { @@ -1273,7 +1305,7 @@ public function setBaseToGlobalRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustment($baseAdjustment) { @@ -1281,7 +1313,7 @@ public function setBaseAdjustment($baseAdjustment) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotal($amount) { @@ -1289,7 +1321,7 @@ public function setBaseSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -1297,7 +1329,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotal($amount) { @@ -1305,7 +1337,7 @@ public function setSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustment($adjustment) { @@ -1313,7 +1345,7 @@ public function setAdjustment($adjustment) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseGrandTotal($amount) { @@ -1321,7 +1353,7 @@ public function setBaseGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -1329,7 +1361,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxAmount($amount) { @@ -1337,7 +1369,7 @@ public function setShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -1345,7 +1377,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -1353,7 +1385,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmailSent($emailSent) { @@ -1361,7 +1393,7 @@ public function setEmailSent($emailSent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreditmemoStatus($creditmemoStatus) { @@ -1369,7 +1401,7 @@ public function setCreditmemoStatus($creditmemoStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setState($state) { @@ -1377,7 +1409,7 @@ public function setState($state) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAddressId($id) { @@ -1385,7 +1417,7 @@ public function setShippingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBillingAddressId($id) { @@ -1393,7 +1425,7 @@ public function setBillingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setInvoiceId($id) { @@ -1401,7 +1433,7 @@ public function setInvoiceId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCurrencyCode($code) { @@ -1409,7 +1441,7 @@ public function setStoreCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderCurrencyCode($code) { @@ -1417,7 +1449,7 @@ public function setOrderCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCurrencyCode($code) { @@ -1425,7 +1457,7 @@ public function setBaseCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGlobalCurrencyCode($code) { @@ -1433,7 +1465,7 @@ public function setGlobalCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIncrementId($id) { @@ -1441,7 +1473,7 @@ public function setIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -1449,7 +1481,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -1457,7 +1489,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -1465,7 +1497,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountTaxCompensationAmount($amount) { @@ -1473,7 +1505,7 @@ public function setShippingDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) { @@ -1481,7 +1513,7 @@ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInclTax($amount) { @@ -1489,7 +1521,7 @@ public function setShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInclTax($amount) { @@ -1497,7 +1529,7 @@ public function setBaseShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountDescription($description) { @@ -1505,9 +1537,7 @@ public function setDiscountDescription($description) } /** - * {@inheritdoc} - * - * @return \Magento\Sales\Api\Data\CreditmemoExtensionInterface|null + * @inheritdoc */ public function getExtensionAttributes() { @@ -1515,15 +1545,11 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} - * - * @param \Magento\Sales\Api\Data\CreditmemoExtensionInterface $extensionAttributes - * @return $this + * @inheritdoc */ public function setExtensionAttributes(\Magento\Sales\Api\Data\CreditmemoExtensionInterface $extensionAttributes) { return $this->_setExtensionAttributes($extensionAttributes); } - //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/CommentRepository.php b/app/code/Magento/Sales/Model/Order/Creditmemo/CommentRepository.php index 14d4ccae22446..a3dde4e5172e7 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/CommentRepository.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/CommentRepository.php @@ -7,6 +7,7 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Sales\Api\CreditmemoCommentRepositoryInterface; @@ -14,7 +15,15 @@ use Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory; use Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterfaceFactory; use Magento\Sales\Model\Spi\CreditmemoCommentResourceInterface; +use Magento\Sales\Model\Order\Email\Sender\CreditmemoCommentSender; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Psr\Log\LoggerInterface; +/** + * Class CommentRepository + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CommentRepository implements CreditmemoCommentRepositoryInterface { /** @@ -37,22 +46,48 @@ class CommentRepository implements CreditmemoCommentRepositoryInterface */ private $collectionProcessor; + /** + * @var CreditmemoCommentSender + */ + private $creditmemoCommentSender; + + /** + * @var CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param CreditmemoCommentResourceInterface $commentResource * @param CreditmemoCommentInterfaceFactory $commentFactory * @param CreditmemoCommentSearchResultInterfaceFactory $searchResultFactory * @param CollectionProcessorInterface $collectionProcessor + * @param CreditmemoCommentSender|null $creditmemoCommentSender + * @param CreditmemoRepositoryInterface|null $creditmemoRepository + * @param LoggerInterface|null $logger */ public function __construct( CreditmemoCommentResourceInterface $commentResource, CreditmemoCommentInterfaceFactory $commentFactory, CreditmemoCommentSearchResultInterfaceFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + CreditmemoCommentSender $creditmemoCommentSender = null, + CreditmemoRepositoryInterface $creditmemoRepository = null, + LoggerInterface $logger = null ) { $this->commentResource = $commentResource; $this->commentFactory = $commentFactory; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor; + $this->creditmemoCommentSender = $creditmemoCommentSender + ?: ObjectManager::getInstance()->get(CreditmemoCommentSender::class); + $this->creditmemoRepository = $creditmemoRepository + ?: ObjectManager::getInstance()->get(CreditmemoRepositoryInterface::class); + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -97,7 +132,14 @@ public function save(CreditmemoCommentInterface $entity) try { $this->commentResource->save($entity); } catch (\Exception $e) { - throw new CouldNotSaveException(__('Could not save the comment.'), $e); + throw new CouldNotSaveException(__('Could not save the creditmemo comment.'), $e); + } + + try { + $creditmemo = $this->creditmemoRepository->get($entity->getParentId()); + $this->creditmemoCommentSender->send($creditmemo, $entity->getIsCustomerNotified(), $entity->getComment()); + } catch (\Exception $exception) { + $this->logger->warning('Something went wrong while sending email.'); } return $entity; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index 435b3aee4d6d7..93c8ed00f9daa 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -7,9 +7,12 @@ use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\Order\Creditmemo\SenderInterface; +use Magento\Framework\DataObject; /** * Email notification sender for Creditmemo. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailSender extends Sender implements SenderInterface { @@ -86,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -93,9 +97,11 @@ public function send( \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'creditmemo' => $creditmemo, @@ -106,13 +112,17 @@ public function send( 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_creditmemo_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $creditmemo->setEmailSent(true); @@ -138,6 +148,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php index bb173f4010311..1fb60ca6583f9 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php @@ -25,7 +25,7 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) * Calculate how much shipping discount should be applied * basing on how much shipping should be refunded. */ - $baseShippingAmount = (float)$creditmemo->getBaseShippingAmount(); + $baseShippingAmount = $this->getBaseShippingAmount($creditmemo); if ($baseShippingAmount) { $baseShippingDiscount = $baseShippingAmount * $order->getBaseShippingDiscountAmount() / @@ -75,4 +75,23 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $creditmemo->setBaseGrandTotal($creditmemo->getBaseGrandTotal() - $baseTotalDiscountAmount); return $this; } + + /** + * Get base shipping amount. + * + * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo + * @return float + */ + private function getBaseShippingAmount(\Magento\Sales\Model\Order\Creditmemo $creditmemo): float + { + $baseShippingAmount = (float)$creditmemo->getBaseShippingAmount(); + + if (!$baseShippingAmount) { + $baseShippingInclTax = (float)$creditmemo->getBaseShippingInclTax(); + $baseShippingTaxAmount = (float)$creditmemo->getBaseShippingTaxAmount(); + $baseShippingAmount = $baseShippingInclTax - $baseShippingTaxAmount; + } + + return $baseShippingAmount; + } } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php index f00334f496b2a..f644d0c3a5a63 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php @@ -37,6 +37,8 @@ public function __construct( } /** + * Collects credit memo shipping totals. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @throws \Magento\Framework\Exception\LocalizedException @@ -55,12 +57,10 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $orderShippingInclTax = $order->getShippingInclTax(); $orderBaseShippingInclTax = $order->getBaseShippingInclTax(); $allowedTaxAmount = $order->getShippingTaxAmount() - $order->getShippingTaxRefunded(); - $baseAllowedTaxAmount = $order->getBaseShippingTaxAmount() - $order->getBaseShippingTaxRefunded(); $allowedAmountInclTax = $allowedAmount + $allowedTaxAmount; - $baseAllowedAmountInclTax = $baseAllowedAmount + $baseAllowedTaxAmount; - - // for the credit memo - $shippingAmount = $baseShippingAmount = $shippingInclTax = $baseShippingInclTax = 0; + $baseAllowedAmountInclTax = $orderBaseShippingInclTax + - $order->getBaseShippingRefunded() + - $order->getBaseShippingTaxRefunded(); // Check if the desired shipping amount to refund was specified (from invoice or another source). if ($creditmemo->hasBaseShippingAmount()) { @@ -128,7 +128,6 @@ private function isSuppliedShippingAmountInclTax($order) /** * Get the Tax Config. - * In a future release, will become a constructor parameter. * * @return \Magento\Tax\Model\Config * diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index a842c0470ad85..d4c2e7b2d6854 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -5,9 +5,14 @@ */ namespace Magento\Sales\Model\Order\Creditmemo\Total; +/** + * Collects credit memo taxes. + */ class Tax extends AbstractTotal { /** + * Collects credit memo taxes. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @@ -70,27 +75,20 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } $isPartialShippingRefunded = false; + $baseOrderShippingAmount = (float)$order->getBaseShippingAmount(); if ($invoice = $creditmemo->getInvoice()) { //recalculate tax amounts in case if refund shipping value was changed - if ($order->getBaseShippingAmount() && $creditmemo->getBaseShippingAmount()) { - $taxFactor = $creditmemo->getBaseShippingAmount() / $order->getBaseShippingAmount(); + if ($baseOrderShippingAmount && $creditmemo->getBaseShippingAmount() !== null) { + $taxFactor = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $invoice->getShippingTaxAmount() * $taxFactor; $baseShippingTaxAmount = $invoice->getBaseShippingTaxAmount() * $taxFactor; $totalDiscountTaxCompensation += $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; $baseTotalDiscountTaxCompensation += $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; - $shippingDiscountTaxCompensationAmount = - $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; - $baseShippingDiscountTaxCompensationAmount = - $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; $shippingTaxAmount = $creditmemo->roundPrice($shippingTaxAmount); $baseShippingTaxAmount = $creditmemo->roundPrice($baseShippingTaxAmount, 'base'); $totalDiscountTaxCompensation = $creditmemo->roundPrice($totalDiscountTaxCompensation); $baseTotalDiscountTaxCompensation = $creditmemo->roundPrice($baseTotalDiscountTaxCompensation, 'base'); - $shippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($shippingDiscountTaxCompensationAmount); - $baseShippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($baseShippingDiscountTaxCompensationAmount, 'base'); if ($taxFactor < 1 && $invoice->getShippingTaxAmount() > 0) { $isPartialShippingRefunded = true; } @@ -99,7 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } } else { $orderShippingAmount = $order->getShippingAmount(); - $baseOrderShippingAmount = $order->getBaseShippingAmount(); $baseOrderShippingRefundedAmount = $order->getBaseShippingRefunded(); diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 5f1bba4c8f2c2..da41f99a65c83 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -5,13 +5,16 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice; +use Magento\Sales\Api\Data\OrderItemInterface; + /** * Factory class for @see \Magento\Sales\Model\Order\Creditmemo */ class CreditmemoFactory { /** - * Quote convert object + * Order convert object * * @var \Magento\Sales\Model\Convert\Order */ @@ -63,31 +66,15 @@ public function createByOrder(\Magento\Sales\Model\Order $order, array $data = [ { $totalQty = 0; $creditmemo = $this->convertor->toCreditmemo($order); - $qtys = isset($data['qtys']) ? $data['qtys'] : []; + $qtyList = isset($data['qtys']) ? $data['qtys'] : []; foreach ($order->getAllItems() as $orderItem) { - if (!$this->canRefundItem($orderItem, $qtys)) { + if (!$this->canRefundItem($orderItem, $qtyList)) { continue; } $item = $this->convertor->itemToCreditmemoItem($orderItem); - if ($orderItem->isDummy()) { - if (isset($data['qtys'][$orderItem->getParentItemId()])) { - $parentQty = $data['qtys'][$orderItem->getParentItemId()]; - } else { - $parentQty = $orderItem->getParentItem() ? $orderItem->getParentItem()->getQtyToRefund() : 1; - } - $qty = $this->calculateProductOptions($orderItem, $parentQty); - $orderItem->setLockedDoShip(true); - } else { - if (isset($qtys[$orderItem->getId()])) { - $qty = (double)$qtys[$orderItem->getId()]; - } elseif (!count($qtys)) { - $qty = $orderItem->getQtyToRefund(); - } else { - continue; - } - } + $qty = $this->getQtyToRefund($orderItem, $qtyList); $totalQty += $qty; $item->setQty($qty); $creditmemo->addItem($item); @@ -106,72 +93,31 @@ public function createByOrder(\Magento\Sales\Model\Order $order, array $data = [ * @param \Magento\Sales\Model\Order\Invoice $invoice * @param array $data * @return Creditmemo - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function createByInvoice(\Magento\Sales\Model\Order\Invoice $invoice, array $data = []) { $order = $invoice->getOrder(); $totalQty = 0; - $qtys = isset($data['qtys']) ? $data['qtys'] : []; + $qtyList = isset($data['qtys']) ? $data['qtys'] : []; $creditmemo = $this->convertor->toCreditmemo($order); $creditmemo->setInvoice($invoice); - $invoiceQtysRefunded = []; - foreach ($invoice->getOrder()->getCreditmemosCollection() as $createdCreditmemo) { - if ($createdCreditmemo->getState() != Creditmemo::STATE_CANCELED && - $createdCreditmemo->getInvoiceId() == $invoice->getId() - ) { - foreach ($createdCreditmemo->getAllItems() as $createdCreditmemoItem) { - $orderItemId = $createdCreditmemoItem->getOrderItem()->getId(); - if (isset($invoiceQtysRefunded[$orderItemId])) { - $invoiceQtysRefunded[$orderItemId] += $createdCreditmemoItem->getQty(); - } else { - $invoiceQtysRefunded[$orderItemId] = $createdCreditmemoItem->getQty(); - } - } - } - } - - $invoiceQtysRefundLimits = []; - foreach ($invoice->getAllItems() as $invoiceItem) { - $invoiceQtyCanBeRefunded = $invoiceItem->getQty(); - $orderItemId = $invoiceItem->getOrderItem()->getId(); - if (isset($invoiceQtysRefunded[$orderItemId])) { - $invoiceQtyCanBeRefunded = $invoiceQtyCanBeRefunded - $invoiceQtysRefunded[$orderItemId]; - } - $invoiceQtysRefundLimits[$orderItemId] = $invoiceQtyCanBeRefunded; - } - + $invoiceRefundLimitsQtyList = $this->getInvoiceRefundLimitsQtyList($invoice); foreach ($invoice->getAllItems() as $invoiceItem) { + /** @var OrderItemInterface $orderItem */ $orderItem = $invoiceItem->getOrderItem(); - if (!$this->canRefundItem($orderItem, $qtys, $invoiceQtysRefundLimits)) { + if (!$this->canRefundItem($orderItem, $qtyList, $invoiceRefundLimitsQtyList)) { continue; } - $item = $this->convertor->itemToCreditmemoItem($orderItem); - if ($orderItem->isDummy()) { - if (isset($data['qtys'][$orderItem->getParentItemId()])) { - $parentQty = $data['qtys'][$orderItem->getParentItemId()]; - } else { - $parentQty = $orderItem->getParentItem() ? $orderItem->getParentItem()->getQtyToRefund() : 1; - } - $qty = $this->calculateProductOptions($orderItem, $parentQty); - } else { - if (isset($qtys[$orderItem->getId()])) { - $qty = (double)$qtys[$orderItem->getId()]; - } elseif (!count($qtys)) { - $qty = $orderItem->getQtyToRefund(); - } else { - continue; - } - if (isset($invoiceQtysRefundLimits[$orderItem->getId()])) { - $qty = min($qty, $invoiceQtysRefundLimits[$orderItem->getId()]); - } - } - $qty = min($qty, $invoiceItem->getQty()); + $qty = min( + $this->getQtyToRefund($orderItem, $qtyList, $invoiceRefundLimitsQtyList), + $invoiceItem->getQty() + ); $totalQty += $qty; + + $item = $this->convertor->itemToCreditmemoItem($orderItem); $item->setQty($qty); $creditmemo->addItem($item); } @@ -179,15 +125,7 @@ public function createByInvoice(\Magento\Sales\Model\Order\Invoice $invoice, arr $this->initData($creditmemo, $data); if (!isset($data['shipping_amount'])) { - $isShippingInclTax = $this->taxConfig->displaySalesShippingInclTax($order->getStoreId()); - if ($isShippingInclTax) { - $baseAllowedAmount = $order->getBaseShippingInclTax() - - $order->getBaseShippingRefunded() - - $order->getBaseShippingTaxRefunded(); - } else { - $baseAllowedAmount = $order->getBaseShippingAmount() - $order->getBaseShippingRefunded(); - $baseAllowedAmount = min($baseAllowedAmount, $invoice->getBaseShippingAmount()); - } + $baseAllowedAmount = $this->getShippingAmount($invoice); $creditmemo->setBaseShippingAmount($baseAllowedAmount); } @@ -262,6 +200,7 @@ protected function initData($creditmemo, $data) { if (isset($data['shipping_amount'])) { $creditmemo->setBaseShippingAmount((double)$data['shipping_amount']); + $creditmemo->setBaseShippingInclTax((double)$data['shipping_amount']); } if (isset($data['adjustment_positive'])) { $creditmemo->setAdjustmentPositive($data['adjustment_positive']); @@ -272,11 +211,13 @@ protected function initData($creditmemo, $data) } /** - * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * Calculate product options. + * + * @param Item $orderItem * @param int $parentQty * @return int */ - private function calculateProductOptions(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, $parentQty) + private function calculateProductOptions(Item $orderItem, $parentQty) { $qty = $parentQty; $productOptions = $orderItem->getProductOptions(); @@ -290,4 +231,113 @@ private function calculateProductOptions(\Magento\Sales\Api\Data\OrderItemInterf } return $qty; } + + /** + * Gets list of quantities based on invoice refunded items. + * + * @param Invoice $invoice + * @return array + */ + private function getInvoiceRefundedQtyList(Invoice $invoice): array + { + $invoiceRefundedQtyList = []; + foreach ($invoice->getOrder()->getCreditmemosCollection() as $creditmemo) { + if ($creditmemo->getState() !== Creditmemo::STATE_CANCELED && + $creditmemo->getInvoiceId() === $invoice->getId() + ) { + foreach ($creditmemo->getAllItems() as $creditmemoItem) { + $orderItemId = $creditmemoItem->getOrderItem()->getId(); + if (isset($invoiceRefundedQtyList[$orderItemId])) { + $invoiceRefundedQtyList[$orderItemId] += $creditmemoItem->getQty(); + } else { + $invoiceRefundedQtyList[$orderItemId] = $creditmemoItem->getQty(); + } + } + } + } + + return $invoiceRefundedQtyList; + } + + /** + * Gets limits of refund based on invoice items. + * + * @param Invoice $invoice + * @return array + */ + private function getInvoiceRefundLimitsQtyList(Invoice $invoice): array + { + $invoiceRefundLimitsQtyList = []; + $invoiceRefundedQtyList = $this->getInvoiceRefundedQtyList($invoice); + + foreach ($invoice->getAllItems() as $invoiceItem) { + $qtyCanBeRefunded = $invoiceItem->getQty(); + $orderItemId = $invoiceItem->getOrderItem()->getId(); + if (isset($invoiceRefundedQtyList[$orderItemId])) { + $qtyCanBeRefunded = $qtyCanBeRefunded - $invoiceRefundedQtyList[$orderItemId]; + } + $invoiceRefundLimitsQtyList[$orderItemId] = $qtyCanBeRefunded; + } + + return $invoiceRefundLimitsQtyList; + } + + /** + * Gets quantity of items to refund based on order item. + * + * @param Item $orderItem + * @param array $qtyList + * @param array $refundLimits + * @return float + */ + private function getQtyToRefund(Item $orderItem, array $qtyList, array $refundLimits = []): float + { + $qty = 0; + if ($orderItem->isDummy()) { + if (isset($qtyList[$orderItem->getParentItemId()])) { + $parentQty = $qtyList[$orderItem->getParentItemId()]; + } elseif ($orderItem->getProductType() === BundlePrice::PRODUCT_TYPE) { + $parentQty = $orderItem->getQtyInvoiced(); + } else { + $parentQty = $orderItem->getParentItem() ? $orderItem->getParentItem()->getQtyToRefund() : 1; + } + $qty = $this->calculateProductOptions($orderItem, $parentQty); + } else { + if (isset($qtyList[$orderItem->getId()])) { + $qty = $qtyList[$orderItem->getId()]; + } elseif (!count($qtyList)) { + $qty = $orderItem->getQtyToRefund(); + } else { + return (float)$qty; + } + + if (isset($refundLimits[$orderItem->getId()])) { + $qty = min($qty, $refundLimits[$orderItem->getId()]); + } + } + + return (float)$qty; + } + + /** + * Gets shipping amount based on invoice. + * + * @param Invoice $invoice + * @return float + */ + private function getShippingAmount(Invoice $invoice): float + { + $order = $invoice->getOrder(); + $isShippingInclTax = $this->taxConfig->displaySalesShippingInclTax($order->getStoreId()); + if ($isShippingInclTax) { + $amount = $order->getBaseShippingInclTax() - + $order->getBaseShippingRefunded() - + $order->getBaseShippingTaxRefunded(); + } else { + $amount = $order->getBaseShippingAmount() - $order->getBaseShippingRefunded(); + $amount = min($amount, $invoice->getBaseShippingAmount()); + } + + return (float)$amount; + } } diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php index b866b0de0abfc..6804ab59649e1 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php @@ -7,13 +7,13 @@ namespace Magento\Sales\Model\Order; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Sales\Model\ResourceModel\Order\Creditmemo as Resource; -use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Sales\Api\Data\CreditmemoSearchResultInterfaceFactory as SearchResultFactory; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Api\Data\CreditmemoSearchResultInterfaceFactory as SearchResultFactory; +use Magento\Sales\Model\ResourceModel\Metadata; /** * Repository class for @see \Magento\Sales\Api\Data\CreditmemoInterface @@ -138,6 +138,8 @@ public function save(\Magento\Sales\Api\Data\CreditmemoInterface $entity) try { $this->metadata->getMapper()->save($entity); $this->registry[$entity->getEntityId()] = $entity; + } catch (LocalizedException $e) { + throw new CouldNotSaveException(__($e->getMessage()), $e); } catch (\Exception $e) { throw new CouldNotSaveException(__('Could not save credit memo'), $e); } diff --git a/app/code/Magento/Sales/Model/Order/CustomerAssignment.php b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php new file mode 100644 index 0000000000000..8bcfc1dc49de4 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CustomerAssignment.php @@ -0,0 +1,59 @@ +eventManager = $eventManager; + $this->orderRepository = $orderRepository; + } + + /** + * @param OrderInterface $order + * @param CustomerInterface $customer + */ + public function execute(OrderInterface $order, CustomerInterface $customer)/*: void*/ + { + $order->setCustomerId($customer->getId()); + $order->setCustomerIsGuest(false); + $this->orderRepository->save($order); + + $this->eventManager->dispatch( + 'sales_order_customer_assign_after', + [ + 'order' => $order, + 'customer' => $customer + ] + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/CustomerManagement.php b/app/code/Magento/Sales/Model/Order/CustomerManagement.php index a84d90693e087..ae3f940dbb2ba 100644 --- a/app/code/Magento/Sales/Model/Order/CustomerManagement.php +++ b/app/code/Magento/Sales/Model/Order/CustomerManagement.php @@ -3,13 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Model\Order; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Sales\Api\Data\OrderAddressInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\AddressFactory as QuoteAddressFactory; +use Magento\Sales\Api\Data\OrderInterface; /** * Class CustomerManagement + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementInterface { @@ -19,17 +27,17 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $accountManagement; /** - * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory + * @deprecated */ protected $customerFactory; /** - * @var \Magento\Customer\Api\Data\AddressInterfaceFactory + * @deprecated */ protected $addressFactory; /** - * @var \Magento\Customer\Api\Data\RegionInterfaceFactory + * @deprecated */ protected $regionFactory; @@ -39,10 +47,20 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $orderRepository; /** - * @var \Magento\Framework\DataObject\Copy + * @deprecated */ protected $objectCopyService; + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var OrderCustomerExtractor + */ + private $customerExtractor; + /** * @param \Magento\Framework\DataObject\Copy $objectCopyService * @param \Magento\Customer\Api\AccountManagementInterface $accountManagement @@ -50,6 +68,8 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn * @param \Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory * @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionFactory * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository + * @param QuoteAddressFactory|null $quoteAddressFactory + * @param OrderCustomerExtractor|null $orderCustomerExtractor */ public function __construct( \Magento\Framework\DataObject\Copy $objectCopyService, @@ -57,7 +77,9 @@ public function __construct( \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory, \Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory, \Magento\Customer\Api\Data\RegionInterfaceFactory $regionFactory, - \Magento\Sales\Api\OrderRepositoryInterface $orderRepository + \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, + QuoteAddressFactory $quoteAddressFactory = null, + OrderCustomerExtractor $orderCustomerExtractor = null ) { $this->objectCopyService = $objectCopyService; $this->accountManagement = $accountManagement; @@ -65,6 +87,10 @@ public function __construct( $this->customerFactory = $customerFactory; $this->addressFactory = $addressFactory; $this->regionFactory = $regionFactory; + $this->quoteAddressFactory = $quoteAddressFactory + ?: ObjectManager::getInstance()->get(QuoteAddressFactory::class); + $this->customerExtractor = $orderCustomerExtractor + ?? ObjectManager::getInstance()->get(OrderCustomerExtractor::class); } /** @@ -74,49 +100,61 @@ public function create($orderId) { $order = $this->orderRepository->get($orderId); if ($order->getCustomerId()) { - throw new AlreadyExistsException(__("This order already has associated customer account")); - } - $customerData = $this->objectCopyService->copyFieldsetToTarget( - 'order_address', - 'to_customer', - $order->getBillingAddress(), - [] - ); - $addresses = $order->getAddresses(); - foreach ($addresses as $address) { - $addressData = $this->objectCopyService->copyFieldsetToTarget( - 'order_address', - 'to_customer_address', - $address, - [] + throw new AlreadyExistsException( + __('This order already has associated customer account') ); - /** @var \Magento\Customer\Api\Data\AddressInterface $customerAddress */ - $customerAddress = $this->addressFactory->create(['data' => $addressData]); - switch ($address->getAddressType()) { - case QuoteAddress::ADDRESS_TYPE_BILLING: - $customerAddress->setIsDefaultBilling(true); - break; - case QuoteAddress::ADDRESS_TYPE_SHIPPING: - $customerAddress->setIsDefaultShipping(true); - break; - } + } - if (is_string($address->getRegion())) { - /** @var \Magento\Customer\Api\Data\RegionInterface $region */ - $region = $this->regionFactory->create(); - $region->setRegion($address->getRegion()); - $region->setRegionCode($address->getRegionCode()); - $region->setRegionId($address->getRegionId()); - $customerAddress->setRegion($region); + $customer = $this->customerExtractor->extract($orderId); + /** @var AddressInterface[] $filteredAddresses */ + $filteredAddresses = []; + foreach ($customer->getAddresses() as $address) { + if ($this->needToSaveAddress($order, $address)) { + $filteredAddresses[] = $address; } - $customerData['addresses'][] = $customerAddress; } + $customer->setAddresses($filteredAddresses); - /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ - $customer = $this->customerFactory->create(['data' => $customerData]); $account = $this->accountManagement->createAccount($customer); + $order = $this->orderRepository->get($orderId); $order->setCustomerId($account->getId()); + $order->setCustomerIsGuest(0); $this->orderRepository->save($order); + return $account; } + + /** + * @param OrderInterface $order + * @param AddressInterface $address + * + * @return bool + */ + private function needToSaveAddress( + OrderInterface $order, + AddressInterface $address + ): bool { + /** @var OrderAddressInterface|null $orderAddress */ + $orderAddress = null; + if ($address->isDefaultBilling()) { + $orderAddress = $order->getBillingAddress(); + } elseif ($address->isDefaultShipping()) { + $orderAddress = $order->getShippingAddress(); + } + if ($orderAddress) { + $quoteAddressId = $orderAddress->getData('quote_address_id'); + if ($quoteAddressId) { + /** @var QuoteAddress $quote */ + $quote = $this->quoteAddressFactory->create() + ->load($quoteAddressId); + if ($quote && $quote->getId()) { + return (bool)(int)$quote->getData('save_in_address_book'); + } + } + + return true; + } + + return false; + } } diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/IdentityInterface.php b/app/code/Magento/Sales/Model/Order/Email/Container/IdentityInterface.php index 55ce24e23e1ec..1d481a8acee65 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/IdentityInterface.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/IdentityInterface.php @@ -3,7 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Sales\Model\Order\Email\Container; + +namespace Magento\Sales\Model\Order\Email\Container; use Magento\Store\Model\Store; diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/NullIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/NullIdentity.php new file mode 100644 index 0000000000000..22348aa9ee2f6 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Email/Container/NullIdentity.php @@ -0,0 +1,59 @@ +send(); } else { - // Email copies are sent as separated emails if their copy method is 'copy' or a customer should not be notified + // Email copies are sent as separated emails if their copy method is 'copy' + // or a customer should not be notified $sender->sendCopyTo(); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender.php b/app/code/Magento/Sales/Model/Order/Email/Sender.php index 8ada6a3f321d2..564fd1e2a4b98 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender.php @@ -65,6 +65,8 @@ public function __construct( } /** + * Send order email if it is enabled in configuration. + * * @param Order $order * @return bool */ @@ -81,15 +83,21 @@ protected function checkAndSend(Order $order) try { $sender->send(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + try { $sender->sendCopyTo(); } catch (\Exception $e) { $this->logger->error($e->getMessage()); } - return true; } /** + * Populate order email template with customer information. + * * @param Order $order * @return void */ @@ -111,6 +119,8 @@ protected function prepareTemplate(Order $order) } /** + * Create Sender object using appropriate template and identity. + * * @return Sender */ protected function getSender() @@ -124,6 +134,8 @@ protected function getSender() } /** + * Get template options. + * * @return array */ protected function getTemplateOptions() @@ -135,6 +147,8 @@ protected function getTemplateOptions() } /** + * Render shipping address into html. + * * @param Order $order * @return string|null */ @@ -146,6 +160,8 @@ protected function getFormattedShippingAddress($order) } /** + * Render billing address into html. + * * @param Order $order * @return string|null */ diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php index 510bc54dc05b3..930791532539f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoCommentSender.php @@ -12,6 +12,7 @@ use Magento\Sales\Model\Order\Email\NotifySender; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class CreditmemoCommentSender @@ -62,6 +63,8 @@ public function __construct( public function send(Creditmemo $creditmemo, $notify = true, $comment = '') { $order = $creditmemo->getOrder(); + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'creditmemo' => $creditmemo, @@ -70,14 +73,22 @@ public function send(Creditmemo $creditmemo, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_creditmemo_comment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); return $this->checkAndSend($order, $notify); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index a4ecd2aa7d000..414a9c5d9b20d 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -14,6 +14,7 @@ use Magento\Sales\Model\ResourceModel\Order\Creditmemo as CreditmemoResource; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class CreditmemoSender @@ -95,14 +96,16 @@ public function __construct( * @param Creditmemo $creditmemo * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Creditmemo $creditmemo, $forceSyncMode = false) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $creditmemo->getOrder(); - + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'creditmemo' => $creditmemo, @@ -112,14 +115,24 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_creditmemo_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $creditmemo->setEmailSent(true); @@ -141,6 +154,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php index 8f6401ff1cb88..9441f0e842925 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceCommentSender.php @@ -12,6 +12,7 @@ use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class InvoiceCommentSender @@ -62,6 +63,8 @@ public function __construct( public function send(Invoice $invoice, $notify = true, $comment = '') { $order = $invoice->getOrder(); + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'invoice' => $invoice, @@ -70,14 +73,22 @@ public function send(Invoice $invoice, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_invoice_comment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); return $this->checkAndSend($order, $notify); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index c3083ddae2dd8..5b41f2d4f9ab4 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -14,6 +14,7 @@ use Magento\Sales\Model\ResourceModel\Order\Invoice as InvoiceResource; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class InvoiceSender @@ -95,13 +96,15 @@ public function __construct( * @param Invoice $invoice * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Invoice $invoice, $forceSyncMode = false) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $invoice->getOrder(); + $this->identityContainer->setStore($order->getStore()); $transport = [ 'order' => $order, @@ -111,15 +114,25 @@ public function send(Invoice $invoice, $forceSyncMode = false) 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_invoice_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $invoice->setEmailSent(true); @@ -141,6 +154,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php index c8c1eb10d4864..4d37fc1b7769a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderCommentSender.php @@ -11,6 +11,7 @@ use Magento\Sales\Model\Order\Email\NotifySender; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class OrderCommentSender @@ -60,6 +61,8 @@ public function __construct( */ public function send(Order $order, $notify = true, $comment = '') { + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'comment' => $comment, @@ -67,14 +70,22 @@ public function send(Order $order, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_order_comment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); return $this->checkAndSend($order, $notify); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index e98f400973c6d..ec86e0ea81f7d 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -13,6 +13,7 @@ use Magento\Sales\Model\ResourceModel\Order as OrderResource; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class OrderSender @@ -96,7 +97,7 @@ public function __construct( */ public function send(Order $order, $forceSyncMode = false) { - $order->setSendEmail(true); + $order->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { if ($this->checkAndSend($order)) { @@ -129,15 +130,25 @@ protected function prepareTemplate(Order $order) 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'created_at_formatted' => $order->getCreatedAtFormatted(2), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; - $transport = new \Magento\Framework\DataObject($transport); + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_order_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject, 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport->getData()); + $this->templateContainer->setTemplateVars($transportObject->getData()); parent::prepareTemplate($order); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php index 80c2ed356061b..ad305c8b7199f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentCommentSender.php @@ -12,6 +12,7 @@ use Magento\Sales\Model\Order\Shipment; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class ShipmentCommentSender @@ -62,6 +63,8 @@ public function __construct( public function send(Shipment $shipment, $notify = true, $comment = '') { $order = $shipment->getOrder(); + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'shipment' => $shipment, @@ -70,14 +73,22 @@ public function send(Shipment $shipment, $notify = true, $comment = '') 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_shipment_comment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); return $this->checkAndSend($order, $notify); } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index ff2311067ba0a..bbe1264e7a917 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -14,6 +14,7 @@ use Magento\Sales\Model\ResourceModel\Order\Shipment as ShipmentResource; use Magento\Sales\Model\Order\Address\Renderer; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\DataObject; /** * Class ShipmentSender @@ -95,14 +96,16 @@ public function __construct( * @param Shipment $shipment * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Shipment $shipment, $forceSyncMode = false) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $shipment->getOrder(); - + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'shipment' => $shipment, @@ -111,15 +114,25 @@ public function send(Shipment $shipment, $forceSyncMode = false) 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_shipment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $shipment->setEmailSent(true); @@ -141,6 +154,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index 93c6f19b08690..5fb89b7855056 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Model\Order\Email; use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Mail\Template\TransportBuilderByStore; use Magento\Sales\Model\Order\Email\Container\IdentityInterface; use Magento\Sales\Model\Order\Email\Container\Template; @@ -27,14 +28,18 @@ class SenderBuilder protected $transportBuilder; /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * * @param Template $templateContainer * @param IdentityInterface $identityContainer * @param TransportBuilder $transportBuilder + * @param TransportBuilderByStore $transportBuilderByStore */ public function __construct( Template $templateContainer, IdentityInterface $identityContainer, - TransportBuilder $transportBuilder + TransportBuilder $transportBuilder, + TransportBuilderByStore $transportBuilderByStore = null ) { $this->templateContainer = $templateContainer; $this->identityContainer = $identityContainer; @@ -98,6 +103,9 @@ protected function configureEmailTemplate() $this->transportBuilder->setTemplateIdentifier($this->templateContainer->getTemplateId()); $this->transportBuilder->setTemplateOptions($this->templateContainer->getTemplateOptions()); $this->transportBuilder->setTemplateVars($this->templateContainer->getTemplateVars()); - $this->transportBuilder->setFrom($this->identityContainer->getEmailIdentity()); + $this->transportBuilder->setFromByScope( + $this->identityContainer->getEmailIdentity(), + $this->identityContainer->getStore()->getId() + ); } } diff --git a/app/code/Magento/Sales/Model/Order/Invoice/CommentRepository.php b/app/code/Magento/Sales/Model/Order/Invoice/CommentRepository.php index 858490132e0c7..00b1bb62556f7 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/CommentRepository.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/CommentRepository.php @@ -7,6 +7,7 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Sales\Api\Data\InvoiceCommentInterface; @@ -14,7 +15,15 @@ use Magento\Sales\Api\Data\InvoiceCommentSearchResultInterfaceFactory; use Magento\Sales\Api\InvoiceCommentRepositoryInterface; use Magento\Sales\Model\Spi\InvoiceCommentResourceInterface; +use Magento\Sales\Model\Order\Email\Sender\InvoiceCommentSender; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Psr\Log\LoggerInterface; +/** + * Class CommentRepository + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CommentRepository implements InvoiceCommentRepositoryInterface { /** @@ -37,22 +46,48 @@ class CommentRepository implements InvoiceCommentRepositoryInterface */ private $collectionProcessor; + /** + * @var InvoiceCommentSender + */ + private $invoiceCommentSender; + + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param InvoiceCommentResourceInterface $commentResource * @param InvoiceCommentInterfaceFactory $commentFactory * @param InvoiceCommentSearchResultInterfaceFactory $searchResultFactory * @param CollectionProcessorInterface $collectionProcessor + * @param InvoiceCommentSender|null $invoiceCommentSender + * @param InvoiceRepositoryInterface|null $invoiceRepository + * @param LoggerInterface|null $logger */ public function __construct( InvoiceCommentResourceInterface $commentResource, InvoiceCommentInterfaceFactory $commentFactory, InvoiceCommentSearchResultInterfaceFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + InvoiceCommentSender $invoiceCommentSender = null, + InvoiceRepositoryInterface $invoiceRepository = null, + LoggerInterface $logger = null ) { $this->commentResource = $commentResource; $this->commentFactory = $commentFactory; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor; + $this->invoiceCommentSender = $invoiceCommentSender + ?:ObjectManager::getInstance()->get(InvoiceCommentSender::class); + $this->invoiceRepository = $invoiceRepository + ?:ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -99,6 +134,13 @@ public function save(InvoiceCommentInterface $entity) } catch (\Exception $e) { throw new CouldNotSaveException(__('Could not save the invoice comment.'), $e); } + + try { + $invoice = $this->invoiceRepository->get($entity->getParentId()); + $this->invoiceCommentSender->send($invoice, $entity->getIsCustomerNotified(), $entity->getComment()); + } catch (\Exception $exception) { + $this->logger->warning('Something went wrong while sending email.'); + } return $entity; } } diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index 5daab1f4d9bd3..004f36c277028 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -7,9 +7,12 @@ use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\Order\Invoice\SenderInterface; +use Magento\Framework\DataObject; /** * Email notification sender for Invoice. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailSender extends Sender implements SenderInterface { @@ -86,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -93,9 +97,11 @@ public function send( \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'invoice' => $invoice, @@ -106,13 +112,17 @@ public function send( 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_invoice_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $invoice->setEmailSent(true); @@ -138,6 +148,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Grand.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Grand.php index ce415d3f0b758..f4e9b0d5a6f76 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Grand.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Grand.php @@ -8,17 +8,14 @@ class Grand extends AbstractTotal { /** + * Collect invoice grand total + * * @param \Magento\Sales\Model\Order\Invoice $invoice * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) { - /** - * Check order grand total and invoice amounts - */ - if ($invoice->isLast()) { - // - } return $this; } } diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php index fd5c015d9db4f..6e12f10f0c679 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +/** + * Collects invoice taxes. + */ class Tax extends AbstractTotal { /** @@ -69,11 +72,24 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } } + $taxDiscountCompensationAmt = $totalDiscountTaxCompensation; + $baseTaxDiscountCompensationAmt = $baseTotalDiscountTaxCompensation; + $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() - + $order->getDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() - + $order->getBaseDiscountTaxCompensationInvoiced(); + if ($this->_canIncludeShipping($invoice)) { $totalTax += $order->getShippingTaxAmount(); $baseTotalTax += $order->getBaseShippingTaxAmount(); $totalDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount(); $baseTotalDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt(); + + $allowedDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount() - + $order->getShippingDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt() - + $order->getBaseShippingDiscountTaxCompensationInvoiced(); + $invoice->setShippingTaxAmount($order->getShippingTaxAmount()); $invoice->setBaseShippingTaxAmount($order->getBaseShippingTaxAmount()); $invoice->setShippingDiscountTaxCompensationAmount($order->getShippingDiscountTaxCompensationAmount()); @@ -81,14 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } $allowedTax = $order->getTaxAmount() - $order->getTaxInvoiced(); $allowedBaseTax = $order->getBaseTaxAmount() - $order->getBaseTaxInvoiced(); - $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() + - $order->getShippingDiscountTaxCompensationAmount() - - $order->getDiscountTaxCompensationInvoiced() - - $order->getShippingDiscountTaxCompensationInvoiced(); - $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() + - $order->getBaseShippingDiscountTaxCompensationAmnt() - - $order->getBaseDiscountTaxCompensationInvoiced() - - $order->getBaseShippingDiscountTaxCompensationInvoiced(); if ($invoice->isLast()) { $totalTax = $allowedTax; @@ -107,8 +115,8 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setTaxAmount($totalTax); $invoice->setBaseTaxAmount($baseTotalTax); - $invoice->setDiscountTaxCompensationAmount($totalDiscountTaxCompensation); - $invoice->setBaseDiscountTaxCompensationAmount($baseTotalDiscountTaxCompensation); + $invoice->setDiscountTaxCompensationAmount($taxDiscountCompensationAmt); + $invoice->setBaseDiscountTaxCompensationAmount($baseTaxDiscountCompensationAmt); $invoice->setGrandTotal($invoice->getGrandTotal() + $totalTax + $totalDiscountTaxCompensation); $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $baseTotalTax + $baseTotalDiscountTaxCompensation); diff --git a/app/code/Magento/Sales/Model/Order/Item.php b/app/code/Magento/Sales/Model/Order/Item.php index da2a06c2db25a..c202c5d0a643f 100644 --- a/app/code/Magento/Sales/Model/Order/Item.php +++ b/app/code/Magento/Sales/Model/Order/Item.php @@ -232,7 +232,7 @@ public function getQtyToShip() */ public function getSimpleQtyToShip() { - $qty = $this->getQtyOrdered() - $this->getQtyShipped() - $this->getQtyRefunded() - $this->getQtyCanceled(); + $qty = $this->getQtyOrdered() - $this->getQtyShipped() - $this->getQtyCanceled(); return max($qty, 0); } diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 8ded805546c8a..6d03f31c76380 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -5,9 +5,7 @@ */ namespace Magento\Sales\Model\Order; -use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Catalog\Model\ProductOptionFactory; use Magento\Catalog\Model\ProductOptionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DataObject; @@ -15,9 +13,9 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\OrderItemInterface; -use Magento\Sales\Api\Data\OrderItemSearchResultInterface; use Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory; use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Metadata; /** @@ -41,16 +39,6 @@ class ItemRepository implements OrderItemRepositoryInterface */ protected $searchResultFactory; - /** - * @var ProductOptionFactory - */ - protected $productOptionFactory; - - /** - * @var ProductOptionExtensionFactory - */ - protected $extensionFactory; - /** * @var ProductOptionProcessorInterface[] */ @@ -62,40 +50,41 @@ class ItemRepository implements OrderItemRepositoryInterface protected $registry = []; /** - * @var \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface + * @var CollectionProcessorInterface */ private $collectionProcessor; /** - * ItemRepository constructor. + * @var ProductOption + */ + private $productOption; + + /** * @param DataObjectFactory $objectFactory * @param Metadata $metadata * @param OrderItemSearchResultInterfaceFactory $searchResultFactory - * @param ProductOptionFactory $productOptionFactory - * @param ProductOptionExtensionFactory $extensionFactory + * @param CollectionProcessorInterface $collectionProcessor + * @param ProductOption $productOption * @param array $processorPool - * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( DataObjectFactory $objectFactory, Metadata $metadata, OrderItemSearchResultInterfaceFactory $searchResultFactory, - ProductOptionFactory $productOptionFactory, - ProductOptionExtensionFactory $extensionFactory, - array $processorPool = [], - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor, + ProductOption $productOption, + array $processorPool = [] ) { $this->objectFactory = $objectFactory; $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; - $this->productOptionFactory = $productOptionFactory; - $this->extensionFactory = $extensionFactory; + $this->collectionProcessor = $collectionProcessor; + $this->productOption = $productOption; $this->processorPool = $processorPool; - $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); } /** - * load entity + * Loads entity. * * @param int $id * @return OrderItemInterface @@ -114,7 +103,8 @@ public function get($id) throw new NoSuchEntityException(__('Requested entity doesn\'t exist')); } - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); + $this->addParentItem($orderItem); $this->registry[$id] = $orderItem; } return $this->registry[$id]; @@ -134,7 +124,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->collectionProcessor->process($searchCriteria, $searchResult); /** @var OrderItemInterface $orderItem */ foreach ($searchResult->getItems() as $orderItem) { - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); } return $searchResult; @@ -175,7 +165,9 @@ public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { $request = $this->getBuyRequest($entity); - $entity->setProductOptions(['info_buyRequest' => $request->toArray()]); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $request->toArray(); + $entity->setProductOptions($productOptions); } $this->metadata->getMapper()->save($entity); @@ -184,60 +176,17 @@ public function save(OrderItemInterface $entity) } /** - * Add product option data - * - * @param OrderItemInterface $orderItem - * @return $this - */ - protected function addProductOption(OrderItemInterface $orderItem) - { - /** @var DataObject $request */ - $request = $orderItem->getBuyRequest(); - - $productType = $orderItem->getProductType(); - if (isset($this->processorPool[$productType]) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool[$productType]->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - if (isset($this->processorPool['custom_options']) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool['custom_options']->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - return $this; - } - - /** - * Set product options data + * Set parent item. * * @param OrderItemInterface $orderItem - * @param array $data - * @return $this + * @throws InputException + * @throws NoSuchEntityException */ - protected function setProductOption(OrderItemInterface $orderItem, array $data) + private function addParentItem(OrderItemInterface $orderItem) { - $productOption = $orderItem->getProductOption(); - if (!$productOption) { - $productOption = $this->productOptionFactory->create(); - $orderItem->setProductOption($productOption); - } - - $extensionAttributes = $productOption->getExtensionAttributes(); - if (!$extensionAttributes) { - $extensionAttributes = $this->extensionFactory->create(); - $productOption->setExtensionAttributes($extensionAttributes); + if ($parentId = $orderItem->getParentItemId()) { + $orderItem->setParentItem($this->get($parentId)); } - - $extensionAttributes->setData(key($data), current($data)); - - return $this; } /** @@ -271,20 +220,4 @@ protected function getBuyRequest(OrderItemInterface $entity) return $request; } - - /** - * Retrieve collection processor - * - * @deprecated 100.2.0 - * @return CollectionProcessorInterface - */ - private function getCollectionProcessor() - { - if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - } - return $this->collectionProcessor; - } } diff --git a/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php b/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php new file mode 100644 index 0000000000000..948ac26cffc62 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php @@ -0,0 +1,55 @@ +customerExtractor = $customerExtractor; + $this->delegateService = $delegateService; + } + + /** + * @inheritDoc + */ + public function delegateNew(int $orderId): Redirect + { + return $this->delegateService->createRedirectForNew( + $this->customerExtractor->extract($orderId), + ['__sales_assign_order_id' => $orderId] + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php b/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php new file mode 100644 index 0000000000000..3cb47c4458bac --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php @@ -0,0 +1,153 @@ +orderRepository = $orderRepository; + $this->customerRepository = $customerRepository; + $this->objectCopyService = $objectCopyService; + $this->addressFactory = $addressFactory; + $this->regionFactory = $regionFactory; + $this->customerFactory = $customerFactory; + $this->quoteAddressFactory = $quoteAddressFactory; + } + + /** + * Extract customer data from order. + * + * @param int $orderId + * @return CustomerInterface + */ + public function extract(int $orderId): CustomerInterface + { + $order = $this->orderRepository->get($orderId); + + //Simply return customer from DB. + if ($order->getCustomerId()) { + return $this->customerRepository->getById($order->getCustomerId()); + } + + //Prepare customer data from order data if customer doesn't exist yet. + $customerData = $this->objectCopyService->copyFieldsetToTarget( + 'order_address', + 'to_customer', + $order->getBillingAddress(), + [] + ); + + $processedAddressData = []; + $customerAddresses = []; + foreach ($order->getAddresses() as $orderAddress) { + $addressData = $this->objectCopyService + ->copyFieldsetToTarget('order_address', 'to_customer_address', $orderAddress, []); + + $index = array_search($addressData, $processedAddressData); + if ($index === false) { + // create new customer address only if it is unique + $customerAddress = $this->addressFactory->create(['data' => $addressData]); + $customerAddress->setIsDefaultBilling(false); + $customerAddress->setIsDefaultBilling(false); + if (is_string($orderAddress->getRegion())) { + /** @var RegionInterface $region */ + $region = $this->regionFactory->create(); + $region->setRegion($orderAddress->getRegion()); + $region->setRegionCode($orderAddress->getRegionCode()); + $region->setRegionId($orderAddress->getRegionId()); + $customerAddress->setRegion($region); + } + + $processedAddressData[] = $addressData; + $customerAddresses[] = $customerAddress; + $index = count($processedAddressData) - 1; + } + + $customerAddress = $customerAddresses[$index]; + // make sure that address type flags from equal addresses are stored in one resulted address + if ($orderAddress->getAddressType() == OrderAddress::TYPE_BILLING) { + $customerAddress->setIsDefaultBilling(true); + } + if ($orderAddress->getAddressType() == OrderAddress::TYPE_SHIPPING) { + $customerAddress->setIsDefaultShipping(true); + } + } + + $customerData['addresses'] = $customerAddresses; + + return $this->customerFactory->create(['data' => $customerData]); + } +} diff --git a/app/code/Magento/Sales/Model/Order/OrderValidator.php b/app/code/Magento/Sales/Model/Order/OrderValidator.php index 1b04e6ca8c362..880b85a993d11 100644 --- a/app/code/Magento/Sales/Model/Order/OrderValidator.php +++ b/app/code/Magento/Sales/Model/Order/OrderValidator.php @@ -6,7 +6,6 @@ namespace Magento\Sales\Model\Order; use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Exception\DocumentValidationException; /** * Class OrderValidator diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index ee93afb12e3a2..97040c0a578c8 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order; use Magento\Framework\App\ObjectManager; @@ -783,7 +781,9 @@ public function registerRefundNotification($amount) if ($amount != $baseGrandTotal) { $order->addStatusHistoryComment( __( - 'IPN "Refunded". Refund issued by merchant. Registered notification about refunded amount of %1. Transaction ID: "%2". Credit Memo has not been created. Please create offline Credit Memo.', + 'IPN "Refunded". Refund issued by merchant. ' + . 'Registered notification about refunded amount of %1. Transaction ID: "%2". ' + . 'Credit Memo has not been created. Please create offline Credit Memo.', $this->formatPrice($notificationAmount), $this->getTransactionId() ), @@ -1270,14 +1270,12 @@ public function prependMessage($messagePrependTo) { $preparedMessage = $this->getPreparedMessage(); if ($preparedMessage) { - if ( - is_string($preparedMessage) + if (is_string($preparedMessage) || $preparedMessage instanceof \Magento\Framework\Phrase ) { return $preparedMessage . ' ' . $messagePrependTo; - } elseif (is_object( - $preparedMessage - ) && $preparedMessage instanceof \Magento\Sales\Model\Order\Status\History + } elseif (is_object($preparedMessage) + && $preparedMessage instanceof \Magento\Sales\Model\Order\Status\History ) { $comment = $preparedMessage->getComment() . ' ' . $messagePrependTo; $preparedMessage->setComment($comment); @@ -1422,7 +1420,8 @@ protected function checkIfTransactionExists() return $this->transactionManager->isTransactionExists( $this->getTransactionId(), $this->getId(), - $this->getOrder()->getId()); + $this->getOrder()->getId() + ); } /** @@ -1442,9 +1441,8 @@ protected function _getInvoiceForTransactionId($transactionId) } } foreach ($this->getOrder()->getInvoiceCollection() as $invoice) { - if ($invoice->getState() == \Magento\Sales\Model\Order\Invoice::STATE_OPEN && $invoice->load( - $invoice->getId() - ) + if ($invoice->getState() == \Magento\Sales\Model\Order\Invoice::STATE_OPEN + && $invoice->load($invoice->getId()) ) { $invoice->setTransactionId($transactionId); diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php index ee12b459118c1..d38e58d7341c1 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php @@ -35,7 +35,7 @@ public function __construct(StatusResolver $statusResolver = null) */ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface $order) { - $state = Order::STATE_PROCESSING; + $state = $order->getState() ?: Order::STATE_PROCESSING; $status = null; $message = 'Registered notification about captured amount of %1.'; diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php index b9b7a142095d9..8d8de47ba99cf 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order\Payment; use Magento\Framework\Api\AttributeValueFactory; @@ -208,7 +206,9 @@ public function setParentTxnId($parentTxnId, $txnId = null) $this->_verifyTxnId($parentTxnId); if (empty($txnId)) { if ('' == $this->getTxnId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The parent transaction ID must have a transaction ID.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('The parent transaction ID must have a transaction ID.') + ); } } else { $this->setTxnId($txnId); @@ -447,7 +447,9 @@ public function hasChildTransaction($whetherHasChild = null) public function setAdditionalInformation($key, $value) { if (is_object($value)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Payment transactions disallow storing objects.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Payment transactions disallow storing objects.') + ); } $info = $this->_getData('additional_information'); if (!$info) { @@ -587,7 +589,9 @@ public function setOrder($order = null) } elseif (!$this->getId() || $this->getOrderId() == $order->getId()) { $this->_order = $order; } else { - throw new \Magento\Framework\Exception\LocalizedException(__('Set order for existing transactions not allowed')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Set order for existing transactions not allowed') + ); } return $this; @@ -733,7 +737,7 @@ public function getTransactionTypes() */ public function getOrderWebsiteId() { - if (is_null($this->_orderWebsiteId)) { + if ($this->_orderWebsiteId === null) { $this->_orderWebsiteId = (int)$this->getResource()->getOrderWebsiteId($this->getOrderId()); } return $this->_orderWebsiteId; @@ -760,7 +764,9 @@ protected function _verifyTxnType($txnType = null) case self::TYPE_REFUND: break; default: - throw new \Magento\Framework\Exception\LocalizedException(__('We found an unsupported transaction type "%1".', $txnType)); + throw new \Magento\Framework\Exception\LocalizedException( + __('We found an unsupported transaction type "%1".', $txnType) + ); } } @@ -807,7 +813,9 @@ protected function _verifyTxnId($txnId) protected function _verifyThisTransactionExists() { if (!$this->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('You can\'t do this without a transaction object.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('You can\'t do this without a transaction object.') + ); } $this->_verifyTxnType(); } diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php index a2f1d6bcdfbcc..2bcc1fd50dbfa 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php @@ -13,14 +13,11 @@ use Magento\Framework\Data\Collection; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\TransactionInterface; -use Magento\Sales\Api\OrderPaymentRepositoryInterface; -use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\Data\TransactionSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Api\TransactionRepositoryInterface; use Magento\Sales\Model\EntityStorage; use Magento\Sales\Model\EntityStorageFactory; -use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Sales\Api\Data\TransactionSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Model\ResourceModel\Order\Payment\Transaction as TransactionResource; /** diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 850e9cf08413b..18e04bbcef3b5 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order\Pdf; use Magento\Framework\App\Filesystem\DirectoryList; @@ -297,14 +295,15 @@ protected function insertAddress(&$page, $store = null) $page->setLineWidth(0); $this->y = $this->y ? $this->y : 815; $top = 815; - foreach (explode( - "\n", - $this->_scopeConfig->getValue( - 'sales/identity/address', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $store - ) - ) as $value) { + $values = explode( + "\n", + $this->_scopeConfig->getValue( + 'sales/identity/address', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ) + ); + foreach ($values as $value) { if ($value !== '') { $value = preg_replace('/]*>/i', "\n", $value); foreach ($this->string->split($value, 45, true, true) as $_value) { @@ -444,7 +443,9 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) /* Shipping Address and Method */ if (!$order->getIsVirtual()) { /* Shipping Address */ - $shippingAddress = $this->_formatAddress($this->addressRenderer->format($order->getShippingAddress(), 'pdf')); + $shippingAddress = $this->_formatAddress( + $this->addressRenderer->format($order->getShippingAddress(), 'pdf') + ); $shippingMethod = $order->getShippingDescription(); } @@ -557,11 +558,11 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) } $yShipments = $this->y; - $totalShippingChargesText = "(" . __( - 'Total Shipping Charges' - ) . " " . $order->formatPriceTxt( - $order->getShippingAmount() - ) . ")"; + $totalShippingChargesText = "(" + . __('Total Shipping Charges') + . " " + . $order->formatPriceTxt($order->getShippingAmount()) + . ")"; $page->drawText($totalShippingChargesText, 285, $yShipments - $topMargin, 'UTF-8'); $yShipments -= $topMargin + 10; @@ -640,11 +641,7 @@ protected function _sortTotalsList($a, $b) return 0; } - if ($a['sort_order'] == $b['sort_order']) { - return 0; - } - - return $a['sort_order'] > $b['sort_order'] ? 1 : -1; + return $a['sort_order'] <=> $b['sort_order']; } /** @@ -804,7 +801,7 @@ protected function _getRenderer($type) throw new \Magento\Framework\Exception\LocalizedException(__('We found an invalid renderer model.')); } - if (is_null($this->_renderers[$type]['renderer'])) { + if ($this->_renderers[$type]['renderer'] === null) { $this->_renderers[$type]['renderer'] = $this->_pdfItemsFactory->get($this->_renderers[$type]['model']); } @@ -832,8 +829,11 @@ public function getRenderer($type) * @param \Magento\Sales\Model\Order $order * @return \Zend_Pdf_Page */ - protected function _drawItem(\Magento\Framework\DataObject $item, \Zend_Pdf_Page $page, \Magento\Sales\Model\Order $order) - { + protected function _drawItem( + \Magento\Framework\DataObject $item, + \Zend_Pdf_Page $page, + \Magento\Sales\Model\Order $order + ) { $type = $item->getOrderItem()->getProductType(); $renderer = $this->_getRenderer($type); $renderer->setOrder($order); @@ -857,7 +857,7 @@ protected function _drawItem(\Magento\Framework\DataObject $item, \Zend_Pdf_Page protected function _setFontRegular($object, $size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_Re-4.4.1.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerif.ttf') ); $object->setFont($font, $size); return $font; @@ -873,7 +873,7 @@ protected function _setFontRegular($object, $size = 7) protected function _setFontBold($object, $size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_Bd-2.8.1.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerifBold.ttf') ); $object->setFont($font, $size); return $font; @@ -889,7 +889,7 @@ protected function _setFontBold($object, $size = 7) protected function _setFontItalic($object, $size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_It-2.8.2.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerifItalic.ttf') ); $object->setFont($font, $size); return $font; @@ -953,7 +953,7 @@ public function newPage(array $settings = []) * feed int; x position (required) * font string; font style, optional: bold, italic, regular * font_file string; path to font file (optional for use your custom font) - * font_size int; font size (default 7) + * font_size int; font size (default 10) * align string; text align (also see feed parametr), optional left, right * height int;line spacing (default 10) * @@ -1005,24 +1005,8 @@ public function drawLineBlocks(\Zend_Pdf_Page $page, array $draw, array $pageSet foreach ($lines as $line) { $maxHeight = 0; foreach ($line as $column) { - $fontSize = empty($column['font_size']) ? 10 : $column['font_size']; - if (!empty($column['font_file'])) { - $font = \Zend_Pdf_Font::fontWithPath($column['font_file']); - $page->setFont($font, $fontSize); - } else { - $fontStyle = empty($column['font']) ? 'regular' : $column['font']; - switch ($fontStyle) { - case 'bold': - $font = $this->_setFontBold($page, $fontSize); - break; - case 'italic': - $font = $this->_setFontItalic($page, $fontSize); - break; - default: - $font = $this->_setFontRegular($page, $fontSize); - break; - } - } + $font = $this->setFont($page, $column); + $fontSize = $column['font_size']; if (!is_array($column['text'])) { $column['text'] = [$column['text']]; @@ -1033,6 +1017,8 @@ public function drawLineBlocks(\Zend_Pdf_Page $page, array $draw, array $pageSet foreach ($column['text'] as $part) { if ($this->y - $lineSpacing < 15) { $page = $this->newPage($pageSettings); + $font = $this->setFont($page, $column); + $fontSize = $column['font_size']; } $feed = $column['feed']; @@ -1066,4 +1052,42 @@ public function drawLineBlocks(\Zend_Pdf_Page $page, array $draw, array $pageSet return $page; } + + /** + * Set page font. + * + * column array format + * font string; font style, optional: bold, italic, regular + * font_file string; path to font file (optional for use your custom font) + * font_size int; font size (default 10) + * + * @param \Zend_Pdf_Page $page + * @param array $column + * @return \Zend_Pdf_Resource_Font + * @throws \Zend_Pdf_Exception + */ + private function setFont($page, &$column) + { + $fontSize = empty($column['font_size']) ? 10 : $column['font_size']; + $column['font_size'] = $fontSize; + if (!empty($column['font_file'])) { + $font = \Zend_Pdf_Font::fontWithPath($column['font_file']); + $page->setFont($font, $fontSize); + } else { + $fontStyle = empty($column['font']) ? 'regular' : $column['font']; + switch ($fontStyle) { + case 'bold': + $font = $this->_setFontBold($page, $fontSize); + break; + case 'italic': + $font = $this->_setFontItalic($page, $fontSize); + break; + default: + $font = $this->_setFontRegular($page, $fontSize); + break; + } + } + + return $font; + } } diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php index f294128a72f9f..ba99ed083e952 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php @@ -96,7 +96,7 @@ protected function _drawHeader(\Zend_Pdf_Page $page) $lines[0][] = ['text' => __('Qty'), 'feed' => 435, 'align' => 'right']; - $lines[0][] = ['text' => __('Price'), 'feed' => 360, 'align' => 'right']; + $lines[0][] = ['text' => __('Price'), 'feed' => 375, 'align' => 'right']; $lines[0][] = ['text' => __('Tax'), 'feed' => 495, 'align' => 'right']; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php index c0d7a43bed862..422ff1746c9a6 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php @@ -338,7 +338,7 @@ public function getItemOptions() protected function _setFontRegular($size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_Re-4.4.1.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerif.ttf') ); $this->getPage()->setFont($font, $size); return $font; @@ -353,7 +353,7 @@ protected function _setFontRegular($size = 7) protected function _setFontBold($size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_Bd-2.8.1.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerifBold.ttf') ); $this->getPage()->setFont($font, $size); return $font; @@ -368,7 +368,7 @@ protected function _setFontBold($size = 7) protected function _setFontItalic($size = 7) { $font = \Zend_Pdf_Font::fontWithPath( - $this->_rootDirectory->getAbsolutePath('lib/internal/LinLibertineFont/LinLibertine_It-2.8.2.ttf') + $this->_rootDirectory->getAbsolutePath('lib/internal/GnuFreeFont/FreeSerifItalic.ttf') ); $this->getPage()->setFont($font, $size); return $font; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index bb6078e425900..49c2c42696191 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -81,8 +81,8 @@ public function draw() // draw item Prices $i = 0; $prices = $this->getItemPricesForDisplay(); - $feedPrice = 395; - $feedSubtotal = $feedPrice + 170; + $feedPrice = 375; + $feedSubtotal = $feedPrice + 190; foreach ($prices as $priceData) { if (isset($priceData['label'])) { // draw Price label @@ -127,7 +127,8 @@ public function draw() 'feed' => 35, ]; - if ($option['value']) { + // Checking whether option value is not null + if ($option['value']!= null) { if (isset($option['print_value'])) { $printValue = $option['print_value']; } else { diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php index 0e6f345e19bc3..433a4b7e314f4 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php @@ -89,7 +89,7 @@ public function draw() ]; // draw options value - if ($option['value']) { + if ($option['value']!= null) { $printValue = isset( $option['print_value'] ) ? $option['print_value'] : $this->filterManager->stripTags( diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php index b171fccdeb05b..32a289c0f5fa8 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php @@ -150,11 +150,11 @@ public function getPdf($shipments = []) $this->_drawItem($item, $page, $order); $page = end($pdf->pages); } + if ($shipment->getStoreId()) { + $this->_localeResolver->revert(); + } } $this->_afterGetPdf(); - if ($shipment->getStoreId()) { - $this->_localeResolver->revert(); - } return $pdf; } diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Total/DefaultTotal.php b/app/code/Magento/Sales/Model/Order/Pdf/Total/DefaultTotal.php index 5cfd864c71ccd..b71f0fd0ec7df 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Total/DefaultTotal.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Total/DefaultTotal.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order\Pdf\Total; /** diff --git a/app/code/Magento/Sales/Model/Order/ProductOption.php b/app/code/Magento/Sales/Model/Order/ProductOption.php new file mode 100644 index 0000000000000..da0bccd3d806b --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/ProductOption.php @@ -0,0 +1,103 @@ +productOptionFactory = $productOptionFactory; + $this->extensionFactory = $extensionFactory; + $this->processorPool = $processorPool; + } + + /** + * Adds product option to the order item. + * + * @param OrderItemInterface $orderItem + * @return void + */ + public function add(OrderItemInterface $orderItem) + { + /** @var DataObject $request */ + $request = $orderItem->getBuyRequest(); + + $productType = $orderItem->getProductType(); + if (isset($this->processorPool[$productType]) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool[$productType]->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + + if (isset($this->processorPool['custom_options']) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool['custom_options']->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + } + + /** + * Sets product options data. + * + * @param OrderItemInterface $orderItem + * @param array $data + * @return void + */ + private function setProductOption(OrderItemInterface $orderItem, array $data) + { + $productOption = $orderItem->getProductOption(); + if (!$productOption) { + $productOption = $this->productOptionFactory->create(); + $orderItem->setProductOption($productOption); + } + + $extensionAttributes = $productOption->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->extensionFactory->create(); + $productOption->setExtensionAttributes($extensionAttributes); + } + + $extensionAttributes->setData(key($data), current($data)); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index 64e20d5a69041..9346283321eac 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -9,6 +9,7 @@ use Magento\Sales\Api\Data\ShipmentInterface; use Magento\Sales\Model\AbstractModel; use Magento\Sales\Model\EntityInterface; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\Collection as CommentsCollection; /** * Sales order shipment model @@ -94,9 +95,14 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa protected $orderRepository; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection|null + * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection */ - private $tracksCollection = null; + private $tracksCollection; + + /** + * @var CommentsCollection + */ + private $commentsCollection; /** * @param \Magento\Framework\Model\Context $context @@ -337,7 +343,15 @@ public function addItem(\Magento\Sales\Model\Order\Shipment\Item $item) public function getTracksCollection() { if ($this->tracksCollection === null) { - $this->tracksCollection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()); + $this->tracksCollection = $this->_trackCollectionFactory->create(); + + if ($this->getId()) { + $this->tracksCollection->setShipmentFilter($this->getId()); + + foreach ($this->tracksCollection as $item) { + $item->setShipment($this); + } + } } return $this->tracksCollection; @@ -377,19 +391,20 @@ public function getTrackById($trackId) */ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) { - $track->setShipment( - $this - )->setParentId( - $this->getId() - )->setOrderId( - $this->getOrderId() - )->setStoreId( - $this->getStoreId() - ); + $track->setShipment($this) + ->setParentId($this->getId()) + ->setOrderId($this->getOrderId()) + ->setStoreId($this->getStoreId()); + if (!$track->getId()) { $this->getTracksCollection()->addItem($track); } + $tracks = $this->getTracks(); + // as it new track entity, collection doesn't contain it + $tracks[] = $track; + $this->setTracks($tracks); + /** * Track saving is implemented in _afterSave() * This enforces \Magento\Framework\Model\AbstractModel::save() not to skip _afterSave() @@ -411,43 +426,45 @@ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) public function addComment($comment, $notify = false, $visibleOnFront = false) { if (!$comment instanceof \Magento\Sales\Model\Order\Shipment\Comment) { - $comment = $this->_commentFactory->create()->setComment( - $comment - )->setIsCustomerNotified( - $notify - )->setIsVisibleOnFront( - $visibleOnFront - ); + $comment = $this->_commentFactory->create() + ->setComment($comment) + ->setIsCustomerNotified($notify) + ->setIsVisibleOnFront($visibleOnFront); } - $comment->setShipment($this)->setParentId($this->getId())->setStoreId($this->getStoreId()); + $comment->setShipment($this) + ->setParentId($this->getId()) + ->setStoreId($this->getStoreId()); if (!$comment->getId()) { $this->getCommentsCollection()->addItem($comment); } + $comments = $this->getComments(); + $comments[] = $comment; + $this->setComments($comments); $this->_hasDataChanges = true; return $this; } /** - * Retrieve comments collection. + * Retrieves comments collection. * * @param bool $reload - * @return \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\Collection + * @return CommentsCollection */ public function getCommentsCollection($reload = false) { - if (!$this->hasData(ShipmentInterface::COMMENTS) || $reload) { - $comments = $this->_commentCollectionFactory->create() - ->setShipmentFilter($this->getId()) - ->setCreatedAtOrder(); - $this->setComments($comments); - + if ($this->commentsCollection === null || $reload) { + $this->commentsCollection = $this->_commentCollectionFactory->create(); if ($this->getId()) { - foreach ($this->getComments() as $comment) { + $this->commentsCollection->setShipmentFilter($this->getId()) + ->setCreatedAtOrder(); + + foreach ($this->commentsCollection as $comment) { $comment->setShipment($this); } } } - return $this->getComments(); + + return $this->commentsCollection; } /** @@ -554,18 +571,16 @@ public function setItems($items) /** * Returns tracks * - * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[] + * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[]|null */ public function getTracks() { + if (!$this->getId()) { + return $this->getData(ShipmentInterface::TRACKS); + } + if ($this->getData(ShipmentInterface::TRACKS) === null) { - $collection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()); - if ($this->getId()) { - foreach ($collection as $item) { - $item->setShipment($this); - } - $this->setData(ShipmentInterface::TRACKS, $collection->getItems()); - } + $this->setData(ShipmentInterface::TRACKS, $this->getTracksCollection()->getItems()); } return $this->getData(ShipmentInterface::TRACKS); } diff --git a/app/code/Magento/Sales/Model/Order/Shipment/CommentRepository.php b/app/code/Magento/Sales/Model/Order/Shipment/CommentRepository.php index b0d3b03aec4ed..4f6e6ee9c2241 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/CommentRepository.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/CommentRepository.php @@ -7,6 +7,7 @@ use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Sales\Api\Data\ShipmentCommentInterface; @@ -14,7 +15,15 @@ use Magento\Sales\Api\Data\ShipmentCommentSearchResultInterfaceFactory; use Magento\Sales\Api\ShipmentCommentRepositoryInterface; use Magento\Sales\Model\Spi\ShipmentCommentResourceInterface; +use Magento\Sales\Model\Order\Email\Sender\ShipmentCommentSender; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Psr\Log\LoggerInterface; +/** + * Class CommentRepository + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CommentRepository implements ShipmentCommentRepositoryInterface { /** @@ -37,22 +46,48 @@ class CommentRepository implements ShipmentCommentRepositoryInterface */ private $collectionProcessor; + /** + * @var ShipmentCommentSender + */ + private $shipmentCommentSender; + + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param ShipmentCommentResourceInterface $commentResource * @param ShipmentCommentInterfaceFactory $commentFactory * @param ShipmentCommentSearchResultInterfaceFactory $searchResultFactory * @param CollectionProcessorInterface $collectionProcessor + * @param ShipmentCommentSender|null $shipmentCommentSender + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param LoggerInterface|null $logger */ public function __construct( ShipmentCommentResourceInterface $commentResource, ShipmentCommentInterfaceFactory $commentFactory, ShipmentCommentSearchResultInterfaceFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + ShipmentCommentSender $shipmentCommentSender = null, + ShipmentRepositoryInterface $shipmentRepository = null, + LoggerInterface $logger = null ) { $this->commentResource = $commentResource; $this->commentFactory = $commentFactory; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor; + $this->shipmentCommentSender = $shipmentCommentSender + ?: ObjectManager::getInstance()->get(ShipmentCommentSender::class); + $this->shipmentRepository = $shipmentRepository + ?: ObjectManager::getInstance()->get(ShipmentRepositoryInterface::class); + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -99,6 +134,13 @@ public function save(ShipmentCommentInterface $entity) } catch (\Exception $e) { throw new CouldNotSaveException(__('Could not save the shipment comment.'), $e); } + + try { + $shipment = $this->shipmentRepository->get($entity->getParentId()); + $this->shipmentCommentSender->send($shipment, $entity->getIsCustomerNotified(), $entity->getComment()); + } catch (\Exception $exception) { + $this->logger->warning('Something went wrong while sending email.'); + } return $entity; } } diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Item.php b/app/code/Magento/Sales/Model/Order/Shipment/Item.php index 531da4df9ae91..0da936e74ca6c 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Item.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Item.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\Order\Shipment; use Magento\Framework\Api\AttributeValueFactory; @@ -146,7 +144,7 @@ public function getOrderItem() * Declare qty * * @param float $qty - * @return \Magento\Sales\Model\Order\Invoice\Item + * @return \Magento\Sales\Model\Order\Shipment\Item * @throws \Magento\Framework\Exception\LocalizedException */ public function setQty($qty) diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index 7c17a2d2d2f64..1d4418c50047d 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -7,9 +7,12 @@ use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\Order\Shipment\SenderInterface; +use Magento\Framework\DataObject; /** * Email notification sender for Shipment. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailSender extends Sender implements SenderInterface { @@ -86,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -93,9 +97,11 @@ public function send( \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $this->identityContainer->setStore($order->getStore()); + $transport = [ 'order' => $order, 'shipment' => $shipment, @@ -106,13 +112,17 @@ public function send( 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) ]; + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_shipment_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport); + $this->templateContainer->setTemplateVars($transportObject->getData()); if ($this->checkAndSend($order)) { $shipment->setEmailSent(true); @@ -138,6 +148,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php b/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php index 5bcf579a1cbf4..b412d4186b1ca 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/TrackRepository.php @@ -14,6 +14,7 @@ use Magento\Sales\Api\Data\ShipmentTrackSearchResultInterfaceFactory; use Magento\Sales\Api\ShipmentTrackRepositoryInterface; use Magento\Sales\Model\Spi\ShipmentTrackResourceInterface; +use Psr\Log\LoggerInterface; class TrackRepository implements ShipmentTrackRepositoryInterface { @@ -37,23 +38,30 @@ class TrackRepository implements ShipmentTrackRepositoryInterface */ private $collectionProcessor; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param ShipmentTrackResourceInterface $trackResource * @param ShipmentTrackInterfaceFactory $trackFactory * @param ShipmentTrackSearchResultInterfaceFactory $searchResultFactory * @param CollectionProcessorInterface $collectionProcessor + * @param LoggerInterface|null $logger */ public function __construct( ShipmentTrackResourceInterface $trackResource, ShipmentTrackInterfaceFactory $trackFactory, ShipmentTrackSearchResultInterfaceFactory $searchResultFactory, - CollectionProcessorInterface $collectionProcessor + CollectionProcessorInterface $collectionProcessor, + LoggerInterface $logger = null ) { - $this->trackResource = $trackResource; $this->trackFactory = $trackFactory; $this->searchResultFactory = $searchResultFactory; $this->collectionProcessor = $collectionProcessor; + $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -64,6 +72,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) $searchResult = $this->searchResultFactory->create(); $this->collectionProcessor->process($searchCriteria, $searchResult); $searchResult->setSearchCriteria($searchCriteria); + return $searchResult; } @@ -74,6 +83,7 @@ public function get($id) { $entity = $this->trackFactory->create(); $this->trackResource->load($entity, $id); + return $entity; } @@ -85,8 +95,10 @@ public function delete(ShipmentTrackInterface $entity) try { $this->trackResource->delete($entity); } catch (\Exception $e) { + $this->logger->error($e->getMessage()); throw new CouldNotDeleteException(__('Could not delete the shipment tracking.'), $e); } + return true; } @@ -98,8 +110,10 @@ public function save(ShipmentTrackInterface $entity) try { $this->trackResource->save($entity); } catch (\Exception $e) { + $this->logger->error($e->getMessage()); throw new CouldNotSaveException(__('Could not save the shipment tracking.'), $e); } + return $entity; } @@ -109,6 +123,7 @@ public function save(ShipmentTrackInterface $entity) public function deleteById($id) { $entity = $this->get($id); + return $this->delete($entity); } } diff --git a/app/code/Magento/Sales/Model/Order/StateResolver.php b/app/code/Magento/Sales/Model/Order/StateResolver.php index 6f84c9b48b6d5..f5575e0388af3 100644 --- a/app/code/Magento/Sales/Model/Order/StateResolver.php +++ b/app/code/Magento/Sales/Model/Order/StateResolver.php @@ -39,7 +39,7 @@ private function isOrderClosed(OrderInterface $order, $arguments) { /** @var $order Order|OrderInterface */ $forceCreditmemo = in_array(self::FORCED_CREDITMEMO, $arguments); - if (floatval($order->getTotalRefunded()) || !$order->getTotalRefunded() && $forceCreditmemo) { + if ((float)$order->getTotalRefunded() || !$order->getTotalRefunded() && $forceCreditmemo) { return true; } return false; diff --git a/app/code/Magento/Sales/Model/Order/Status/History.php b/app/code/Magento/Sales/Model/Order/Status/History.php index 37aef2a5a29aa..d563f9f15b94a 100644 --- a/app/code/Magento/Sales/Model/Order/Status/History.php +++ b/app/code/Magento/Sales/Model/Order/Status/History.php @@ -142,7 +142,7 @@ public function getOrder() */ public function getStatusLabel() { - if ($this->getOrder()) { + if ($this->getOrder() && $this->getStatus() !== null) { return $this->getOrder()->getConfig()->getStatusLabel($this->getStatus()); } return null; diff --git a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php new file mode 100644 index 0000000000000..a1015c102b3af --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php @@ -0,0 +1,59 @@ +priceRenderer = $priceRenderer; + $this->defaultRenderer = $defaultRenderer; + } + + /** + * Changing row total for webapi order item response. + * + * @param OrderItemInterface $dataObject + * @param array $result + * @return array + */ + public function execute( + OrderItemInterface $dataObject, + array $result + ): array { + $result[OrderItemInterface::ROW_TOTAL] = $this->priceRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL] = $this->priceRenderer->getBaseTotalAmount($dataObject); + $result[OrderItemInterface::ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getBaseTotalAmount($dataObject); + + return $result; + } +} diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index 26e833c44d70a..6b1228ebd52b7 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -5,7 +5,9 @@ */ namespace Magento\Sales\Model; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\OrderExtensionFactory; @@ -15,7 +17,10 @@ use Magento\Sales\Api\Data\ShippingAssignmentInterface; use Magento\Sales\Model\Order\ShippingAssignmentBuilder; use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; +use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; /** * Repository class @@ -54,6 +59,26 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface */ protected $registry = []; + /** + * @var JoinProcessorInterface + */ + private $extensionAttributesJoinProcessor; + + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var PaymentAdditionalInfoFactory + */ + private $paymentAdditionalInfoFactory; + + /** + * @var JsonSerializer + */ + private $serializer; + /** * Constructor * @@ -61,12 +86,20 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface * @param SearchResultFactory $searchResultFactory * @param CollectionProcessorInterface|null $collectionProcessor * @param \Magento\Sales\Api\Data\OrderExtensionFactory|null $orderExtensionFactory + * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * @param OrderTaxManagementInterface|null $orderTaxManagement + * @param PaymentAdditionalInfoInterfaceFactory|null $paymentAdditionalInfoFactory + * @param JsonSerializer|null $serializer */ public function __construct( Metadata $metadata, SearchResultFactory $searchResultFactory, CollectionProcessorInterface $collectionProcessor = null, - \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null + \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null, + JoinProcessorInterface $extensionAttributesJoinProcessor = null, + OrderTaxManagementInterface $orderTaxManagement = null, + PaymentAdditionalInfoInterfaceFactory $paymentAdditionalInfoFactory = null, + JsonSerializer $serializer = null ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; @@ -74,10 +107,18 @@ public function __construct( ->get(\Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class); $this->orderExtensionFactory = $orderExtensionFactory ?: ObjectManager::getInstance() ->get(\Magento\Sales\Api\Data\OrderExtensionFactory::class); + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor + ?: ObjectManager::getInstance()->get(JoinProcessorInterface::class); + $this->orderTaxManagement = $orderTaxManagement ?: ObjectManager::getInstance() + ->get(OrderTaxManagementInterface::class); + $this->paymentAdditionalInfoFactory = $paymentAdditionalInfoFactory ?: ObjectManager::getInstance() + ->get(PaymentAdditionalInfoInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(JsonSerializer::class); } /** - * load entity + * Load entity. * * @param int $id * @return \Magento\Sales\Api\Data\OrderInterface @@ -95,12 +136,65 @@ public function get($id) if (!$entity->getEntityId()) { throw new NoSuchEntityException(__('Requested entity doesn\'t exist')); } + $this->setOrderTaxDetails($entity); $this->setShippingAssignments($entity); + $this->setPaymentAdditionalInfo($entity); $this->registry[$id] = $entity; } return $this->registry[$id]; } + /** + * Set order tax details to extension attributes. + * + * @param OrderInterface $order + * @return void + */ + private function setOrderTaxDetails(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getEntityId()); + $appliedTaxes = $orderTaxDetails->getAppliedTaxes(); + + $extensionAttributes->setAppliedTaxes($appliedTaxes); + if (!empty($appliedTaxes)) { + $extensionAttributes->setConvertingFromQuote(true); + } + + $items = $orderTaxDetails->getItems(); + $extensionAttributes->setItemAppliedTaxes($items); + + $order->setExtensionAttributes($extensionAttributes); + } + + /** + * Set payment additional info to the order. + * + * @param OrderInterface $order + * @return void + */ + private function setPaymentAdditionalInfo(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $paymentAdditionalInformation = $order->getPayment()->getAdditionalInformation(); + + $objects = []; + foreach ($paymentAdditionalInformation as $key => $value) { + /** @var PaymentAdditionalInfoInterface $additionalInformationObject */ + $additionalInformationObject = $this->paymentAdditionalInfoFactory->create(); + $additionalInformationObject->setKey($key); + + if (!is_string($value)) { + $value = $this->serializer->serialize($value); + } + + $additionalInformationObject->setValue($value); + $objects[] = $additionalInformationObject; + } + $extensionAttributes->setPaymentAdditionalInfo($objects); + $order->setExtensionAttributes($extensionAttributes); + } + /** * Find entities by criteria * @@ -112,9 +206,12 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr /** @var \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult */ $searchResult = $this->searchResultFactory->create(); $this->collectionProcessor->process($searchCriteria, $searchResult); + $this->extensionAttributesJoinProcessor->process($searchResult); $searchResult->setSearchCriteria($searchCriteria); foreach ($searchResult->getItems() as $order) { $this->setShippingAssignments($order); + $this->setOrderTaxDetails($order); + $this->setPaymentAdditionalInfo($order); } return $searchResult; } @@ -168,6 +265,8 @@ public function save(\Magento\Sales\Api\Data\OrderInterface $entity) } /** + * Set shipping assignments to extension attributes. + * * @param OrderInterface $order * @return void */ diff --git a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php index d6b81039214e9..87c5b917f6963 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php @@ -48,7 +48,6 @@ protected function _construct() /** * Returns connection * - * @todo: make method protected * @return AdapterInterface */ public function getConnection() diff --git a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php index 1b781890e0f7f..80612277e68d5 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php +++ b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php @@ -123,10 +123,15 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { /** @var \Magento\Sales\Model\AbstractModel $object */ if ($object instanceof EntityInterface && $object->getIncrementId() == null) { + $store = $object->getStore(); + $storeId = $store->getId(); + if ($storeId === null) { + $storeId = $store->getGroup()->getDefaultStoreId(); + } $object->setIncrementId( $this->sequenceManager->getSequence( $object->getEntityType(), - $object->getStore()->getGroup()->getDefaultStoreId() + $storeId )->getNextValue() ); } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Grid.php b/app/code/Magento/Sales/Model/ResourceModel/Grid.php index b3425baf1e727..48dbc42f9ae02 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Grid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Grid.php @@ -45,6 +45,11 @@ class Grid extends AbstractGrid */ private $notSyncedDataProvider; + /** + * Order grid rows batch size + */ + const BATCH_SIZE = 100; + /** * @param Context $context * @param string $mainTableName @@ -88,15 +93,20 @@ public function refresh($value, $field = null) { $select = $this->getGridOriginSelect() ->where(($field ?: $this->mainTableName . '.entity_id') . ' = ?', $value); - return $this->getConnection()->query( - $this->getConnection() - ->insertFromSelect( - $select, - $this->getTable($this->gridTableName), - array_keys($this->columns), - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + $sql = $this->getConnection() + ->insertFromSelect( + $select, + $this->getTable($this->gridTableName), + array_keys($this->columns), + AdapterInterface::INSERT_ON_DUPLICATE + ); + + $this->addCommitCallback(function () use ($sql) { + $this->getConnection()->query($sql); + }); + + // need for backward compatibility + return $this->getConnection()->query($sql); } /** @@ -104,25 +114,20 @@ public function refresh($value, $field = null) * * Only orders created/updated since the last method call will be added. * - * @return \Zend_Db_Statement_Interface + * @return void */ public function refreshBySchedule() { - $select = $this->getGridOriginSelect() - ->where( - $this->mainTableName . '.entity_id IN (?)', - $this->notSyncedDataProvider->getIds($this->mainTableName, $this->gridTableName) + $notSyncedIds = $this->notSyncedDataProvider->getIds($this->mainTableName, $this->gridTableName); + foreach (array_chunk($notSyncedIds, self::BATCH_SIZE) as $bunch) { + $select = $this->getGridOriginSelect()->where($this->mainTableName . '.entity_id IN (?)', $bunch); + $fetchResult = $this->getConnection()->fetchAll($select); + $this->getConnection()->insertOnDuplicate( + $this->getTable($this->gridTableName), + $fetchResult, + array_keys($this->columns) ); - - return $this->getConnection()->query( - $this->getConnection() - ->insertFromSelect( - $select, - $this->getTable($this->gridTableName), - array_keys($this->columns), - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + } } /** diff --git a/app/code/Magento/Sales/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Grid/Collection.php index 82d436cb6e047..b3bc05056a1b1 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Grid/Collection.php @@ -6,7 +6,7 @@ namespace Magento\Sales\Model\ResourceModel\Grid; use Magento\Framework\Api\Search\SearchResultInterface; -use Magento\Framework\Search\AggregationInterface; +use Magento\Framework\Api\Search\AggregationInterface; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; /** @@ -78,6 +78,7 @@ public function getAggregations() public function setAggregations($aggregations) { $this->aggregations = $aggregations; + return $this; } /** diff --git a/app/code/Magento/Sales/Model/ResourceModel/GridInterface.php b/app/code/Magento/Sales/Model/ResourceModel/GridInterface.php index 9b0e53347e21a..6c7d983f066e5 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/GridInterface.php +++ b/app/code/Magento/Sales/Model/ResourceModel/GridInterface.php @@ -29,7 +29,7 @@ public function refresh($value, $field = null); * * Only rows created/updated since the last method call should be added. * - * @return \Zend_Db_Statement_Interface + * @return void */ public function refreshBySchedule(); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php index f95a3206ce786..5ecbbd777a14e 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php @@ -50,6 +50,10 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) $object->setBillingAddressId($object->getOrder()->getBillingAddress()->getId()); } + if (!$object->getInvoiceId() && $object->getInvoice()) { + $object->setInvoiceId($object->getInvoice()->getId()); + } + return parent::_beforeSave($object); } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php index f6dd8f8527a53..82c612c1a781d 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php @@ -35,4 +35,19 @@ public function __construct( ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel); } + + /** + * @inheritdoc + */ + protected function _initSelect() + { + parent::_initSelect(); + + $tableDescription = $this->getConnection()->describeTable($this->getMainTable()); + foreach ($tableDescription as $columnInfo) { + $this->addFilterToMap($columnInfo['COLUMN_NAME'], 'main_table.' . $columnInfo['COLUMN_NAME']); + } + + return $this; + } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index c7bac874fa330..820ef71a7ea79 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -9,12 +9,12 @@ use Magento\Sales\Model\Order; /** - * Class State + * Class to check and adjust order state/status. */ class State { /** - * Check order status before save + * Check order status and adjust the status before save. * * @param Order $order * @return $this @@ -23,25 +23,26 @@ class State */ public function check(Order $order) { - if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice() && !$order->canShip()) { - if (0 == $order->getBaseGrandTotal() || $order->canCreditmemo()) { - if ($order->getState() !== Order::STATE_COMPLETE) { - $order->setState(Order::STATE_COMPLETE) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); - } - } elseif (floatval($order->getTotalRefunded()) - || !$order->getTotalRefunded() && $order->hasForcedCanCreditmemo() - ) { - if ($order->getState() !== Order::STATE_CLOSED) { - $order->setState(Order::STATE_CLOSED) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); - } - } - } - if ($order->getState() == Order::STATE_NEW && $order->getIsInProcess()) { + $currentState = $order->getState(); + if ($currentState == Order::STATE_NEW && $order->getIsInProcess()) { $order->setState(Order::STATE_PROCESSING) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $currentState = Order::STATE_PROCESSING; + } + + if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) { + if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) + && !$order->canCreditmemo() + && !$order->canShip() + ) { + $order->setState(Order::STATE_CLOSED) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); + } elseif ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { + $order->setState(Order::STATE_COMPLETE) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); + } } + return $this; } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php index f004a1ee37e65..8758fc1da92d8 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php @@ -57,14 +57,16 @@ protected function _construct() } /** - * Used to emulate after load functionality for each item without loading them + * Unserialize packages in each item * * @return $this */ protected function _afterLoad() { - $this->walk('afterLoad'); + foreach ($this->_items as $item) { + $this->getResource()->unserializeFields($item); + } - return $this; + return parent::_afterLoad(); } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php index 9c8671d02c578..5851b2d936139 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php @@ -62,8 +62,8 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $object) $this->shipmentItemResource->save($item); } } - if (null !== $object->getTracksCollection()) { - foreach ($object->getTracksCollection() as $track) { + if (null !== $object->getTracks()) { + foreach ($object->getTracks() as $track) { $track->setParentId($object->getId()); $this->shipmentTrackResource->save($track); } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Track.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Track.php index 4879df7d5f44a..9790983ab857b 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Track.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Track.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Model\ResourceModel\Order\Shipment; +use Magento\Framework\Exception\LocalizedException; use Magento\Sales\Model\ResourceModel\EntityAbstract as SalesResource; use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; use Magento\Sales\Model\Spi\ShipmentTrackResourceInterface; @@ -74,7 +75,7 @@ protected function _construct() * * @param \Magento\Framework\Model\AbstractModel $object * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { @@ -86,11 +87,16 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) parent::_beforeSave($object); $errors = $this->validator->validate($object); if (!empty($errors)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Cannot save track:\n%1", implode("\n", $errors)) ); } + if ($object->getShipment()->getOrder()->getId() != $object->getOrderId()) { + $errorMessage = 'Shipment with requested ID %1 doesn\'t correspond with Order with requested ID %2.'; + throw new LocalizedException(__($errorMessage, $object->getParentId(), $object->getOrderId())); + } + return $this; } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers/Collection.php index 7f0aaff02d104..25a963d1b7cbc 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers/Collection.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Model\ResourceModel\Report\Bestsellers; /** @@ -255,8 +253,8 @@ protected function _beforeLoad() $selectUnions = []; // apply date boundaries (before calling $this->_applyDateRangeFilter()) - $periodFrom = !is_null($this->_from) ? new \DateTime($this->_from) : null; - $periodTo = !is_null($this->_to) ? new \DateTime($this->_to) : null; + $periodFrom = ($this->_from !== null) ? new \DateTime($this->_from) : null; + $periodTo = ($this->_to !== null) ? new \DateTime($this->_to) : null; if ('year' == $this->_period) { if ($periodFrom) { // not the first day of the year diff --git a/app/code/Magento/Sales/Model/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/Rss/OrderStatus.php index d45c0e664f9d2..95be887323113 100644 --- a/app/code/Magento/Sales/Model/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/Rss/OrderStatus.php @@ -6,10 +6,10 @@ namespace Magento\Sales\Model\Rss; use Magento\Framework\App\Rss\DataProviderInterface; +use Magento\Framework\App\ObjectManager; /** - * Class OrderStatus - * @package Magento\Sales\Model\Rss + * Rss renderer for order statuses. */ class OrderStatus implements DataProviderInterface { @@ -55,6 +55,11 @@ class OrderStatus implements DataProviderInterface */ protected $orderFactory; + /** + * @var Signature + */ + private $signature; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\UrlInterface $urlBuilder @@ -63,6 +68,7 @@ class OrderStatus implements DataProviderInterface * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param Signature|null $signature */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, @@ -71,7 +77,8 @@ public function __construct( \Magento\Sales\Model\ResourceModel\Order\Rss\OrderStatusFactory $orderResourceFactory, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Sales\Model\OrderFactory $orderFactory, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + Signature $signature = null ) { $this->objectManager = $objectManager; $this->urlBuilder = $urlBuilder; @@ -80,6 +87,7 @@ public function __construct( $this->localeDate = $localeDate; $this->orderFactory = $orderFactory; $this->config = $scopeConfig; + $this->signature = $signature ?: ObjectManager::getInstance()->get(Signature::class); } /** @@ -96,7 +104,10 @@ public function isAllowed() } /** + * Get rss data. + * * @return array + * @throws \InvalidArgumentException */ public function getRssData() { @@ -108,6 +119,8 @@ public function getRssData() } /** + * Get cache key. + * * @return string */ public function getCacheKey() @@ -121,6 +134,8 @@ public function getCacheKey() } /** + * Get cache lifetime. + * * @return int */ public function getCacheLifetime() @@ -129,6 +144,8 @@ public function getCacheLifetime() } /** + * Get order. + * * @return \Magento\Sales\Model\Order */ protected function getOrder() @@ -137,8 +154,11 @@ protected function getOrder() return $this->order; } - $data = null; - $json = base64_decode((string)$this->request->getParam('data')); + $data = (string)$this->request->getParam('data'); + if (!$this->signature->isValid($data, (string)$this->request->getParam('signature'))) { + return null; + } + $json = base64_decode($data); if ($json) { $data = json_decode($json, true); } @@ -154,7 +174,7 @@ protected function getOrder() $order = $this->orderFactory->create(); $order->load($data['order_id']); - if ($order->getIncrementId() !== $data['increment_id'] || $order->getCustomerId() !== $data['customer_id']) { + if (!$this->isOrderSuitable($order, $data)) { $order = null; } $this->order = $order; @@ -162,6 +182,18 @@ protected function getOrder() return $this->order; } + /** + * Check if selected order data correspond incoming data. + * + * @param \Magento\Sales\Model\Order $order + * @param array $data + * @return bool + */ + private function isOrderSuitable(\Magento\Sales\Model\Order $order, array $data): bool + { + return $order->getIncrementId() === $data['increment_id'] && $order->getCustomerId() === $data['customer_id']; + } + /** * Get RSS feed items * @@ -218,6 +250,8 @@ protected function getHeader() } /** + * Get feeds. + * * @return array */ public function getFeeds() @@ -226,7 +260,7 @@ public function getFeeds() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAuthRequired() { diff --git a/app/code/Magento/Sales/Model/Rss/Signature.php b/app/code/Magento/Sales/Model/Rss/Signature.php new file mode 100644 index 0000000000000..28f8dc15984b4 --- /dev/null +++ b/app/code/Magento/Sales/Model/Rss/Signature.php @@ -0,0 +1,54 @@ +encryptor = $encryptor; + } + + /** + * Sign data. + * + * @param string $data + * @return string + */ + public function signData(string $data): string + { + return $this->encryptor->hash($data); + } + + /** + * Check if valid signature is provided for given data. + * + * @param string $data + * @param string $signature + * @return bool + */ + public function isValid(string $data, string $signature): bool + { + return $this->encryptor->validateHash($data, $signature); + } +} diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index 2f08c26de9058..1173fa4b7eb5a 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -177,8 +177,8 @@ public function refund( $creditmemo->getOrder(), !$offlineRequested ); - $this->getOrderRepository()->save($order); $this->creditmemoRepository->save($creditmemo); + $this->getOrderRepository()->save($order); $connection->commit(); } catch (\Exception $e) { $connection->rollBack(); @@ -189,13 +189,15 @@ public function refund( } /** + * Checks if credit memo is available for refund. + * * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface $creditmemo) { - if ($creditmemo->getId()) { + if ($creditmemo->getId() && $creditmemo->getState() != \Magento\Sales\Model\Order\Creditmemo::STATE_OPEN) { throw new \Magento\Framework\Exception\LocalizedException( __('We cannot register an existing credit memo.') ); @@ -211,7 +213,7 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface throw new \Magento\Framework\Exception\LocalizedException( __( 'The most money available to refund is %1.', - $creditmemo->getOrder()->formatBasePrice($baseAvailableRefund) + $creditmemo->getOrder()->formatPriceTxt($baseAvailableRefund) ) ); } @@ -219,8 +221,9 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface } /** - * @return \Magento\Sales\Model\Order\RefundAdapterInterface + * Initializes RefundAdapterInterface dependency. * + * @return \Magento\Sales\Model\Order\RefundAdapterInterface * @deprecated 100.1.3 */ private function getRefundAdapter() @@ -233,8 +236,9 @@ private function getRefundAdapter() } /** - * @return \Magento\Framework\App\ResourceConnection|mixed + * Initializes ResourceConnection dependency. * + * @return \Magento\Framework\App\ResourceConnection|mixed * @deprecated 100.1.3 */ private function getResource() @@ -247,8 +251,9 @@ private function getResource() } /** - * @return \Magento\Sales\Api\OrderRepositoryInterface + * Initializes OrderRepositoryInterface dependency. * + * @return \Magento\Sales\Api\OrderRepositoryInterface * @deprecated 100.1.3 */ private function getOrderRepository() @@ -261,8 +266,9 @@ private function getOrderRepository() } /** - * @return \Magento\Sales\Api\InvoiceRepositoryInterface + * Initializes InvoiceRepositoryInterface dependency. * + * @return \Magento\Sales\Api\InvoiceRepositoryInterface * @deprecated 100.1.3 */ private function getInvoiceRepository() diff --git a/app/code/Magento/Sales/Model/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 718f55c3e551c..b66f59d2a2962 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Model\Service; +use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Model\Order; @@ -136,14 +137,14 @@ public function prepareInvoice(Order $order, array $qtys = []) $totalQty = 0; $qtys = $this->prepareItemsQty($order, $qtys); foreach ($order->getAllItems() as $orderItem) { - if (!$this->_canInvoiceItem($orderItem)) { + if (!$this->_canInvoiceItem($orderItem, $qtys)) { continue; } $item = $this->orderConverter->itemToInvoiceItem($orderItem); - if ($orderItem->isDummy()) { - $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; - } elseif (isset($qtys[$orderItem->getId()])) { + if (isset($qtys[$orderItem->getId()])) { $qty = (double) $qtys[$orderItem->getId()]; + } elseif ($orderItem->isDummy()) { + $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; } elseif (empty($qtys)) { $qty = $orderItem->getQtyToInvoice(); } else { @@ -170,38 +171,55 @@ private function prepareItemsQty(Order $order, array $qtys = []) { foreach ($order->getAllItems() as $orderItem) { if (empty($qtys[$orderItem->getId()])) { - continue; - } - if ($orderItem->isDummy()) { - if ($orderItem->getHasChildren()) { - foreach ($orderItem->getChildrenItems() as $child) { - if (!isset($qtys[$child->getId()])) { - $qtys[$child->getId()] = $child->getQtyToInvoice(); - } - } - } elseif ($orderItem->getParentItem()) { - $parent = $orderItem->getParentItem(); - if (!isset($qtys[$parent->getId()])) { - $qtys[$parent->getId()] = $parent->getQtyToInvoice(); - } + $parentId = $orderItem->getParentItemId(); + if ($parentId && array_key_exists($parentId, $qtys)) { + $qtys[$orderItem->getId()] = $qtys[$parentId]; + } else { + continue; } } + $this->prepareItemQty($orderItem, $qtys); } return $qtys; } + /** + * Prepare qty to invoice item. + * + * @param OrderItemInterface $orderItem + * @param array $qtys + * @return void + */ + private function prepareItemQty(OrderItemInterface $orderItem, array &$qtys) + { + if ($orderItem->isDummy()) { + if ($orderItem->getHasChildren()) { + foreach ($orderItem->getChildrenItems() as $child) { + if (!isset($qtys[$child->getId()])) { + $qtys[$child->getId()] = $child->getQtyToInvoice(); + } + } + } elseif ($orderItem->getParentItem()) { + $parent = $orderItem->getParentItem(); + if (!isset($qtys[$parent->getId()])) { + $qtys[$parent->getId()] = $parent->getQtyToInvoice(); + } + } + } + } + /** * Check if order item can be invoiced. Dummy item can be invoiced or with his children or * with parent item which is included to invoice * - * @param \Magento\Sales\Api\Data\OrderItemInterface $item + * @param OrderItemInterface $item + * @param array $qtys * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $item) + protected function _canInvoiceItem(OrderItemInterface $item, array $qtys = []) { - $qtys = []; if ($item->getLockedDoInvoice()) { return false; } diff --git a/app/code/Magento/Sales/Model/Service/OrderService.php b/app/code/Magento/Sales/Model/Service/OrderService.php index 1eb3fad11278f..2e062caca9a24 100644 --- a/app/code/Magento/Sales/Model/Service/OrderService.php +++ b/app/code/Magento/Sales/Model/Service/OrderService.php @@ -6,6 +6,8 @@ namespace Magento\Sales\Model\Service; use Magento\Sales\Api\OrderManagementInterface; +use Magento\Payment\Gateway\Command\CommandException; +use Psr\Log\LoggerInterface; /** * Class OrderService @@ -49,6 +51,16 @@ class OrderService implements OrderManagementInterface */ protected $orderCommentSender; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var LoggerInterface + */ + private $logger; + /** * Constructor * @@ -59,6 +71,8 @@ class OrderService implements OrderManagementInterface * @param \Magento\Sales\Model\OrderNotifier $notifier * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender + * @param \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures + * @param LoggerInterface $logger */ public function __construct( \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, @@ -67,7 +81,9 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Sales\Model\OrderNotifier $notifier, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender + \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures, + LoggerInterface $logger ) { $this->orderRepository = $orderRepository; $this->historyRepository = $historyRepository; @@ -76,6 +92,8 @@ public function __construct( $this->notifier = $notifier; $this->eventManager = $eventManager; $this->orderCommentSender = $orderCommentSender; + $this->paymentFailures = $paymentFailures; + $this->logger = $logger; } /** @@ -179,22 +197,31 @@ public function unHold($id) } /** + * Perform place order. + * * @param \Magento\Sales\Api\Data\OrderInterface $order * @return \Magento\Sales\Api\Data\OrderInterface * @throws \Exception */ public function place(\Magento\Sales\Api\Data\OrderInterface $order) { - // transaction will be here - //begin transaction try { $order->place(); - return $this->orderRepository->save($order); - //commit + } catch (CommandException $e) { + $this->paymentFailures->handle((int)$order->getQuoteId(), __($e->getMessage())); + throw $e; + } + + try { + $order = $this->orderRepository->save($order); } catch (\Exception $e) { + $this->logger->critical( + 'Saving order ' . $order->getIncrementId() . ' failed: ' . $e->getMessage() + ); throw $e; - //rollback; } + + return $order; } /** diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php new file mode 100644 index 0000000000000..22951d6012b6f --- /dev/null +++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php @@ -0,0 +1,313 @@ + Configuration > Sales > Checkout > Payment Failed Emails configuration. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PaymentFailuresService implements PaymentFailuresInterface +{ + /** + * Store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StateInterface + */ + private $inlineTranslation; + + /** + * @var TransportBuilder + */ + private $transportBuilder; + + /** + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StateInterface $inlineTranslation + * @param TransportBuilder $transportBuilder + * @param TimezoneInterface $localeDate + * @param CartRepositoryInterface $cartRepository + * @param LoggerInterface|null $logger + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StateInterface $inlineTranslation, + TransportBuilder $transportBuilder, + TimezoneInterface $localeDate, + CartRepositoryInterface $cartRepository, + LoggerInterface $logger = null + ) { + $this->scopeConfig = $scopeConfig; + $this->inlineTranslation = $inlineTranslation; + $this->transportBuilder = $transportBuilder; + $this->localeDate = $localeDate; + $this->cartRepository = $cartRepository; + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); + } + + /** + * Sends an email about failed transaction. + * + * @param int $cartId + * @param string $message + * @param string $checkoutType + * @return PaymentFailuresInterface + */ + public function handle( + int $cartId, + string $message, + string $checkoutType = 'onepage' + ): PaymentFailuresInterface { + $this->inlineTranslation->suspend(); + $quote = $this->cartRepository->get($cartId); + + $template = $this->getConfigValue('checkout/payment_failed/template', $quote); + $receiver = $this->getConfigValue('checkout/payment_failed/receiver', $quote); + $sendTo = [ + [ + 'email' => $this->getConfigValue('trans_email/ident_' . $receiver . '/email', $quote), + 'name' => $this->getConfigValue('trans_email/ident_' . $receiver . '/name', $quote), + ], + ]; + + $copyMethod = $this->getConfigValue('checkout/payment_failed/copy_method', $quote); + $copyTo = $this->getConfigEmails($quote); + + $bcc = []; + if (!empty($copyTo)) { + switch ($copyMethod) { + case 'bcc': + $bcc = $copyTo; + break; + case 'copy': + foreach ($copyTo as $email) { + $sendTo[] = ['email' => $email, 'name' => null]; + } + break; + } + } + + foreach ($sendTo as $recipient) { + $transport = $this->transportBuilder + ->setTemplateIdentifier($template) + ->setTemplateOptions( + [ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID, + ] + ) + ->setTemplateVars($this->getTemplateVars($quote, $message, $checkoutType)) + ->setFrom($this->getSendFrom($quote)) + ->addTo($recipient['email'], $recipient['name']) + ->addBcc($bcc) + ->getTransport(); + + try { + $transport->sendMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + } + } + + $this->inlineTranslation->resume(); + + return $this; + } + + /** + * Returns mail template variables. + * + * @param Quote $quote + * @param string $message + * @param string $checkoutType + * @return array + */ + private function getTemplateVars(Quote $quote, string $message, string $checkoutType): array + { + return [ + 'reason' => $message, + 'checkoutType' => $checkoutType, + 'dateAndTime' => $this->getLocaleDate(), + 'customer' => $this->getCustomerName($quote), + 'customerEmail' => $quote->getBillingAddress()->getEmail(), + 'billingAddress' => $quote->getBillingAddress(), + 'shippingAddress' => $quote->getShippingAddress(), + 'billingAddressHtml' => $quote->getBillingAddress()->format('html'), + 'shippingAddressHtml' => $quote->getShippingAddress()->format('html'), + 'shippingMethod' => $this->getConfigValue( + 'carriers/' . $this->getShippingMethod($quote) . '/title', + $quote + ), + 'paymentMethod' => $this->getConfigValue( + 'payment/' . $this->getPaymentMethod($quote) . '/title', + $quote + ), + 'items' => implode('
    ', $this->getQuoteItems($quote)), + 'total' => $quote->getCurrency()->getStoreCurrencyCode() . ' ' . $quote->getGrandTotal(), + ]; + } + + /** + * Returns scope config value by config path. + * + * @param string $configPath + * @param Quote $quote + * @return mixed + */ + private function getConfigValue(string $configPath, Quote $quote) + { + return $this->scopeConfig->getValue( + $configPath, + ScopeInterface::SCOPE_STORE, + $quote->getStoreId() + ); + } + + /** + * Returns shipping method from quote. + * + * @param Quote $quote + * @return string + */ + private function getShippingMethod(Quote $quote) + { + $shippingMethod = ''; + if ($shippingInfo = $quote->getShippingAddress()->getShippingMethod()) { + $data = explode('_', $shippingInfo); + $shippingMethod = $data[0]; + } + + return $shippingMethod; + } + + /** + * Returns payment method title from quote. + * + * @param Quote $quote + * @return string + */ + private function getPaymentMethod(Quote $quote) + { + $paymentMethod = ''; + if ($paymentInfo = $quote->getPayment()) { + $paymentMethod = $paymentInfo->getMethod(); + } + + return $paymentMethod; + } + + /** + * Returns quote visible items. + * + * @param Quote $quote + * @return array + */ + private function getQuoteItems(Quote $quote): array + { + $items = []; + foreach ($quote->getAllVisibleItems() as $item) { + $itemData = $item->getProduct()->getName() . ' x ' . $item->getQty() . ' ' . + $quote->getCurrency()->getStoreCurrencyCode() . ' ' . + $item->getProduct()->getFinalPrice($item->getQty()); + $items[] = $itemData; + } + + return $items; + } + + /** + * Gets email values by configuration path. + * + * @param Quote $quote + * @return array|false + */ + private function getConfigEmails(Quote $quote) + { + $configData = $this->getConfigValue('checkout/payment_failed/copy_to', $quote); + if (!empty($configData)) { + return explode(',', $configData); + } + + return false; + } + + /** + * Returns sender identity. + * + * @param Quote $quote + * @return string + */ + private function getSendFrom(Quote $quote): string + { + return $this->getConfigValue('checkout/payment_failed/identity', $quote); + } + + /** + * Returns current locale date and time + * + * @return string + */ + private function getLocaleDate(): string + { + return $this->localeDate->formatDateTime( + new \DateTime(), + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + } + + /** + * Returns customer name. + * + * @param Quote $quote + * @return string + */ + private function getCustomerName(Quote $quote): string + { + $customer = __('Guest'); + if (!$quote->getCustomerIsGuest()) { + $customer = $quote->getCustomer()->getFirstname() . ' ' . + $quote->getCustomer()->getLastname(); + } + + return $customer; + } +} diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php new file mode 100644 index 0000000000000..80e909941c5ce --- /dev/null +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -0,0 +1,75 @@ +orderRepository = $orderRepository; + $this->assignmentService = $assignmentService; + } + + /** + * @inheritDoc + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + /** @var CustomerInterface $customer */ + $customer = $event->getData('customer_data_object'); + /** @var array $delegateData */ + $delegateData = $event->getData('delegate_data'); + if (array_key_exists('__sales_assign_order_id', $delegateData)) { + $orderId = $delegateData['__sales_assign_order_id']; + $order = $this->orderRepository->get($orderId); + if (!$order->getCustomerId() && $customer->getId()) { + // Assign customer info to order after customer creation. + $order->setCustomerId($customer->getId()) + ->setCustomerIsGuest(0) + ->setCustomerEmail($customer->getEmail()) + ->setCustomerFirstname($customer->getFirstname()) + ->setCustomerLastname($customer->getLastname()) + ->setCustomerMiddlename($customer->getMiddlename()) + ->setCustomerPrefix($customer->getPrefix()) + ->setCustomerSuffix($customer->getSuffix()) + ->setCustomerGroupId($customer->getGroupId()); + + $this->assignmentService->execute($order, $customer); + } + } + } +} diff --git a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php index 775a7dab95cfe..cd8c705750d6c 100644 --- a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php +++ b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php @@ -31,6 +31,6 @@ public function __construct(\Magento\Quote\Model\ResourceModel\Quote $quote) public function execute(\Magento\Framework\Event\Observer $observer) { $product = $observer->getEvent()->getProduct(); - $this->_quote->substractProductFromQuotes($product); + $this->_quote->subtractProductFromQuotes($product); } } diff --git a/app/code/Magento/Sales/Plugin/ShippingLabelConverter.php b/app/code/Magento/Sales/Plugin/ShippingLabelConverter.php new file mode 100644 index 0000000000000..c70f8189f8c98 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/ShippingLabelConverter.php @@ -0,0 +1,63 @@ +getItems() as $item) { + if ($item->getShippingLabel() !== null) { + $item->setShippingLabel(base64_encode($item->getShippingLabel())); + } + } + + return $searchResult; + } + + /** + * Convert shipping label from blob to base64encoded string. + * + * @param ShipmentRepositoryInterface $shipmentRepository + * @param ShipmentInterface $shipment + * @return ShipmentInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGet( + ShipmentRepositoryInterface $shipmentRepository, + ShipmentInterface $shipment + ): ShipmentInterface { + if ($shipment->getShippingLabel() !== null) { + $shipment->setShippingLabel(base64_encode($shipment->getShippingLabel())); + } + + return $shipment; + } +} diff --git a/app/code/Magento/Sales/Setup/UpgradeData.php b/app/code/Magento/Sales/Setup/UpgradeData.php index 8b104b0b35590..2e5a454e62fdd 100644 --- a/app/code/Magento/Sales/Setup/UpgradeData.php +++ b/app/code/Magento/Sales/Setup/UpgradeData.php @@ -3,18 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Setup; use Magento\Eav\Model\Config; +use Magento\Framework\App\State; use Magento\Framework\DB\AggregatedFieldDataConverter; use Magento\Framework\DB\DataConverter\SerializedToJson; use Magento\Framework\DB\FieldToConvert; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\OrderFactory; +use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory; /** * Data upgrade script + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UpgradeData implements UpgradeDataInterface { @@ -36,20 +44,34 @@ class UpgradeData implements UpgradeDataInterface private $aggregatedFieldConverter; /** - * Constructor + * @var State + */ + private $state; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * * @param SalesSetupFactory $salesSetupFactory * @param Config $eavConfig * @param AggregatedFieldDataConverter $aggregatedFieldConverter + * @param AddressCollectionFactory $addressCollFactory + * @param OrderFactory $orderFactory + * @param QuoteFactory $quoteFactory + * @param State $state */ public function __construct( SalesSetupFactory $salesSetupFactory, Config $eavConfig, - AggregatedFieldDataConverter $aggregatedFieldConverter + AggregatedFieldDataConverter $aggregatedFieldConverter, + AddressCollectionFactory $addressCollFactory, + OrderFactory $orderFactory, + QuoteFactory $quoteFactory, + State $state ) { $this->salesSetupFactory = $salesSetupFactory; $this->eavConfig = $eavConfig; $this->aggregatedFieldConverter = $aggregatedFieldConverter; + $this->state = $state; } /** @@ -64,6 +86,21 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface if (version_compare($context->getVersion(), '2.0.6', '<')) { $this->convertSerializedDataToJson($context->getVersion(), $salesSetup); } + if (version_compare($context->getVersion(), '2.0.8', '<')) { + $this->state->emulateAreaCode( + \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, + [$this, 'fillQuoteAddressIdInSalesOrderAddress'], + [$setup] + ); + } + if (version_compare($context->getVersion(), '2.0.9', '<')) { + //Correct wrong source model for "invoice" entity type, introduced by mistake in 2.0.1 upgrade. + $salesSetup->updateEntityType( + 'invoice', + 'entity_model', + \Magento\Sales\Model\ResourceModel\Order\Invoice::class + ); + } $this->eavConfig->clear(); } @@ -73,6 +110,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface * @param string $setupVersion * @param SalesSetup $salesSetup * @return void + * @throws \Magento\Framework\DB\FieldDataConversionException */ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSetup) { @@ -118,4 +156,98 @@ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSet } $this->aggregatedFieldConverter->convert($fieldsToUpdate, $salesSetup->getConnection()); } + + /** + * Fill quote_address_id in table sales_order_address if it is empty. + * @param ModuleDataSetupInterface $setup + */ + public function fillQuoteAddressIdInSalesOrderAddress(ModuleDataSetupInterface $setup) + { + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_SHIPPING); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_BILLING); + } + + /** + * @param ModuleDataSetupInterface $setup + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressByType(ModuleDataSetupInterface $setup, $addressType) + { + $salesConnection = $setup->getConnection('sales'); + + $orderTable = $setup->getTable('sales_order', 'sales'); + $orderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $salesConnection + ->select() + ->from( + ['sales_order_address' => $orderAddressTable], + ['entity_id', 'address_type'] + ) + ->joinInner( + ['sales_order' => $orderTable], + 'sales_order_address.parent_id = sales_order.entity_id', + ['quote_id' => 'sales_order.quote_id'] + ) + ->where('sales_order_address.quote_address_id IS NULL') + ->where('sales_order_address.address_type = ?', $addressType) + ->order('sales_order_address.entity_id'); + + $batchSize = 5000; + $result = $salesConnection->query($query); + $count = $result->rowCount(); + $batches = ceil($count / $batchSize); + + for ($batch = $batches; $batch > 0; $batch--) { + $query->limitPage($batch, $batchSize); + $result = $salesConnection->fetchAssoc($query); + + $this->fillQuoteAddressIdInSalesOrderAddressProcessBatch($setup, $result, $addressType); + } + } + + /** + * @param ModuleDataSetupInterface $setup + * @param array $orderAddresses + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressProcessBatch( + ModuleDataSetupInterface $setup, + array $orderAddresses, + $addressType + ) { + $salesConnection = $setup->getConnection('sales'); + $quoteConnection = $setup->getConnection('checkout'); + + $quoteAddressTable = $setup->getTable('quote_address', 'checkout'); + $quoteTable = $setup->getTable('quote', 'checkout'); + $salesOrderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $quoteConnection + ->select() + ->from( + ['quote_address' => $quoteAddressTable], + ['quote_id', 'address_id'] + ) + ->joinInner( + ['quote' => $quoteTable], + 'quote_address.quote_id = quote.entity_id', + [] + ) + ->where('quote.entity_id in (?)', array_column($orderAddresses, 'quote_id')) + ->where('address_type = ?', $addressType); + + $quoteAddresses = $quoteConnection->fetchAssoc($query); + + foreach ($orderAddresses as $orderAddress) { + $bind = [ + 'quote_address_id' => $quoteAddresses[$orderAddress['quote_id']]['address_id'] ?? null, + ]; + $where = [ + 'entity_id = ?' => $orderAddress['entity_id'] + ]; + + $salesConnection->update($salesOrderAddressTable, $bind, $where); + } + } } diff --git a/app/code/Magento/Sales/Setup/UpgradeSchema.php b/app/code/Magento/Sales/Setup/UpgradeSchema.php index 5dbf71e69e9b8..6e625a1eaaa19 100644 --- a/app/code/Magento/Sales/Setup/UpgradeSchema.php +++ b/app/code/Magento/Sales/Setup/UpgradeSchema.php @@ -106,6 +106,12 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con ] ); } + if (version_compare($context->getVersion(), '2.0.10', '<')) { + $this->expandRemoteIpField($installer); + } + if (version_compare($context->getVersion(), '2.0.11', '<')) { + $this->expandLastTransIdField($installer); + } } /** @@ -141,4 +147,38 @@ private function addIndexBaseGrandTotal(SchemaSetupInterface $installer) ['base_grand_total'] ); } + + /** + * @param SchemaSetupInterface $installer + * @return void + */ + private function expandRemoteIpField(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(self::$connectionName); + $connection->modifyColumn( + $installer->getTable('sales_order', self::$connectionName), + 'remote_ip', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 45 + ] + ); + } + + /** + * @param SchemaSetupInterface $installer + * @return void + */ + private function expandLastTransIdField(SchemaSetupInterface $installer) + { + $connection = $installer->getConnection(self::$connectionName); + $connection->modifyColumn( + $installer->getTable('sales_order_payment', self::$connectionName), + 'last_trans_id', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 'length' => 255 + ] + ); + } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml new file mode 100644 index 0000000000000..de1ed79db81ad --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml new file mode 100644 index 0000000000000..033b6e512f63a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml new file mode 100644 index 0000000000000..9377b8adcfe1b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{price}} + grabProductPriceFromGrid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml new file mode 100644 index 0000000000000..a5e72779aeb31 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml new file mode 100644 index 0000000000000..149850ffd3752 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml new file mode 100644 index 0000000000000..5a5943da91ce6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontSearchGuestOrderActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml new file mode 100644 index 0000000000000..920618a70dfb8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml @@ -0,0 +1,45 @@ + + + + + + Joe + Buyer + Joe Buyer + + 11501 Domain Dr + #150 + + Austin + Texas + TX + 1 + US + 78758 + joe.buyer@email.com + 512-345-6789 + + + Joe + Buyer + Joe Buyer + + 11501 Domain Dr + #150 + + Austin + Texas + TX + 1 + US + 78758 + joe.buyer@email.com + 512-345-6789 + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml new file mode 100644 index 0000000000000..523b13ae99c38 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/ConstData.xml @@ -0,0 +1,20 @@ + + + + + + Complete + Closed + Pending + Processing + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Sales/Test/Mftf/Data/CustomerGroupData.xml new file mode 100644 index 0000000000000..bd442a86c3973 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/CustomerGroupData.xml @@ -0,0 +1,19 @@ + + + + + + General + + + + General + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderData.xml new file mode 100644 index 0000000000000..33f608231489a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderData.xml @@ -0,0 +1,21 @@ + + + + + + 123.00 + 5.00 + 128.00 + + + 110.70 + 5.00 + 115.70 + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml new file mode 100644 index 0000000000000..15f18c2ad2a6c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/SalesConfigData.xml @@ -0,0 +1,46 @@ + + + + + EnableMinimumOrderCheck + MinimumOrderAmount500 + + + 1 + + + 500 + + + + DisableMinimumOrderCheck + + + 0 + + + + ShippingTotalsSortOrder + + + 27 + + + + DefaultShippingTotalSortOrder + + + + DefaultTotalFlagDisabled + + + + 0 + + diff --git a/app/code/Magento/Sales/Test/Mftf/Data/SalesData.xml b/app/code/Magento/Sales/Test/Mftf/Data/SalesData.xml new file mode 100644 index 0000000000000..75be6e210917b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/SalesData.xml @@ -0,0 +1,14 @@ + + + + + + data + + diff --git a/app/code/Magento/Sales/Test/Mftf/LICENSE.txt b/app/code/Magento/Sales/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Sales/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/app/code/Magento/Sales/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Sales/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Sales/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/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml new file mode 100644 index 0000000000000..98c9fdb043dd6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_config-meta.xml @@ -0,0 +1,67 @@ + + + + + + + + + string + + + string + + + + + + + + + + + integer + + integer + + + + integer + + integer + + + + integer + + integer + + + + integer + + integer + + + + integer + + integer + + + + integer + + integer + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminCreditMemoNewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminCreditMemoNewPage.xml new file mode 100644 index 0000000000000..a9e37bc3a9df2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminCreditMemoNewPage.xml @@ -0,0 +1,15 @@ + + + + + +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml new file mode 100644 index 0000000000000..4d01371f1efc5 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminInvoiceNewPage.xml @@ -0,0 +1,19 @@ + + + + + +
    +
    +
    +
    +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml new file mode 100644 index 0000000000000..603250e8bac7b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderAddressEditPage.xml @@ -0,0 +1,15 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml new file mode 100644 index 0000000000000..ca4239bb060ca --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml @@ -0,0 +1,24 @@ + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml new file mode 100644 index 0000000000000..fbe9406a13dda --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderDetailsPage.xml @@ -0,0 +1,25 @@ + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml new file mode 100644 index 0000000000000..9f21245224748 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderInvoiceViewPage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrdersPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrdersPage.xml new file mode 100644 index 0000000000000..be6d90031f42f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrdersPage.xml @@ -0,0 +1,15 @@ + + + + + +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/InvoiceDetailsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/InvoiceDetailsPage.xml new file mode 100644 index 0000000000000..2bf33f7d67873 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/InvoiceDetailsPage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/InvoicesPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/InvoicesPage.xml new file mode 100644 index 0000000000000..f1cab89d01c93 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/InvoicesPage.xml @@ -0,0 +1,15 @@ + + + + + +
    +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/OrderDetailsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/OrderDetailsPage.xml new file mode 100644 index 0000000000000..78e3887e6feb7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/OrderDetailsPage.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml new file mode 100644 index 0000000000000..d2a4f4f0459c2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml new file mode 100644 index 0000000000000..69c7fa76129ea --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml @@ -0,0 +1,14 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml new file mode 100644 index 0000000000000..874e6889ec58c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml @@ -0,0 +1,15 @@ + + + + + +
    + + diff --git a/app/code/Magento/Sales/Test/Mftf/README.md b/app/code/Magento/Sales/Test/Mftf/README.md new file mode 100644 index 0000000000000..8a515e2b6233b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sales Functional Tests + +The Functional Test Module for **Magento Sales** module. diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml new file mode 100644 index 0000000000000..15d4df39416f2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml new file mode 100644 index 0000000000000..cb491faf3a631 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -0,0 +1,21 @@ + + + + +
    + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml new file mode 100644 index 0000000000000..a3fca029096ec --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceAddressInformationSection.xml @@ -0,0 +1,17 @@ + + + + +
    + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml new file mode 100644 index 0000000000000..93d1b32f58dc0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -0,0 +1,19 @@ + + + + +
    + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml new file mode 100644 index 0000000000000..48dc5e5b109fd --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceNewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceNewSection.xml new file mode 100644 index 0000000000000..27dff10c1ff13 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceNewSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml new file mode 100644 index 0000000000000..e549f27155309 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderAndAccountInformationSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml new file mode 100644 index 0000000000000..b20ceee7a0181 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml new file mode 100644 index 0000000000000..5f2fbcbdde784 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressEditSection.xml @@ -0,0 +1,13 @@ + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml new file mode 100644 index 0000000000000..2e9c7ee13c848 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderAddressInformationSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsHistoryTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsHistoryTabSection.xml new file mode 100644 index 0000000000000..ae0438fe76404 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCommentsHistoryTabSection.xml @@ -0,0 +1,15 @@ + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml new file mode 100644 index 0000000000000..1e801dab4b134 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml new file mode 100644 index 0000000000000..065770791cd54 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml @@ -0,0 +1,19 @@ + + + + +
    + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml new file mode 100644 index 0000000000000..59e16908ff088 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml @@ -0,0 +1,24 @@ + + + + +
    + + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml new file mode 100644 index 0000000000000..3794def0ac77a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -0,0 +1,22 @@ + + + + +
    + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml new file mode 100644 index 0000000000000..e0830440e6395 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml new file mode 100644 index 0000000000000..1ee84c88a23e0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml new file mode 100644 index 0000000000000..5191545c2eadb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml @@ -0,0 +1,40 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml new file mode 100644 index 0000000000000..44a488204f775 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml new file mode 100644 index 0000000000000..efffa48103b93 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml new file mode 100644 index 0000000000000..3cc09f6fef40f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml @@ -0,0 +1,33 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml new file mode 100644 index 0000000000000..22bff9c286d0f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -0,0 +1,20 @@ + + + + +
    + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormSelectWebsiteSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormSelectWebsiteSection.xml new file mode 100644 index 0000000000000..b94c1dd1c5deb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormSelectWebsiteSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml new file mode 100644 index 0000000000000..aae90589a390f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml @@ -0,0 +1,23 @@ + + + + +
    + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShoppingCartSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShoppingCartSection.xml new file mode 100644 index 0000000000000..020d41e78c8ef --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShoppingCartSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml new file mode 100644 index 0000000000000..d25dab8431bff --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml new file mode 100644 index 0000000000000..56ff0c8182386 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoiceViewMainActionsSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml new file mode 100644 index 0000000000000..264b57733eea7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml new file mode 100644 index 0000000000000..2315c26414638 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml @@ -0,0 +1,38 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderShipmentsTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderShipmentsTabSection.xml new file mode 100644 index 0000000000000..f71b603a40b7d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderShipmentsTabSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml new file mode 100644 index 0000000000000..cbe17499319f9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml new file mode 100644 index 0000000000000..f5df4aa8c29d7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTransactionsTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTransactionsTabSection.xml new file mode 100644 index 0000000000000..604c339594cb7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTransactionsTabSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml new file mode 100644 index 0000000000000..ff6cd808c8e47 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml new file mode 100644 index 0000000000000..f9e954548a4d0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -0,0 +1,22 @@ + + + + +
    + + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/InvoiceDetailsInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/InvoiceDetailsInformationSection.xml new file mode 100644 index 0000000000000..aab401e71b0e9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/InvoiceDetailsInformationSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/InvoicesFiltersSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/InvoicesFiltersSection.xml new file mode 100644 index 0000000000000..41363f8699df5 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/InvoicesFiltersSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/InvoicesGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/InvoicesGridSection.xml new file mode 100644 index 0000000000000..2740476d918ae --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/InvoicesGridSection.xml @@ -0,0 +1,16 @@ + + + + +
    + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInformationSection.xml new file mode 100644 index 0000000000000..f5407d76b69c9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInformationSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInvoicesSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInvoicesSection.xml new file mode 100644 index 0000000000000..5ca0912f7630a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsInvoicesSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMainActionsSection.xml new file mode 100644 index 0000000000000..98b25eb03d186 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMainActionsSection.xml @@ -0,0 +1,21 @@ + + + + +
    + + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMessagesSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMessagesSection.xml new file mode 100644 index 0000000000000..4f09cb1fe5d4e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsMessagesSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsOrderViewSection.xml new file mode 100644 index 0000000000000..cb12c572b9d88 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrderDetailsOrderViewSection.xml @@ -0,0 +1,15 @@ + + + + +
    + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml new file mode 100644 index 0000000000000..2ff00288390bb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml @@ -0,0 +1,20 @@ + + + + +
    + + + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml new file mode 100644 index 0000000000000..b08a66140fabf --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml new file mode 100644 index 0000000000000..b3fd2eeb05c96 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml @@ -0,0 +1,18 @@ + + + + +
    + + + + + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml new file mode 100644 index 0000000000000..31c6b4e95796e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml @@ -0,0 +1,14 @@ + + + + +
    + +
    +
    diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml new file mode 100644 index 0000000000000..5c0ebe99287b5 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml @@ -0,0 +1,184 @@ + + + + + + + <description value="Admin should be able to ship remaining ordered items if some of them are already refunded"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95524"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--login to Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create new order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="startToCreateNewOrder"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrderWithUserDefinedQty"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="quantity" value="10"/> + </actionGroup> + + <!--Fill customer group and customer email which both are now required fields--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" after="addSimpleProductToOrderWithUserDefinedQty" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" after="selectCustomerGroup" stepKey="fillCustomerEmail"/> + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" after="fillCustomerEmail" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" after="fillCustomerAddress" stepKey="selectFlatRateShipping"/> + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="$1,230.00" after="selectFlatRateShipping" stepKey="seeOrderSubTotal"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="$50.00" after="seeOrderSubTotal" stepKey="seeOrderShippingAmount"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="$1,280.00" after="scrollToOrderGrandTotal" stepKey="seeCorrectGrandTotal"/> + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" after="seeCorrectGrandTotal" stepKey="clickSubmitOrder"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskTodisappear"/> + <waitForPageLoad stepKey="waitForOrderSubmitted"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" after="grabOrderId" stepKey="seeViewOrderPage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." after="seeViewOrderPage" stepKey="seeSuccessMessage"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusPending}}" stepKey="seeOrderPending"/> + <grabTextFrom selector="|Order # (\d+)|" after="seeSuccessMessage" stepKey="getOrderId"/> + <scrollTo selector="{{AdminOrderItemsOrderedSection.qtyColumn}}" after="getOrderId" stepKey="scrollToItemsOrdered"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Ordered 10" after="scrollToItemsOrdered" stepKey="seeQtyOfItemsOrdered"/> + + <!--Create order invoice for first half of ordered items--> + <comment userInput="Admin creates invoice for order" stepKey="adminCreateInvoiceComment" /> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceActionButton"/> + <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" after="clickInvoiceActionButton" stepKey="seeNewInvoicePageUrl"/> + <scrollTo selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced"/> + <!--Change invoiced items count--> + <fillField selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" userInput="5" stepKey="invoiceHalfTheItems"/> + <waitForElementVisible selector="{{AdminInvoiceItemsSection.updateQtyEnabled}}" stepKey="waitForEnabledQtyToBeInvoicedSubmitButton"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQtyToBeInvoiced"/> + <waitForLoadingMaskToDisappear stepKey="waitForQtyToUpdate"/> + <waitForElementVisible selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="waitforSubmitInvoiceBtn"/> + <!--Submit Invoice--> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice"/> + <waitForPageLoad stepKey="waitForInvoiceToSubmit1"/> + <!--Invoice created successfully--> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing1"/> + <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Invoiced 5" stepKey="see5itemsInvoiced"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage"/> + <click selector="{{AdminOrderViewTabsSection.invoices}}" stepKey="clickInvoicesTab"/> + <waitForLoadingMaskToDisappear after="clickInvoicesTab" stepKey="waitForInvoiceGridToLoad"/> + <see selector="{{AdminOrderInvoicesTabSection.gridRow('1')}}" userInput="$665.00" after="waitForInvoiceGridToLoad" stepKey="seeOrderInvoiceTabInGrid"/> + + <!--Ship Order--> + <comment userInput="Admin creates shipment" stepKey="adminCreatesShipmentComment"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" after="adminCreatesShipmentComment" stepKey="clickShip"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" after="clickShip" stepKey="seeOrderShipmentUrl" /> + <scrollTo selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" stepKey="scrollToItemsToShip"/> + <see selector="{{AdminShipmentItemsSection.itemQty('1')}}" userInput="Invoiced 5" stepKey="see5ItemsInvoiced"/> + <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="5" stepKey="fillQtyOfItemsToShip"/> + <!--Submit Shipment--> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" after="fillQtyOfItemsToShip" stepKey="submitShipment"/> + <waitForLoadingMaskToDisappear stepKey="waitForShipmentToSubmit"/> + <!--Verify shipment created successfully--> + <see selector="{{AdminMessagesSection.success}}" userInput="The shipment has been created." after="submitShipment" stepKey="successfullShipmentCreation"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing2"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="$getOrderId" stepKey="seeOrderIdInPageTitleAfterShip"/> + <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems1"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 5" stepKey="see5itemsShipped"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage1"/> + <click selector="{{AdminOrderViewTabsSection.shipments}}" stepKey="clickShipmentsTab"/> + <waitForLoadingMaskToDisappear after="clickShipmentsTab" stepKey="waitForShipmentsGridToLoad"/> + <see selector="{{AdminOrderShipmentsTabSection.gridRow('1')}}" userInput="5.0000" after="waitForShipmentsGridToLoad" stepKey="seeOrderShipmentsTabInGrid"/> + + <!--Create Credit Memo--> + <comment userInput="Admin creates credit memo" stepKey="createCreditMemoComment"/> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" after="createCreditMemoComment" stepKey="clickCreateCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> + <!--Submit refund--> + <scrollTo selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" stepKey="scrollToItemsToRefund"/> + <seeInField selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" userInput="5" after="scrollToItemsToRefund" stepKey="checkQtyOfItemsToRefund"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="waitForSubmitRefundOfflineEnabled"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="submitRefundOffline"/> + <!--Verify Credit Memo created successfully--> + <waitForElementVisible + selector="{{AdminMessagesSection.success}}" + time="20" + stepKey="waitForMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." after="submitRefundOffline" stepKey="seeCreditMemoSuccessMsg"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing3"/> + <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems2"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Refunded 5" stepKey="see5itemsRefunded"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminOrderViewTabsSection.creditMemos}}" stepKey="clickCreditMemosTab"/> + <waitForLoadingMaskToDisappear after="clickCreditMemosTab" stepKey="waitForCreditMemosGridToLoad"/> + <see selector="{{AdminOrderCreditMemosTabSection.gridRow('1')}}" userInput="$665.00" after="waitForCreditMemosGridToLoad" stepKey="seeOrderCreditMemoTabInGrid"/> + + <!--Create invoice for rest of the ordered items--> + <comment userInput="Admin creates invoice for rest of the items" stepKey="adminCreateInvoiceComment2" /> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceActionForRestOfItems"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" after="clickInvoiceActionForRestOfItems" stepKey="seePageNameNewInvoicePage2"/> + <comment userInput="Qty To Invoice is 5" stepKey="seeRemainderInQtyToInvoice"/> + <scrollTo selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" stepKey="scrollToItemsInvoiced2"/> + <seeInField selector="{{AdminInvoiceItemsSection.itemQtyToInvoice('1')}}" userInput="5" stepKey="see5InTheQtyToInvoice"/> + <!--Verify items invoiced information--> + <see selector="{{AdminInvoiceItemsSection.itemQty('1')}}" userInput="Refunded 5" stepKey="seeQtyOfItemsRefunded"/> + <!--Submit Invoice--> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="submitInvoice2" /> + <waitForPageLoad stepKey="waitForInvoiceToSubmit2"/> + <!--Invoice created successfully for the rest of the ordered items--> + <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceSuccessMessage2"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusProcessing}}" stepKey="seeOrderProcessing4"/> + <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToOrderItems3"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Invoiced 10" stepKey="see10itemsInvoiced"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage3"/> + <click selector="{{AdminOrderViewTabsSection.invoices}}" stepKey="clickInvoicesTab1"/> + <waitForLoadingMaskToDisappear after="clickInvoicesTab1" stepKey="waitForInvoiceGridToLoad1"/> + <see selector="{{AdminOrderInvoicesTabSection.gridRow('2')}}" userInput="$615.00" after="waitForInvoiceGridToLoad1" stepKey="seeOrderInvoiceTabInGrid1"/> + + <!--Verify Ship Action can be done for the rest of the invoiced items --> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage4"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" after="seeOrderInvoiceTabInGrid1" stepKey="clickShipActionForRestOfItems"/> + + <!--Verify Items in Ship section --> + <scrollTo selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" stepKey="scrollToItemsToShip2"/> + <see selector="{{AdminShipmentItemsSection.itemQty('1')}}" userInput="Invoiced 10" stepKey="seeAll10ItemsInvoiced"/> + <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="5" stepKey="fillRestOfItemsToShip"/> + <!--Submit Shipment--> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" after="fillRestOfItemsToShip" stepKey="submitShipment2" /> + <see selector="{{AdminMessagesSection.success}}" userInput="The shipment has been created." after="submitShipment2" stepKey="successfullyCreatedShipment"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusComplete}}" stepKey="seeOrderComplete"/> + + <!--Verify Items Status and Shipped Qty in the Items Ordered section--> + <scrollTo selector="{{AdminOrderItemsOrderedSection.itemStatus('1')}}" stepKey="scrollToItemsOrdered2"/> + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Shipped 10" stepKey="seeAllItemsShipped"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToTopOfPage5"/> + <click selector="{{AdminOrderViewTabsSection.shipments}}" stepKey="clickShipmentsTab1"/> + <waitForLoadingMaskToDisappear after="clickShipmentsTab1" stepKey="waitForShipmentsGridToLoad1"/> + <see selector="{{AdminOrderShipmentsTabSection.gridRow('2')}}" userInput="5.0000" after="waitForShipmentsGridToLoad1" stepKey="seeOrderShipmentsTabInGrid1"/> + + <!--Remove created customer--> + <actionGroup ref="RemoveCustomerFromAdminActionGroup" stepKey="removeCustomer"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + </test> +</tests> + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml new file mode 100644 index 0000000000000..9bd906a8abe08 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAvailabilityCreditMemoWithNoPaymentTest"> + <annotations> + <features value="Sales"/> + <stories value="MAGETWO-86292: Unable to create Credit memo for order with no payment required"/> + <title value="Checking availability of 'Credit memo' button for order with no payment required"/> + <description value="*Credit Memo* button should be displayed"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96102"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Enable *Free Shipping* --> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Disable *Free Shipping* --> + <actionGroup ref="RemoveCustomerFromAdminActionGroup" stepKey="deleteCustomer"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Flush Magento Cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Proceed to Admin panel > SALES > Orders. Create order--> + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="navigateToNewOrderPage"/> + + <!--Add simple product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addFirstProductToOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!--Click *Custom Price* link, enter 0 and click *Update Items and Quantities* button--> + <click selector="{{AdminOrderFormItemsSection.customPrice($$createProduct.name$$)}}" stepKey="clickCustomPriceCheckbox"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.customPriceField}}" stepKey="waitForPriceFieldAppears"/> + <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="0" stepKey="fillCustomPriceField"/> + <click selector="{{AdminOrderFormItemsSection.update}}" stepKey="clickUpdateItemsAndQuantitiesButton"/> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="clickUpdateItemsAndQuantitiesButton"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" after="selectCustomerGroup" stepKey="fillCustomerEmail"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" after="fillCustomerEmail" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select Free shipping --> + <actionGroup ref="orderSelectFreeShipping" after="fillCustomerAddress" stepKey="selectFreeShippingOption"/> + + <!--Click *Submit Order* button--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" after="selectFreeShippingOption" stepKey="clickSubmitOrder"/> + + <!--Click *Invoice* button--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Verify that *Credit Memo* button is displayed--> + <seeElement selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="seeCreditMemo"/> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoItem"/> + <waitForPageLoad stepKey="waitForCreditMemoPageLoaded"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewMemoUrlOnPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml new file mode 100644 index 0000000000000..048f3a9c14671 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingCreditMemoTotalsTest"> + <annotations> + <features value="CreditMemo"/> + <stories value="MAGETWO-82400: Credit Memo - Wrong tax calculation! #10982"/> + <title value="Checking Credit Memo Totals"/> + <description value="Checking Credit Memo Totals"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97140"/> + <group value="sales"/> + <group value="tax"/> + <skip> + <issueId value="MAGETWO-99951"/> + </skip> + </annotations> + <before> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create simple product--> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + <!--Create Tax Rule--> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + <!--Create customer--> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <!--Configure Tax Class for shipping--> + <createData entity="TaxClassForShippingConfig" stepKey="configureTaxClassForShipping"/> + <!--Login to admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Configure Braintree--> + <actionGroup ref="AdminFillBasicBraintreeConfigActionGroup" stepKey="configureBraintree"/> + </before> + <after> + <!--Delete category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!--Delete Tax Rule--> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Restore default configuration for Tax Class for shipping--> + <createData entity="DefaultTaxClassForShippingConfig" stepKey="restoreTaxClassForShippingConfig"/> + <!--Restore default configuration for Braintree--> + <actionGroup ref="DisableBraintreeActionGroup" stepKey="restoreBraintreeConfig"/> + <!--Logout from admin page--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new order with existing customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add simple product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Select Flat Rate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Fill Braintree credit card for payment method--> + <actionGroup ref="AdminOrderFillBraintreeCreditCardActionGroup" stepKey="fillBraintreeCreditCard"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + <waitForPageLoad stepKey="waitForSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." + stepKey="seeSuccessMessage"/> + + <!--Create invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!--Submit invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Go to invoice page--> + <click selector="{{AdminOrderViewTabsSection.invoices}}" stepKey="clickInvoicesTab"/> + <waitForPageLoad stepKey="waitForInvoiceGridToLoad"/> + <see selector="{{AdminOrderInvoicesTabSection.gridRow('1')}}" userInput="$113.66" stepKey="seeInvoiceInGrid"/> + <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="clickViewInvoice"/> + + <!--Create Credit Memo--> + <click selector="{{AdminOrderInvoiceViewMainActionsSection.creditMemo}}" stepKey="clickCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <fillField selector="{{AdminCreditMemoTotalSection.refundShipping}}" userInput="0" stepKey="setRefundShipping"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.updateTotals}}" time="30" stepKey="waitUpdateTotalsButtonEnabled"/> + <click selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="clickUpdateTotals"/> + <waitForLoadingMaskToDisappear stepKey="waitForUpdateTotals"/> + <actionGroup ref="SubmitCreditMemo" stepKey="submitCreditMemo"/> + + <!--Go to Credit Memo tab--> + <click selector="{{AdminOrderViewTabsSection.creditMemos}}" stepKey="clickCreditMemosTab"/> + <waitForPageLoad stepKey="waitForCreditMemosGridToLoad"/> + + <!--Check refunded total --> + <see selector="{{AdminOrderCreditMemosTabSection.gridRow('1')}}" userInput="$108.25" + stepKey="seeCreditMemoInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml new file mode 100644 index 0000000000000..fe65b7bd35a6e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingDateAfterChangeInterfaceLocaleTest"> + <annotations> + <features value="Ui"/> + <stories value="Checking the value in a grid column"/> + <title value="Checking date format in orders grid column after changing admin interface locale"/> + <description value="Checking date format in orders grid column after changing admin interface locale"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17915"/> + <useCaseId value="MC-17268"/> + <group value="backend"/> + <group value="ui"/> + <group value="sales"/> + </annotations> + <before> + <!--Deploy static content with French locale--> + <magentoCLI command="setup:static-content:deploy" arguments="fr_FR" stepKey="deployStaticContentWithFrenchLocale"/> + <!--Create entities--> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Login to Admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete entities--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Set Admin "Interface Locale" to default value--> + <actionGroup ref="AdminAccountSetInterfaceLocaleActionGroup" stepKey="setAdminInterfaceLocaleToDefaultValue"> + <argument name="localeName" value="en_US"/> + </actionGroup> + <!--Clear filters--> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <!--Logout from Admin page--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create order with existing customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!--Select shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShippingMethod"/> + <!--Select payment method--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPaymentMethod"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <!--Filter orders grid by ID on Admin page--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="{$getOrderIdVerifyCreatedOrderInformation}"/> + </actionGroup> + + <!--Get date from "Purchase Date" column--> + <grabTextFrom selector="{{AdminDataGridTableSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateInDefaultLocale"/> + + <!--Get month name in default locale (US)--> + <executeJS function="return (new Date('{$grabPurchaseDateInDefaultLocale}').toLocaleDateString('en-US', {month: 'short'}))" stepKey="getMonthNameInUS"/> + + <!--Checking Date with default "Interface Locale"--> + <assertContains stepKey="checkingDateWithDefaultInterfaceLocale"> + <expectedResult type="variable">getMonthNameInUS</expectedResult> + <actualResult type="variable">grabPurchaseDateInDefaultLocale</actualResult> + </assertContains> + + <!--Set Admin "Interface Locale" to "Français (France) / français (France)"--> + <actionGroup ref="AdminAccountSetInterfaceLocaleActionGroup" stepKey="setAdminInterfaceLocaleToFrance"> + <argument name="localeName" value="fr_FR"/> + </actionGroup> + + <!--Filter orders grid by ID on Admin page after changing "Interface Locale"--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridByIdAfterSetFrenchLocale"> + <argument name="orderId" value="{$getOrderIdVerifyCreatedOrderInformation}"/> + </actionGroup> + + <!--Get date from "Purchase Date" column after changing "Interface Locale"--> + <grabTextFrom selector="{{AdminDataGridTableSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateInFrenchLocale"/> + + <!--Get month name in French--> + <executeJS function="return (new Date('{$grabPurchaseDateInDefaultLocale}').toLocaleDateString('fr-FR', {month: 'short'}))" stepKey="getMonthNameInFrench"/> + + <!--Checking Date after changing "Interface Locale"--> + <assertContains stepKey="checkingDateAfterChangeInterfaceLocale"> + <expectedResult type="variable">getMonthNameInFrench</expectedResult> + <actualResult type="variable">grabPurchaseDateInFrenchLocale</actualResult> + </assertContains> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml new file mode 100644 index 0000000000000..5386ade5dffd2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWhenCartRuleDeletedTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCreditMemoWhenCartRuleDeletedTest"> + <annotations> + <stories value="MAGETWO-95722: Cannot create credit memo if the used in the order cart rule is deleted"/> + <title value="Checking creating of credit memo"/> + <description value="Verify Credit Memo created if the used in the order cart rule is deleted"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97232"/> + <group value="sales"/> + <skip> + <issueId value="MAGETWO-97825"/> + </skip> + </annotations> + <before> + <!--Create product with category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Clear filters--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearOrdersFilters"/> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceRulePage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCartPriceRuleFilters"/> + <!--Delete product and category--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + <!--Create Cart Price Rule with a specific coupon--> + <actionGroup ref="AdminCreateCartPriceRuleSpecificCouponActionGroup" stepKey="createCartPriceRule"> + <argument name="rule" value="TestSalesRule"/> + <argument name="userPerCoupon" value="99"/> + </actionGroup> + <!--Go to Storefront. Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="goToProductPage"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="addProductToCardAndApplyCartRule"> + <argument name="product" value="$$createProduct$$"/> + <argument name="couponCode" value="{{_defaultCoupon.code}}"/> + </actionGroup> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutPage"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!--Select Check/Money payment--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!--Place order--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open Order--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById1"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow1"/> + <!--Create and Submit Invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Delete the cart price rule --> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{TestSalesRule.name}}"/> + </actionGroup> + <!--Open Order--> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById2"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow2"/> + <!--Create Credit Memo--> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreateCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> + <!--Make sure that Credit memo was created successfully--> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." stepKey="seeCreditMemoSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml new file mode 100644 index 0000000000000..951bc68bc100a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateInvoiceTest"> + <annotations> + <features value="Invoice Creation"/> + <stories value="Create an Invoice via the Admin"/> + <title value="Create Invoice"/> + <description value="Should be able to create an invoice via the admin."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-72096"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct" stepKey="deleteCategory1"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderGridFilters"/> + <amOnPage url="{{InvoicesPage.url}}" stepKey="navigateToInvoicesGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearInvoicesGridFilters"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- todo: Create an order via the api instead of driving the browser --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!-- end todo --> + + <actionGroup ref="OpenOrderById" stepKey="openCreatedOrder"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <!--Create invoice--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!--Submit invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <see selector="{{OrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderStatus"/> + <click selector="{{OrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> + <see selector="{{OrderDetailsInvoicesSection.content}}" userInput="{$grabOrderNumber}" stepKey="seeInvoice1"/> + <see selector="{{OrderDetailsInvoicesSection.content}}" userInput="{{CustomerEntityOne.fullname}}" stepKey="seeInvoice2"/> + + <actionGroup ref="openInvoiceByOrderId" stepKey="openAndFilterInvoicesGrid"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <see selector="{{InvoiceDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeCorrectOrderStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml new file mode 100644 index 0000000000000..9808782f39f9b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderWithBundleProductTest"> + <annotations> + <title value="Create Order in Admin and update bundle product configuration"/> + <stories value="MAGETWO-96394: Wrong price calculation for bundle product on creating order from the admin panel"/> + <description value="Add bundle product with bundle option items with default quantity 2 to order in Admin and check price in product grid"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <!--Set default flat rate shipping method settings--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + + <!--Create simple customer--> + <createData entity="Simple_US_CA_Customer" stepKey="simpleCustomer"/> + + <!--Create simple product 1--> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + + <!--Create simple product 2--> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + + <!--Create bundle product with checkbox bundle option--> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Add drop-down bundle option--> + <createData entity="DropDownBundleOption" stepKey="dropDownBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink3"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink4"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add bundle product to order and check product price in grid--> + <actionGroup ref="addBundleProductToOrderAndCheckPriceInGrid" stepKey="addBundleProductToOrder"> + <argument name="product" value="$$product$$"/> + <argument name="quantity" value="1"/> + <argument name="price" value="$738.00"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="product" stepKey="delete"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml new file mode 100644 index 0000000000000..70b39a664c0f2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderWithMinimumAmountEnabledTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin create order"/> + <title value="Admin order is not restricted by 'Minimum Order Amount' configuration."/> + <description value="Admin should be able to create an order regardless of the minimum order amount."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94483"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="EnabledMinimumOrderAmount500" stepKey="enableMinimumOrderAmount"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCacheBefore"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="DisabledMinimumOrderAmount" stepKey="disableMinimumOrderAmount"/> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCacheAfter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Admin creates order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrder"/> + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="navigateToNewOrderPage"/> + + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <!--Fill customer group information--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmail"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPage"/> + + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderPendingStatus"/> + <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <assertNotEmpty actual="$orderId" stepKey="assertOrderIdIsNotEmpty"/> + <actionGroup ref="verifyBasicOrderInformation" stepKey="verifyOrderInformation"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="shippingAddress" value="US_Address_TX"/> + <argument name="billingAddress" value="US_Address_TX"/> + </actionGroup> + <actionGroup ref="seeProductInItemsOrdered" stepKey="seeSimpleProductInItemsOrdered"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml new file mode 100644 index 0000000000000..df61748d56425 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderWithSelectedShoppingCartItemsTest"> + <annotations> + <title value="Shopping cart items must not be added to the order unless they were moved manually"/> + <stories value="MC-17838: Shopping cart items added to the order created in the admin"/> + <description value="Shopping cart items must not be added to the order unless they were moved manually"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + <before> + <!--Set default flat rate shipping method settings--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + + <!--Create customer--> + <createData entity="Simple_US_CA_Customer" stepKey="simpleCustomer"/> + + <!--Create category--> + <createData entity="_defaultCategory" stepKey="category1"/> + + <!--Create product1--> + <createData entity="_defaultProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + + <!--Create product2--> + <createData entity="_defaultProduct" stepKey="product2"> + <requiredEntity createDataKey="category1"/> + </createData> + + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$simpleCustomer$$" /> + </actionGroup> + + <!-- Step 2: Add product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$product1.custom_attributes[url_key]$)}}" stepKey="amOnPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$product1$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!--Step 3: Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Step 4: Add product2 to the order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + + <!--Step 5: Select product1 in the shopping cart--> + <click selector="{{AdminOrderFormShoppingCartSection.addProduct($$product1.name$$)}}" stepKey="selectProduct1InTheShoppingCart"/> + + <!--Step 6: Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Step 7: Submit order--> + <actionGroup ref="SubmitOrderActionGroup" stepKey="submitOrder"/> + + <!--Step 8: Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <!--Step 9: Check that product 2 is in the order items list --> + <actionGroup ref="seeProductInItemsOrdered" stepKey="seeProduct2InItemsOrdered"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + + <!--Step 10: Check that product 1 is not in the order items list --> + <actionGroup ref="dontSeeProductInItemsOrdered" stepKey="dontSeeProduct1InItemsOrdered"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <after> + <!--Delete product1--> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + + <!--Delete product2--> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + + <!--Delete category--> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + + <!--Delete customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml new file mode 100644 index 0000000000000..7a9d3a1e2773f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreditMemoTotalAfterShippingDiscountTest.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreditMemoTotalAfterShippingDiscountTest"> + <annotations> + <features value="Credit memo"/> + <stories value="Create credit memo"/> + <title value="Verify credit memo grand total after shipping discount is applied via Cart Price Rule"/> + <description value="Verify credit memo grand total after shipping discount is applied via Cart Price Rule"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93194"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + <magentoCLI command="config:set {{ShippingTaxClassTaxableGoods.path}} {{ShippingTaxClassTaxableGoods.value}}" stepKey="setShippingTaxClassToTaxableGoods"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Clear filter in orders grid--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{ShippingTaxClassNone.path}} {{ShippingTaxClassNone.value}}" stepKey="setShippingTaxClassToNone"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="ordersGridClearFilters"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> + <argument name="ruleName" value="{{ApiSalesRule.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearCartPriceRulesGridFilter"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- Create a cart price rule for $10 Fixed amount discount --> + <actionGroup ref="deleteAllCartPriceRule" stepKey="clearRuleList"/> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleName}}" time="30" stepKey="waitRuleFormFullyLoaded"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="chooseNotLoggedInCustomerGroup"/> + + <!-- Open the Actions Tab in the Rules Edit page --> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="waitForElementToBeVisible"/> + <click selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="enableApplyDiscountToShiping"/> + <seeCheckboxIsChecked selector="{{AdminCartPriceRulesFormSection.applyDiscountToShipping}}" stepKey="DiscountIsAppliedToShiping"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{ApiSalesRule.discount_amount}}" stepKey="fillDiscountAmount"/> + + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessageAppeared"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="checkSuccessMessage"/> + + <!-- Place an order from Storefront as a Guest --> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicartActionGroup"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Verify order totals --> + <waitForElementVisible selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="waitSummaryFormAppears"/> + <scrollTo selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="scrollToStorefrontOrderTotals"/> + <see selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" userInput="$123.00" stepKey="checkStorefrontOrderSubtotal"/> + <see selector="{{CheckoutPaymentSection.discountPrice}}" userInput="-$15.00" stepKey="checkStorefrontOrderDiscount"/> + <see selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" userInput="$5.00" stepKey="checkStorefrontOrderShippingAmount"/> + <see selector="{{CheckoutPaymentSection.orderSummaryTotal}}" userInput="$113.00" stepKey="checkStorefrontOrderGrandTotal"/> + <!-- Place order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!-- Open created order by Number --> + <actionGroup ref="OpenOrderById" stepKey="openOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + + <!-- Verify order totals in admin --> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{CONST.orderStatusPending}}" stepKey="seeOrderPending"/> + <waitForElementVisible selector="{{AdminOrderTotalSection.subTotal}}" stepKey="waitOrderSubtotalAppears"/> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToAdminOrderTotals"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$123.00" stepKey="checkAdminOrderSubtotal"/> + <see selector="{{AdminOrderTotalSection.discount}}" userInput="-$15.00" stepKey="checkAdminOrderDiscount"/> + <see selector="{{AdminOrderTotalSection.shippingAndHandling}}" userInput="$5.00" stepKey="checkAdminOrderShippingAmount"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$113.00" stepKey="checkAdminOrderGrandTotal"/> + + <!-- Start creating Invoice --> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <!-- Verify Invoice totals --> + <waitForElementVisible selector="{{AdminInvoiceTotalSection.subTotal}}" stepKey="waitInvoiceSubtotalAppears"/> + <scrollTo selector="{{AdminInvoiceTotalSection.subTotal}}" stepKey="scrollToAdminInvoiceTotals"/> + <see selector="{{AdminInvoiceTotalSection.subTotal}}" userInput="$123.00" stepKey="checkInvoiceSubtotal"/> + <see selector="{{AdminInvoiceTotalSection.discount}}" userInput="-$15.00" stepKey="checkInvoiceDiscount"/> + <see selector="{{AdminInvoiceTotalSection.shippingAndHandling}}" userInput="$5.00" stepKey="checkInvoiceShippingAmount"/> + <see selector="{{AdminInvoiceTotalSection.grandTotal}}" userInput="$113.00" stepKey="checkInvoiceGrandTotal"/> + <!-- Submit invoice--> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Start creating Memo --> + <actionGroup ref="StartCreateCreditMemoFromOrderPage" stepKey="startCreateMemo"/> + <!-- Verify Memo totals --> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.subTotal}}" stepKey="waitMemoSubtotalAppears"/> + <scrollTo selector="{{AdminCreditMemoTotalSection.subTotal}}" stepKey="scrollToAdminMemoTotals"/> + <see selector="{{AdminCreditMemoTotalSection.subTotal}}" userInput="$123.00" stepKey="checkMemoSubtotal"/> + <see selector="{{AdminCreditMemoTotalSection.discount}}" userInput="-$15.00" stepKey="checkMemoDiscount"/> + <see selector="{{AdminCreditMemoTotalSection.grandTotal}}" userInput="$113.00" stepKey="checkMemoGrandTotal"/> + <grabValueFrom selector="{{AdminCreditMemoTotalSection.refundShipping}}" stepKey="grabRefundShipping"/> + <assertEquals expected="5" actual="grabRefundShipping" actualType="variable" message="Refund shipping is invalid" stepKey="checkMemoRefundShipping"/> + + <!-- Submit Memo --> + <actionGroup ref="SubmitCreditMemo" stepKey="submitMemo"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml new file mode 100644 index 0000000000000..e4e2c44887840 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin create order"/> + <title value="Free Shipping is not available in Admin if Minimum Order Amount does not match Order total"/> + <description value="Admin should not be able place order with Free Shipping method if Minimum Order Amount does not match Order total"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77572"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> + <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="cache:flush" stepKey="flushCache2"/> + </after> + <!--Create new order with existing customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> + <waitForPageLoad stepKey="waitForJavascriptToFinish"/> + <!--Click *Get shipping methods and rates* and see that Free Shipping is absent--> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickGetShippingMehods"/> + <dontSeeElement selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" stepKey="seeAbsentFreeShipping"/> + <!--Submit Order and verify that Order isn't placed--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + + <dontSeeElement selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + <seeElement selector="{{AdminMessagesSection.errorMessage}}" stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml new file mode 100644 index 0000000000000..2a769332eba98 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReorderWithCatalogPriceTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin create order"/> + <title value="Reorder doesn't show discount price in Order Totals block"/> + <description value="Reorder doesn't show discount price in Order Totals block"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-16768"/> + <useCaseId value="MAGETWO-99674"/> + <group value="sales"/> + <group value="catalogRule"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllCatalogPriceRule" stepKey="removeAllExistingCartPriceRules"/> + <!--Create the catalog price rule --> + <createData entity="CatalogRuleToPercent" stepKey="createCatalogRule"/> + <!--Create product--> + <createData entity="SimpleProduct3" stepKey="createSimpleProductApi"/> + <!--Create order via API--> + <createData entity="GuestCart" stepKey="createGuestCart"/> + <createData entity="SimpleCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="createGuestCart"/> + <requiredEntity createDataKey="createSimpleProductApi"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddress"> + <requiredEntity createDataKey="createGuestCart"/> + </createData> + <updateData createDataKey="createGuestCart" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformation"> + <requiredEntity createDataKey="createGuestCart"/> + </updateData> + <!--END Create order via API--> + </before> + <after> + <deleteData createDataKey="createSimpleProductApi" stepKey="deleteSimpleProductApi"/> + <!-- Delete the rule --> + <actionGroup ref="deleteAllCatalogPriceRule" stepKey="removeAllExistingCartPriceRules"/> + + <!--Clear all filters in grid--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open order by Id--> + <actionGroup ref="OpenOrderByName" stepKey="openOrderById"> + <argument name="orderId" value="$createGuestCart.return$"/> + <argument name="orderName" value="{{ShippingAddressTX.fullname}}"/> + </actionGroup> + <!--Reorder--> + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> + <!--Verify order item row--> + <waitForElementVisible selector="{{AdminOrderItemsOrderedSection.productPrice('2')}}" stepKey="waitOrderItemPriceToBeVisible"/> + <see selector="{{AdminOrderItemsOrderedSection.productPrice('2')}}" userInput="${{AdminOrderSimpleProductWithCatalogRule.subtotal}}" stepKey="seeOrderItemPrice"/> + <!--Verify totals on Order page--> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <waitForElementVisible selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" stepKey="waitOrderSubtotalToBeVisible"/> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProductWithCatalogRule.subtotal}}" stepKey="seeOrderSubTotal"/> + <waitForElementVisible selector="{{AdminOrderFormTotalSection.total('Shipping')}}" stepKey="waitOrderShippingToBeVisible"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProductWithCatalogRule.shipping}}" stepKey="seeOrderShipping"/> + <waitForElementVisible selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="waitOrderGrandTotalToBeVisible"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProductWithCatalogRule.grandTotal}}" stepKey="seeCorrectGrandTotal"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml new file mode 100644 index 0000000000000..f2fcf7c59c829 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminSubmitConfigurableProductOrderTest"> + <annotations> + <title value="Create Order in Admin and update product configuration"/> + <stories value="MAGETWO-73883: Create Sales > Order from admin add configurable product and change options click OK does not update Items Ordered List"/> + <description value="Create Order in Admin and update product configuration"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-77679"/> + <group value="Sales"/> + </annotations> + <before> + <!--Set default flat rate shipping method settings--> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <!--Create simple customer--> + <createData entity="Simple_US_CA_Customer" stepKey="createSimpleCustomer"/> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create a simple product and give it the attribute with option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <!--Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createSimpleCustomer$$"/> + </actionGroup> + <!--Add configurable product to order--> + <actionGroup ref="addConfigurableProductToOrderFromAdmin" stepKey="addConfigurableProductToOrder"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="attribute" value="$$createConfigProductAttribute$$"/> + <argument name="option" value="$$getConfigAttributeOption1$$"/> + </actionGroup> + <!--Configure ordered configurable product--> + <actionGroup ref="configureOrderedConfigurableProduct" stepKey="configureOrderedConfigurableProduct"> + <argument name="attribute" value="$$createConfigProductAttribute$$"/> + <argument name="option" value="$$getConfigAttributeOption2$$"/> + <argument name="quantity" value="2"/> + </actionGroup> + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="submitOrder"/> + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml new file mode 100644 index 0000000000000..0bc26674e7dc8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSubmitsOrderWithAndWithoutEmailTest"> + <annotations> + <title value="Email is required to create an order from Admin Panel"/> + <description value="Admin should not be able to submit orders without an email address"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93119"/> + <group value="sales"/> + </annotations> + + <!-- Create Default Category and Product --> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!-- Delete Category and Product created in Before --> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Create order via Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <!--<actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="navigateToNewOrderPage"/>--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{OrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.createNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + + <!--Check if order can be submitted without the required fields including email address--> + <actionGroup ref="checkRequiredFieldsNewOrderForm" stepKey="checkRequiredFieldsNewOrder" after="seeNewOrderPageTitle"/> + <scrollToTopOfPage after="checkRequiredFieldsNewOrder" stepKey="scrollToTopOfOrderFormPage"/> + <actionGroup ref="addSimpleProductToOrder" after="scrollToTopOfOrderFormPage" stepKey="addSimpleProductToOrder"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" after="addSimpleProductToOrder" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" after="selectCustomerGroup" stepKey="fillCustomerEmail"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" after="fillCustomerEmail" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" after="fillCustomerAddress" stepKey="selectFlatRateShipping"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" after="selectFlatRateShipping" stepKey="seeOrderSubTotal"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" after="seeOrderSubTotal" stepKey="seeOrderShipping"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" after="scrollToOrderGrandTotal" stepKey="seeCorrectGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder" after="seeCorrectGrandTotal"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" after="grabOrderId" stepKey="seeViewOrderPage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." after="seeViewOrderPage" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml new file mode 100644 index 0000000000000..6990615bde3b6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/CheckXSSVulnerabilityDuringOrderCreationTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckXSSVulnerabilityDuringOrderCreationTest"> + <annotations> + <features value="Sales"/> + <stories value="Create order"/> + <title value="Check XSS vulnerability during order creation test"/> + <description value="Order should not be created with XSS vulnerability in email address"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-7710"/> + <group value="sales"/> + </annotations> + <before> + <!-- Create product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + </before> + <after> + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="adminLogout"/> + </after> + + <!-- Add product to the shopping cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Try to create order on Storefront with provided email --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailStorefront"> + <argument name="email" value="{{Simple_US_Customer_Incorrect_Email.email}}"/> + </actionGroup> + + <!-- Order can not be created --> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="assertErrorMessageStorefront"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Try to create order in admin with provided email --> + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="navigateToNewOrderPage"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer_Incorrect_Email.email}}" stepKey="fillEmailAddressAdminPanel"/> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + + <!-- Order can not be created --> + <actionGroup ref="AssertAdminEmailValidationMessageOnCheckoutActionGroup" stepKey="assertErrorMessageAdminPanel"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml new file mode 100644 index 0000000000000..5f0a5078734d7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest"> + <annotations> + <features value="Sales"/> + <title value="Checkout Free Shipping Recalculation after Coupon Code Applied"/> + <stories value="Checkout Free Shipping Recalculation after Coupon Code Applied"/> + <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15412"/> + <useCaseId value="MAGETWO-96375"/> + <group value="sales"/> + <group value="salesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <field key="price">90</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"/> + + <!-- Enable Free Shipping and set minimum order amount --> + <createData entity="FreeShippingMethodWithMinimumOrderAmount90" stepKey="enableFreeShippingAndSetMinimumOrderAmount"/> + + <!-- Create Cart Price Rule --> + <createData entity="SalesRuleSpecificCouponWithPercentDiscount" stepKey="createCartPriceRule"> + <field key="simple_free_shipping">0</field> + </createData> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefront"> + <argument name="customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <createData entity="ResetFreeShippingMethodWithMinimumOrderAmount90" stepKey="resetFreeShippingMethodAndMinimumOrderAmount"/> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromStorefront"/> + </after> + + <!-- Add product to Shopping Cart and apply coupon --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="navigateToProductPage"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + </actionGroup> + + <!-- Proceed to Checkout and make sure Free Shipping method isn't displaying --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="dontSeeFreeShipping"/> + + <!-- Back to Shopping Cart page and cancel coupon--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="cancelCoupon"/> + + <!-- Proceed to Checkout, select Free Shipping method and apply coupon --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <actionGroup ref="CheckoutSelectShippingMethodActionGroup" stepKey="selectFreeShipping"> + <argument name="shippingMethod" value="Free"/> + </actionGroup> + <actionGroup ref="StorefrontApplyCouponOnCheckoutActionGroup" stepKey="applyCouponOnCheckout"> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + <argument name="successMessage" value="Your coupon was successfully applied."/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + + <!-- Try to Place Order --> + <actionGroup ref="AssertStorefrontErrorMessageOnOrderSubmit" stepKey="tryToPlaceOrder"> + <argument name="errorMessage" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + + <!-- Go back to Shipping step and select Shipping method --> + <amOnPage url="{{CheckoutPage.url}}/#shipping" stepKey="navigateToShippingStep"/> + <actionGroup ref="CheckoutSelectShippingMethodActionGroup" stepKey="selectFlatRateShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!-- Select Payment method and Place order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod1"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml new file mode 100644 index 0000000000000..11279c4046003 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontRedirectToOrderHistory"> + <annotations> + <features value="Redirection Rules"/> + <title value="Create Invoice"/> + <description + value="Check while order printing URL with an id of not relevant order redirects to order history"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92854"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer2"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="signUp"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Create an order at Storefront as Customer 1 --> + <actionGroup ref="CreateOrderToPrintPageActionGroup" stepKey="createOrderToPrint"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + + <!--Go to 'print order' page by grabbed order id--> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderIdFromURL"/> + <switchToNextTab stepKey="switchToPrintPage"/> + <waitForElement selector="{{SalesOrderPrintSection.isOrderPrintPage}}" stepKey="checkPrintPage"/> + <openNewTab stepKey="openNewTab"/> + <switchToNextTab stepKey="switchForward"/> + <amOnPage url="{{StorefrontSalesOrderPrintPage.url({$grabOrderIdFromURL})}}" stepKey="duplicatePrintPage"/> + + <!--Log out as customer 1--> + <switchToNextTab stepKey="switchForward2"/> + <openNewTab stepKey="openNewTab2"/> + <amOnPage url="{{StorefrontCustomerSignOutPage.url}}" stepKey="signOut"/> + <waitForLoadingMaskToDisappear stepKey="waitSignOutPage"/> + + <!--Log in to Storefront as Customer 2 --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="signUp2"> + <argument name="customer" value="$$createCustomer2$$"/> + </actionGroup> + + <!--Create an order at Storefront as Customer 2 --> + <actionGroup ref="CreateOrderToPrintPageActionGroup" stepKey="createOrderToPrint2"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + + <!--Try to load 'print order' page with not relevant order id to be redirected to 'order history' page--> + <switchToNextTab stepKey="switchToPrintPage2"/> + <waitForElement selector="{{SalesOrderPrintSection.isOrderPrintPage}}" stepKey="checkPrintPage2"/> + <openNewTab stepKey="openNewTab3"/> + <switchToNextTab stepKey="switchForward4"/> + <amOnPage url="{{StorefrontSalesOrderPrintPage.url({$grabOrderIdFromURL})}}" stepKey="duplicatePrintPage2"/> + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" stepKey="waitOrderHistoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml new file mode 100644 index 0000000000000..e427b649cbd87 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectSales"> + <annotations> + <features value="Sales"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Sales Pages"/> + <description value="Verify that the Secure URL configuration applies to the Sales pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15705"/> + <group value="sales"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/sales" stepKey="goToUnsecureSalesURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/sales" stepKey="seeSecureSalesURL"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractItemsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractItemsTest.php index 20f7a7061b6b0..a390c43276085 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractItemsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractItemsTest.php @@ -84,7 +84,7 @@ public function testGetItemRenderer() */ public function testGetItemRendererThrowsExceptionForNonexistentRenderer() { - $renderer = $this->createMock(\StdClass::class); + $renderer = $this->createMock(\stdClass::class); $layout = $this->createPartialMock( \Magento\Framework\View\Layout::class, ['getChildName', 'getBlock', '__wakeup'] diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractTest.php index 311e5f697675b..a34373f516c42 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Items/AbstractTest.php @@ -62,7 +62,7 @@ public function testGetItemRenderer() */ public function testGetItemRendererThrowsExceptionForNonexistentRenderer() { - $renderer = $this->createMock(\StdClass::class); + $renderer = $this->createMock(\stdClass::class); $layout = $this->createPartialMock( \Magento\Framework\View\Layout::class, ['getChildName', 'getBlock', '__wakeup'] diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php new file mode 100644 index 0000000000000..15312d66825be --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Address; + +use Magento\Customer\Model\Metadata\Form as CustomerForm; +use Magento\Customer\Model\Metadata\FormFactory as CustomerFormFactory; +use Magento\Directory\Model\ResourceModel\Country\Collection; +use Magento\Framework\Data\Form as DataForm; +use Magento\Framework\Data\Form\Element\Fieldset; +use Magento\Framework\Data\Form\Element\Select; +use Magento\Framework\Data\FormFactory; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Block\Adminhtml\Order\Address\Form; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Backend\Model\Session\Quote as QuoteSession; +use Magento\Sales\Model\AdminOrder\Create; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FormTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Form + */ + private $addressBlock; + + /** + * @var MockObject + */ + private $formFactory; + + /** + * @var MockObject + */ + private $customerFormFactory; + + /** + * @var MockObject + */ + private $coreRegistry; + + /** + * @var MockObject + */ + private $countriesCollection; + + /** + * @var Create|MockObject + */ + private $orderCreate; + + /** + * @var QuoteSession|MockObject + */ + private $sessionQuote; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->formFactory = $this->createMock(FormFactory::class); + $this->customerFormFactory = $this->createMock(CustomerFormFactory::class); + $this->coreRegistry = $this->createMock(Registry::class); + $this->countriesCollection = $this->createMock( + Collection::class + ); + $this->sessionQuote = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId', 'getStore']) + ->getMock(); + $this->orderCreate = $this->getMockBuilder(Create::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderCreate->method('getSession') + ->willReturn($this->sessionQuote); + + $this->addressBlock = $objectManager->getObject( + Form::class, + [ + '_formFactory' => $this->formFactory, + '_customerFormFactory' => $this->customerFormFactory, + '_coreRegistry' => $this->coreRegistry, + 'countriesCollection' => $this->countriesCollection, + 'sessionQuote' => $this->sessionQuote, + '_orderCreate' => $this->orderCreate + ] + ); + } + + public function testGetForm() + { + $storeId = 5; + $form = $this->createMock(DataForm::class); + $fieldset = $this->createMock(Fieldset::class); + $addressForm = $this->createMock(CustomerForm::class); + $address = $this->createMock(Address::class); + $select = $this->createMock(Select::class); + $order = $this->createMock(Order::class); + + $this->formFactory->method('create') + ->willReturn($form); + $form->method('addFieldset') + ->willReturn($fieldset); + $this->customerFormFactory->method('create') + ->willReturn($addressForm); + $addressForm->method('getAttributes') + ->willReturn([]); + $this->coreRegistry->method('registry') + ->willReturn($address); + $form->method('getElement') + ->willReturnOnConsecutiveCalls( + $select, + $select, + $select, + $select, + $select, + $select, + $select, + null + ); + $address->method('getOrder') + ->willReturn($order); + $order->method('getStoreId') + ->willReturn($storeId); + $this->sessionQuote->method('getStoreId') + ->willReturn($storeId); + $this->countriesCollection->method('loadByStore') + ->with($storeId) + ->willReturn($this->countriesCollection); + + $this->addressBlock->getForm(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Form/AddressTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Form/AddressTest.php new file mode 100644 index 0000000000000..5b6d6ded1561a --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Form/AddressTest.php @@ -0,0 +1,263 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Create\Form; + +use Magento\Backend\Model\Session\Quote as QuoteSession; +use Magento\Store\Model\Store; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Eav\Model\AttributeDataFactory; +use Magento\Sales\Block\Adminhtml\Order\Create\Form\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressSearchResultsInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Customer\Model\Address\Mapper; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AddressTest extends TestCase +{ + /** + * @var QuoteSession|MockObject + */ + private $quoteSession; + + /** + * @var Store|MockObject + */ + private $store; + + /** + * @var DirectoryHelper|MockObject + */ + private $directoryHelper; + + /** + * @var int + */ + private $defaultCountryId; + + /** + * @var int + */ + private $customerId; + + /** + * @var int + */ + private $addressId; + + /** + * @var FormFactory|MockObject + */ + private $formFactory; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilder; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $criteriaBuilder; + + /** + * @var AddressInterface|MockObject + */ + private $addressItem; + + /** + * @var AddressRepositoryInterface|MockObject + */ + private $addressService; + + /** + * @var Mapper|MockObject + */ + private $addressMapper; + + /** + * @var Address + */ + private $address; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->defaultCountryId = 1; + $this->customerId = 10; + $this->addressId = 100; + + $this->quoteSession = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getStore', 'getCustomerId']) + ->getMock(); + $this->store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteSession->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->quoteSession->expects($this->any()) + ->method('getCustomerId') + ->willReturn($this->customerId); + $this->directoryHelper = $this->getMockBuilder(DirectoryHelper::class) + ->disableOriginalConstructor() + ->setMethods(['getDefaultCountry']) + ->getMock(); + $this->directoryHelper->expects($this->any()) + ->method('getDefaultCountry') + ->willReturn($this->defaultCountryId); + $this->formFactory = $this->getMockBuilder(FormFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->filterBuilder = $this->getMockBuilder(FilterBuilder::class) + ->disableOriginalConstructor() + ->setMethods(['setField', 'setValue', 'setConditionType', 'create']) + ->getMock(); + $this->criteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->setMethods(['create', 'addFilters']) + ->getMock(); + $this->addressService = $this->getMockBuilder(AddressRepositoryInterface::class) + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $this->addressItem = $this->getMockBuilder(AddressInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $this->addressItem->expects($this->any()) + ->method('getId') + ->willReturn($this->addressId); + $this->addressMapper = $this->getMockBuilder(Mapper::class) + ->disableOriginalConstructor() + ->setMethods(['toFlatArray']) + ->getMock(); + + $this->address = $this->objectManager->getObject( + Address::class, + [ + 'directoryHelper' => $this->directoryHelper, + 'sessionQuote' => $this->quoteSession, + 'customerFormFactory' => $this->formFactory, + 'filterBuilder' => $this->filterBuilder, + 'criteriaBuilder' => $this->criteriaBuilder, + 'addressService' => $this->addressService, + 'addressMapper' => $this->addressMapper + ] + ); + } + + public function testGetAddressCollectionJson() + { + /** @var Form|MockObject $emptyForm */ + $emptyForm = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->setMethods(['outputData']) + ->getMock(); + $emptyForm->expects($this->once()) + ->method('outputData') + ->with(AttributeDataFactory::OUTPUT_FORMAT_JSON) + ->willReturn('emptyFormData'); + + /** @var Filter|MockObject $filter */ + $filter = $this->getMockBuilder(Filter::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filterBuilder->expects($this->once()) + ->method('setField') + ->with('parent_id') + ->willReturnSelf(); + $this->filterBuilder->expects($this->once()) + ->method('setValue') + ->with($this->customerId) + ->willReturnSelf(); + $this->filterBuilder->expects($this->once()) + ->method('setConditionType') + ->with('eq') + ->willReturnSelf(); + $this->filterBuilder->expects($this->once()) + ->method('create') + ->willReturn($filter); + + /** @var SearchCriteria|MockObject $searchCriteria */ + $searchCriteria = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMock(); + $this->criteriaBuilder->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $this->criteriaBuilder->expects($this->once()) + ->method('addFilters') + ->with([$filter]); + + /** @var AddressSearchResultsInterface|MockObject $result */ + $result = $this->getMockBuilder(AddressSearchResultsInterface::class) + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $result->expects($this->once()) + ->method('getItems') + ->willReturn([$this->addressItem]); + $this->addressService->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($result); + + /** @var Form|MockObject $emptyForm */ + $addressForm = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->setMethods(['outputData']) + ->getMock(); + $addressForm->expects($this->once()) + ->method('outputData') + ->with(AttributeDataFactory::OUTPUT_FORMAT_JSON) + ->willReturn('addressFormData'); + $this->addressMapper->expects($this->once()) + ->method('toFlatArray') + ->with($this->addressItem) + ->willReturn([]); + + $this->directoryHelper->expects($this->once()) + ->method('getDefaultCountry') + ->with($this->store) + ->willReturn($this->defaultCountryId); + $this->formFactory->expects($this->at(0)) + ->method('create') + ->with( + 'customer_address', + 'adminhtml_customer_address', + [AddressInterface::COUNTRY_ID => $this->defaultCountryId] + ) + ->willReturn($emptyForm); + $this->formFactory->expects($this->at(1)) + ->method('create') + ->with('customer_address', 'adminhtml_customer_address', [], false, false) + ->willReturn($addressForm); + + $this->address->getAddressCollectionJson(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php new file mode 100644 index 0000000000000..712b08793ad75 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Create; + +use Magento\Backend\Block\Template\Context; +use Magento\Backend\Model\Session\Quote as QuoteSession; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Framework\Currency; +use Magento\Framework\Json\EncoderInterface; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Payment; +use Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Sales\Model\AdminOrder\Create; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FormTest extends TestCase +{ + /** + * @var QuoteSession|MockObject + */ + private $quoteSession; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepository; + + /** + * @var CurrencyInterface|MockObject + */ + private $localeCurrency; + + /** + * @var Form + */ + private $block; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + /** @var Context|MockObject $context */ + $context = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $context->method('getStoreManager') + ->willReturn($this->storeManager); + $this->quoteSession = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getQuoteId', 'getStoreId', 'getStore', 'getQuote']) + ->getMock(); + /** @var Create|MockObject $create */ + $create = $this->getMockBuilder(Create::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var PriceCurrencyInterface|MockObject $priceCurrency */ + $priceCurrency = $this->getMockForAbstractClass(PriceCurrencyInterface::class); + /** @var EncoderInterface|MockObject $encoder */ + $encoder = $this->getMockForAbstractClass(EncoderInterface::class); + $encoder->method('encode') + ->willReturnCallback(function ($param) { + return json_encode($param); + }); + /** @var FormFactory|MockObject $formFactory */ + $formFactory = $this->getMockBuilder(FormFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + + $this->localeCurrency = $this->getMockForAbstractClass(CurrencyInterface::class); + /** @var Mapper|MockObject $addressMapper */ + $addressMapper = $this->getMockBuilder(Mapper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->block = new Form( + $context, + $this->quoteSession, + $create, + $priceCurrency, + $encoder, + $formFactory, + $this->customerRepository, + $this->localeCurrency, + $addressMapper + ); + } + + /** + * Checks if order contains all needed data. + */ + public function testGetOrderDataJson() + { + $customerId = 1; + $storeId = 1; + $quoteId = 2; + $expected = [ + 'customer_id' => $customerId, + 'addresses' => [], + 'store_id' => $storeId, + 'currency_symbol' => '$', + 'shipping_method_reseted' => false, + 'payment_method' => 'free', + 'quote_id' => $quoteId + ]; + + $this->storeManager->method('setCurrentStore') + ->with($storeId); + $this->quoteSession->method('getCustomerId') + ->willReturn($customerId); + $this->quoteSession->method('getStoreId') + ->willReturn($storeId); + $this->quoteSession->method('getQuoteId') + ->willReturn($quoteId); + + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $customer->method('getAddresses') + ->willReturn([]); + $this->customerRepository->method('getById') + ->with($customerId) + ->willReturn($customer); + + $this->withCurrencySymbol('$'); + + $this->withQuote(); + + self::assertEquals($expected, json_decode($this->block->getOrderDataJson(), true)); + } + + /** + * Configures mock object for currency. + * + * @param string $symbol + */ + private function withCurrencySymbol(string $symbol) + { + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->method('getCurrentCurrencyCode') + ->willReturn('USD'); + $this->quoteSession->method('getStore') + ->willReturn($store); + + $currency = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $currency->method('getSymbol') + ->willReturn($symbol); + $this->localeCurrency->method('getCurrency') + ->with('USD') + ->willReturn($currency); + } + + /** + * Configures shipping and payment mock objects. + */ + private function withQuote() + { + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteSession->method('getQuote') + ->willReturn($quote); + + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->getMock(); + $address->method('getShippingMethod') + ->willReturn('free'); + $quote->method('getShippingAddress') + ->willReturn($address); + + $payment = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->getMock(); + $payment->method('getMethod') + ->willReturn('free'); + $quote->method('getPayment') + ->willReturn($payment); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Items/GridTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Items/GridTest.php index e07fecf4cb116..5f0c66b7c77fe 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Items/GridTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Items/GridTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Create\Items; /** @@ -61,9 +59,8 @@ protected function setUp() { $orderCreateMock = $this->createPartialMock(\Magento\Sales\Model\AdminOrder\Create::class, ['__wakeup']); $taxData = $this->getMockBuilder(\Magento\Tax\Helper\Data::class)->disableOriginalConstructor()->getMock(); - $this->priceCurrency = $this->getMockBuilder( - \Magento\Framework\Pricing\PriceCurrencyInterface::class)->getMock( - ); + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->getMock(); $sessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session\Quote::class) ->disableOriginalConstructor() ->setMethods(['getQuote', '__wakeup']) @@ -100,9 +97,15 @@ protected function setUp() ->setMethods(['getStockItem', '__wakeup']) ->getMock(); - $this->stockItemMock = $this->createPartialMock(\Magento\CatalogInventory\Model\Stock\Item::class, ['getIsInStock', '__wakeup']); + $this->stockItemMock = $this->createPartialMock( + \Magento\CatalogInventory\Model\Stock\Item::class, + ['getIsInStock', '__wakeup'] + ); - $this->stockState = $this->createPartialMock(\Magento\CatalogInventory\Model\StockState::class, ['checkQuoteItemQty', '__wakeup']); + $this->stockState = $this->createPartialMock( + \Magento\CatalogInventory\Model\StockState::class, + ['checkQuoteItemQty', '__wakeup'] + ); $this->stockRegistry->expects($this->any()) ->method('getStockItem') @@ -218,8 +221,14 @@ public function testGetItems() $layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); $blockMock = $this->createPartialMock(\Magento\Framework\View\Element\AbstractBlock::class, ['getItems']); - $itemMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['getProduct', 'setHasError', 'setQty', 'getQty', '__sleep', '__wakeup', 'getChildren']); - $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getStockItem', 'getID', '__sleep', '__wakeup', 'getStatus']); + $itemMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Item::class, + ['getProduct', 'setHasError', 'setQty', 'getQty', '__sleep', '__wakeup', 'getChildren'] + ); + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getStockItem', 'getID', '__sleep', '__wakeup', 'getStatus'] + ); $checkMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getMessage', 'getHasError']); @@ -258,6 +267,9 @@ public function testGetItems() $this->assertEquals(true, $this->block->getQuote()->getIsSuperMode()); } + /** + * @return \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid + */ protected function getGrid() { /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid $grid */ @@ -266,8 +278,8 @@ protected function getGrid() [ 'context' => $this->objectManager->getObject( \Magento\Backend\Block\Template\Context::class, - ['layout' => $this->layoutMock] - ) + ['layout' => $this->layoutMock] + ) ] ); @@ -348,8 +360,14 @@ public function testGetItemRowTotalWithDiscountHtml() */ public function testGetSubtotalWithDiscount($orderData, $displayTotalsIncludeTax, $expected) { - $quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getSubtotal', 'getTaxAmount','getDiscountTaxCompensationAmount','getDiscountAmount']); - $gridMock = $this->createPartialMock(\Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid::class, ['getQuoteAddress','displayTotalsIncludeTax']); + $quoteAddressMock = $this->createPartialMock( + \Magento\Quote\Model\Quote\Address::class, + ['getSubtotal', 'getTaxAmount','getDiscountTaxCompensationAmount','getDiscountAmount'] + ); + $gridMock = $this->createPartialMock( + \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid::class, + ['getQuoteAddress','displayTotalsIncludeTax'] + ); $gridMock->expects($this->any())->method('getQuoteAddress') ->will($this->returnValue($quoteAddressMock)); diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebarTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebarTest.php index a52d8e1b7f9f9..7b94e769eff9a 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebarTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebarTest.php @@ -39,6 +39,9 @@ public function testGetItemQty($itemQty, $qty, $expectedValue) $this->assertEquals($expectedValue, $this->abstractSidebar->getItemQty($this->itemMock)); } + /** + * @return array + */ public function getItemQtyDataProvider() { return ['whenQtyIsset' => [2, 10, 10], 'whenQtyNotIsset' => [1, false, 1]]; diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/TotalsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/TotalsTest.php index bee757a6a7e4b..2ffa6464384f7 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/TotalsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/TotalsTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Create; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Invoice/ViewTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Invoice/ViewTest.php index 773694897291c..8f94ea8982ddf 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Invoice/ViewTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Invoice/ViewTest.php @@ -54,6 +54,9 @@ public function testIsPaymentReview($canReviewPayment, $canFetchUpdate, $expecte $this->assertEquals($expectedResult, $testMethod->invoke($block)); } + /** + * @return array + */ public function isPaymentReviewDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php index 2a839dd018dba..492cfee5f5d83 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php @@ -70,10 +70,10 @@ protected function setUp() $this->quoteMock->expects($this->any()) ->method('getBillingAddress') - ->willreturn($this->billingAddressMock); + ->willReturn($this->billingAddressMock); $this->quoteMock->expects($this->any()) ->method('getShippingAddress') - ->willreturn($this->shippingAddressMock); + ->willReturn($this->shippingAddressMock); $this->sessionQuoteMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); $this->totals = $this->helperManager->getObject( \Magento\Sales\Block\Adminhtml\Order\Create\Totals::class, @@ -88,7 +88,7 @@ public function testGetTotals($isVirtual) { $expected = 'expected'; $this->quoteMock->expects($this->at(1))->method('collectTotals'); - $this->quoteMock->expects($this->once())->method('isVirtual')->willreturn($isVirtual); + $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn($isVirtual); if ($isVirtual) { $this->billingAddressMock->expects($this->once())->method('getTotals')->willReturn($expected); } else { diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php index 1270db34e02ca..59ffe9d720180 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Block\Order\Email\Items; class DefaultItemsTest extends \PHPUnit\Framework\TestCase @@ -50,8 +48,8 @@ protected function setUp() [ 'context' => $this->objectManager->getObject( \Magento\Backend\Block\Template\Context::class, - ['layout' => $this->layoutMock] - ) + ['layout' => $this->layoutMock] + ) ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/Order/DefaultOrderTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/Order/DefaultOrderTest.php index e920f5e72b145..e1b98c3c6183b 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/Order/DefaultOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/Order/DefaultOrderTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Block\Order\Email\Items\Order; class DefaultOrderTest extends \PHPUnit\Framework\TestCase @@ -50,8 +48,8 @@ protected function setUp() [ 'context' => $this->objectManager->getObject( \Magento\Backend\Block\Template\Context::class, - ['layout' => $this->layoutMock] - ) + ['layout' => $this->layoutMock] + ) ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php index d36952e6aeee1..965a80ef189f6 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Test\Unit\Block\Order\Info\Buttons; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Rss\Signature; /** * Class RssTest @@ -43,6 +44,14 @@ class RssTest extends \PHPUnit\Framework\TestCase */ protected $scopeConfigInterface; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Signature + */ + private $signature; + + /** + * @inheritdoc + */ protected function setUp() { $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); @@ -50,6 +59,7 @@ protected function setUp() $this->urlBuilderInterface = $this->createMock(\Magento\Framework\App\Rss\UrlBuilderInterface::class); $this->scopeConfigInterface = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $request = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->signature = $this->createMock(Signature::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->rss = $this->objectManagerHelper->getObject( @@ -58,7 +68,8 @@ protected function setUp() 'request' => $request, 'orderFactory' => $this->orderFactory, 'rssUrlBuilder' => $this->urlBuilderInterface, - 'scopeConfig' => $this->scopeConfigInterface + 'scopeConfig' => $this->scopeConfigInterface, + 'signature' => $this->signature, ] ); } @@ -75,15 +86,17 @@ public function testGetLink() $order->expects($this->once())->method('getIncrementId')->will($this->returnValue('100000001')); $this->orderFactory->expects($this->once())->method('create')->will($this->returnValue($order)); - $data = base64_encode(json_encode(['order_id' => 1, 'increment_id' => '100000001', 'customer_id' => 1])); - $link = 'http://magento.com/rss/feed/index/type/order_status?data=' . $data; + $signature = '651932dfc862406b72628d95623bae5ea18242be757b3493b337942d61f834be'; + $this->signature->expects($this->once())->method('signData')->willReturn($signature); + $link = 'http://magento.com/rss/feed/index/type/order_status?data=' . $data .'&signature='.$signature; $this->urlBuilderInterface->expects($this->once())->method('getUrl') ->with([ 'type' => 'order_status', '_secure' => true, - '_query' => ['data' => $data], - ])->will($this->returnValue($link)); + '_query' => ['data' => $data, 'signature' => $signature], + ])->willReturn($link); + $this->assertEquals($link, $this->rss->getLink()); } diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php index 9561baf6bd5f4..5ab8ad0a3024e 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Item/Renderer/DefaultRendererTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Block\Order\Item\Renderer; class DefaultRendererTest extends \PHPUnit\Framework\TestCase @@ -50,8 +48,8 @@ protected function setUp() [ 'context' => $this->objectManager->getObject( \Magento\Backend\Block\Template\Context::class, - ['layout' => $this->layoutMock] - ) + ['layout' => $this->layoutMock] + ) ] ); @@ -60,18 +58,7 @@ protected function setUp() ->setMethods(['setItem', 'toHtml']) ->getMock(); - $itemMockMethods = [ - '__wakeup', - 'getRowTotal', - 'getTaxAmount', - 'getDiscountAmount', - 'getDiscountTaxCompensationAmount', - 'getWeeeTaxAppliedRowAmount', - ]; - $this->itemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->setMethods($itemMockMethods) - ->getMock(); + $this->itemMock = $this->createMock(\Magento\Sales\Model\Order\Item::class); } public function testGetItemPriceHtml() @@ -142,7 +129,11 @@ public function testGetTotalAmount() $discountAmount = 20; $weeeTaxAppliedRowAmount = 10; - $expectedResult = $rowTotal + $taxAmount + $discountTaxCompensationAmount - $discountAmount + $weeeTaxAppliedRowAmount; + $expectedResult = $rowTotal + + $taxAmount + + $discountTaxCompensationAmount + - $discountAmount + + $weeeTaxAppliedRowAmount; $this->itemMock->expects($this->once()) ->method('getRowTotal') ->will($this->returnValue($rowTotal)); @@ -161,4 +152,20 @@ public function testGetTotalAmount() $this->assertEquals($expectedResult, $this->block->getTotalAmount($this->itemMock)); } + + /** + * @return void + */ + public function testGetBaseTotalAmount() + { + $expectedBaseTotalAmount = 10; + + $this->itemMock->expects($this->once())->method('getBaseRowTotal')->willReturn(8); + $this->itemMock->expects($this->once())->method('getBaseTaxAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseDiscountTaxCompensationAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseWeeeTaxAppliedAmount')->willReturn(1); + $this->itemMock->expects($this->once())->method('getBaseDiscountAmount')->willReturn(1); + + $this->assertEquals($expectedBaseTotalAmount, $this->block->getBaseTotalAmount($this->itemMock)); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/RecentTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/RecentTest.php index 99528983a13c9..96162aca42e12 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/RecentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/RecentTest.php @@ -5,6 +5,15 @@ */ namespace Magento\Sales\Test\Unit\Block\Order; +use Magento\Framework\View\Element\Template\Context; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\Customer\Model\Session; +use Magento\Sales\Model\Order\Config; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\View\Layout; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; + class RecentTest extends \PHPUnit\Framework\TestCase { /** @@ -32,26 +41,33 @@ class RecentTest extends \PHPUnit\Framework\TestCase */ protected $orderConfig; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $storeManagerMock; + protected function setUp() { - $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->context = $this->createMock(Context::class); $this->orderCollectionFactory = $this->createPartialMock( - \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class, + CollectionFactory::class, ['create'] ); - $this->customerSession = $this->createPartialMock(\Magento\Customer\Model\Session::class, ['getCustomerId']); + $this->customerSession = $this->createPartialMock(Session::class, ['getCustomerId']); $this->orderConfig = $this->createPartialMock( - \Magento\Sales\Model\Order\Config::class, + Config::class, ['getVisibleOnFrontStatuses'] ); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); } public function testConstructMethod() { - $data = []; - $attribute = ['customer_id', 'status']; + $attribute = ['customer_id', 'store_id', 'status']; $customerId = 25; - $layout = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getBlock']); + $storeId = 4; + $layout = $this->createPartialMock(Layout::class, ['getBlock']); $this->context->expects($this->once()) ->method('getLayout') ->will($this->returnValue($layout)); @@ -64,14 +80,20 @@ public function testConstructMethod() ->method('getVisibleOnFrontStatuses') ->will($this->returnValue($statuses)); - $orderCollection = $this->createPartialMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class, [ - 'addAttributeToSelect', - 'addFieldToFilter', - 'addAttributeToFilter', - 'addAttributeToSort', - 'setPageSize', - 'load' - ]); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $storeMock = $this->getMockBuilder(StoreInterface::class)->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $storeMock->expects($this->any())->method('getId')->willReturn($storeId); + + $orderCollection = $this->createPartialMock(Collection::class, [ + 'addAttributeToSelect', + 'addFieldToFilter', + 'addAttributeToFilter', + 'addAttributeToSort', + 'setPageSize', + 'load' + ]); $this->orderCollectionFactory->expects($this->once()) ->method('create') ->will($this->returnValue($orderCollection)); @@ -85,17 +107,21 @@ public function testConstructMethod() ->willReturnSelf(); $orderCollection->expects($this->at(2)) ->method('addAttributeToFilter') - ->with($attribute[1], $this->equalTo(['in' => $statuses])) - ->will($this->returnSelf()); + ->with($attribute[1], $this->equalTo($storeId)) + ->willReturnSelf(); $orderCollection->expects($this->at(3)) + ->method('addAttributeToFilter') + ->with($attribute[2], $this->equalTo(['in' => $statuses])) + ->will($this->returnSelf()); + $orderCollection->expects($this->at(4)) ->method('addAttributeToSort') ->with('created_at', 'desc') ->will($this->returnSelf()); - $orderCollection->expects($this->at(4)) + $orderCollection->expects($this->at(5)) ->method('setPageSize') ->with('5') ->will($this->returnSelf()); - $orderCollection->expects($this->at(5)) + $orderCollection->expects($this->at(6)) ->method('load') ->will($this->returnSelf()); $this->block = new \Magento\Sales\Block\Order\Recent( @@ -103,7 +129,8 @@ public function testConstructMethod() $this->orderCollectionFactory, $this->customerSession, $this->orderConfig, - $data + [], + $this->storeManagerMock ); $this->assertEquals($orderCollection, $this->block->getOrders()); } diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/TotalsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/TotalsTest.php new file mode 100644 index 0000000000000..be0ae41347ba8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/TotalsTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Block\Order; + +use Magento\Framework\Registry; +use Magento\Sales\Block\Order\Totals; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Total; + +class TotalsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Sales\Block\Order\Totals + */ + protected $block; + + /** + * @var \Magento\Framework\View\Element\Template\Context|\PHPUnit_Framework_MockObject_MockObject + */ + protected $context; + + protected function setUp() + { + $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->block = new Totals($this->context, new Registry); + $this->block->setOrder($this->createMock(Order::class)); + } + + public function testApplySortOrder() + { + $this->block->addTotal(new Total(['code' => 'one']), 'last'); + $this->block->addTotal(new Total(['code' => 'two']), 'last'); + $this->block->addTotal(new Total(['code' => 'three']), 'last'); + $this->block->applySortOrder( + [ + 'one' => 10, + 'two' => 30, + 'three' => 20, + ] + ); + $this->assertEqualsSorted( + [ + 'one' => new Total(['code' => 'one']), + 'three' => new Total(['code' => 'three']), + 'two' => new Total(['code' => 'two']), + ], + $this->block->getTotals() + ); + } + + /** + * @param array $expected + * @param array $actual + */ + private function assertEqualsSorted(array $expected, array $actual) + { + $this->assertEquals($expected, $actual, 'Array contents should be equal.'); + $this->assertEquals(array_keys($expected), array_keys($actual), 'Array sort order should be equal.'); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php new file mode 100644 index 0000000000000..d72121878e350 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; + +/** + * Test for AddComment. + */ +class AddCommentTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Sales\Controller\Adminhtml\Order\AddComment + */ + private $addCommentController; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var \Magento\Sales\Model\Order\Status\History|\PHPUnit_Framework_MockObject_MockObject + */ + private $statusHistoryCommentMock; + + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->orderRepositoryMock = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class); + $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); + $this->resultRedirectFactoryMock = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); + $this->authorizationMock = $this->createMock(\Magento\Framework\AuthorizationInterface::class); + $this->statusHistoryCommentMock = $this->createMock(\Magento\Sales\Model\Order\Status\History::class); + $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->addCommentController = $objectManagerHelper->getObject( + \Magento\Sales\Controller\Adminhtml\Order\AddComment::class, + [ + 'context' => $this->contextMock, + 'orderRepository' => $this->orderRepositoryMock, + '_authorization' => $this->authorizationMock, + '_objectManager' => $this->objectManagerMock, + ] + ); + } + + /** + * Test for execute method with different data. + * + * @param array $historyData + * @param bool $userHasResource + * @param bool $expectedNotify + * + * @return void + * @dataProvider executeWillNotifyCustomerDataProvider + */ + public function testExecuteWillNotifyCustomer(array $historyData, bool $userHasResource, bool $expectedNotify) + { + $orderId = 30; + $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->requestMock->expects($this->once())->method('getPost')->with('history')->willReturn($historyData); + $this->authorizationMock->expects($this->any())->method('isAllowed')->willReturn($userHasResource); + $this->orderMock->expects($this->once()) + ->method('addStatusHistoryComment') + ->willReturn($this->statusHistoryCommentMock); + $this->statusHistoryCommentMock->expects($this->once())->method('setIsCustomerNotified')->with($expectedNotify); + $this->objectManagerMock->expects($this->once())->method('create')->willReturn( + $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderCommentSender::class) + ); + + $this->addCommentController->execute(); + } + + /** + * Data provider for testExecuteWillNotifyCustomer method. + * + * @return array + */ + public function executeWillNotifyCustomerDataProvider(): array + { + return [ + 'User Has Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => true, + ], + 'User Has Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => false, + ], + 'User Has Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing', + ], + 'userHasResource' => true, + 'expectedNotify' => false, + ], + 'User No Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + 'User No Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + 'User No Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing', + ], + 'userHasResource' => false, + 'expectedNotify' => false, + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php index 363f78e738f12..d37b0f6121b37 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php @@ -74,10 +74,12 @@ protected function setUp() ['setRedirect', 'sendResponse'] ); $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); + ->disableOriginalConstructor() + ->getMock(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->messageManager = $this->createPartialMock( \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addError'] + ['addSuccessMessage', 'addErrorMessage'] ); $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) ->disableOriginalConstructor() @@ -101,8 +103,6 @@ protected function setUp() \Magento\Sales\Controller\Adminhtml\Order\Cancel::class, [ 'context' => $this->context, - 'request' => $this->request, - 'response' => $this->response, 'orderRepository' => $this->orderRepositoryMock ] ); @@ -117,7 +117,7 @@ public function testExecuteNotPost() ->method('isPost') ->willReturn(false); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('You have not canceled the item.'); $this->resultRedirect->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ProcessDataTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ProcessDataTest.php index 2b6436395a0cf..ab4bf5fbbe491 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ProcessDataTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ProcessDataTest.php @@ -235,6 +235,9 @@ public function testExecute($noDiscount, $couponCode, $errorMessage, $actualCoup $this->assertInstanceOf(\Magento\Backend\Model\View\Result\Forward::class, $this->processData->execute()); } + /** + * @return array + */ public function isApplyDiscountDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ReorderTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ReorderTest.php index 1fdd9759f5045..8203b4486d193 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -118,7 +118,9 @@ protected function setUp() ->getMock(); $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass(); $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)->getMockForAbstractClass(); - $this->resultForwardFactoryMock = $this->getMockBuilder(ForwardFactory::class)->getMock(); + $this->resultForwardFactoryMock = $this->getMockBuilder(ForwardFactory::class) + ->disableOriginalConstructor() + ->getMock(); $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/PrintActionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/PrintActionTest.php index 881af16f5fe69..e12a4195db4c6 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/PrintActionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/PrintActionTest.php @@ -155,7 +155,8 @@ public function testExecute() $creditmemoId = 2; $date = '2015-01-19_13-03-45'; $fileName = 'creditmemo2015-01-19_13-03-45.pdf'; - $fileContents = 'pdf0123456789'; + $pdfContent = 'pdf0123456789'; + $fileData = ['type' => 'string', 'value' => $pdfContent, 'rm' => true]; $this->prepareTestExecute($creditmemoId); $this->objectManagerMock->expects($this->any()) @@ -184,12 +185,12 @@ public function testExecute() ->willReturn($date); $this->pdfMock->expects($this->once()) ->method('render') - ->willReturn($fileContents); + ->willReturn($pdfContent); $this->fileFactoryMock->expects($this->once()) ->method('create') ->with( $fileName, - $fileContents, + $fileData, \Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR, 'application/pdf' ) diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php index 71a474c390de3..648778207afc8 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Creditmemo; /** @@ -77,8 +75,10 @@ protected function setUp() $this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); $this->_responseMock->headersSentThrowsException = false; $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $constructArguments = $objectManager->getConstructArguments(\Magento\Backend\Model\Session::class, + $constructArguments = $objectManager->getConstructArguments( + \Magento\Backend\Model\Session::class, ['storage' => new \Magento\Framework\Session\Storage()] ); $this->_sessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session::class) @@ -86,7 +86,8 @@ protected function setUp() ->setConstructorArgs($constructArguments) ->getMock(); $this->resultForwardFactoryMock = $this->getMockBuilder( - \Magento\Backend\Model\View\Result\ForwardFactory::class) + \Magento\Backend\Model\View\Result\ForwardFactory::class + ) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -94,7 +95,8 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->resultRedirectFactoryMock = $this->getMockBuilder( - \Magento\Backend\Model\View\Result\RedirectFactory::class) + \Magento\Backend\Model\View\Result\RedirectFactory::class + ) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -152,7 +154,10 @@ public function testSaveActionOnlineRefundToStoreCredit() ); $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValue(null)); - $creditmemoMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo::class, ['load', 'getGrandTotal', '__wakeup']); + $creditmemoMock = $this->createPartialMock( + \Magento\Sales\Model\Order\Creditmemo::class, + ['load', 'getGrandTotal', '__wakeup'] + ); $creditmemoMock->expects($this->once())->method('getGrandTotal')->will($this->returnValue('1')); $this->memoLoaderMock->expects( $this->once() @@ -175,7 +180,7 @@ public function testSaveActionOnlineRefundToStoreCredit() ); $this->assertInstanceOf( - \Magento\Backend\Model\View\Result\Redirect::class, + \Magento\Backend\Model\View\Result\Redirect::class, $this->_controller->execute() ); } @@ -197,9 +202,11 @@ public function testSaveActionWithNegativeCreditmemo() ); $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValue(null)); - $creditmemoMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo::class, ['load', 'getGrandTotal', 'getAllowZeroGrandTotal', '__wakeup']); - $creditmemoMock->expects($this->once())->method('getGrandTotal')->will($this->returnValue('0')); - $creditmemoMock->expects($this->once())->method('getAllowZeroGrandTotal')->will($this->returnValue(false)); + $creditmemoMock = $this->createPartialMock( + \Magento\Sales\Model\Order\Creditmemo::class, + ['load', 'isValidGrandTotal', '__wakeup'] + ); + $creditmemoMock->expects($this->once())->method('isValidGrandTotal')->willReturn(false); $this->memoLoaderMock->expects( $this->once() )->method( @@ -228,7 +235,7 @@ public function testSaveActionWithNegativeCreditmemo() */ protected function _setSaveActionExpectationForMageCoreException($data, $errorMessage) { - $this->_messageManager->expects($this->once())->method('addError')->with($this->equalTo($errorMessage)); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($this->equalTo($errorMessage)); $this->_sessionMock->expects($this->once())->method('setFormData')->with($this->equalTo($data)); } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php index e2554eefb9b4e..47c6aea844aca 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php @@ -115,6 +115,7 @@ protected function setUp() $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() ->getMock(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php index 899e3defc19a8..90374a2597a02 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php @@ -89,8 +89,8 @@ protected function setUp() ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() - ->setMethods([]) ->getMock(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) ->disableOriginalConstructor() ->getMock(); @@ -191,14 +191,13 @@ public function testExecute() $invoiceData = ['comment_text' => 'test']; $response = 'test data'; - $this->requestMock->expects($this->at(0)) - ->method('getParam') - ->with('order_id') - ->will($this->returnValue($orderId)); - $this->requestMock->expects($this->at(1)) - ->method('getParam') - ->with('invoice', []) - ->will($this->returnValue($invoiceData)); + $this->requestMock->expects($this->any())->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['invoice', [], $invoiceData], + ] + ); $invoiceMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Invoice::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php index 569a210d993f0..53f2a070ead31 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php @@ -85,6 +85,11 @@ class MassCancelTest extends \PHPUnit\Framework\TestCase */ protected $filterMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderManagementMock; + protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); @@ -124,6 +129,7 @@ protected function setUp() ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) ->willReturn($redirectMock); + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); @@ -145,12 +151,15 @@ protected function setUp() $this->orderCollectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); + $this->massAction = $objectManagerHelper->getObject( \Magento\Sales\Controller\Adminhtml\Order\MassCancel::class, [ 'context' => $this->contextMock, 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock + 'collectionFactory' => $this->orderCollectionFactoryMock, + 'orderManagement' => $this->orderManagementMock ] ); } @@ -161,6 +170,9 @@ protected function setUp() */ public function testExecuteCanCancelOneOrder() { + $order1id = 100; + $order2id = 200; + $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() ->getMock(); @@ -175,20 +187,19 @@ public function testExecuteCanCancelOneOrder() ->willReturn($orders); $order1->expects($this->once()) - ->method('canCancel') - ->willReturn(true); - $order1->expects($this->once()) - ->method('cancel'); - $order1->expects($this->once()) - ->method('save'); + ->method('getEntityId') + ->willReturn($order1id); + + $order2->expects($this->once()) + ->method('getEntityId') + ->willReturn($order2id); $this->orderCollectionMock->expects($this->once()) ->method('count') ->willReturn($countOrders); - $order2->expects($this->once()) - ->method('canCancel') - ->willReturn(false); + $this->orderManagementMock->expects($this->at(0))->method('cancel')->with($order1id)->willReturn(true); + $this->orderManagementMock->expects($this->at(1))->method('cancel')->with($order2id)->willReturn(false); $this->messageManagerMock->expects($this->once()) ->method('addError') @@ -222,21 +233,23 @@ public function testExcludedCannotCancelOrders() $orders = [$order1, $order2]; $countOrders = count($orders); + $order1->expects($this->once()) + ->method('getEntityId') + ->willReturn(100); + + $order2->expects($this->once()) + ->method('getEntityId') + ->willReturn(200); + $this->orderCollectionMock->expects($this->any()) ->method('getItems') ->willReturn([$order1, $order2]); - $order1->expects($this->once()) - ->method('canCancel') - ->willReturn(false); - $this->orderCollectionMock->expects($this->once()) ->method('count') ->willReturn($countOrders); - $order2->expects($this->once()) - ->method('canCancel') - ->willReturn(false); + $this->orderManagementMock->expects($this->atLeastOnce())->method('cancel')->willReturn(false); $this->messageManagerMock->expects($this->once()) ->method('addError') @@ -265,11 +278,10 @@ public function testException() ->willReturn([$order1]); $order1->expects($this->once()) - ->method('canCancel') - ->willReturn(true); - $order1->expects($this->once()) - ->method('cancel') - ->willThrowException($exception); + ->method('getEntityId') + ->willReturn(100); + + $this->orderManagementMock->expects($this->atLeastOnce())->method('cancel')->willThrowException($exception); $this->messageManagerMock->expects($this->once()) ->method('addError') diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php deleted file mode 100644 index 02ff208445596..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php +++ /dev/null @@ -1,253 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassHoldTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassHoldTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Controller\Adminhtml\Order\MassHold - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject - */ - protected $actionFlagMock; - - /** - * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \Magento\Sales\Api\OrderManagementInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderManagementMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - $this->orderManagementMock = $this->getMockBuilder(\Magento\Sales\Api\OrderManagementInterface::class) - ->getMockForAbstractClass(); - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create'] - ); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->orderCollectionMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $orderCollection = \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class; - $this->orderCollectionFactoryMock = $this->getMockBuilder($orderCollection) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $this->sessionMock = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice']); - $this->actionFlagMock = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get', 'set']); - $this->helperMock = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->contextMock->expects($this->once())->method('getActionFlag')->willReturn($this->actionFlagMock); - $this->contextMock->expects($this->once())->method('getHelper')->willReturn($this->helperMock); - $this->contextMock - ->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->orderCollectionMock) - ->willReturn($this->orderCollectionMock); - $this->orderCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->orderCollectionMock); - - $this->massAction = $objectManagerHelper->getObject( - \Magento\Sales\Controller\Adminhtml\Order\MassHold::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock, - 'orderManagement' => $this->orderManagementMock - ] - ); - } - - public function testExecuteOneOrderPutOnHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canHold') - ->willReturn(true); - $this->orderManagementMock->expects($this->once()) - ->method('hold'); - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $order2->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('1 order(s) were not put on hold.'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with('You have put 1 order(s) on hold.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - public function testExecuteNoOrdersPutOnHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $order2->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('No order(s) were put on hold.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php deleted file mode 100644 index 7003d445e1b50..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php +++ /dev/null @@ -1,244 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassHoldTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassUnholdTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Controller\Adminhtml\Order\MassUnhold - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject - */ - protected $actionFlagMock; - - /** - * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - - $this->orderCollectionMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $orderCollection = \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class; - $this->orderCollectionFactoryMock = $this->getMockBuilder($orderCollection) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->sessionMock = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice']); - $this->actionFlagMock = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get', 'set']); - $this->helperMock = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $this->contextMock->expects($this->once())->method('getMessageManager')->willReturn($this->messageManagerMock); - $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->once())->method('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->contextMock->expects($this->once())->method('getActionFlag')->willReturn($this->actionFlagMock); - $this->contextMock->expects($this->once())->method('getHelper')->willReturn($this->helperMock); - $this->contextMock - ->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->orderCollectionMock) - ->willReturn($this->orderCollectionMock); - $this->orderCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->orderCollectionMock); - - $this->massAction = $objectManagerHelper->getObject( - \Magento\Sales\Controller\Adminhtml\Order\MassUnhold::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock - ] - ); - } - - public function testExecuteOneOrdersReleasedFromHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canUnhold') - ->willReturn(true); - $order1->expects($this->once()) - ->method('unhold'); - $order1->expects($this->once()) - ->method('save'); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn(count($orders)); - - $order2->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('1 order(s) were not released from on hold status.'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with('1 order(s) have been released from on hold status.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - public function testExecuteNoReleasedOrderFromHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn(count($orders)); - - $order2->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with('No order(s) were released from on hold status.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php index 19cbec9d1a06b..d0d7e7efa97f7 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php @@ -216,6 +216,9 @@ public function testExecute($itemOptionValues, $productOptionValues, $noRouteOcc $this->objectMock->execute(); } + /** + * @return array + */ public function executeDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Guest/ViewTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Guest/ViewTest.php index 2b35f4f98c3cf..a1daffcdd2f18 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Guest/ViewTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Guest/ViewTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Test\Unit\Controller\Guest; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; class ViewTest extends \PHPUnit\Framework\TestCase @@ -49,13 +50,19 @@ class ViewTest extends \PHPUnit\Framework\TestCase */ protected $resultPageMock; + /** + * @var Validator|\PHPUnit_Framework_MockObject_MockObject + */ + private $formKeyValidatorMock; + /** * @return void */ protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->getMock(); + ->setMethods(['isPost']) + ->getMockForAbstractClass(); $this->guestHelperMock = $this->getMockBuilder(\Magento\Sales\Helper\Guest::class) ->disableOriginalConstructor() ->getMock(); @@ -69,6 +76,13 @@ protected function setUp() $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->disableOriginalConstructor() ->getMock(); + $this->formKeyValidatorMock = $this->createMock(Validator::class); + + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); + $this->formKeyValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->requestMock) + ->willReturn(true); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->context = $this->objectManagerHelper->getObject( @@ -82,7 +96,8 @@ protected function setUp() [ 'context' => $this->context, 'guestHelper' => $this->guestHelperMock, - 'resultPageFactory' => $this->resultPageFactoryMock + 'resultPageFactory' => $this->resultPageFactoryMock, + 'formKeyValidator' => $this->formKeyValidatorMock, ] ); } diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php index fd20e0cb7d94a..e424cae85f223 100644 --- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php +++ b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php @@ -70,6 +70,9 @@ public function testExecute($lifetimes, $additionalFilterFields) $this->observer->execute(); } + /** + * @return array + */ public function cleanExpiredQuotesDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php b/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php index 0802bafd5ac34..e14da1ac1b5ab 100644 --- a/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php +++ b/app/code/Magento/Sales/Test/Unit/CustomerData/LastOrderedItemsTest.php @@ -48,11 +48,21 @@ class LastOrderedItemsTest extends \PHPUnit\Framework\TestCase */ private $orderMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + /** * @var \Magento\Sales\CustomerData\LastOrderedItems */ private $section; + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -74,64 +84,101 @@ protected function setUp() $this->orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() ->getMock(); + $this->productRepository = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->getMockForAbstractClass(); $this->section = new \Magento\Sales\CustomerData\LastOrderedItems( $this->orderCollectionFactoryMock, $this->orderConfigMock, $this->customerSessionMock, $this->stockRegistryMock, - $this->storeManagerMock + $this->storeManagerMock, + $this->productRepository, + $this->loggerMock ); } public function testGetSectionData() { + $storeId = 1; $websiteId = 4; - $expectedItem = [ + $expectedItem1 = [ 'id' => 1, - 'name' => 'Product Name', + 'name' => 'Product Name 1', 'url' => 'http://example.com', 'is_saleable' => true, ]; - $productId = 10; + $expectedItem2 = [ + 'id' => 2, + 'name' => 'Product Name 2', + 'url' => null, + 'is_saleable' => true, + ]; + $productIdVisible = 1; + $productIdNotVisible = 2; $stockItemMock = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) ->getMockForAbstractClass(); - $itemWithProductMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + $itemWithVisibleProduct = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) ->disableOriginalConstructor() ->getMock(); - $itemWithoutProductMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + $itemWithNotVisibleProduct = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) ->disableOriginalConstructor() ->getMock(); - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $productVisible = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) ->disableOriginalConstructor() ->getMock(); - $items = [$itemWithoutProductMock, $itemWithProductMock]; + $productNotVisible = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + $items = [$itemWithVisibleProduct, $itemWithNotVisibleProduct]; $this->getLastOrderMock(); $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->any())->method('getWebsiteId')->willReturn($websiteId); + $storeMock->expects($this->any())->method('getId')->willReturn($storeId); $this->orderMock->expects($this->once()) ->method('getParentItemsRandomCollection') ->with(\Magento\Sales\CustomerData\LastOrderedItems::SIDEBAR_ORDER_LIMIT) ->willReturn($items); - $itemWithProductMock->expects($this->once())->method('hasData')->with('product')->willReturn(true); - $itemWithProductMock->expects($this->any())->method('getProduct')->willReturn($productMock); - $productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1, 4]); - $itemWithProductMock->expects($this->once())->method('getId')->willReturn($expectedItem['id']); - $itemWithProductMock->expects($this->once())->method('getName')->willReturn($expectedItem['name']); - $productMock->expects($this->once())->method('getProductUrl')->willReturn($expectedItem['url']); - $this->stockRegistryMock->expects($this->once())->method('getStockItem')->willReturn($stockItemMock); - $productMock->expects($this->once())->method('getId')->willReturn($productId); - $itemWithProductMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $productVisible->expects($this->once())->method('isVisibleInSiteVisibility')->willReturn(true); + $productVisible->expects($this->once())->method('getProductUrl')->willReturn($expectedItem1['url']); + $productVisible->expects($this->once())->method('getWebsiteIds')->willReturn([1, 4]); + $productVisible->expects($this->once())->method('getId')->willReturn($productIdVisible); + $productNotVisible->expects($this->once())->method('isVisibleInSiteVisibility')->willReturn(false); + $productNotVisible->expects($this->never())->method('getProductUrl'); + $productNotVisible->expects($this->once())->method('getWebsiteIds')->willReturn([1, 4]); + $productNotVisible->expects($this->once())->method('getId')->willReturn($productIdNotVisible); + $itemWithVisibleProduct->expects($this->once())->method('getProductId')->willReturn($productIdVisible); + $itemWithVisibleProduct->expects($this->once())->method('getProduct')->willReturn($productVisible); + $itemWithVisibleProduct->expects($this->once())->method('getId')->willReturn($expectedItem1['id']); + $itemWithVisibleProduct->expects($this->once())->method('getName')->willReturn($expectedItem1['name']); + $itemWithVisibleProduct->expects($this->once())->method('getStore')->willReturn($storeMock); + $itemWithNotVisibleProduct->expects($this->once())->method('getProductId')->willReturn($productIdNotVisible); + $itemWithNotVisibleProduct->expects($this->once())->method('getProduct')->willReturn($productNotVisible); + $itemWithNotVisibleProduct->expects($this->once())->method('getId')->willReturn($expectedItem2['id']); + $itemWithNotVisibleProduct->expects($this->once())->method('getName')->willReturn($expectedItem2['name']); + $itemWithNotVisibleProduct->expects($this->once())->method('getStore')->willReturn($storeMock); + $this->productRepository->expects($this->any()) + ->method('getById') + ->willReturnMap([ + [$productIdVisible, false, $storeId, false, $productVisible], + [$productIdNotVisible, false, $storeId, false, $productNotVisible], + ]); $this->stockRegistryMock - ->expects($this->once()) + ->expects($this->any()) ->method('getStockItem') - ->with($productId, $websiteId) - ->willReturn($stockItemMock); - $stockItemMock->expects($this->once())->method('getIsInStock')->willReturn($expectedItem['is_saleable']); - $itemWithoutProductMock->expects($this->once())->method('hasData')->with('product')->willReturn(false); - $this->assertEquals(['items' => [$expectedItem]], $this->section->getSectionData()); + ->willReturnMap([ + [$productIdVisible, $websiteId, $stockItemMock], + [$productIdNotVisible, $websiteId, $stockItemMock], + ]); + $stockItemMock->expects($this->exactly(2))->method('getIsInStock')->willReturn($expectedItem1['is_saleable']); + $this->assertEquals(['items' => [$expectedItem1, $expectedItem2]], $this->section->getSectionData()); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getLastOrderMock() { $customerId = 1; @@ -160,4 +207,34 @@ private function getLastOrderMock() ->willReturnSelf(); return $this->orderMock; } + + public function testGetSectionDataWithNotExistingProduct() + { + $storeId = 1; + $websiteId = 4; + $productId = 1; + $exception = new \Magento\Framework\Exception\NoSuchEntityException(__("Product doesn't exist")); + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->setMethods(['getProductId']) + ->getMock(); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); + + $this->getLastOrderMock(); + $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); + $storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->orderMock->expects($this->once()) + ->method('getParentItemsRandomCollection') + ->with(\Magento\Sales\CustomerData\LastOrderedItems::SIDEBAR_ORDER_LIMIT) + ->willReturn([$orderItemMock]); + $orderItemMock->expects($this->once())->method('getProductId')->willReturn($productId); + $this->productRepository->expects($this->once()) + ->method('getById') + ->with($productId, false, $storeId) + ->willThrowException($exception); + $this->loggerMock->expects($this->once())->method('critical')->with($exception); + + $this->assertEquals(['items' => []], $this->section->getSectionData()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php b/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php index 180cfca0bc3cb..ae876df8e7035 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php @@ -71,7 +71,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( + $this->adminHelper = (new ObjectManager($this))->getObject( \Magento\Sales\Helper\Admin::class, [ 'context' => $this->contextMock, @@ -198,6 +198,9 @@ public function testDisplayPriceAttribute( ); } + /** + * @return array + */ public function displayPricesDataProvider() { return [ @@ -310,6 +313,9 @@ public function testApplySalableProductTypesFilter($itemKey, $type, $calledTimes $this->adminHelper->applySalableProductTypesFilter($collectionMock); } + /** + * @return array + */ public function applySalableProductTypesFilterDataProvider() { return [ @@ -324,72 +330,16 @@ public function applySalableProductTypesFilterDataProvider() } /** - * @param string $data - * @param string $expected - * @param null|array $allowedTags - * @dataProvider escapeHtmlWithLinksDataProvider + * @return void */ - public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) + public function testEscapeHtmlWithLinks() { + $expected = '<a>some text in tags</a>'; $this->escaperMock ->expects($this->any()) ->method('escapeHtml') ->will($this->returnValue($expected)); - $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); + $actual = $this->adminHelper->escapeHtmlWithLinks('<a>some text in tags</a>'); $this->assertEquals($expected, $actual); } - - /** - * @return array - */ - public function escapeHtmlWithLinksDataProvider() - { - return [ - [ - '<a>some text in tags</a>', - '<a>some text in tags</a>', - 'allowedTags' => null - ], - [ - 'Transaction ID: "<a target="_blank" href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', - 'Transaction ID: "<a target="_blank" href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', - 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] - ], - [ - '<a>some text in tags</a>', - '<a>some text in tags</a>', - 'allowedTags' => ['a'] - ], - 'Not replacement with placeholders' => [ - "<a><script>alert(1)</script></a>", - '<a><script>alert(1)</script></a>', - 'allowedTags' => ['a'] - ], - 'Normal usage, url escaped' => [ - '<a href=\"#\">Foo</a>', - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Normal usage, url not escaped' => [ - "<a href=http://example.com?foo=1&bar=2&baz[name]=BAZ>Foo</a>", - '<a href="http://example.com?foo=1&bar=2&baz[name]=BAZ">Foo</a>', - 'allowedTags' => ['a'] - ], - 'XSS test' => [ - "<a href=\"javascript:alert(59)\">Foo</a>", - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Additional regex test' => [ - "<a href=\"http://example1.com\" href=\"http://example2.com\">Foo</a>", - '<a href="http://example1.com">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Break of valid urls' => [ - "<a href=\"http://example.com?foo=text with space\">Foo</a>", - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - ]; - } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php index a265d39bafd93..4321e28d28397 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php @@ -4,12 +4,32 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\AdminOrder; +use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\AttributeMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Customer\Model\Customer\Mapper; +use Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\Updater; +use Magento\Sales\Model\AdminOrder\Create; use Magento\Sales\Model\AdminOrder\Product; +use Magento\Quote\Model\QuoteFactory; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; +use Magento\Store\Api\Data\StoreInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,162 +39,143 @@ class CreateTest extends \PHPUnit\Framework\TestCase { const CUSTOMER_ID = 1; - /** @var \Magento\Sales\Model\AdminOrder\Create */ - protected $adminOrderCreate; - - /** @var \Magento\Backend\Model\Session\Quote|\PHPUnit_Framework_MockObject_MockObject */ - protected $sessionQuoteMock; - - /** @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $formFactoryMock; - - /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerFactoryMock; - - /** @var \Magento\Quote\Model\Quote\Item\Updater|\PHPUnit_Framework_MockObject_MockObject */ - protected $itemUpdater; - - /** @var \Magento\Customer\Model\Customer\Mapper|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerMapper; + /** + * @var Create + */ + private $adminOrderCreate; /** - * @var Product\Quote\Initializer|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Api\CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $quoteInitializerMock; + private $quoteRepository; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\QuoteFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRepositoryMock; + private $quoteFactory; /** - * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var SessionQuote|MockObject */ - protected $addressRepositoryMock; + private $sessionQuote; /** - * @var \Magento\Customer\Api\Data\AddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var FormFactory|MockObject */ - protected $addressFactoryMock; + private $formFactory; /** - * @var \Magento\Customer\Api\GroupRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CustomerInterfaceFactory|MockObject */ - protected $groupRepositoryMock; + private $customerFactory; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Updater|MockObject */ - protected $scopeConfigMock; + private $itemUpdater; /** - * @var \Magento\Sales\Model\AdminOrder\EmailSender|\PHPUnit_Framework_MockObject_MockObject + * @var Mapper|MockObject */ - protected $emailSenderMock; + private $customerMapper; /** - * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var GroupRepositoryInterface|MockObject */ - protected $accountManagementMock; + private $groupRepository; /** - * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + * @var DataObjectHelper|MockObject */ - protected $dataObjectHelper; + private $dataObjectHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Order|MockObject */ - protected $objectFactory; + private $orderMock; /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @inheritdoc */ protected function setUp() { - $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $registryMock = $this->createMock(\Magento\Framework\Registry::class); - $configMock = $this->createMock(\Magento\Sales\Model\Config::class); - $this->sessionQuoteMock = $this->createMock(\Magento\Backend\Model\Session\Quote::class); - $loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $copyMock = $this->createMock(\Magento\Framework\DataObject\Copy::class); - $messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->formFactoryMock = $this->createPartialMock(\Magento\Customer\Model\Metadata\FormFactory::class, ['create']); - $this->customerFactoryMock = $this->createPartialMock(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class, ['create']); - - $this->itemUpdater = $this->createMock(\Magento\Quote\Model\Quote\Item\Updater::class); - - $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class) + $this->formFactory = $this->createPartialMock(FormFactory::class, ['create']); + $this->quoteFactory = $this->createPartialMock(QuoteFactory::class, ['create']); + $this->customerFactory = $this->createPartialMock(CustomerInterfaceFactory::class, ['create']); + + $this->itemUpdater = $this->createMock(Updater::class); + + $this->quoteRepository = $this->getMockBuilder(\Magento\Quote\Api\CartRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getForCustomer']) + ->getMockForAbstractClass(); + + $this->sessionQuote = $this->getMockBuilder(\Magento\Backend\Model\Session\Quote::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods( + [ + 'getQuote', + 'getStoreId', + 'getCustomerId', + 'setData', + 'setCurrencyId', + 'setCustomerId', + 'setStoreId', + 'setCustomerGroupId', + 'getData', + 'getStore', + 'getUseOldShippingMethod', + ] + ) ->getMock(); - $this->customerMapper = $this->getMockBuilder( - \Magento\Customer\Model\Customer\Mapper::class - )->setMethods(['toFlatArray'])->disableOriginalConstructor()->getMock(); + $storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $this->sessionQuote->method('getStore') + ->willReturn($storeMock); - $this->quoteInitializerMock = $this->createMock(\Magento\Sales\Model\AdminOrder\Product\Quote\Initializer::class); - $this->customerRepositoryMock = $this->getMockForAbstractClass( - \Magento\Customer\Api\CustomerRepositoryInterface::class, - [], - '', - false - ); - $this->addressRepositoryMock = $this->getMockForAbstractClass( - \Magento\Customer\Api\AddressRepositoryInterface::class, - [], - '', - false - ); - $this->addressFactoryMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); - $this->groupRepositoryMock = $this->getMockForAbstractClass( - \Magento\Customer\Api\GroupRepositoryInterface::class, - [], - '', - false - ); - $this->scopeConfigMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Config\ScopeConfigInterface::class, - [], - '', - false - ); - $this->emailSenderMock = $this->createMock(\Magento\Sales\Model\AdminOrder\EmailSender::class); - $this->accountManagementMock = $this->getMockForAbstractClass( - \Magento\Customer\Api\AccountManagementInterface::class, - [], - '', - false - ); - $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) + $this->customerMapper = $this->getMockBuilder(Mapper::class) + ->setMethods(['toFlatArray']) + ->disableOriginalConstructor() + ->getMock(); + + $this->groupRepository = $this->getMockForAbstractClass(GroupRepositoryInterface::class); + $this->dataObjectHelper = $this->getMockBuilder(DataObjectHelper::class) ->disableOriginalConstructor() ->getMock(); + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getEntityId', + 'getId', + 'setReordered', + 'getReordered', + 'getOrderCurrencyCode', + 'getCustomerGroupId', + 'getItemsCollection', + 'getShippingAddress', + 'getBillingAddress', + 'getCouponCode', + ] + ) + ->getMock(); + $objectManagerHelper = new ObjectManagerHelper($this); $this->adminOrderCreate = $objectManagerHelper->getObject( - \Magento\Sales\Model\AdminOrder\Create::class, + Create::class, [ - 'objectManager' => $objectManagerMock, - 'eventManager' => $eventManagerMock, - 'coreRegistry' => $registryMock, - 'salesConfig' => $configMock, - 'quoteSession' => $this->sessionQuoteMock, - 'logger' => $loggerMock, - 'objectCopyService' => $copyMock, - 'messageManager' => $messageManagerMock, - 'quoteInitializer' => $this->quoteInitializerMock, - 'customerRepository' => $this->customerRepositoryMock, - 'addressRepository' => $this->addressRepositoryMock, - 'addressFactory' => $this->addressFactoryMock, - 'metadataFormFactory' => $this->formFactoryMock, - 'customerFactory' => $this->customerFactoryMock, - 'groupRepository' => $this->groupRepositoryMock, + 'quoteSession' => $this->sessionQuote, + 'metadataFormFactory' => $this->formFactory, + 'customerFactory' => $this->customerFactory, + 'groupRepository' => $this->groupRepository, 'quoteItemUpdater' => $this->itemUpdater, 'customerMapper' => $this->customerMapper, - 'objectFactory' => $this->objectFactory, - 'accountManagement' => $this->accountManagementMock, 'dataObjectHelper' => $this->dataObjectHelper, + 'quoteRepository' => $this->quoteRepository, + 'quoteFactory' => $this->quoteFactory, ] ); } @@ -188,64 +189,61 @@ public function testSetAccountData() ]; $attributeMocks = []; - foreach ($attributes as $attribute) { - $attributeMock = $this->createMock(\Magento\Customer\Api\Data\AttributeMetadataInterface::class); - - $attributeMock->expects($this->any())->method('getAttributeCode')->will($this->returnValue($attribute[0])); + foreach ($attributes as $value) { + $attribute = $this->createMock(AttributeMetadataInterface::class); + $attribute->method('getAttributeCode') + ->willReturn($value[0]); - $attributeMocks[] = $attributeMock; + $attributeMocks[] = $attribute; } - $customerGroupMock = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\GroupInterface::class, - [], - '', - false, - true, - true, - ['getTaxClassId'] - ); - $customerGroupMock->expects($this->once())->method('getTaxClassId')->will($this->returnValue($taxClassId)); - $customerFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); - $customerFormMock->expects($this->any()) - ->method('getAttributes') - ->will($this->returnValue([$attributeMocks[1]])); - $customerFormMock->expects($this->any())->method('extractData')->will($this->returnValue([])); - $customerFormMock->expects($this->any())->method('restoreData')->will($this->returnValue(['group_id' => 1])); - - $customerFormMock->expects($this->any()) - ->method('prepareRequest') - ->will($this->returnValue($this->createMock(\Magento\Framework\App\RequestInterface::class))); - - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerMapper->expects($this->atLeastOnce()) + $customerGroup = $this->getMockForAbstractClass(GroupInterface::class); + $customerGroup->method('getTaxClassId') + ->willReturn($taxClassId); + $customerForm = $this->createMock(Form::class); + $customerForm->method('getAttributes') + ->willReturn([$attributeMocks[1]]); + $customerForm + ->method('extractData') + ->willReturn([]); + $customerForm + ->method('restoreData') + ->willReturn(['group_id' => 1]); + + $customerForm->method('prepareRequest') + ->willReturn($this->createMock(RequestInterface::class)); + + $customer = $this->createMock(CustomerInterface::class); + $this->customerMapper->expects(self::atLeastOnce()) ->method('toFlatArray') ->willReturn(['group_id' => 1]); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $quoteMock->expects($this->any())->method('getCustomer')->will($this->returnValue($customerMock)); - $quoteMock->expects($this->once()) - ->method('addData') + $quote = $this->createMock(Quote::class); + $quote->method('getCustomer')->willReturn($customer); + $quote->method('addData') ->with( - [ - 'customer_group_id' => $attributes[1][1], - 'customer_tax_class_id' => $taxClassId - ] - ); - $this->dataObjectHelper->expects($this->once()) - ->method('populateWithArray') + [ + 'customer_group_id' => $attributes[1][1], + 'customer_tax_class_id' => $taxClassId, + ] + ); + $this->dataObjectHelper->method('populateWithArray') ->with( - $customerMock, - ['group_id' => 1], \Magento\Customer\Api\Data\CustomerInterface::class + $customer, + ['group_id' => 1], + CustomerInterface::class ); - $this->formFactoryMock->expects($this->any())->method('create')->will($this->returnValue($customerFormMock)); - $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock)); - $this->customerFactoryMock->expects($this->any())->method('create')->will($this->returnValue($customerMock)); + $this->formFactory->method('create') + ->willReturn($customerForm); + $this->sessionQuote + ->method('getQuote') + ->willReturn($quote); + $this->customerFactory->method('create') + ->willReturn($customer); - $this->groupRepositoryMock->expects($this->once()) - ->method('getById') - ->will($this->returnValue($customerGroupMock)); + $this->groupRepository->method('getById') + ->willReturn($customerGroup); $this->adminOrderCreate->setAccountData(['group_id' => 1]); } @@ -253,7 +251,7 @@ public function testSetAccountData() public function testUpdateQuoteItemsNotArray() { $object = $this->adminOrderCreate->updateQuoteItems('string'); - $this->assertEquals($this->adminOrderCreate, $object); + self::assertEquals($this->adminOrderCreate, $object); } public function testUpdateQuoteItemsEmptyConfiguredOption() @@ -266,22 +264,21 @@ public function testUpdateQuoteItemsEmptyConfiguredOption() ] ]; - $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class); + $item = $this->createMock(Item::class); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $quoteMock->expects($this->once()) - ->method('getItemById') - ->will($this->returnValue($itemMock)); + $quote = $this->createMock(Quote::class); + $quote->method('getItemById') + ->willReturn($item); - $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock)); - $this->itemUpdater->expects($this->once()) - ->method('update') - ->with($this->equalTo($itemMock), $this->equalTo($items[1])) - ->will($this->returnSelf()); + $this->sessionQuote->method('getQuote') + ->willReturn($quote); + $this->itemUpdater->method('update') + ->with(self::equalTo($item), self::equalTo($items[1])) + ->willReturnSelf(); $this->adminOrderCreate->setRecollect(false); $object = $this->adminOrderCreate->updateQuoteItems($items); - $this->assertEquals($this->adminOrderCreate, $object); + self::assertEquals($this->adminOrderCreate, $object); } public function testUpdateQuoteItemsWithConfiguredOption() @@ -295,43 +292,158 @@ public function testUpdateQuoteItemsWithConfiguredOption() ] ]; - $itemMock = $this->createMock(\Magento\Quote\Model\Quote\Item::class); - $itemMock->expects($this->once()) - ->method('getQty') - ->will($this->returnValue($qty)); + $item = $this->createMock(Item::class); + $item->method('getQty') + ->willReturn($qty); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $quoteMock->expects($this->once()) - ->method('updateItem') - ->will($this->returnValue($itemMock)); + $quote = $this->createMock(Quote::class); + $quote->method('updateItem') + ->willReturn($item); - $this->sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quoteMock)); + $this->sessionQuote + ->method('getQuote') + ->willReturn($quote); $expectedInfo = $items[1]; $expectedInfo['qty'] = $qty; - $this->itemUpdater->expects($this->once()) - ->method('update') - ->with($this->equalTo($itemMock), $this->equalTo($expectedInfo)); + $this->itemUpdater->method('update') + ->with(self::equalTo($item), self::equalTo($expectedInfo)); $this->adminOrderCreate->setRecollect(false); $object = $this->adminOrderCreate->updateQuoteItems($items); - $this->assertEquals($this->adminOrderCreate, $object); + self::assertEquals($this->adminOrderCreate, $object); } public function testApplyCoupon() { - $couponCode = ''; - $quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getShippingAddress', 'setCouponCode']); - $this->sessionQuoteMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); + $couponCode = '123'; + $quote = $this->createPartialMock(Quote::class, ['getShippingAddress', 'setCouponCode']); + $this->sessionQuote->method('getQuote') + ->willReturn($quote); + + $address = $this->createPartialMock(Address::class, ['setCollectShippingRates', 'setFreeShipping']); + $quote->method('getShippingAddress') + ->willReturn($address); + $quote->method('setCouponCode') + ->with($couponCode) + ->willReturnSelf(); + + $address->method('setCollectShippingRates') + ->with(true) + ->willReturnSelf(); + $address->method('setFreeShipping') + ->with(0) + ->willReturnSelf(); + + $object = $this->adminOrderCreate->applyCoupon($couponCode); + self::assertEquals($this->adminOrderCreate, $object); + } + + public function testGetCustomerCart() + { + $storeId = 2; + $customerId = 2; + $cartResult = [ + 'cart' => true + ]; - $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['setCollectShippingRates', 'setFreeShipping']); - $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($addressMock); - $quoteMock->expects($this->once())->method('setCouponCode')->with($couponCode)->willReturnSelf(); + $this->quoteFactory->expects($this->once()) + ->method('create'); - $addressMock->expects($this->once())->method('setCollectShippingRates')->with(true)->willReturnSelf(); - $addressMock->expects($this->once())->method('setFreeShipping')->with(0)->willReturnSelf(); + $this->sessionQuote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); - $object = $this->adminOrderCreate->applyCoupon($couponCode); - $this->assertEquals($this->adminOrderCreate, $object); + $this->sessionQuote->expects($this->once()) + ->method('getCustomerId') + ->willReturn($customerId); + + $this->quoteRepository->expects($this->once()) + ->method('getForCustomer') + ->with($customerId, [$storeId]) + ->willReturn($cartResult); + + $this->assertEquals($cartResult, $this->adminOrderCreate->getCustomerCart()); + } + + public function testInitFromOrder() + { + $this->sessionQuote->method('getData') + ->with('reordered') + ->willReturn(true); + + $address = $this->createPartialMock( + Address::class, + [ + 'setSameAsBilling', + 'setCustomerAddressId', + 'getSameAsBilling', + ] + ); + $address->method('getSameAsBilling') + ->willReturn(true); + $address->method('setCustomerAddressId') + ->willReturnSelf(); + + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'setCustomerGroupId', + 'getBillingAddress', + 'getShippingAddress', + 'isVirtual', + 'collectTotals', + ] + ) + ->getMock(); + + $quote->method('getBillingAddress') + ->willReturn($address); + $quote->method('getShippingAddress') + ->willReturn($address); + + $this->sessionQuote + ->method('getQuote') + ->willReturn($quote); + + $orderItem = $this->createPartialMock( + OrderItem::class, + [ + 'getParentItem', + 'getQtyOrdered', + 'getQtyShipped', + 'getQtyInvoiced', + ] + ); + $orderItem->method('getQtyOrdered') + ->willReturn(2); + $orderItem->method('getParentItem') + ->willReturn(false); + + $iterator = new \ArrayIterator([$orderItem]); + + $itemCollectionMock = $this->getMockBuilder(ItemCollection::class) + ->disableOriginalConstructor() + ->setMethods(['getIterator']) + ->getMock(); + $itemCollectionMock->method('getIterator') + ->willReturn($iterator); + + $this->orderMock->method('getItemsCollection') + ->willReturn($itemCollectionMock); + $this->orderMock->method('getReordered') + ->willReturn(false); + $this->orderMock->method('getShippingAddress') + ->willReturn($address); + $this->orderMock->method('getBillingAddress') + ->willReturn($address); + $this->orderMock->method('getCouponCode') + ->willReturn(true); + + $quote->expects($this->once()) + ->method('setCustomerGroupId'); + + $this->adminOrderCreate->initFromOrder($this->orderMock); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php index 269ce829e64d3..6844b908ea98d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php @@ -26,6 +26,11 @@ class CleanExpiredOrdersTest extends \PHPUnit\Framework\TestCase */ protected $orderCollectionMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderManagementMock; + /** * @var ObjectManager */ @@ -44,10 +49,12 @@ protected function setUp() ['create'] ); $this->orderCollectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); $this->model = new CleanExpiredOrders( $this->storesConfigMock, - $this->collectionFactoryMock + $this->collectionFactoryMock, + $this->orderManagementMock ); } @@ -64,8 +71,11 @@ public function testExecute() $this->collectionFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->exactly(2)) + ->method('getAllIds') + ->willReturn([1, 2]); $this->orderCollectionMock->expects($this->exactly(4))->method('addFieldToFilter'); - $this->orderCollectionMock->expects($this->exactly(4))->method('walk'); + $this->orderManagementMock->expects($this->exactly(4))->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->exactly(2))->method('where')->willReturnSelf(); @@ -92,14 +102,18 @@ public function testExecuteWithException() $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->once()) + ->method('getAllIds') + ->willReturn([1]); $this->orderCollectionMock->expects($this->exactly(2))->method('addFieldToFilter'); + $this->orderManagementMock->expects($this->once())->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->once())->method('where')->willReturnSelf(); $this->orderCollectionMock->expects($this->once())->method('getSelect')->willReturn($selectMock); - $this->orderCollectionMock->expects($this->once()) - ->method('walk') + $this->orderManagementMock->expects($this->once()) + ->method('cancel') ->willThrowException(new \Exception($exceptionMessage)); $this->model->execute(); diff --git a/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php b/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php deleted file mode 100644 index dd430f8a03304..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php +++ /dev/null @@ -1,262 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Model; - -use Magento\Framework\App\Filesystem\DirectoryList; - -class DownloadTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Model\Download - */ - protected $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $filesystemMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $storageMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $storageFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $httpFileFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $writeDirectoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $driverMock; - - protected function setUp() - { - $this->writeDirectoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Write::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->will($this->returnValue($this->writeDirectoryMock)); - - $this->driverMock = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\DriverInterface::class); - $this->storageMock = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storageFactoryMock = $this->getMockBuilder( - \Magento\MediaStorage\Model\File\Storage\DatabaseFactory::class - )->disableOriginalConstructor() - ->setMethods(['create', 'checkDbUsage']) - ->getMock(); - $this->httpFileFactoryMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http\FileFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->model = new \Magento\Sales\Model\Download( - $this->filesystemMock, - $this->storageMock, - $this->storageFactoryMock, - $this->httpFileFactoryMock - ); - } - - public function testInstanceOf() - { - $model = new \Magento\Sales\Model\Download( - $this->filesystemMock, - $this->storageMock, - $this->storageFactoryMock, - $this->httpFileFactoryMock - ); - $this->assertInstanceOf(\Magento\Sales\Model\Download::class, $model); - } - - /** - * @param $realPatchCheck - * @param $isFile - * @param $isReadable - * @expectedException \Magento\Framework\Exception\LocalizedException - * @dataProvider dataProviderForTestDownloadFileException - */ - public function testDownloadFileException($realPatchCheck, $isFile, $isReadable) - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn($realPatchCheck); - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - - $this->storageFactoryMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(false)); - $this->httpFileFactoryMock->expects($this->never())->method('create'); - - $this->model->downloadFile($info); - } - - /** - * @return array - */ - public function dataProviderForTestDownloadFileException() - { - return [ - [1, true, false], - [1, false, true], - [false, true, true], - ]; - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - */ - public function testDownloadFileNoStorage() - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - $isFile = true; - $isReadable = false; - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn(true); - - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - - $this->storageMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(true)); - - $storageDatabaseMock = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Storage\Database::class) - ->disableOriginalConstructor() - ->getMock(); - $storageDatabaseMock->expects($this->at(0)) - ->method('loadByFilename') - ->with($this->equalTo($info['order_path'])) - ->will($this->returnSelf()); - $storageDatabaseMock->expects($this->at(2)) - ->method('loadByFilename') - ->with($this->equalTo($info['quote_path'])) - ->will($this->returnSelf()); - - $storageDatabaseMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(false)); - - $this->storageFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($storageDatabaseMock)); - $this->httpFileFactoryMock->expects($this->never())->method('create'); - - $this->model->downloadFile($info); - } - - public function testDownloadFile() - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - $isFile = true; - $isReadable = false; - - $writeMock = $this->getMockBuilder(\Magento\Framework\Filesystem\File\Write::class) - ->disableOriginalConstructor() - ->getMock(); - $writeMock->expects($this->any()) - ->method('lock'); - $writeMock->expects($this->any()) - ->method('write'); - $writeMock->expects($this->any()) - ->method('unlock'); - $writeMock->expects($this->any()) - ->method('close'); - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn(true); - - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - $this->writeDirectoryMock->expects($this->any()) - ->method('openFile') - ->will($this->returnValue($writeMock)); - $this->writeDirectoryMock->expects($this->once()) - ->method('getRelativePath') - ->with($info['order_path']) - ->will($this->returnArgument(0)); - - $this->storageMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(true)); - - $storageDatabaseMock = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Storage\Database::class) - ->disableOriginalConstructor() - ->setMethods(['loadByFilename', 'getId', '__wakeup']) - ->getMock(); - $storageDatabaseMock->expects($this->any()) - ->method('loadByFilename') - ->will($this->returnSelf()); - - $storageDatabaseMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(true)); - - $this->storageFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($storageDatabaseMock)); - - $this->httpFileFactoryMock->expects($this->once()) - ->method('create') - ->with( - $info['title'], - ['value' => $info['order_path'], 'type' => 'filename'], - DirectoryList::MEDIA, - 'application/octet-stream', - null - ); - - $result = $this->model->downloadFile($info); - $this->assertNull($result); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php index 8d3aaa4dae616..e26b6e52b8d17 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php @@ -47,6 +47,16 @@ class EmailSenderHandlerTest extends \PHPUnit\Framework\TestCase */ protected $globalConfig; + /** + * @var \Magento\Sales\Model\Order\Email\Container\IdentityInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $identityContainerMock; + + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -70,18 +80,28 @@ protected function setUp() false, false, true, - ['addFieldToFilter', 'getItems'] + ['addFieldToFilter', 'getItems', 'addAttributeToSelect', 'getSelect'] ); $this->globalConfig = $this->createMock(\Magento\Framework\App\Config::class); + $this->identityContainerMock = $this->createMock( + \Magento\Sales\Model\Order\Email\Container\IdentityInterface::class + ); + + $this->storeManagerMock = $this->createMock( + \Magento\Store\Model\StoreManagerInterface::class + ); + $this->object = $objectManager->getObject( \Magento\Sales\Model\EmailSenderHandler::class, [ - 'emailSender' => $this->emailSender, - 'entityResource' => $this->entityResource, - 'entityCollection' => $this->entityCollection, - 'globalConfig' => $this->globalConfig + 'emailSender' => $this->emailSender, + 'entityResource' => $this->entityResource, + 'entityCollection' => $this->entityCollection, + 'globalConfig' => $this->globalConfig, + 'identityContainer' => $this->identityContainerMock, + 'storeManager' => $this->storeManagerMock, ] ); } @@ -98,7 +118,7 @@ public function testExecute($configValue, $collectionItems, $emailSendingResult) $path = 'sales_email/general/async_sending'; $this->globalConfig - ->expects($this->once()) + ->expects($this->at(0)) ->method('getValue') ->with($path) ->willReturn($configValue); @@ -114,12 +134,32 @@ public function testExecute($configValue, $collectionItems, $emailSendingResult) ->method('addFieldToFilter') ->with('email_sent', ['null' => true]); + $this->entityCollection + ->expects($this->any()) + ->method('addAttributeToSelect') + ->with('store_id') + ->willReturnSelf(); + + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + + $selectMock + ->expects($this->atLeastOnce()) + ->method('group') + ->with('store_id') + ->willReturnSelf(); + + $this->entityCollection + ->expects($this->any()) + ->method('getSelect') + ->willReturn($selectMock); + $this->entityCollection ->expects($this->any()) ->method('getItems') ->willReturn($collectionItems); if ($collectionItems) { + /** @var \Magento\Sales\Model\AbstractModel|\PHPUnit_Framework_MockObject_MockObject $collectionItem */ $collectionItem = $collectionItems[0]; @@ -129,6 +169,23 @@ public function testExecute($configValue, $collectionItems, $emailSendingResult) ->with($collectionItem, true) ->willReturn($emailSendingResult); + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + + $this->storeManagerMock + ->expects($this->any()) + ->method('getStore') + ->willReturn($storeMock); + + $this->identityContainerMock + ->expects($this->any()) + ->method('setStore') + ->with($storeMock); + + $this->identityContainerMock + ->expects($this->any()) + ->method('isEnabled') + ->willReturn(true); + if ($emailSendingResult) { $collectionItem ->expects($this->once()) @@ -159,14 +216,30 @@ public function executeDataProvider() false, false, true, - ['setEmailSent'] + ['setEmailSent', 'getOrder'] ); return [ - [1, [$entityModel], true], - [1, [$entityModel], false], - [1, [], null], - [0, null, null] + [ + 'configValue' => 1, + 'collectionItems' => [clone $entityModel], + 'emailSendingResult' => true, + ], + [ + 'configValue' => 1, + 'collectionItems' => [clone $entityModel], + 'emailSendingResult' => false, + ], + [ + 'configValue' => 1, + 'collectionItems' => [], + 'emailSendingResult' => null, + ], + [ + 'configValue' => 0, + 'collectionItems' => null, + 'emailSendingResult' => null, + ] ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Grid/Child/CollectionUpdaterTest.php b/app/code/Magento/Sales/Test/Unit/Model/Grid/Child/CollectionUpdaterTest.php index ca63a93ebfe4e..c9072abe46f68 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Grid/Child/CollectionUpdaterTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Grid/Child/CollectionUpdaterTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Grid\Child; class CollectionUpdaterTest extends \PHPUnit\Framework\TestCase @@ -31,7 +29,9 @@ protected function setUp() public function testUpdateIfOrderExists() { - $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class); + $collectionMock = $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class + ); $transactionMock = $this->createMock(\Magento\Sales\Model\Order\Payment\Transaction::class); $this->registryMock ->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Grid/CollectionUpdaterTest.php b/app/code/Magento/Sales/Test/Unit/Model/Grid/CollectionUpdaterTest.php index 0175c2b4f7c0c..13aa4bf7bf6c4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Grid/CollectionUpdaterTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Grid/CollectionUpdaterTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Grid; class CollectionUpdaterTest extends \PHPUnit\Framework\TestCase @@ -31,7 +29,9 @@ protected function setUp() public function testUpdateIfOrderNotExists() { - $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class); + $collectionMock = $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class + ); $this->registryMock ->expects($this->once()) ->method('registry') @@ -48,7 +48,9 @@ public function testUpdateIfOrderNotExists() public function testUpdateIfOrderExists() { - $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class); + $collectionMock = $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Payment\Transaction\Collection::class + ); $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); $this->registryMock ->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php index 014e0e28b49ee..02f855929d9d6 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/InvoiceOrderTest.php @@ -428,6 +428,9 @@ public function testCouldNotInvoiceException() ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/AddressRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/AddressRepositoryTest.php index 87f4a9103be6f..e48fea847a743 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/AddressRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/AddressRepositoryTest.php @@ -3,127 +3,180 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Model\Order; +use Magento\Customer\Model\AttributeMetadataDataProvider; +use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Sales\Model\Order\AddressRepository; +use Magento\Sales\Model\ResourceModel\Order\Address\Collection as OrderAddressCollection; +use Magento\Customer\Model\ResourceModel\Form\Attribute\Collection as FormAttributeCollection; +use Magento\Framework\Api\SearchCriteria; +use Magento\Sales\Api\Data\OrderAddressSearchResultInterfaceFactory; +use Magento\Sales\Model\ResourceModel\Metadata; +use Magento\Sales\Model\Order\AddressRepository as OrderAddressRepository; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\InputException; /** * Unit test for order address repository class. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AddressRepositoryTest extends \PHPUnit\Framework\TestCase +class AddressRepositoryTest extends TestCase { /** * Subject of testing. * - * @var \Magento\Sales\Model\Order\AddressRepository + * @var OrderAddressRepository */ protected $subject; /** * Sales resource metadata. * - * @var \Magento\Sales\Model\ResourceModel\Metadata|\PHPUnit_Framework_MockObject_MockObject + * @var Metadata|MockObject */ protected $metadata; /** - * @var \Magento\Sales\Api\Data\OrderAddressSearchResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var OrderAddressSearchResultInterfaceFactory|MockObject */ protected $searchResultFactory; /** - * @var CollectionProcessorInterface |\PHPUnit_Framework_MockObject_MockObject + * @var CollectionProcessorInterface|MockObject */ private $collectionProcessorMock; + /** + * @var Attribute[] + */ + private $attributesList; + + /** + * @var AttributeMetadataDataProvider + */ + private $attributeMetadataDataProvider; + + /** + * @var OrderAddress|MockObject + */ + private $orderAddress; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ protected function setUp() { - $objectManager = new ObjectManager($this); + $this->objectManager = new ObjectManager($this); + $this->orderAddress = $this->createPartialMock(OrderAddress::class, ['getEntityId', 'load']); $this->metadata = $this->createPartialMock( - \Magento\Sales\Model\ResourceModel\Metadata::class, + Metadata::class, ['getNewInstance', 'getMapper'] ); + $this->attributeMetadataDataProvider = $this->getMockBuilder(AttributeMetadataDataProvider::class) + ->disableOriginalConstructor() + ->setMethods(['loadAttributesCollection']) + ->getMock(); + $collectionAttribute = $this->getMockBuilder(FormAttributeCollection::class) + ->setMethods(['addFieldToFilter', 'getIterator']) + ->disableOriginalConstructor() + ->getMock(); + $collectionAttribute->method('getIterator') + ->willReturn(new \ArrayIterator([])); + $this->attributeMetadataDataProvider->method('loadAttributesCollection')->willReturn($collectionAttribute); + $this->searchResultFactory = $this->createPartialMock( - \Magento\Sales\Api\Data\OrderAddressSearchResultInterfaceFactory::class, + OrderAddressSearchResultInterfaceFactory::class, ['create'] ); $this->collectionProcessorMock = $this->getMockBuilder(CollectionProcessorInterface::class) ->getMock(); - $this->subject = $objectManager->getObject( - \Magento\Sales\Model\Order\AddressRepository::class, + $this->subject = $this->objectManager->getObject( + OrderAddressRepository::class, [ 'metadata' => $this->metadata, 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessorMock, + 'attributeMetadataDataProvider' => $this->attributeMetadataDataProvider ] ); } /** + * Test for get order address + * * @param int|null $id * @param int|null $entityId + * + * @return void * @dataProvider getDataProvider */ public function testGet($id, $entityId) { if (!$id) { - $this->expectException( - \Magento\Framework\Exception\InputException::class - ); - + $this->expectException(InputException::class); $this->subject->get($id); } else { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['load', 'getEntityId']); - $address->expects($this->once()) + $this->orderAddress->expects($this->once()) ->method('load') ->with($id) - ->willReturn($address); - $address->expects($this->once()) + ->willReturn($this->orderAddress); + $this->orderAddress->expects($this->once()) ->method('getEntityId') ->willReturn($entityId); $this->metadata->expects($this->once()) ->method('getNewInstance') - ->willReturn($address); + ->willReturn($this->orderAddress); if (!$entityId) { - $this->expectException( - \Magento\Framework\Exception\NoSuchEntityException::class - ); - + $this->expectException(NoSuchEntityException::class); $this->subject->get($id); } else { - $this->assertEquals($address, $this->subject->get($id)); + $this->assertEquals($this->orderAddress, $this->subject->get($id)); - $address->expects($this->never()) + $this->orderAddress->expects($this->never()) ->method('load') ->with($id) - ->willReturn($address); - $address->expects($this->never()) + ->willReturn($this->orderAddress); + $this->orderAddress->expects($this->never()) ->method('getEntityId') ->willReturn($entityId); $this->metadata->expects($this->never()) ->method('getNewInstance') - ->willReturn($address); + ->willReturn($this->orderAddress); // Retrieve Address from registry. - $this->assertEquals($address, $this->subject->get($id)); + $this->assertEquals($this->orderAddress, $this->subject->get($id)); } } } /** + * Data for testGet + * * @return array */ - public function getDataProvider() + public function getDataProvider(): array { return [ [null, null], @@ -132,10 +185,15 @@ public function getDataProvider() ]; } + /** + * Test for get list order address + * + * @return void + */ public function testGetList() { - $searchCriteria = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); - $collection = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Address\Collection::class); + $searchCriteria = $this->createMock(SearchCriteria::class); + $collection = $this->createMock(OrderAddressCollection::class); $this->collectionProcessorMock->expects($this->once()) ->method('process') @@ -147,15 +205,19 @@ public function testGetList() $this->assertEquals($collection, $this->subject->getList($searchCriteria)); } + /** + * Test for delete order address + * + * @return void + */ public function testDelete() { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['getEntityId']); - $address->expects($this->once()) + $this->orderAddress->expects($this->once()) ->method('getEntityId') ->willReturn(1); $mapper = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, + AbstractDb::class, [], '', false, @@ -165,27 +227,29 @@ public function testDelete() ); $mapper->expects($this->once()) ->method('delete') - ->with($address); + ->with($this->orderAddress); $this->metadata->expects($this->any()) ->method('getMapper') ->willReturn($mapper); - $this->assertTrue($this->subject->delete($address)); + $this->assertTrue($this->subject->delete($this->orderAddress)); } /** + * Test for delete order address with exception + * + * @return void * @expectedException \Magento\Framework\Exception\CouldNotDeleteException * @expectedExceptionMessage Could not delete order address */ public function testDeleteWithException() { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['getEntityId']); - $address->expects($this->never()) + $this->orderAddress->expects($this->never()) ->method('getEntityId'); $mapper = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, + AbstractDb::class, [], '', false, @@ -201,18 +265,22 @@ public function testDeleteWithException() ->method('getMapper') ->willReturn($mapper); - $this->subject->delete($address); + $this->subject->delete($this->orderAddress); } + /** + * Test for save order address + * + * @return void + */ public function testSave() { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['getEntityId']); - $address->expects($this->any()) + $this->orderAddress->expects($this->any()) ->method('getEntityId') ->willReturn(1); $mapper = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, + AbstractDb::class, [], '', false, @@ -222,27 +290,29 @@ public function testSave() ); $mapper->expects($this->once()) ->method('save') - ->with($address); + ->with($this->orderAddress); $this->metadata->expects($this->any()) ->method('getMapper') ->willReturn($mapper); - $this->assertEquals($address, $this->subject->save($address)); + $this->assertEquals($this->orderAddress, $this->subject->save($this->orderAddress)); } /** + * Test for save order address with exception + * + * @return void * @expectedException \Magento\Framework\Exception\CouldNotSaveException * @expectedExceptionMessage Could not save order address */ public function testSaveWithException() { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['getEntityId']); - $address->expects($this->never()) + $this->orderAddress->expects($this->never()) ->method('getEntityId'); $mapper = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, + AbstractDb::class, [], '', false, @@ -258,17 +328,117 @@ public function testSaveWithException() ->method('getMapper') ->willReturn($mapper); - $this->assertEquals($address, $this->subject->save($address)); + $this->assertEquals($this->orderAddress, $this->subject->save($this->orderAddress)); } + /** + * Tets for create order address + * + * @return void + */ public function testCreate() { - $address = $this->createPartialMock(\Magento\Sales\Model\Order\Address::class, ['getEntityId']); - $this->metadata->expects($this->once()) ->method('getNewInstance') - ->willReturn($address); + ->willReturn($this->orderAddress); + + $this->assertEquals($this->orderAddress, $this->subject->create()); + } + + /** + * Test for save sales address with multi-attribute. + * + * @param string $attributeType + * @param string $attributeCode + * @param array $attributeValue + * @param string $expected + * + * @return void + * @dataProvider dataMultiAttribute + */ + public function testSaveWithMultiAttribute( + string $attributeType, + string $attributeCode, + array $attributeValue, + string $expected + ) { + $orderAddress = $this->getMockBuilder(OrderAddress::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId', 'hasData', 'getData', 'setData']) + ->getMock(); + + $orderAddress->expects($this->any()) + ->method('getEntityId') + ->willReturn(1); + + $mapper = $this->getMockForAbstractClass( + AbstractDb::class, + [], + '', + false, + true, + true, + ['save'] + ); + $mapper->method('save') + ->with($orderAddress); + $this->metadata->method('getMapper') + ->willReturn($mapper); + + $attributeModel = $this->getMockBuilder(Attribute::class) + ->setMethods(['getFrontendInput', 'getAttributeCode']) + ->disableOriginalConstructor() + ->getMock(); + $attributeModel->method('getFrontendInput')->willReturn($attributeType); + $attributeModel->method('getAttributeCode')->willReturn($attributeCode); + $this->attributesList = [$attributeModel]; + + $this->subject = $this->objectManager->getObject( + AddressRepository::class, + [ + 'metadata' => $this->metadata, + 'searchResultFactory' => $this->searchResultFactory, + 'collectionProcessor' => $this->collectionProcessorMock, + 'attributeMetadataDataProvider' => $this->attributeMetadataDataProvider, + 'attributesList' => $this->attributesList, + ] + ); + + $orderAddress->method('hasData')->with($attributeCode)->willReturn(true); + $orderAddress->method('getData')->with($attributeCode)->willReturn($attributeValue); + $orderAddress->expects($this->once())->method('setData')->with($attributeCode, $expected); + + $this->assertEquals($orderAddress, $this->subject->save($orderAddress)); + } + + /** + * Data for testSaveWithMultiAttribute + * + * @return array + */ + public function dataMultiAttribute(): array + { + $data = [ + 'multiselect' => [ + 'multiselect', + 'attr_multiselect', + [ + 'opt1', + 'opt2', + ], + 'opt1,opt2', + ], + 'multiline' => [ + 'multiline', + 'attr_multiline', + [ + 'line1', + 'line2', + ], + 'line1'.PHP_EOL.'line2', + ], + ]; - $this->assertEquals($address, $this->subject->create()); + return $data; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/AddressTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/AddressTest.php index 73838379490fd..93eb56a07955c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/AddressTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/AddressTest.php @@ -78,6 +78,9 @@ public function testGetRegionCodeRegionIsSet() $this->assertEquals('region', $this->address->getRegionCode()); } + /** + * @return array + */ public function regionProvider() { return [ [1, null], [null, 1]]; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ConfigTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ConfigTest.php index 86419c0c905b6..decce409b3331 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ConfigTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ConfigTest.php @@ -47,6 +47,7 @@ protected function setUp() 'storeManager' => $this->storeManagerMock, ]); $this->statusFactoryMock = $this->getMockBuilder(\Magento\Sales\Model\Order\StatusFactory::class) + ->disableOriginalConstructor() ->setMethods(['load', 'create']) ->getMock(); $this->orderStatusCollectionFactoryMock = $this->createPartialMock( diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/CommentRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/CommentRepositoryTest.php new file mode 100644 index 0000000000000..115cb4bc7c91c --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/CommentRepositoryTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\Data\CreditmemoCommentInterfaceFactory; +use Magento\Sales\Api\Data\CreditmemoCommentSearchResultInterfaceFactory; +use Magento\Sales\Model\Order\Creditmemo; +use Magento\Sales\Model\Order\Creditmemo\Comment; +use Magento\Sales\Model\Order\Creditmemo\CommentRepository; +use Magento\Sales\Model\Order\Email\Sender\CreditmemoCommentSender; +use Magento\Sales\Model\Spi\CreditmemoCommentResourceInterface; +use Psr\Log\LoggerInterface; + +/** + * Class CommentRepositoryTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CommentRepositoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CreditmemoCommentResourceInterface + */ + private $commentResource; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CreditmemoCommentInterfaceFactory + */ + private $commentFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CreditmemoCommentSearchResultInterfaceFactory + */ + private $searchResultFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CommentRepository + */ + private $commentRepository; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CreditmemoCommentSender + */ + private $creditmemoCommentSender; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CreditmemoRepositoryInterface + */ + private $creditmemoRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Creditmemo + */ + private $creditmemoMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Comment + */ + private $commentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + */ + private $loggerMock; + + protected function setUp() + { + $this->commentResource = $this->getMockBuilder(CreditmemoCommentResourceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentFactory = $this->getMockBuilder(CreditmemoCommentInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchResultFactory = $this->getMockBuilder(CreditmemoCommentSearchResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoRepositoryMock = $this->getMockBuilder(CreditmemoRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->creditmemoCommentSender = $this->getMockBuilder(CreditmemoCommentSender::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + + $this->creditmemoMock = $this->getMockBuilder(Creditmemo::class)->disableOriginalConstructor()->getMock(); + $this->commentMock = $this->getMockBuilder(Comment::class)->disableOriginalConstructor()->getMock(); + + $this->commentRepository = new CommentRepository( + $this->commentResource, + $this->commentFactory, + $this->searchResultFactory, + $this->collectionProcessor, + $this->creditmemoCommentSender, + $this->creditmemoRepositoryMock, + $this->loggerMock + ); + } + + public function testSave() + { + $comment = "Comment text"; + $creditmemoId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($creditmemoId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + $this->creditmemoCommentSender->expects($this->once()) + ->method('send') + ->with($this->creditmemoMock, true, $comment) + ->willReturn(true); + $this->commentRepository->save($this->commentMock); + } + + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage Could not save the creditmemo comment. + */ + public function testSaveWithException() + { + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willThrowException( + new \Magento\Framework\Exception\CouldNotSaveException(__('Could not save the creditmemo comment.')) + ); + + $this->commentRepository->save($this->commentMock); + } + + public function testSaveSendCatchException() + { + $comment = "Comment text"; + $creditmemoId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($creditmemoId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->creditmemoMock); + $this->creditmemoCommentSender->expects($this->once()) + ->method('send') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('warning'); + + $this->commentRepository->save($this->commentMock); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php index c911af3044e5d..24a64c37a5e13 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Item/Validation/CreateQuantityValidatorTest.php @@ -97,6 +97,9 @@ public function testValidateCreditMemoProductItems($orderItemId, $expectedResult $this->assertEquals($expectedResult, $this->createQuantityValidator->validate($this->entity)); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php index f917d69e69f79..9172a6f45bbcd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/RefundOperationTest.php @@ -364,6 +364,9 @@ public function baseAmountsDataProvider() ]; } + /** + * @param $amounts + */ private function setBaseAmounts($amounts) { foreach ($amounts as $amountName => $summands) { @@ -403,6 +406,9 @@ private function registerItems() ->willReturn([$item1, $item2, $item3]); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getCreditmemoItemMock() { return $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoItemInterface::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index fa155cfd1d4ed..859fbde31f5d8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -262,6 +262,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', ]; + $transport = new \Magento\Framework\DataObject($transport); $this->eventManagerMock->expects($this->once()) ->method('dispatch') @@ -269,15 +270,16 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'email_creditmemo_set_template_vars_before', [ 'sender' => $this->subject, - 'transport' => $transport, + 'transport' => $transport->getData(), + 'transportObject' => $transport ] ); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') - ->with($transport); + ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php index 2531a26321d67..0dfce6f043393 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php @@ -44,15 +44,15 @@ protected function setUp() ); $this->orderItemMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ 'isDummy', 'getDiscountInvoiced', 'getBaseDiscountInvoiced', 'getQtyInvoiced', 'getQty', - 'getDiscountRefunded', 'getQtyRefunded' + 'getDiscountRefunded', 'getQtyRefunded', ]); $this->creditmemoMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo::class, [ 'setBaseCost', 'getAllItems', 'getOrder', 'getBaseShippingAmount', 'roundPrice', - 'setDiscountAmount', 'setBaseDiscountAmount' + 'setDiscountAmount', 'setBaseDiscountAmount', 'getBaseShippingInclTax', 'getBaseShippingTaxAmount', ]); $this->creditmemoItemMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo\Item::class, [ 'getHasChildren', 'getBaseCost', 'getQty', 'getOrderItem', 'setDiscountAmount', - 'setBaseDiscountAmount', 'isLast' + 'setBaseDiscountAmount', 'isLast', ]); $this->total = new \Magento\Sales\Model\Order\Creditmemo\Total\Discount(); } @@ -127,6 +127,82 @@ public function testCollect() $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); } + public function testCollectNoBaseShippingAmount() + { + $this->creditmemoMock->expects($this->exactly(2)) + ->method('setDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->exactly(2)) + ->method('setBaseDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingAmount') + ->willReturn(0); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingInclTax') + ->willReturn(1); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingTaxAmount') + ->willReturn(0); + $this->orderMock->expects($this->once()) + ->method('getBaseShippingDiscountAmount') + ->willReturn(1); + $this->orderMock->expects($this->exactly(2)) + ->method('getBaseShippingAmount') + ->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getShippingAmount') + ->willReturn(1); + $this->creditmemoMock->expects($this->once()) + ->method('getAllItems') + ->willReturn([$this->creditmemoItemMock]); + $this->creditmemoItemMock->expects($this->atLeastOnce()) + ->method('getOrderItem') + ->willReturn($this->orderItemMock); + $this->orderItemMock->expects($this->once()) + ->method('isDummy') + ->willReturn(false); + $this->orderItemMock->expects($this->once()) + ->method('getDiscountInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getBaseDiscountInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getQtyInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getDiscountRefunded') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getQtyRefunded') + ->willReturn(0); + $this->creditmemoItemMock->expects($this->once()) + ->method('isLast') + ->willReturn(false); + $this->creditmemoItemMock->expects($this->atLeastOnce()) + ->method('getQty') + ->willReturn(1); + $this->creditmemoItemMock->expects($this->exactly(1)) + ->method('setDiscountAmount') + ->willReturnSelf(); + $this->creditmemoItemMock->expects($this->exactly(1)) + ->method('setBaseDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->exactly(2)) + ->method('roundPrice') + ->willReturnMap( + [ + [1, 'regular', true, 1], + [1, 'base', true, 1], + ] + ); + $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); + } + public function testCollectZeroShipping() { $this->creditmemoMock->expects($this->exactly(2)) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php index f05b2795f58e9..62fff6e2fa66d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/ShippingTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo\Total; class ShippingTest extends \PHPUnit\Framework\TestCase diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index 01c0565a557a6..f37814779b218 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Order\Creditmemo\Total; use Magento\Framework\DataObject as MagentoObject; @@ -183,13 +181,13 @@ public function collectDataProvider() 'tax_amount' => 0.82, 'base_tax_amount' => 0.82, 'invoice' => new MagentoObject( - [ - 'shipping_tax_amount' => 2.45, - 'base_shipping_tax_amount' => 2.45, - 'shipping_discount_tax_compensation_amount' => 0, - 'base_shipping_discount_tax_compensation_amount' => 0, - ] - ), + [ + 'shipping_tax_amount' => 2.45, + 'base_shipping_tax_amount' => 2.45, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + ] + ), ], ], 'expected_results' => [ @@ -347,13 +345,13 @@ public function collectDataProvider() 'tax_amount' => 1.65, 'base_tax_amount' => 1.65, 'invoice' => new MagentoObject( - [ - 'shipping_tax_amount' => 1.24, - 'base_shipping_tax_amount' => 1.24, - 'shipping_discount_tax_compensation_amount' => 0, - 'base_shipping_discount_tax_compensation_amount' => 0, - ] - ), + [ + 'shipping_tax_amount' => 1.24, + 'base_shipping_tax_amount' => 1.24, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + ] + ), ], ], 'expected_results' => [ @@ -423,13 +421,13 @@ public function collectDataProvider() 'tax_amount' => 0.82, 'base_tax_amount' => 0.82, 'invoice' => new MagentoObject( - [ - 'shipping_tax_amount' => 1.24, - 'base_shipping_tax_amount' => 1.24, - 'shipping_discount_tax_compensation_amount' => 0, - 'base_shipping_discount_tax_compensation_amount' => 0, - ] - ), + [ + 'shipping_tax_amount' => 1.24, + 'base_shipping_tax_amount' => 1.24, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + ] + ), ], ], 'expected_results' => [ @@ -440,10 +438,10 @@ public function collectDataProvider() ], ], 'creditmemo_data' => [ - 'grand_total' => 64.95, - 'base_grand_total' => 64.95, - 'tax_amount' => 4.95, - 'base_tax_amount' => 4.95, + 'grand_total' => 64.94, + 'base_grand_total' => 64.94, + 'tax_amount' => 4.94, + 'base_tax_amount' => 4.94, ], ], ]; @@ -504,13 +502,13 @@ public function collectDataProvider() 'tax_amount' => 0.76, 'base_tax_amount' => 0.76, 'invoice' => new MagentoObject( - [ - 'shipping_tax_amount' => 0, - 'base_shipping_tax_amount' => 0, - 'shipping_discount_tax_compensation_amount' => 0, - 'base_shipping_discount_tax_compensation_amount' => 0, - ] - ), + [ + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + ] + ), ], ], 'expected_results' => [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php index 0b65c2d972d32..83e55b966d34f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Order; use Magento\Sales\Model\ResourceModel\OrderFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\CollectionFactory; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection as ItemCollection; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Class CreditmemoTest @@ -30,6 +29,11 @@ class CreditmemoTest extends \PHPUnit\Framework\TestCase */ protected $creditmemo; + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + /** * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ @@ -38,6 +42,7 @@ class CreditmemoTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->orderFactory = $this->createPartialMock(\Magento\Sales\Model\OrderFactory::class, ['create']); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $objectManagerHelper = new ObjectManagerHelper($this); $this->cmItemCollectionFactoryMock = $this->getMockBuilder( @@ -50,16 +55,21 @@ protected function setUp() 'context' => $this->createMock(\Magento\Framework\Model\Context::class), 'registry' => $this->createMock(\Magento\Framework\Registry::class), 'localeDate' => $this->createMock( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class), + \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class + ), 'dateTime' => $this->createMock(\Magento\Framework\Stdlib\DateTime::class), 'creditmemoConfig' => $this->createMock( - \Magento\Sales\Model\Order\Creditmemo\Config::class), + \Magento\Sales\Model\Order\Creditmemo\Config::class + ), 'orderFactory' => $this->orderFactory, 'cmItemCollectionFactory' => $this->cmItemCollectionFactoryMock, 'calculatorFactory' => $this->createMock(\Magento\Framework\Math\CalculatorFactory::class), 'storeManager' => $this->createMock(\Magento\Store\Model\StoreManagerInterface::class), 'commentFactory' => $this->createMock(\Magento\Sales\Model\Order\Creditmemo\CommentFactory::class), - 'commentCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Creditmemo\Comment\CollectionFactory::class), + 'commentCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Comment\CollectionFactory::class + ), + 'scopeConfig' => $this->scopeConfigMock ]; $this->creditmemo = $objectManagerHelper->getObject( \Magento\Sales\Model\Order\Creditmemo::class, @@ -72,7 +82,10 @@ public function testGetOrder() $orderId = 100000041; $this->creditmemo->setOrderId($orderId); $entityName = 'creditmemo'; - $order = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['load', 'setHistoryEntityName', '__wakeUp']); + $order = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + ['load', 'setHistoryEntityName', '__wakeUp'] + ); $this->creditmemo->setOrderId($orderId); $order->expects($this->atLeastOnce()) ->method('setHistoryEntityName') @@ -95,17 +108,52 @@ public function testGetEntityType() $this->assertEquals('creditmemo', $this->creditmemo->getEntityType()); } - public function testIsValidGrandTotalGrandTotalEmpty() + /** + * @dataProvider validGrandTotalDataProvider + * @param int $grandTotal + * @param int $allowZero + * @param bool $expectedResult + * + * @return void + */ + public function testIsValidGrandTotalGrandTotal(int $grandTotal, int $allowZero, bool $expectedResult) { - $this->creditmemo->setGrandTotal(0); - $this->assertFalse($this->creditmemo->isValidGrandTotal()); + $this->creditmemo->setGrandTotal($grandTotal); + $this->scopeConfigMock->expects($this->any()) + ->method('getValue') + ->with( + 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ->willReturn($allowZero); + + $this->assertEquals($expectedResult, $this->creditmemo->isValidGrandTotal()); } - public function testIsValidGrandTotalGrandTotal() + /** + * Data provider for the testIsValidGrantTotalGrantTotal() + * + * @return array + */ + public function validGrandTotalDataProvider(): array { - $this->creditmemo->setGrandTotal(0); - $this->creditmemo->getAllowZeroGrandTotal(true); - $this->assertFalse($this->creditmemo->isValidGrandTotal()); + return [ + [ + 'grandTotal' => 0, + 'allowZero' => 0, + 'expectedResult' => false, + ], + [ + 'grandTotal' => 0, + 'allowZero' => 1, + 'expectedResult' => true, + ], + [ + 'grandTotal' => 1, + 'allowZero' => 0, + 'expectedResult' => true, + ], + ]; } public function testIsValidGrandTotal() @@ -136,7 +184,8 @@ public function testGetItemsCollectionWithId() /** @var ItemCollection|\PHPUnit_Framework_MockObject_MockObject $itemCollectionMock */ $itemCollectionMock = $this->getMockBuilder( - \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection::class) + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection::class + ) ->disableOriginalConstructor() ->getMock(); $itemCollectionMock->expects($this->once()) @@ -164,7 +213,8 @@ public function testGetItemsCollectionWithoutId() /** @var ItemCollection|\PHPUnit_Framework_MockObject_MockObject $itemCollectionMock */ $itemCollectionMock = $this->getMockBuilder( - \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection::class) + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection::class + ) ->disableOriginalConstructor() ->getMock(); $itemCollectionMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php deleted file mode 100644 index 8a2305543c490..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Model\Order; - -use Magento\Quote\Model\Quote\Address; - -/** - * Class BuilderTest - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class CustomerManagementTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $objectCopyService; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $accountManagement; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $addressFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $orderRepository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $regionFactory; - - /** - * @var \Magento\Sales\Model\Order\CustomerManagement - */ - protected $service; - - protected function setUp() - { - $this->objectCopyService = $this->createMock(\Magento\Framework\DataObject\Copy::class); - $this->accountManagement = $this->createMock(\Magento\Customer\Api\AccountManagementInterface::class); - $this->customerFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, - ['create'] - ); - $this->addressFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\AddressInterfaceFactory::class, - ['create'] - ); - $this->regionFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\RegionInterfaceFactory::class, - ['create'] - ); - $this->orderRepository = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class); - - $this->service = new \Magento\Sales\Model\Order\CustomerManagement( - $this->objectCopyService, - $this->accountManagement, - $this->customerFactory, - $this->addressFactory, - $this->regionFactory, - $this->orderRepository - ); - } - - /** - * @expectedException \Magento\Framework\Exception\AlreadyExistsException - */ - public function testCreateThrowsExceptionIfCustomerAlreadyExists() - { - $orderMock = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class); - $orderMock->expects($this->once())->method('getCustomerId')->will($this->returnValue('customer_id')); - $this->orderRepository->expects($this->once())->method('get')->with(1)->will($this->returnValue($orderMock)); - $this->service->create(1); - } - - public function testCreateCreatesCustomerBasedonGuestOrder() - { - $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); - $orderMock->expects($this->once())->method('getCustomerId')->will($this->returnValue(null)); - $orderMock->expects($this->any())->method('getBillingAddress')->will($this->returnValue('billing_address')); - - $orderBillingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); - $orderBillingAddress->expects($this->once()) - ->method('getAddressType') - ->willReturn(Address::ADDRESS_TYPE_BILLING); - - $orderShippingAddress = $this->createMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); - $orderShippingAddress->expects($this->once()) - ->method('getAddressType') - ->willReturn(Address::ADDRESS_TYPE_SHIPPING); - - $orderMock->expects($this->any()) - ->method('getAddresses') - ->will($this->returnValue([$orderBillingAddress, $orderShippingAddress])); - - $this->orderRepository->expects($this->once())->method('get')->with(1)->will($this->returnValue($orderMock)); - $this->objectCopyService->expects($this->any())->method('copyFieldsetToTarget')->will($this->returnValueMap( - [ - ['order_address', 'to_customer', 'billing_address', [], 'global', ['customer_data' => []]], - ['order_address', 'to_customer_address', $orderBillingAddress, [], 'global', 'address_data'], - ['order_address', 'to_customer_address', $orderShippingAddress, [], 'global', 'address_data'], - ] - )); - - $addressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - $addressMock->expects($this->any()) - ->method('setIsDefaultBilling') - ->with(true) - ->willReturnSelf(); - $addressMock->expects($this->any()) - ->method('setIsDefaultShipping') - ->with(true) - ->willReturnSelf(); - - $this->addressFactory->expects($this->any())->method('create')->with(['data' => 'address_data'])->will( - $this->returnValue($addressMock) - ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $customerMock->expects($this->any())->method('getId')->will($this->returnValue('customer_id')); - $this->customerFactory->expects($this->once())->method('create')->with( - ['data' => ['customer_data' => [], 'addresses' => [$addressMock, $addressMock]]] - )->will($this->returnValue($customerMock)); - $this->accountManagement->expects($this->once())->method('createAccount')->with($customerMock)->will( - $this->returnValue($customerMock) - ); - $orderMock->expects($this->once())->method('setCustomerId')->with('customer_id'); - $this->orderRepository->expects($this->once())->method('save')->with($orderMock); - $this->assertEquals($customerMock, $this->service->create(1)); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php index 97dc973e63437..0b7858e84490c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php @@ -92,12 +92,16 @@ public function stepMockSetup() $this->storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getStoreId', '__wakeup']); - $this->orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ + $this->orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + [ 'getStore', 'getBillingAddress', 'getPayment', '__wakeup', 'getCustomerIsGuest', 'getCustomerName', 'getCustomerEmail', 'getShippingAddress', 'setSendEmail', - 'setEmailSent' - ]); + 'setEmailSent', 'getCreatedAtFormatted', 'getIsNotVirtual', + 'getEmailCustomerNote', 'getFrontendStatusLabel' + ] + ); $this->orderMock->expects($this->any()) ->method('getStore') ->will($this->returnValue($this->storeMock)); @@ -120,6 +124,10 @@ public function stepMockSetup() $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); } + /** + * @param $billingAddress + * @param bool $isVirtual + */ public function stepAddressFormat($billingAddress, $isVirtual = false) { $this->orderMock->expects($this->any()) @@ -145,6 +153,9 @@ public function stepSendWithCallSendCopyTo() $this->stepSend($this->never(), $this->once()); } + /** + * @param $identityMockClassName + */ public function stepIdentityContainerInit($identityMockClassName) { $this->identityContainerMock = $this->createPartialMock( @@ -156,6 +167,10 @@ public function stepIdentityContainerInit($identityMockClassName) ->will($this->returnValue($this->storeMock)); } + /** + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $sendExpects + * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $sendCopyToExpects + */ protected function stepSend( \PHPUnit_Framework_MockObject_Matcher_InvokedCount $sendExpects, \PHPUnit_Framework_MockObject_Matcher_InvokedCount $sendCopyToExpects diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php index 3b97c8451d32c..40e7ce4568d20 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php @@ -59,6 +59,16 @@ public function testSendVirtualOrder() { $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, true); $billingAddress = $this->addressMock; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -70,7 +80,11 @@ public function testSendVirtualOrder() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -83,6 +97,15 @@ public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -102,7 +125,11 @@ public function testSendTrueWithCustomerCopy() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -115,6 +142,15 @@ public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -134,7 +170,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 31bf846689230..260b1d751f715 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -87,10 +87,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -115,6 +118,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getCustomerNote') ->willReturn($comment); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -126,11 +145,17 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -197,17 +222,38 @@ public function sendDataProvider() * @param bool $isVirtualOrder * @param int $formatCallCount * @param string|null $expectedShippingAddress + * + * @return void * @dataProvider sendVirtualOrderDataProvider */ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expectedShippingAddress) { $billingAddress = 'address_test'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -238,11 +284,18 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $billingAddress + 'formattedBillingAddress' => $billingAddress, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] + ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php index 3e29bf04e358d..f0a05586cd972 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php @@ -63,9 +63,20 @@ public function testSendTrueWithCustomerCopy() $billingAddress = $this->addressMock; $this->stepAddressFormat($billingAddress); $comment = 'comment_test'; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); @@ -80,7 +91,11 @@ public function testSendTrueWithCustomerCopy() 'billing' => $billingAddress, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -93,12 +108,22 @@ public function testSendTrueWithCustomerCopy() public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Processing'; $this->stepAddressFormat($billingAddress); $comment = 'comment_test'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); @@ -113,7 +138,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -127,6 +156,16 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->identityContainerMock->expects($this->once()) ->method('isEnabled') @@ -142,7 +181,11 @@ public function testSendVirtualOrder() 'comment' => '', 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index 9c54c716e4207..7b78a92a9f83d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -87,10 +87,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'Test Customer'; + $isNotVirtual = true; + $frontendStatusLabel = 'Processing'; $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -113,6 +116,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getShippingAddress') ->willReturn($addressMock); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->invoiceMock->expects($this->once()) ->method('getCustomerNoteNotify') ->willReturn($customerNoteNotify); @@ -132,11 +151,17 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -209,10 +234,13 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $billingAddress = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -231,6 +259,21 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->invoiceMock->expects($this->once()) ->method('getCustomerNoteNotify') ->willReturn(false); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') @@ -243,11 +286,17 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $billingAddress + 'formattedBillingAddress' => $billingAddress, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => false, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php index e5d6cacb25637..049cc75d3e42c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderCommentSenderTest.php @@ -40,10 +40,18 @@ public function testSendTrue() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->stepAddressFormat($billingAddress); $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->identityContainerMock->expects($this->once()) ->method('isEnabled') @@ -58,7 +66,11 @@ public function testSendTrue() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -72,10 +84,18 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName='Test Customer'; + $frontendStatusLabel='Complete'; $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -86,7 +106,11 @@ public function testSendVirtualOrder() 'billing' => $this->addressMock, 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 411dd9e1433d7..bdb94a6d71cd9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -53,17 +53,23 @@ protected function setUp() * @param int $configValue * @param bool|null $forceSyncMode * @param bool|null $emailSendingResult - * @dataProvider sendDataProvider + * @param $senderSendException * @return void + * @dataProvider sendDataProvider + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testSend($configValue, $forceSyncMode, $emailSendingResult) + public function testSend($configValue, $forceSyncMode, $emailSendingResult, $senderSendException) { $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $createdAtFormatted='Oct 14, 2019, 4:11:58 PM'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Processing'; + $isNotVirtual = true; $this->orderMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -71,7 +77,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult) ->willReturn($configValue); if (!$configValue || $forceSyncMode) { - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -91,6 +97,27 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult) ->method('getShippingAddress') ->willReturn($addressMock); + $this->orderMock->expects($this->once()) + ->method('getCreatedAtFormatted') + ->with(2) + ->willReturn($createdAtFormatted); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -100,7 +127,15 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult) 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'created_at_formatted'=>$createdAtFormatted, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] + ] ); @@ -110,19 +145,23 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult) $this->senderMock->expects($this->once())->method('send'); - $this->senderMock->expects($this->once())->method('sendCopyTo'); + if ($senderSendException) { + $this->checkSenderSendExceptionCase(); + } else { + $this->senderMock->expects($this->once())->method('sendCopyTo'); - $this->orderMock->expects($this->once()) - ->method('setEmailSent') - ->with(true); + $this->orderMock->expects($this->once()) + ->method('setEmailSent') + ->with($emailSendingResult); - $this->orderResourceMock->expects($this->once()) - ->method('saveAttribute') - ->with($this->orderMock, ['send_email', 'email_sent']); + $this->orderResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->orderMock, ['send_email', 'email_sent']); - $this->assertTrue( - $this->sender->send($this->orderMock) - ); + $this->assertTrue( + $this->sender->send($this->orderMock) + ); + } } else { $this->orderResourceMock->expects($this->once()) ->method('saveAttribute') @@ -146,19 +185,42 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult) } } + /** + * Methods check case when method "send" in "senderMock" throw exception. + * + * @return void + */ + protected function checkSenderSendExceptionCase() + { + $this->senderMock->expects($this->once()) + ->method('send') + ->willThrowException(new \Exception('exception')); + + $this->orderResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->orderMock, 'send_email'); + + $this->assertFalse( + $this->sender->send($this->orderMock) + ); + } + /** * @return array */ public function sendDataProvider() { return [ - [0, 0, true], - [0, 0, true], - [0, 0, false], - [0, 0, false], - [0, 1, true], - [0, 1, true], - [1, null, null, null] + [0, 0, true, false], + [0, 0, true, false], + [0, 0, true, true], + [0, 0, false, false], + [0, 0, false, false], + [0, 0, false, true], + [0, 1, true, false], + [0, 1, true, false], + [0, 1, true, false], + [1, null, null, false] ]; } @@ -172,6 +234,10 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $address = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $createdAtFormatted='Oct 14, 2019, 4:11:58 PM'; + $customerName = 'test customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->orderMock->expects($this->once()) ->method('setSendEmail') @@ -182,7 +248,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->with('sales_email/general/async_sending') ->willReturn(false); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(true); @@ -195,6 +261,27 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->stepAddressFormat($addressMock, $isVirtualOrder); + $this->orderMock->expects($this->once()) + ->method('getCreatedAtFormatted') + ->with(2) + ->willReturn($createdAtFormatted); + + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -204,7 +291,14 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'created_at_formatted'=>$createdAtFormatted, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php index f5a2e4d0148cd..90664216e87bc 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php @@ -56,6 +56,8 @@ public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -65,6 +67,12 @@ public function testSendTrueWithCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -76,7 +84,11 @@ public function testSendTrueWithCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -89,6 +101,8 @@ public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; + $customerName='Test Customer'; + $frontendStatusLabel='Processing'; $this->orderMock->expects($this->once()) ->method('getCustomerIsGuest') @@ -98,6 +112,12 @@ public function testSendTrueWithoutCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(true)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -109,7 +129,11 @@ public function testSendTrueWithoutCustomerCopy() 'comment' => $comment, 'store' => $this->storeMock, 'formattedShippingAddress' => 1, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] ] ) ); @@ -123,10 +147,18 @@ public function testSendVirtualOrder() $isVirtualOrder = true; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); $this->stepAddressFormat($this->addressMock, $isVirtualOrder); + $customerName='Test Customer'; + $frontendStatusLabel='Complete'; $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->will($this->returnValue(false)); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -138,7 +170,12 @@ public function testSendVirtualOrder() 'comment' => '', 'store' => $this->storeMock, 'formattedShippingAddress' => null, - 'formattedBillingAddress' => 1 + 'formattedBillingAddress' => 1, + 'order_data' => [ + 'customer_name' => $customerName, + 'frontend_status_label' => $frontendStatusLabel + ] + ] ) ); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index b1b18af63b590..2c69a5d82379b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -87,10 +87,13 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $comment = 'comment_test'; $address = 'address_test'; $configPath = 'sales_email/general/async_sending'; + $customerName = 'Test Customer'; + $isNotVirtual = true; + $frontendStatusLabel = 'Processing'; $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -121,6 +124,22 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ->method('getCustomerNote') ->willReturn($comment); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -132,11 +151,17 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $address, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => $isNotVirtual, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -209,10 +234,13 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte { $address = 'address_test'; $this->orderMock->setData(\Magento\Sales\Api\Data\OrderInterface::IS_VIRTUAL, $isVirtualOrder); + $customerName = 'Test Customer'; + $frontendStatusLabel = 'Complete'; + $isNotVirtual = false; $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -232,6 +260,22 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->method('getCustomerNoteNotify') ->willReturn(false); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($customerName); + + $this->orderMock->expects($this->once()) + ->method('getIsNotVirtual') + ->willReturn($isNotVirtual); + + $this->orderMock->expects($this->once()) + ->method('getEmailCustomerNote') + ->willReturn(''); + + $this->orderMock->expects($this->once()) + ->method('getFrontendStatusLabel') + ->willReturn($frontendStatusLabel); + $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( @@ -243,11 +287,17 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte 'payment_html' => 'payment', 'store' => $this->storeMock, 'formattedShippingAddress' => $expectedShippingAddress, - 'formattedBillingAddress' => $address + 'formattedBillingAddress' => $address, + 'order_data' => [ + 'customer_name' => $customerName, + 'is_not_virtual' => false, + 'email_customer_note' => '', + 'frontend_status_label' => $frontendStatusLabel + ] ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); $this->shipmentResourceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 5319aa510bedf..24cd54e3a46b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model\Order\Email; use Magento\Sales\Model\Order\Email\SenderBuilder; @@ -29,6 +30,11 @@ class SenderBuilderTest extends \PHPUnit\Framework\TestCase */ protected $transportBuilder; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + protected function setUp() { $templateId = 'test_template_id'; @@ -42,7 +48,11 @@ protected function setUp() ['getTemplateVars', 'getTemplateOptions', 'getTemplateId'] ); - $this->storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getStoreId', '__wakeup']); + $this->storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, [ + 'getStoreId', + '__wakeup', + 'getId', + ]); $this->identityContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity::class, @@ -52,15 +62,23 @@ protected function setUp() 'getCustomerName', 'getTemplateOptions', 'getEmailCopyTo', - 'getCopyMethod' + 'getCopyMethod', + 'getStore', ] ); - $this->transportBuilder = $this->createPartialMock(\Magento\Framework\Mail\Template\TransportBuilder::class, [ - 'addTo', 'addBcc', 'getTransport', - 'setTemplateIdentifier', 'setTemplateOptions', 'setTemplateVars', - 'setFrom', - ]); + $this->transportBuilder = $this->createPartialMock( + \Magento\Framework\Mail\Template\TransportBuilder::class, + [ + 'addTo', + 'addBcc', + 'getTransport', + 'setTemplateIdentifier', + 'setTemplateOptions', + 'setTemplateVars', + 'setFromByScope', + ] + ); $this->templateContainerMock->expects($this->once()) ->method('getTemplateId') @@ -85,8 +103,8 @@ protected function setUp() ->method('getEmailIdentity') ->will($this->returnValue($emailIdentity)); $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($this->equalTo($emailIdentity)); + ->method('setFromByScope') + ->with($this->equalTo($emailIdentity), 1); $this->identityContainerMock->expects($this->once()) ->method('getEmailCopyTo') @@ -103,6 +121,8 @@ public function testSend() { $customerName = 'test_name'; $customerEmail = 'test_email'; + $identity = 'email_identity_test'; + $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -119,6 +139,15 @@ public function testSend() $this->identityContainerMock->expects($this->once()) ->method('getCustomerName') ->will($this->returnValue($customerName)); + $this->identityContainerMock->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo($customerEmail), $this->equalTo($customerName)); @@ -132,6 +161,7 @@ public function testSend() public function testSendCopyTo() { + $identity = 'email_identity_test'; $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -145,7 +175,15 @@ public function testSendCopyTo() $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo('example@mail.com')); - + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); + $this->identityContainerMock->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn(1); $this->transportBuilder->expects($this->once()) ->method('getTransport') ->will($this->returnValue($transportMock)); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/CommentRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/CommentRepositoryTest.php new file mode 100644 index 0000000000000..4790490c1ecf3 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/CommentRepositoryTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Invoice; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\Data\InvoiceCommentInterfaceFactory; +use Magento\Sales\Api\Data\InvoiceCommentSearchResultInterfaceFactory; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Order\Invoice\Comment; +use Magento\Sales\Model\Order\Invoice\CommentRepository; +use Magento\Sales\Model\Order\Email\Sender\InvoiceCommentSender; +use Magento\Sales\Model\Spi\InvoiceCommentResourceInterface; +use Psr\Log\LoggerInterface; + +/** + * Class CommentRepositoryTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CommentRepositoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|InvoiceCommentResourceInterface + */ + private $commentResource; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|InvoiceCommentInterfaceFactory + */ + private $commentFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|InvoiceCommentSearchResultInterfaceFactory + */ + private $searchResultFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CommentRepository + */ + private $commentRepository; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|InvoiceCommentSender + */ + private $invoiceCommentSender; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|InvoiceRepositoryInterface + */ + private $invoiceRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Invoice + */ + private $invoiceMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Comment + */ + private $commentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + */ + private $loggerMock; + + protected function setUp() + { + $this->commentResource = $this->getMockBuilder(InvoiceCommentResourceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentFactory = $this->getMockBuilder(InvoiceCommentInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchResultFactory = $this->getMockBuilder(InvoiceCommentSearchResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->invoiceCommentSender = $this->getMockBuilder(InvoiceCommentSender::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + + $this->invoiceMock = $this->getMockBuilder(Invoice::class)->disableOriginalConstructor()->getMock(); + $this->commentMock = $this->getMockBuilder(Comment::class)->disableOriginalConstructor()->getMock(); + + $this->commentRepository = new CommentRepository( + $this->commentResource, + $this->commentFactory, + $this->searchResultFactory, + $this->collectionProcessor, + $this->invoiceCommentSender, + $this->invoiceRepositoryMock, + $this->loggerMock + ); + } + + public function testSave() + { + $comment = "Comment text"; + $invoiceId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($invoiceId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->with($invoiceId) + ->willReturn($this->invoiceMock); + $this->invoiceCommentSender->expects($this->once()) + ->method('send') + ->with($this->invoiceMock, true, $comment) + ->willReturn(true); + $this->commentRepository->save($this->commentMock); + } + + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage Could not save the invoice comment. + */ + public function testSaveWithException() + { + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willThrowException( + new \Magento\Framework\Exception\CouldNotSaveException(__('Could not save the invoice comment.')) + ); + + $this->commentRepository->save($this->commentMock); + } + + public function testSaveSendCatchException() + { + $comment = "Comment text"; + $creditmemoId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($creditmemoId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->invoiceMock); + $this->invoiceCommentSender->expects($this->once()) + ->method('send') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('warning'); + + $this->commentRepository->save($this->commentMock); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index f470b097dd73f..dcf689cf7d53b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -247,7 +247,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -260,6 +260,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', ]; + $transport = new \Magento\Framework\DataObject($transport); $this->eventManagerMock->expects($this->once()) ->method('dispatch') @@ -267,15 +268,16 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'email_invoice_set_template_vars_before', [ 'sender' => $this->subject, - 'transport' => $transport, + 'transport' => $transport->getData(), + 'transportObject' => $transport, ] ); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') - ->with($transport); + ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php index 5d3e9ae22e78b..5a0478dd2e36f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php @@ -156,6 +156,12 @@ public function testValidateNoInvoiceItems() ); } + /** + * @param $orderItemId + * @param $qty + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getInvoiceItemMock($orderItemId, $qty) { $invoiceItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceItemInterface::class) @@ -167,6 +173,13 @@ private function getInvoiceItemMock($orderItemId, $qty) return $invoiceItemMock; } + /** + * @param $id + * @param $qtyToInvoice + * @param $isDummy + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getOrderItemMock($id, $qtyToInvoice, $isDummy) { $orderItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderItemInterface::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceTest.php index 0517da8b85cf0..997f7aff7a5c0 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\Order; use Magento\Sales\Api\Data\InvoiceInterface; @@ -99,9 +97,13 @@ protected function setUp() 'context' => $contextMock, 'orderFactory' => $this->orderFactory, 'calculatorFactory' => $this->createMock(\Magento\Framework\Math\CalculatorFactory::class), - 'invoiceItemCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Invoice\Item\CollectionFactory::class), + 'invoiceItemCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Invoice\Item\CollectionFactory::class + ), 'invoiceCommentFactory' => $this->createMock(\Magento\Sales\Model\Order\Invoice\CommentFactory::class), - 'commentCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Invoice\Comment\CollectionFactory::class), + 'commentCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Invoice\Comment\CollectionFactory::class + ), ]; $this->model = $this->helperManager->getObject(\Magento\Sales\Model\Order\Invoice::class, $arguments); $this->model->setOrder($this->order); @@ -138,6 +140,9 @@ public function testDefaultCanVoid($canVoid) $this->assertEquals($canVoid, $this->model->canVoid()); } + /** + * @return array + */ public function canVoidDataProvider() { return [[true], [false]]; @@ -203,7 +208,6 @@ public function testGetStore() $store = $this->helperManager->getObject(\Magento\Store\Model\Store::class, []); $this->order->expects($this->once())->method('getStore')->willReturn($store); $this->assertEquals($store, $this->model->getStore()); - } public function testGetShippingAddress() @@ -211,7 +215,6 @@ public function testGetShippingAddress() $address = $this->helperManager->getObject(\Magento\Sales\Model\Order\Address::class, []); $this->order->expects($this->once())->method('getShippingAddress')->willReturn($address); $this->assertEquals($address, $this->model->getShippingAddress()); - } /** @@ -382,6 +385,9 @@ public function testPay( self::assertEquals($expectedTotal, $this->order->getTotalPaid()); } + /** + * @return array + */ public function payDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php deleted file mode 100644 index 0c34e5bdffd4a..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php +++ /dev/null @@ -1,365 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Model\Order; - -use Magento\Sales\Model\Order\ItemRepository; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ItemRepositoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\DataObject\Factory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectFactory; - - /** - * @var \Magento\Sales\Model\ResourceModel\Metadata|\PHPUnit_Framework_MockObject_MockObject - */ - protected $metadata; - - /** - * @var \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $searchResultFactory; - - /** - * @var \Magento\Catalog\Model\ProductOptionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionProcessorMock; - - /** - * @var \Magento\Catalog\Model\ProductOptionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionFactory; - - /** - * @var \Magento\Catalog\Api\Data\ProductOptionExtensionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $extensionFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $collectionProcessor; - - /** - * @var array - */ - protected $productOptionData = []; - - protected function setUp() - { - $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->metadata = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Metadata::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->searchResultFactory = $this->getMockBuilder( - \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\ProductOptionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - - $this->collectionProcessor = $this->createMock( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - - $this->extensionFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionExtensionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage ID required - */ - public function testGetWithNoId() - { - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get(null); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Requested entity doesn't exist - */ - public function testGetEmptyEntity() - { - $orderItemId = 1; - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn(null); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get($orderItemId); - } - - public function testGet() - { - $orderItemId = 1; - $productType = 'configurable'; - - $this->productOptionData = ['option1' => 'value1']; - - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderMock($productType, $productOption); - - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($orderItemMock, $model->get($orderItemId)); - - // Assert already registered - $this->assertSame($orderItemMock, $model->get($orderItemId)); - } - - public function testGetList() - { - $productType = 'configurable'; - $this->productOptionData = ['option1' => 'value1']; - $searchCriteriaMock = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteria::class) - ->disableOriginalConstructor() - ->getMock(); - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderMock($productType, $productOption); - - $searchResultMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Item\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $searchResultMock->expects($this->once()) - ->method('getItems') - ->willReturn([$orderItemMock]); - - $this->searchResultFactory->expects($this->once()) - ->method('create') - ->willReturn($searchResultMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($searchResultMock, $model->getList($searchCriteriaMock)); - } - - public function testDeleteById() - { - $orderItemId = 1; - $productType = 'configurable'; - - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - $orderItemMock->expects($this->once()) - ->method('getProductType') - ->willReturn($productType); - $orderItemMock->expects($this->once()) - ->method('getBuyRequest') - ->willReturn($requestMock); - - $orderItemResourceMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemResourceMock->expects($this->once()) - ->method('delete') - ->with($orderItemMock) - ->willReturnSelf(); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - $this->metadata->expects($this->exactly(1)) - ->method('getMapper') - ->willReturn($orderItemResourceMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertTrue($model->deleteById($orderItemId)); - } - - /** - * @param \PHPUnit_Framework_MockObject_MockObject $orderItemMock - * @param string $productType - * @param array $data - * @return ItemRepository - */ - protected function getModel( - \PHPUnit_Framework_MockObject_MockObject $orderItemMock, - $productType, - array $data = [] - ) { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $requestUpdateMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - $requestUpdateMock->expects($this->any()) - ->method('getData') - ->willReturn($data); - - $this->productOptionProcessorMock = $this->getMockBuilder( - \Magento\Catalog\Model\ProductOptionProcessorInterface::class - ) - ->getMockForAbstractClass(); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToProductOption') - ->with($requestMock) - ->willReturn($this->productOptionData); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToBuyRequest') - ->with($orderItemMock) - ->willReturn($requestUpdateMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [ - $productType => $this->productOptionProcessorMock, - 'custom_options' => $this->productOptionProcessorMock - ], - $this->collectionProcessor - ); - return $model; - } - - /** - * @param string $productType - * @param \PHPUnit_Framework_MockObject_MockObject $productOption - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getOrderMock($productType, $productOption) - { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('getProductType') - ->willReturn($productType); - $orderItemMock->expects($this->once()) - ->method('getBuyRequest') - ->willReturn($requestMock); - $orderItemMock->expects($this->any()) - ->method('getProductOption') - ->willReturn(null); - $orderItemMock->expects($this->any()) - ->method('setProductOption') - ->with($productOption) - ->willReturnSelf(); - - return $orderItemMock; - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getProductOptionMock() - { - $productOption = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionInterface::class) - ->getMockForAbstractClass(); - $productOption->expects($this->any()) - ->method('getExtensionAttributes') - ->willReturn(null); - - $this->productOptionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOption); - - return $productOption; - } - - protected function getProductOptionExtensionMock() - { - $productOptionExtension = $this->getMockBuilder( - \Magento\Catalog\Api\Data\ProductOptionExtensionInterface::class - ) - ->setMethods([ - 'setData', - ]) - ->getMockForAbstractClass(); - $productOptionExtension->expects($this->any()) - ->method('setData') - ->with(key($this->productOptionData), current($this->productOptionData)) - ->willReturnSelf(); - - $this->extensionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOptionExtension); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php index 03a388410f335..ce7fcca42ccb4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemTest.php @@ -140,6 +140,9 @@ public function testGetStatusId( $this->assertEquals($expectedStatus, $this->model->getStatusId()); } + /** + * @return array + */ public function getStatusIdDataProvider() { return [ @@ -235,4 +238,115 @@ public function getProductOptionsDataProvider() ] ]; } + + /** + * Test different combinations of item qty setups + * + * @param array $options + * @param array $expectedResult + * @return void + * + * @dataProvider getItemQtyVariants + */ + public function testGetSimpleQtyToMethods(array $options, array $expectedResult) + { + $this->model->setData($options); + $this->assertSame($this->model->getSimpleQtyToShip(), $expectedResult['to_ship']); + $this->assertSame($this->model->getQtyToInvoice(), $expectedResult['to_invoice']); + } + + /** + * Provides different combinations of qty options for an item and the + * expected qtys pending shipment and invoice + * + * @return array + */ + public function getItemQtyVariants(): array + { + return [ + 'empty_item' => [ + 'options' => [ + 'qty_ordered' => 0, 'qty_invoiced' => 0, 'qty_refunded' => 0, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 0, 'to_invoice' => 0], + ], + 'ordered_item' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 0, 'qty_refunded' => 0, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 12, 'to_invoice' => 12], + ], + 'partially_invoiced' => [ + 'options' => ['qty_ordered' => 12, 'qty_invoiced' => 4, 'qty_refunded' => 0, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 12, 'to_invoice' => 8], + ], + 'completely_invoiced' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 0, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 12, 'to_invoice' => 0], + ], + 'partially_invoiced_refunded' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 5, 'qty_refunded' => 5, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 12, 'to_invoice' => 7], + ], + 'partially_refunded' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 5, 'qty_shipped' => 0, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 12, 'to_invoice' => 0], + ], + 'partially_shipped' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 0, 'qty_refunded' => 0, 'qty_shipped' => 4, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 8, 'to_invoice' => 12], + ], + 'partially_refunded_partially_shipped' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 5, 'qty_shipped' => 4, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 8, 'to_invoice' => 0], + ], + 'complete' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 12, 'qty_refunded' => 0, 'qty_shipped' => 12, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 0, 'to_invoice' => 0], + ], + 'canceled' => [ + 'options' => [ + 'qty_ordered' => 12, 'qty_invoiced' => 0, 'qty_refunded' => 0, 'qty_shipped' => 0, + 'qty_canceled' => 12, + ], + 'expectedResult' => ['to_ship' => 0, 'to_invoice' => 0], + ], + 'completely_shipped_using_decimals' => [ + 'options' => [ + 'qty_ordered' => 4.4, 'qty_invoiced' => 0.4, 'qty_refunded' => 0.4, 'qty_shipped' => 4, + 'qty_canceled' => 0, + ], + 'expectedResult' => ['to_ship' => 0.4, 'to_invoice' => 4.0], + ], + 'completely_invoiced_using_decimals' => [ + 'options' => [ + 'qty_ordered' => 4.4, 'qty_invoiced' => 4, 'qty_refunded' => 0, 'qty_shipped' => 4, + 'qty_canceled' => 0.4, + ], + 'expectedResult' => ['to_ship' => 0.0, 'to_invoice' => 0.0], + ], + ]; + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php index 70e5ad127e44c..293c2eea1231d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php @@ -179,7 +179,7 @@ public function testDecrypt() */ public function testSetAdditionalInformationException() { - $this->info->setAdditionalInformation('object', new \StdClass()); + $this->info->setAdditionalInformation('object', new \stdClass()); } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/RepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/RepositoryTest.php index 66ac821006266..8a40fbd20ef48 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/RepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/RepositoryTest.php @@ -149,6 +149,11 @@ public function testGetList() $this->assertSame($this->collection, $this->repository->getList($this->searchCriteria)); } + /** + * @param bool $id + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function mockPayment($id = false) { $payment = $this->createMock(\Magento\Sales\Model\Order\Payment::class); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php index 32ea9d8869344..b3cd2145e5df8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/State/RegisterCaptureNotificationCommandTest.php @@ -41,17 +41,19 @@ class RegisterCaptureNotificationCommandTest extends \PHPUnit\Framework\TestCase public function testExecute( $isTransactionPending, $isFraudDetected, + $currentState, $expectedState, $expectedStatus, $expectedMessage ) { + $order = $this->getOrder($currentState); $actualReturn = (new RegisterCaptureNotificationCommand($this->getStatusResolver()))->execute( $this->getPayment($isTransactionPending, $isFraudDetected), $this->amount, - $this->getOrder() + $order ); - $this->assertOrderStateAndStatus($this->getOrder(), $expectedState, $expectedStatus); + $this->assertOrderStateAndStatus($order, $expectedState, $expectedStatus); self::assertEquals(__($expectedMessage, $this->amount), $actualReturn); } @@ -64,6 +66,15 @@ public function commandResultDataProvider() [ false, false, + Order::STATE_COMPLETE, + Order::STATE_COMPLETE, + $this->newOrderStatus, + 'Registered notification about captured amount of %1.' + ], + [ + false, + false, + null, Order::STATE_PROCESSING, $this->newOrderStatus, 'Registered notification about captured amount of %1.' @@ -71,6 +82,7 @@ public function commandResultDataProvider() [ true, false, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, $this->newOrderStatus, 'An amount of %1 will be captured after being approved at the payment gateway.' @@ -78,6 +90,7 @@ public function commandResultDataProvider() [ false, true, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, Order::STATUS_FRAUD, 'Order is suspended as its capture amount %1 is suspected to be fraudulent.' @@ -85,6 +98,7 @@ public function commandResultDataProvider() [ true, true, + Order::STATE_PROCESSING, Order::STATE_PAYMENT_REVIEW, Order::STATUS_FRAUD, 'Order is suspended as its capture amount %1 is suspected to be fraudulent.' @@ -107,15 +121,19 @@ private function getStatusResolver() } /** + * @param string|null $state * @return Order|MockObject */ - private function getOrder() + private function getOrder($state) { + /** @var Order|MockObject $order */ $order = $this->getMockBuilder(Order::class) ->disableOriginalConstructor() + ->setMethods(['getBaseCurrency', 'getOrderStatusByState']) ->getMock(); $order->method('getBaseCurrency') ->willReturn($this->getCurrency()); + $order->setState($state); return $order; } @@ -159,7 +177,7 @@ private function getCurrency() */ private function assertOrderStateAndStatus($order, $expectedState, $expectedStatus) { - $order->method('setState')->with($expectedState); - $order->method('setStatus')->with($expectedStatus); + self::assertEquals($expectedState, $order->getState(), 'The order {state} should match.'); + self::assertEquals($expectedStatus, $order->getStatus(), 'The order {status} should match.'); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/BuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/BuilderTest.php index 1ea3aeedea51c..ea11604c53c45 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/BuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/BuilderTest.php @@ -257,6 +257,9 @@ protected function expectsIsPaymentTransactionClosed($isPaymentTransactionClosed ->willReturn($isPaymentTransactionClosed); } + /** + * @return array + */ public function createDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/ManagerTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/ManagerTest.php index 34b874c073a5a..13f6b9c607586 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/ManagerTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/Transaction/ManagerTest.php @@ -155,6 +155,9 @@ public function generateTransactionIdDataProvider() ]; } + /** + * @return array + */ public function isTransactionExistsDataProvider() { return [ @@ -165,6 +168,9 @@ public function isTransactionExistsDataProvider() ]; } + /** + * @return array + */ public function getAuthorizationDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php index ee7e07873da51..52762b0dbf315 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php @@ -600,6 +600,9 @@ public function testAcceptApprovePaymentTrue() self::assertEquals($baseGrandTotal, $this->payment->getBaseAmountPaidOnline()); } + /** + * @return array + */ public function acceptPaymentFalseProvider() { return [ @@ -1532,6 +1535,9 @@ public function testRefund() static::assertEquals($amount, $this->payment->getData('base_amount_refunded')); } + /** + * @return array + */ public function boolProvider() { return [ @@ -1570,6 +1576,9 @@ public function testGetShouldCloseParentTransaction() static::assertFalse($this->payment->getShouldCloseParentTransaction()); } + /** + * @return object + */ protected function initPayment() { return (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( @@ -1589,6 +1598,12 @@ protected function initPayment() ); } + /** + * @param $state + * @param null $status + * @param null $message + * @param null $isCustomerNotified + */ protected function assertOrderUpdated( $state, $status = null, @@ -1617,6 +1632,11 @@ protected function assertOrderUpdated( ->willReturn($statusHistory); } + /** + * @param $state + * @param $status + * @param array $allStatuses + */ protected function mockGetDefaultStatus($state, $status, $allStatuses = []) { /** @var \Magento\Sales\Model\Order\Config | \PHPUnit_Framework_MockObject_MockObject $orderConfigMock */ @@ -1642,6 +1662,11 @@ protected function mockGetDefaultStatus($state, $status, $allStatuses = []) ->will($this->returnValue($orderConfigMock)); } + /** + * @param $transactionId + * + * @return MockObject + */ protected function getTransactionMock($transactionId) { $transaction = $this->createPartialMock(\Magento\Sales\Model\Order\Payment\Transaction::class, [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php index c91d4edb155a4..0761b5abb5d45 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php @@ -19,7 +19,7 @@ public function testInsertTotals() // Setup parameters, that will be passed to the tested model method $page = $this->createMock(\Zend_Pdf_Page::class); - $order = new \StdClass(); + $order = new \stdClass(); $source = $this->createMock(\Magento\Sales\Model\Order\Invoice::class); $source->expects($this->any())->method('getOrder')->will($this->returnValue($order)); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/ReaderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/ReaderTest.php index b1b51c3f12330..b808a4139e84e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/ReaderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/ReaderTest.php @@ -87,7 +87,7 @@ protected function setUp() public function testRead() { $expectedResult = new \stdClass(); - $constraint = function (\DOMDOcument $actual) { + $constraint = function (\DOMDocument $actual) { try { $expected = __DIR__ . '/_files/pdf_merged.xml'; \PHPUnit\Framework\Assert::assertXmlStringEqualsXmlFile($expected, $actual->saveXML()); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/CommentRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/CommentRepositoryTest.php new file mode 100644 index 0000000000000..9cab366ef2c33 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/CommentRepositoryTest.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Shipment; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\Sales\Api\Data\ShipmentCommentInterfaceFactory; +use Magento\Sales\Api\Data\ShipmentCommentSearchResultInterfaceFactory; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Comment; +use Magento\Sales\Model\Order\Shipment\CommentRepository; +use Magento\Sales\Model\Order\Email\Sender\ShipmentCommentSender; +use Magento\Sales\Model\Spi\ShipmentCommentResourceInterface; +use Psr\Log\LoggerInterface; + +/** + * Class CommentRepositoryTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CommentRepositoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ShipmentCommentResourceInterface + */ + private $commentResource; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ShipmentCommentInterfaceFactory + */ + private $commentFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ShipmentCommentSearchResultInterfaceFactory + */ + private $searchResultFactory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CommentRepository + */ + private $commentRepository; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ShipmentCommentSender + */ + private $shipmentCommentSender; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ShipmentRepositoryInterface + */ + private $shipmentRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Shipment + */ + private $shipmentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Comment + */ + private $commentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + */ + private $loggerMock; + + protected function setUp() + { + $this->commentResource = $this->getMockBuilder(ShipmentCommentResourceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentFactory = $this->getMockBuilder(ShipmentCommentInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchResultFactory = $this->getMockBuilder(ShipmentCommentSearchResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shipmentRepositoryMock = $this->getMockBuilder(ShipmentRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shipmentCommentSender = $this->getMockBuilder(ShipmentCommentSender::class) + ->disableOriginalConstructor() + ->getMock(); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + + $this->shipmentMock = $this->getMockBuilder(Shipment::class)->disableOriginalConstructor()->getMock(); + $this->commentMock = $this->getMockBuilder(Comment::class)->disableOriginalConstructor()->getMock(); + + $this->commentRepository = new CommentRepository( + $this->commentResource, + $this->commentFactory, + $this->searchResultFactory, + $this->collectionProcessor, + $this->shipmentCommentSender, + $this->shipmentRepositoryMock, + $this->loggerMock + ); + } + + public function testSave() + { + $comment = "Comment text"; + $shipmentId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($shipmentId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->shipmentRepositoryMock->expects($this->once()) + ->method('get') + ->with($shipmentId) + ->willReturn($this->shipmentMock); + $this->shipmentCommentSender->expects($this->once()) + ->method('send') + ->with($this->shipmentMock, true, $comment); + $this->assertEquals($this->commentMock, $this->commentRepository->save($this->commentMock)); + } + + /** + * @expectedException \Magento\Framework\Exception\CouldNotSaveException + * @expectedExceptionMessage Could not save the shipment comment. + */ + public function testSaveWithException() + { + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willThrowException( + new \Magento\Framework\Exception\CouldNotSaveException(__('Could not save the shipment comment.')) + ); + + $this->commentRepository->save($this->commentMock); + } + + public function testSaveSendCatchException() + { + $comment = "Comment text"; + $creditmemoId = 123; + $this->commentResource->expects($this->once()) + ->method('save') + ->with($this->commentMock) + ->willReturnSelf(); + $this->commentMock->expects($this->once()) + ->method('getIsCustomerNotified') + ->willReturn(1); + $this->commentMock->expects($this->once()) + ->method('getParentId') + ->willReturn($creditmemoId); + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->shipmentRepositoryMock->expects($this->once()) + ->method('get') + ->with($creditmemoId) + ->willReturn($this->shipmentMock); + $this->shipmentCommentSender->expects($this->once()) + ->method('send') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('warning'); + + $this->commentRepository->save($this->commentMock); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 3d37018a61bb3..391e99ba6f835 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -262,6 +262,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', ]; + $transport = new \Magento\Framework\DataObject($transport); $this->eventManagerMock->expects($this->once()) ->method('dispatch') @@ -269,15 +270,16 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'email_shipment_set_template_vars_before', [ 'sender' => $this->subject, - 'transport' => $transport, + 'transport' => $transport->getData(), + 'transportObject' => $transport, ] ); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') - ->with($transport); + ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentTest.php index 5204073454345..f1724899f22f5 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentTest.php @@ -25,7 +25,7 @@ class ShipmentTest extends \PHPUnit\Framework\TestCase private $commentCollection; /** - * @var \Magento\Sales\Model\Order\shipment + * @var Shipment */ private $shipmentModel; @@ -46,9 +46,6 @@ public function testGetIncrementId() $this->assertEquals('test_increment_id', $this->shipmentModel->getIncrementId()); } - /** - * @covers \Magento\Sales\Model\Order\Shipment::getCommentsCollection - */ public function testGetCommentsCollection() { $shipmentId = 1; @@ -58,36 +55,29 @@ public function testGetCommentsCollection() ->disableOriginalConstructor() ->setMethods(['setShipment']) ->getMock(); - $shipmentItem->expects(static::once()) - ->method('setShipment') + $shipmentItem->method('setShipment') ->with($this->shipmentModel); $collection = [$shipmentItem]; - $this->commentCollection->expects(static::once()) + $this->commentCollection->expects(self::once()) ->method('setShipmentFilter') ->with($shipmentId) ->willReturnSelf(); - $this->commentCollection->expects(static::once()) + $this->commentCollection->expects(self::once()) ->method('setCreatedAtOrder') ->willReturnSelf(); - $this->commentCollection->expects(static::once()) - ->method('load') - ->willReturnSelf(); - $reflection = new \ReflectionClass(Collection::class); $reflectionProperty = $reflection->getProperty('_items'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->commentCollection, $collection); - $expected = $this->shipmentModel->getCommentsCollection(); + $actual = $this->shipmentModel->getCommentsCollection(); - static::assertEquals($expected, $this->commentCollection); + self::assertTrue(is_object($actual)); + self::assertEquals($this->commentCollection, $actual); } - /** - * @covers \Magento\Sales\Model\Order\Shipment::getComments - */ public function testGetComments() { $shipmentId = 1; @@ -97,30 +87,27 @@ public function testGetComments() ->disableOriginalConstructor() ->setMethods(['setShipment']) ->getMock(); - $shipmentItem->expects(static::once()) + $shipmentItem->expects(self::once()) ->method('setShipment') ->with($this->shipmentModel); $collection = [$shipmentItem]; - $this->commentCollection->expects(static::once()) - ->method('setShipmentFilter') + $this->commentCollection->method('setShipmentFilter') ->with($shipmentId) ->willReturnSelf(); - $this->commentCollection->expects(static::once()) - ->method('load') - ->willReturnSelf(); - $reflection = new \ReflectionClass(Collection::class); $reflectionProperty = $reflection->getProperty('_items'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->commentCollection, $collection); - $this->commentCollection->expects(static::once()) + $this->commentCollection->expects(self::once()) ->method('getItems') ->willReturn($collection); - static::assertEquals($this->shipmentModel->getComments(), $collection); + $actual = $this->shipmentModel->getComments(); + self::assertTrue(is_array($actual)); + self::assertEquals($collection, $actual); } /** @@ -131,7 +118,7 @@ private function initCommentsCollectionFactoryMock() { $this->commentCollection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() - ->setMethods(['setShipmentFilter', 'setCreatedAtOrder', 'getItems', 'load', '__wakeup']) + ->setMethods(['setShipmentFilter', 'setCreatedAtOrder', 'getItems', 'load']) ->getMock(); $this->commentCollectionFactory = $this->getMockBuilder(CollectionFactory::class) @@ -139,8 +126,7 @@ private function initCommentsCollectionFactoryMock() ->setMethods(['create']) ->getMock(); - $this->commentCollectionFactory->expects(static::any()) - ->method('create') + $this->commentCollectionFactory->method('create') ->willReturn($this->commentCollection); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php index e7187219d56b6..866fef378a1e9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php @@ -83,6 +83,17 @@ public function testGetStatusLabel() $this->assertEquals($status, $this->model->getStatusLabel()); } + /** + * @return void + */ + public function testGetStatusLabelWithNullStatus() + { + $this->model->setOrder($this->order); + $this->model->setStatus(null); + + $this->assertNull($this->model->getStatusLabel()); + } + public function testGetStoreFromStoreManager() { $resultStore = 1; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/StatusResolverTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/StatusResolverTest.php index 28c29cd7a3bdf..57a4d5f40aa36 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/StatusResolverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/StatusResolverTest.php @@ -27,6 +27,9 @@ public function testGetOrderStatusByState($order, $expectedReturn) self::assertEquals($expectedReturn, $actualReturn); } + /** + * @return array + */ public function statesDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php new file mode 100644 index 0000000000000..83c40707c0079 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Webapi/ChangeOutputArrayTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order\Webapi; + +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer; +use Magento\Sales\Model\Order\Webapi\ChangeOutputArray; + +/** + * Test for Magento\Sales\Model\Order\Webapi\ChangeOutputArray class. + */ +class ChangeOutputArrayTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DefaultColumn|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceRendererMock; + + /** + * @var DefaultRenderer|\PHPUnit_Framework_MockObject_MockObject + */ + private $defaultRendererMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ChangeOutputArray + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->priceRendererMock = $this->createMock(DefaultColumn::class); + $this->defaultRendererMock = $this->createMock(DefaultRenderer::class); + + $this->model = $this->objectManager->getObject( + ChangeOutputArray::class, + [ + 'priceRenderer' => $this->priceRendererMock, + 'defaultRenderer' => $this->defaultRendererMock, + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $expectedResult = [ + OrderItemInterface::ROW_TOTAL => 10, + OrderItemInterface::BASE_ROW_TOTAL => 10, + OrderItemInterface::ROW_TOTAL_INCL_TAX => 11, + OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX => 11, + ]; + $orderItemInterfaceMock = $this->createMock(OrderItemInterface::class); + + $this->priceRendererMock->expects($this->once()) + ->method('getTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(10); + $this->priceRendererMock->expects($this->once()) + ->method('getBaseTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(10); + $this->defaultRendererMock->expects($this->once()) + ->method('getTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(11); + $this->defaultRendererMock->expects($this->once()) + ->method('getBaseTotalAmount') + ->with($orderItemInterfaceMock) + ->willReturn(11); + + $this->assertEquals($expectedResult, $this->model->execute($orderItemInterfaceMock, [])); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php index 3df667094f2a9..04b774c8a74fd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php @@ -9,6 +9,8 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Model\ResourceModel\Metadata; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -41,7 +43,17 @@ class OrderRepositoryTest extends \PHPUnit\Framework\TestCase private $collectionProcessor; /** - * Setup the test + * @var OrderTaxManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderTaxManagementMock; + + /** + * @var PaymentAdditionalInfoInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdditionalInfoFactory; + + /** + * @inheritdoc */ protected function setUp() { @@ -58,34 +70,67 @@ protected function setUp() $orderExtensionFactoryMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderExtensionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->orderTaxManagementMock = $this->getMockBuilder(OrderTaxManagementInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentAdditionalInfoFactory = $this->getMockBuilder(PaymentAdditionalInfoInterfaceFactory::class) + ->disableOriginalConstructor()->setMethods(['create'])->getMockForAbstractClass(); $this->orderRepository = $this->objectManager->getObject( \Magento\Sales\Model\OrderRepository::class, [ 'metadata' => $this->metadata, 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessor, - 'orderExtensionFactory' => $orderExtensionFactoryMock + 'orderExtensionFactory' => $orderExtensionFactoryMock, + 'orderTaxManagement' => $this->orderTaxManagementMock, + 'paymentAdditionalInfoFactory' => $this->paymentAdditionalInfoFactory, ] ); } + /** + * Test for method getList. + * + * @return void + */ public function testGetList() { $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); - $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor()->getMock(); + $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderTaxDetailsMock = $this->getMockBuilder(\Magento\Tax\Api\Data\OrderTaxDetailsInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAppliedTaxes', 'getItems'])->getMockForAbstractClass(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderPaymentInterface::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $paymentAdditionalInfo = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentAdditionalInfoInterface::class) + ->disableOriginalConstructor()->setMethods(['setKey', 'setValue'])->getMockForAbstractClass(); $extensionAttributes = $this->createPartialMock( \Magento\Sales\Api\Data\OrderExtension::class, - ['getShippingAssignments'] + [ + 'getShippingAssignments', 'setShippingAssignments', 'setConvertingFromQuote', + 'setAppliedTaxes', 'setItemAppliedTaxes', 'setPaymentAdditionalInfo', + ] ); $shippingAssignmentBuilder = $this->createMock( \Magento\Sales\Model\Order\ShippingAssignmentBuilder::class ); + $itemsMock->expects($this->atLeastOnce())->method('getEntityId')->willReturn(1); $this->collectionProcessor->expects($this->once()) ->method('process') ->with($searchCriteriaMock, $collectionMock); - $itemsMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atLeastOnce())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atleastOnce())->method('getPayment')->willReturn($paymentMock); + $paymentMock->expects($this->atLeastOnce())->method('getAdditionalInformation') + ->willReturn(['method' => 'checkmo']); + $this->paymentAdditionalInfoFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($paymentAdditionalInfo); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setKey')->willReturnSelf(); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setValue')->willReturnSelf(); + $this->orderTaxManagementMock->expects($this->atLeastOnce())->method('getOrderTaxDetails') + ->willReturn($orderTaxDetailsMock); $extensionAttributes->expects($this->any()) ->method('getShippingAssignments') ->willReturn($shippingAssignmentBuilder); @@ -96,6 +141,11 @@ public function testGetList() $this->assertEquals($collectionMock, $this->orderRepository->getList($searchCriteriaMock)); } + /** + * Test for method save. + * + * @return void + */ public function testSave() { $mapperMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index dab92632426fa..6784e10babfee 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -7,6 +7,8 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Order\Status\History\CollectionFactory as HistoryCollectionFactory; @@ -15,6 +17,7 @@ * Test class for \Magento\Sales\Model\Order * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ class OrderTest extends \PHPUnit\Framework\TestCase { @@ -49,6 +52,7 @@ class OrderTest extends \PHPUnit\Framework\TestCase protected $item; /** + * @var HistoryCollectionFactory|\PHPUnit_Framework_MockObject_MockObject * @var HistoryCollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $historyCollectionFactoryMock; @@ -73,6 +77,16 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ protected $productCollectionFactoryMock; + /** + * @var ResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $localeResolver; + + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $timezone; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -102,7 +116,9 @@ protected function setUp() 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice', - 'getProductId' + 'getProductId', + 'getQtyRefunded', + 'getQtyInvoiced', ]); $this->salesOrderCollectionMock = $this->getMockBuilder( \Magento\Sales\Model\ResourceModel\Order\Collection::class @@ -124,6 +140,8 @@ protected function setUp() true, ['round'] ); + $this->localeResolver = $this->createMock(ResolverInterface::class); + $this->timezone = $this->createMock(TimezoneInterface::class); $this->incrementId = '#00000001'; $this->eventManager = $this->createMock(\Magento\Framework\Event\Manager::class); $context = $this->createPartialMock(\Magento\Framework\Model\Context::class, ['getEventDispatcher']); @@ -138,7 +156,9 @@ protected function setUp() 'historyCollectionFactory' => $this->historyCollectionFactoryMock, 'salesOrderCollectionFactory' => $this->salesOrderCollectionFactoryMock, 'priceCurrency' => $this->priceCurrency, - 'productListFactory' => $this->productCollectionFactoryMock + 'productListFactory' => $this->productCollectionFactoryMock, + 'localeResolver' => $this->localeResolver, + 'timezone' => $this->timezone, ] ); } @@ -316,6 +336,20 @@ public function testCanCreditMemo() $this->assertTrue($this->order->canCreditmemo()); } + /** + * Test canCreditMemo method when grand total and paid total are zero. + * + * @return void + */ + public function testCanCreditMemoForZeroTotal() + { + $grandTotal = 0; + $totalPaid = 0; + $this->order->setGrandTotal($grandTotal); + $this->order->setTotalPaid($totalPaid); + $this->assertFalse($this->order->canCreditmemo()); + } + public function testCanNotCreditMemoWithTotalNull() { $totalPaid = 0; @@ -603,6 +637,124 @@ public function testCanCancelAllInvoiced() $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(0); + $this->item->expects($this->any()) + ->method('getQtyRefunded') + ->willReturn(0); + $this->item->expects($this->any()) + ->method('getQtyInvoiced') + ->willReturn(1); + + $this->assertFalse($this->order->canCancel()); + } + + /** + * @return void + */ + public function testCanCancelAllRefunded() + { + $collectionMock = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, + ['getItems', 'setOrderFilter'] + ); + $this->orderItemCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collectionMock); + $collectionMock->expects($this->any()) + ->method('setOrderFilter') + ->willReturnSelf(); + + $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); + $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); + + $this->item->expects($this->any()) + ->method('isDeleted') + ->willReturn(false); + $this->item->expects($this->once()) + ->method('getQtyRefunded') + ->willReturn(10); + $this->item->expects($this->once()) + ->method('getQtyInvoiced') + ->willReturn(10); + + $this->assertTrue($this->order->canCancel()); + } + + /** + * Test that order can be canceled if some items were partially invoiced with certain qty + * and then refunded for this qty. + * Sample: + * - ordered qty = 20 + * - invoiced = 10 + * - refunded = 10 + */ + public function testCanCancelPartiallyInvoicedAndRefunded() + { + $collectionMock = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, + ['getItems', 'setOrderFilter'] + ); + $this->orderItemCollectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($collectionMock); + $collectionMock->expects($this->any()) + ->method('setOrderFilter') + ->willReturnSelf(); + + $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); + $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); + + $this->item->expects($this->any()) + ->method('isDeleted') + ->willReturn(false); + $this->item->expects($this->once()) + ->method('getQtyToInvoice') + ->willReturn(10); + $this->item->expects($this->any()) + ->method('getQtyRefunded') + ->willReturn(10); + $this->item->expects($this->any()) + ->method('getQtyInvoiced') + ->willReturn(10); + + $this->assertTrue($this->order->canCancel()); + } + + /** + * Test that order CAN NOT be canceled if some items were partially invoiced with certain qty + * and then refunded for less than that qty. + * Sample: + * - ordered qty = 10 + * - invoiced = 10 + * - refunded = 5 + */ + public function testCanCancelPartiallyInvoicedAndNotFullyRefunded() + { + $collectionMock = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Order\Item\Collection::class, + ['getItems', 'setOrderFilter'] + ); + $this->orderItemCollectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($collectionMock); + $collectionMock->expects($this->any()) + ->method('setOrderFilter') + ->willReturnSelf(); + + $this->order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD, false); + $this->order->setState(\Magento\Sales\Model\Order::STATE_NEW); + + $this->item->expects($this->any()) + ->method('isDeleted') + ->willReturn(false); + $this->item->expects($this->any()) + ->method('getQtyToInvoice') + ->willReturn(0); + $this->item->expects($this->any()) + ->method('getQtyRefunded') + ->willReturn(5); + $this->item->expects($this->any()) + ->method('getQtyInvoiced') + ->willReturn(10); $this->assertFalse($this->order->canCancel()); } @@ -829,6 +981,9 @@ protected function prepareItemMock($qtyInvoiced) ->will($this->returnValue($itemCollectionMock)); } + /** + * @return array + */ public function canVoidPaymentDataProvider() { $data = []; @@ -840,6 +995,9 @@ public function canVoidPaymentDataProvider() return $data; } + /** + * @return array + */ public function dataProviderActionFlag() { return [ @@ -1044,6 +1202,25 @@ public function testResetOrderWillResetPayment() ); } + public function testGetCreatedAtFormattedUsesCorrectLocale() + { + $localeCode = 'nl_NL'; + + $this->localeResolver->expects($this->once())->method('getDefaultLocale')->willReturn($localeCode); + $this->timezone->expects($this->once())->method('formatDateTime') + ->with( + $this->anything(), + $this->anything(), + $this->anything(), + $localeCode + ); + + $this->order->getCreatedAtFormatted(\IntlDateFormatter::SHORT); + } + + /** + * @return array + */ public function notInvoicingStatesProvider() { return [ @@ -1053,6 +1230,9 @@ public function notInvoicingStatesProvider() ]; } + /** + * @return array + */ public function canNotCreditMemoStatesProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php index 5a8c1032cd4fc..5148752e9831a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundInvoiceTest.php @@ -463,6 +463,9 @@ public function testCouldNotCreditmemoException() ); } + /** + * @return array + */ public function dataProvider() { $creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index 8bc3288af04cf..c95b56d81d6f4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -407,6 +407,9 @@ public function testCouldNotCreditmemoException() ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridPoolTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridPoolTest.php index 8c19d68efc0cd..a39c00ae7f0fd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridPoolTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridPoolTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\ResourceModel; /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridTest.php new file mode 100644 index 0000000000000..50cbdcb974c5b --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/GridTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\ResourceModel; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\ResourceModel\Provider\NotSyncedDataProviderInterface; +use Magento\Framework\DB\Adapter\AdapterInterface as ConnectionAdapterInterface; +use Magento\Sales\Model\ResourceModel\Grid; + +/** + * Unit tests for \Magento\Sales\Model\ResourceModel\Grid class + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Grid + */ + private $grid; + + /** + * @var NotSyncedDataProviderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $notSyncedDataProvider; + + /** + * @var ConnectionAdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connection; + + /** + * @var string + */ + private $mainTable = 'sales_order'; + + /** + * @var string + */ + private $gridTable = 'sales_order_grid'; + + /** + * @var array + */ + private $columns = [ + 'column_1_key' => 'column_1_value', + 'column_2_key' => 'column_2_value' + ]; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $this->notSyncedDataProvider = $this->getMockBuilder(NotSyncedDataProviderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getIds']) + ->getMockForAbstractClass(); + $this->connection = $this->getMockBuilder(ConnectionAdapterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['select', 'fetchAll', 'insertOnDuplicate']) + ->getMockForAbstractClass(); + + $this->grid = $objectManager->getObject( + \Magento\Sales\Model\ResourceModel\Grid::class, + [ + 'notSyncedDataProvider' => $this->notSyncedDataProvider, + 'mainTableName' => $this->mainTable, + 'gridTableName' => $this->gridTable, + 'connection' => $this->connection, + '_tables' => ['sales_order' => $this->mainTable, 'sales_order_grid' => $this->gridTable], + 'columns' => $this->columns + ] + ); + } + + /** + * Test for refreshBySchedule() method + */ + public function testRefreshBySchedule() + { + $notSyncedIds = ['1', '2', '3']; + $fetchResult = ['column_1' => '1', 'column_2' => '2']; + + $this->notSyncedDataProvider->expects($this->atLeastOnce())->method('getIds')->willReturn($notSyncedIds); + $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->setMethods(['from', 'columns', 'where']) + ->getMock(); + $select->expects($this->atLeastOnce())->method('from')->with(['sales_order' => $this->mainTable], []) + ->willReturnSelf(); + $select->expects($this->atLeastOnce())->method('columns')->willReturnSelf(); + $select->expects($this->atLeastOnce())->method('where') + ->with($this->mainTable . '.entity_id IN (?)', $notSyncedIds) + ->willReturnSelf(); + + $this->connection->expects($this->atLeastOnce())->method('select')->willReturn($select); + $this->connection->expects($this->atLeastOnce())->method('fetchAll')->with($select)->willReturn($fetchResult); + $this->connection->expects($this->atLeastOnce())->method('insertOnDuplicate') + ->with($this->gridTable, $fetchResult, array_keys($this->columns)) + ->willReturn(array_count_values($notSyncedIds)); + + $this->grid->refreshBySchedule(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index e120d613e323c..65313c584454b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -8,7 +8,7 @@ use Magento\Sales\Model\Order; /** - * Class StateTest + * Tests for State. */ class StateTest extends \PHPUnit\Framework\TestCase { @@ -22,9 +22,14 @@ class StateTest extends \PHPUnit\Framework\TestCase */ protected $orderMock; + /** + * @inheritdoc + */ protected function setUp() { - $this->orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ + $this->orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + [ '__wakeup', 'getId', 'hasCustomerNoteNotify', @@ -35,13 +40,12 @@ protected function setUp() 'canShip', 'getBaseGrandTotal', 'canCreditmemo', - 'getState', - 'setState', 'getTotalRefunded', 'hasForcedCanCreditmemo', 'getIsInProcess', 'getConfig', - ]); + ] + ); $this->orderMock->expects($this->any()) ->method('getConfig') ->willReturnSelf(); @@ -53,127 +57,96 @@ protected function setUp() } /** - * test check order - order without id + * Test for check method with different states. + * + * @param bool $isCanceled + * @param bool $canUnhold + * @param bool $canInvoice + * @param bool $canShip + * @param int $callCanSkipNum + * @param bool $canCreditmemo + * @param int $callCanCreditmemoNum + * @param string $currentState + * @param string $expectedState + * @param int $callSetStateNum + * @return void + * @dataProvider stateCheckDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ - public function testCheckOrderEmpty() - { - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->willReturn(100); - $this->orderMock->expects($this->never()) - ->method('setState'); - - $this->state->check($this->orderMock); - } - - /** - * test check order - set state complete - */ - public function testCheckSetStateComplete() - { + public function testCheck( + bool $canCreditmemo, + int $callCanCreditmemoNum, + bool $canShip, + int $callCanSkipNum, + string $currentState, + string $expectedState = '', + bool $isInProcess = false, + int $callGetIsInProcessNum = 0, + bool $isCanceled = false, + bool $canUnhold = false, + bool $canInvoice = false + ) { + $this->orderMock->setState($currentState); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) - ->method('canCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_COMPLETE) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); - } - - /** - * test check order - set state closed - */ - public function testCheckSetStateClosed() - { + ->willReturn($isCanceled); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canUnhold); + $this->orderMock->expects($this->any()) ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canInvoice); + $this->orderMock->expects($this->exactly($callCanSkipNum)) ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) + ->willReturn($canShip); + $this->orderMock->expects($this->exactly($callCanCreditmemoNum)) ->method('canCreditmemo') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->exactly(2)) - ->method('getTotalRefunded') - ->will($this->returnValue(null)); - $this->orderMock->expects($this->once()) - ->method('hasForcedCanCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_CLOSED) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + ->willReturn($canCreditmemo); + $this->orderMock->expects($this->exactly($callGetIsInProcessNum)) + ->method('getIsInProcess') + ->willReturn($isInProcess); + $this->state->check($this->orderMock); + $this->assertEquals($expectedState, $this->orderMock->getState()); } /** - * test check order - set state processing + * Data provider for testCheck method. + * + * @return array */ - public function testCheckSetStateProcessing() + public function stateCheckDataProvider(): array { - $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('getState') - ->will($this->returnValue(Order::STATE_NEW)); - $this->orderMock->expects($this->once()) - ->method('getIsInProcess') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_PROCESSING) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + return [ + 'processing - !canCreditmemo!canShip -> closed' => + [false, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_CLOSED], + 'complete - !canCreditmemo,!canShip -> closed' => + [false, 1, false, 1, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - !canCreditmemo,canShip -> processing' => + [false, 1, true, 2, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - !canCreditmemo,canShip -> complete' => + [false, 1, true, 1, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'processing - canCreditmemo,!canShip -> complete' => + [true, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_COMPLETE], + 'complete - canCreditmemo,!canShip -> complete' => + [true, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'processing - canCreditmemo, canShip -> processing' => + [true, 1, true, 1, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - canCreditmemo, canShip -> complete' => + [true, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'new - canCreditmemo, canShip, IsInProcess -> processing' => + [true, 1, true, 1, Order::STATE_NEW, Order::STATE_PROCESSING, true, 1], + 'new - canCreditmemo, !canShip, IsInProcess -> processing' => + [true, 1, false, 1, Order::STATE_NEW, Order::STATE_COMPLETE, true, 1], + 'new - canCreditmemo, canShip, !IsInProcess -> new' => + [true, 0, true, 0, Order::STATE_NEW, Order::STATE_NEW, false, 1], + 'hold - canUnhold -> hold' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, false, true], + 'payment_review - canUnhold -> payment_review' => + [true, 0, true, 0, Order::STATE_PAYMENT_REVIEW, Order::STATE_PAYMENT_REVIEW, false, 0, false, true], + 'pending_payment - canUnhold -> pending_payment' => + [true, 0, true, 0, Order::STATE_PENDING_PAYMENT, Order::STATE_PENDING_PAYMENT, false, 0, false, true], + 'cancelled - isCanceled -> cancelled' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, true], + ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php index a7a615fb0f508..9eee4b809ba0e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php @@ -3,145 +3,125 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Test\Unit\Model\ResourceModel\Order\Shipment; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Comment as CommentEntity; +use Magento\Sales\Model\Order\Shipment\Item as ItemEntity; +use Magento\Sales\Model\Order\Shipment\Track as TrackEntity; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Item; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Relation; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Track; +use PHPUnit\Framework\MockObject\MockObject; + /** * Class RelationTest */ class RelationTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation + * @var Relation */ - protected $relationProcessor; + private $relationProcessor; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var Item|MockObject */ - protected $itemResourceMock; + private $itemResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var Track|MockObject */ - protected $trackResourceMock; + private $trackResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var Comment|MockObject */ - protected $commentResourceMock; + private $commentResource; /** - * @var \Magento\Sales\Model\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var CommentEntity|MockObject */ - protected $commentMock; + private $comment; /** - * @var \Magento\Sales\Model\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var TrackEntity|MockObject */ - protected $trackMock; + private $track; /** - * @var \Magento\Sales\Model\Order\Shipment|\PHPUnit_Framework_MockObject_MockObject + * @var Shipment|MockObject */ - protected $shipmentMock; + private $shipment; /** - * @var \Magento\Sales\Model\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var ItemEntity|MockObject */ - protected $itemMock; + private $item; + /** + * @inheritdoc + */ protected function setUp() { - $this->itemResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Item::class) + $this->itemResource = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->commentResourceMock = $this->getMockBuilder( - \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment::class - ) + $this->commentResource = $this->getMockBuilder(Comment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->trackResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Track::class) + $this->trackResource = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->shipmentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->shipment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'getId', - 'getItems', - 'getTracks', - 'getComments', - 'getTracksCollection', - ] - ) ->getMock(); - $this->itemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + $this->item = $this->getMockBuilder(ItemEntity::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'setParentId' - ] - ) ->getMock(); - $this->trackMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $this->track = $this->getMockBuilder(TrackEntity::class) ->disableOriginalConstructor() ->getMock(); - $this->commentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->comment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() ->getMock(); - $this->relationProcessor = new \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation( - $this->itemResourceMock, - $this->trackResourceMock, - $this->commentResourceMock + $this->relationProcessor = new Relation( + $this->itemResource, + $this->trackResource, + $this->commentResource ); } + /** + * Checks saving shipment relations. + * + * @throws \Exception + */ public function testProcessRelations() { - $this->shipmentMock->expects($this->exactly(3)) - ->method('getId') + $this->shipment->method('getId') ->willReturn('shipment-id-value'); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getItems') - ->willReturn([$this->itemMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getComments') - ->willReturn([$this->commentMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getTracksCollection') - ->willReturn([$this->trackMock]); - $this->itemMock->expects($this->once()) - ->method('setParentId') + $this->shipment->method('getItems') + ->willReturn([$this->item]); + $this->shipment->method('getComments') + ->willReturn([$this->comment]); + $this->shipment->method('getTracks') + ->willReturn([$this->track]); + $this->item->method('setParentId') ->with('shipment-id-value') ->willReturnSelf(); - $this->itemResourceMock->expects($this->once()) - ->method('save') - ->with($this->itemMock) + $this->itemResource->method('save') + ->with($this->item) ->willReturnSelf(); - $this->commentResourceMock->expects($this->once()) - ->method('save') - ->with($this->commentMock) + $this->commentResource->method('save') + ->with($this->comment) ->willReturnSelf(); - $this->trackResourceMock->expects($this->once()) - ->method('save') - ->with($this->trackMock) + $this->trackResource->method('save') + ->with($this->track) ->willReturnSelf(); - $this->relationProcessor->processRelation($this->shipmentMock); + $this->relationProcessor->processRelation($this->shipment); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/TrackTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/TrackTest.php index ea19ce7d7ff9d..588101167de17 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/TrackTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/TrackTest.php @@ -89,6 +89,8 @@ protected function setUp() */ public function testSave() { + $shipmentMock = $this->createMock(\Magento\Sales\Model\Order\Shipment::class); + $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); $this->entitySnapshotMock->expects($this->once()) ->method('isModified') ->with($this->trackModelMock) @@ -98,6 +100,8 @@ public function testSave() ->with($this->equalTo($this->trackModelMock)) ->will($this->returnValue([])); $this->trackModelMock->expects($this->any())->method('getData')->willReturn([]); + $this->trackModelMock->expects($this->atLeastOnce())->method('getShipment')->willReturn($shipmentMock); + $shipmentMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); $this->trackResource->save($this->trackModelMock); $this->assertTrue(true); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/StatusTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/StatusTest.php index 32428bb598561..0be9662842199 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/StatusTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/StatusTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sales\Test\Unit\Model\ResourceModel\Order; /** @@ -46,7 +44,10 @@ protected function setUp() $this->selectMock->expects($this->any())->method('from')->will($this->returnSelf()); $this->selectMock->expects($this->any())->method('where'); - $this->connectionMock = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['update', 'insertOnDuplicate', 'select']); + $this->connectionMock = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + ['update', 'insertOnDuplicate', 'select'] + ); $this->connectionMock->expects($this->any())->method('select')->will($this->returnValue($this->selectMock)); $this->resourceMock = $this->createMock(\Magento\Framework\App\ResourceConnection::class); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php b/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php index ce2d09c71b52e..f11e1499db2ce 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php @@ -6,9 +6,11 @@ namespace Magento\Sales\Test\Unit\Model\Rss; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Rss\Signature; /** * Class OrderStatusTest + * * @package Magento\Sales\Model\Rss * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -64,6 +66,11 @@ class OrderStatusTest extends \PHPUnit\Framework\TestCase */ protected $order; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Signature + */ + private $signature; + /** * @var array */ @@ -86,6 +93,9 @@ class OrderStatusTest extends \PHPUnit\Framework\TestCase ], ]; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); @@ -119,7 +129,7 @@ protected function setUp() $this->order->expects($this->any())->method('formatPrice')->will($this->returnValue('15.00')); $this->order->expects($this->any())->method('getGrandTotal')->will($this->returnValue(15)); $this->order->expects($this->any())->method('load')->with(1)->will($this->returnSelf()); - + $this->signature = $this->createMock(Signature::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->model = $this->objectManagerHelper->getObject( \Magento\Sales\Model\Rss\OrderStatus::class, @@ -130,17 +140,33 @@ protected function setUp() 'orderResourceFactory' => $this->orderStatusFactory, 'localeDate' => $this->timezoneInterface, 'orderFactory' => $this->orderFactory, - 'scopeConfig' => $this->scopeConfigInterface + 'scopeConfig' => $this->scopeConfigInterface, + 'signature' => $this->signature, ] ); } + /** + * Positive scenario. + */ public function testGetRssData() { $this->orderFactory->expects($this->once())->method('create')->willReturn($this->order); $requestData = base64_encode('{"order_id":1,"increment_id":"100000001","customer_id":1}'); + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); - $this->requestInterface->expects($this->any())->method('getParam')->with('data')->willReturn($requestData); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); $resource = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Rss\OrderStatus::class) ->setMethods(['getAllCommentCollection']) @@ -162,24 +188,64 @@ public function testGetRssData() } /** + * Case when invalid data is provided. + * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Order not found. */ public function testGetRssDataWithError() { $this->orderFactory->expects($this->once())->method('create')->willReturn($this->order); - $requestData = base64_encode('{"order_id":"1","increment_id":true,"customer_id":true}'); - - $this->requestInterface->expects($this->any())->method('getParam')->with('data')->willReturn($requestData); - + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); $this->orderStatusFactory->expects($this->never())->method('create'); - $this->urlInterface->expects($this->never())->method('getUrl'); + $this->assertEquals($this->feedData, $this->model->getRssData()); + } + /** + * Case when invalid signature is provided. + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Order not found. + */ + public function testGetRssDataWithWrongSignature() + { + $requestData = base64_encode('{"order_id":"1","increment_id":true,"customer_id":true}'); + $this->signature->expects($this->never()) + ->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(false); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); + $this->orderStatusFactory->expects($this->never())->method('create'); + $this->urlInterface->expects($this->never())->method('getUrl'); $this->assertEquals($this->feedData, $this->model->getRssData()); } + /** + * Testing allowed getter. + */ public function testIsAllowed() { $this->scopeConfigInterface->expects($this->once())->method('getValue') @@ -189,6 +255,8 @@ public function testIsAllowed() } /** + * Test caching. + * * @param string $requestData * @param string $result * @dataProvider getCacheKeyDataProvider @@ -196,13 +264,22 @@ public function testIsAllowed() public function testGetCacheKey($requestData, $result) { $this->requestInterface->expects($this->any())->method('getParam') - ->with('data') - ->will($this->returnValue($requestData)); + ->willReturnMap([ + ['data', null, $requestData], + ['signature', null, 'signature'], + ]); + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); $this->orderFactory->expects($this->once())->method('create')->will($this->returnValue($this->order)); $this->assertEquals('rss_order_status_data_' . $result, $this->model->getCacheKey()); } /** + * Test data for caching test. + * * @return array */ public function getCacheKeyDataProvider() @@ -213,6 +290,9 @@ public function getCacheKeyDataProvider() ]; } + /** + * Test for cache lifetime getter. + */ public function testGetCacheLifetime() { $this->assertEquals(600, $this->model->getCacheLifetime()); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php index 9ecab6cf9ab52..68681c6c5a66b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Model\Service; use Magento\Sales\Model\Order; +use Magento\Sales\Api\Data\CreditmemoInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class CreditmemoServiceTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,34 +19,34 @@ class CreditmemoServiceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|MockObject */ protected $creditmemoRepositoryMock; /** - * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|MockObject */ protected $creditmemoCommentRepositoryMock; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\SearchCriteriaBuilder|MockObject */ protected $searchCriteriaBuilderMock; /** - * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\FilterBuilder|MockObject */ protected $filterBuilderMock; /** - * @var \Magento\Sales\Model\Order\CreditmemoNotifier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Model\Order\CreditmemoNotifier|MockObject */ protected $creditmemoNotifierMock; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|MockObject */ - private $priceCurrencyMock; + private $priceCurrency; /** * @var \Magento\Sales\Model\Service\CreditmemoService @@ -79,7 +84,7 @@ protected function setUp() ['setField', 'setValue', 'setConditionType', 'create'] ); $this->creditmemoNotifierMock = $this->createMock(\Magento\Sales\Model\Order\CreditmemoNotifier::class); - $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) ->getMockForAbstractClass(); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -91,7 +96,7 @@ protected function setUp() 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'filterBuilder' => $this->filterBuilderMock, 'creditmemoNotifier' => $this->creditmemoNotifierMock, - 'priceCurrency' => $this->priceCurrencyMock, + 'priceCurrency' => $this->priceCurrency, ] ); } @@ -187,7 +192,79 @@ public function testRefund() $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); - $this->priceCurrencyMock->expects($this->any()) + $this->priceCurrency->expects($this->any()) + ->method('round') + ->willReturnArgument(0); + + // Set payment adapter dependency + $refundAdapterMock = $this->getMockBuilder(\Magento\Sales\Model\Order\RefundAdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->creditmemoService, + 'refundAdapter', + $refundAdapterMock + ); + + // Set resource dependency + $resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->creditmemoService, + 'resource', + $resourceMock + ); + + // Set order repository dependency + $orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->creditmemoService, + 'orderRepository', + $orderRepositoryMock + ); + + $adapterMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $resourceMock->expects($this->once())->method('getConnection')->with('sales')->willReturn($adapterMock); + $adapterMock->expects($this->once())->method('beginTransaction'); + $refundAdapterMock->expects($this->once()) + ->method('refund') + ->with($creditMemoMock, $orderMock, false) + ->willReturn($orderMock); + $orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($orderMock); + $creditMemoMock->expects($this->once()) + ->method('getInvoice') + ->willReturn(null); + $adapterMock->expects($this->once())->method('commit'); + $this->creditmemoRepositoryMock->expects($this->once()) + ->method('save'); + + $this->assertSame($creditMemoMock, $this->creditmemoService->refund($creditMemoMock, true)); + } + + public function testRefundPendingCreditMemo() + { + $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder', 'getState', 'getInvoice']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $creditMemoMock->expects($this->once())->method('getId')->willReturn(444); + $creditMemoMock->expects($this->once())->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Creditmemo::STATE_OPEN); + $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); + + $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); + $orderMock->expects($this->once())->method('getBaseTotalRefunded')->willReturn(0); + $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); + $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); + + $this->priceCurrency->expects($this->any()) ->method('round') ->willReturnArgument(0); @@ -252,27 +329,32 @@ public function testRefundExpectsMoneyAvailableToReturn() $baseGrandTotal = 10; $baseTotalRefunded = 9; $baseTotalPaid = 10; - $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) - ->setMethods(['getId', 'getOrder', 'formatBasePrice']) + /** @var CreditmemoInterface|MockObject $creditMemo */ + $creditMemo = $this->getMockBuilder(CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder']) ->getMockForAbstractClass(); - $creditMemoMock->expects($this->once())->method('getId')->willReturn(null); - $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); - $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); - $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn($baseGrandTotal); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalRefunded')->willReturn($baseTotalRefunded); - $this->priceCurrencyMock->expects($this->exactly(2))->method('round')->withConsecutive( - [$baseTotalRefunded + $baseGrandTotal], - [$baseTotalPaid] - )->willReturnOnConsecutiveCalls( - $baseTotalRefunded + $baseGrandTotal, - $baseTotalPaid - ); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalPaid')->willReturn($baseTotalPaid); + $creditMemo->method('getId') + ->willReturn(null); + /** @var Order|MockObject $order */ + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $creditMemo->method('getOrder') + ->willReturn($order); + $creditMemo->method('getBaseGrandTotal') + ->willReturn($baseGrandTotal); + $order->method('getBaseTotalRefunded') + ->willReturn($baseTotalRefunded); + $this->priceCurrency->method('round') + ->withConsecutive([$baseTotalRefunded + $baseGrandTotal], [$baseTotalPaid]) + ->willReturnOnConsecutiveCalls($baseTotalRefunded + $baseGrandTotal, $baseTotalPaid); + $order->method('getBaseTotalPaid') + ->willReturn($baseTotalPaid); $baseAvailableRefund = $baseTotalPaid - $baseTotalRefunded; - $orderMock->expects($this->once())->method('formatBasePrice')->with( - $baseAvailableRefund - )->willReturn($baseAvailableRefund); - $this->creditmemoService->refund($creditMemoMock, true); + $order->method('formatPriceTxt') + ->with($baseAvailableRefund) + ->willReturn($baseAvailableRefund); + $this->creditmemoService->refund($creditMemo, true); } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php index 067f83d1e5b32..72fe7380ce8e4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Test\Unit\Model\Service; +use Magento\Sales\Api\PaymentFailuresInterface; +use Psr\Log\LoggerInterface; + /** * Class OrderUnHoldTest * @@ -140,6 +143,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + /** @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject $paymentFailures */ + $paymentFailures = $this->createMock(PaymentFailuresInterface::class); + + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject $logger */ + $logger = $this->createMock(LoggerInterface::class); + $this->orderService = new \Magento\Sales\Model\Service\OrderService( $this->orderRepositoryMock, $this->orderStatusHistoryRepositoryMock, @@ -147,7 +156,9 @@ protected function setUp() $this->filterBuilderMock, $this->orderNotifierMock, $this->eventManagerMock, - $this->orderCommentSender + $this->orderCommentSender, + $paymentFailures, + $logger ); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php new file mode 100644 index 0000000000000..ca2dd0a31b1e7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\ValidatorResultInterface; +use Magento\Sales\Model\ValidatorResultInterfaceFactory; +use Magento\Sales\Model\ValidatorResultMerger; + +/** + * @covers \Magento\Sales\Model\ValidatorResultMerger + */ +class ValidatorResultMergerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ValidatorResultMerger + */ + private $validatorResultMerger; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ValidatorResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultFactoryMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->validatorResultFactoryMock = $this->getMockBuilder(ValidatorResultInterfaceFactory::class) + ->setMethods(['create'])->disableOriginalConstructor()->getMock(); + $this->objectManager = new ObjectManager($this); + $this->validatorResultMerger = $this->objectManager->getObject( + ValidatorResultMerger::class, + [ + 'validatorResultInterfaceFactory' => $this->validatorResultFactoryMock, + ] + ); + } + + /** + * Test merge method + * + * @return void + */ + public function testMerge() + { + $validatorResultMock = $this->createMock(ValidatorResultInterface::class); + $validationResult = $this->createMock(ValidatorResultInterface::class); + $cmValidationResult = $this->createMock(ValidatorResultInterface::class); + $validationMessages = [['test04', 'test05'], ['test06']]; + $this->validatorResultFactoryMock->expects($this->once())->method('create') + ->willReturn($validatorResultMock); + $validationResult->expects($this->once())->method('getMessages')->willReturn(['test01', 'test02']); + $cmValidationResult->expects($this->once())->method('getMessages')->willReturn(['test03']); + + $validatorResultMock->expects($this->at(0))->method('addMessage')->with('test01'); + $validatorResultMock->expects($this->at(1))->method('addMessage')->with('test02'); + $validatorResultMock->expects($this->at(2))->method('addMessage')->with('test03'); + $validatorResultMock->expects($this->at(3))->method('addMessage')->with('test04'); + $validatorResultMock->expects($this->at(4))->method('addMessage')->with('test05'); + $validatorResultMock->expects($this->at(5))->method('addMessage')->with('test06'); + $expected = $validatorResultMock; + $actual = $this->validatorResultMerger->merge( + $validationResult, + $cmValidationResult, + ...$validationMessages + ); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php new file mode 100644 index 0000000000000..f4ab2d4f48e6f --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\ValidatorResult; + +/** + * @covers \Magento\Sales\Model\ValidatorResult + */ +class ValidatorResultTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ValidatorResult + */ + private $validatorResult; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->validatorResult = $this->objectManager->getObject(ValidatorResult::class); + } + + /** + * Test addMessage method + * + * @return void + */ + public function testAddMessages() + { + $messageFirst = 'Sample message 01.'; + $messageSecond = 'Sample messages 02.'; + $messageThird = 'Sample messages 03.'; + $expected = [$messageFirst, $messageSecond, $messageThird]; + $this->validatorResult->addMessage($messageFirst); + $this->validatorResult->addMessage($messageSecond); + $this->validatorResult->addMessage($messageThird); + $actual = $this->validatorResult->getMessages(); + $this->assertEquals($expected, $actual); + } + + /** + * Test hasMessages method + * + * @return void + */ + public function testHasMessages() + { + $this->assertFalse($this->validatorResult->hasMessages()); + $messageFirst = 'Sample message 01.'; + $messageSecond = 'Sample messages 02.'; + $this->validatorResult->addMessage($messageFirst); + $this->validatorResult->addMessage($messageSecond); + $this->assertTrue($this->validatorResult->hasMessages()); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php new file mode 100644 index 0000000000000..a334cf0e6096f --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Validator; +use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; +use Magento\Sales\Model\ValidatorResultInterfaceFactory; + +/** + * @covers \Magento\Sales\Model\Validator + */ +class ValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var Validator + */ + private $validator; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var ValidatorResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultFactoryMock; + + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultMock; + + /** + * @var ValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorMock; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->entityMock = $this->createMock(OrderInterface::class); + $this->validatorMock = $this->createMock(ValidatorInterface::class); + $this->validatorResultFactoryMock = $this->getMockBuilder(ValidatorResultInterfaceFactory::class) + ->setMethods(['create'])->disableOriginalConstructor()->getMock(); + $this->validatorResultMock = $this->createMock(ValidatorResultInterface::class); + $this->validatorResultFactoryMock->expects($this->any())->method('create') + ->willReturn($this->validatorResultMock); + $this->objectManager = new ObjectManager($this); + $this->validator = $this->objectManager->getObject( + Validator::class, + [ + 'objectManager' => $this->objectManagerMock, + 'validatorResult' => $this->validatorResultFactoryMock, + ] + ); + } + + /** + * Test validate method + * + * @return void + * + * @throws ConfigurationMismatchException + */ + public function testValidate() + { + $validatorName = 'test'; + $validators = [$validatorName]; + $context = new DataObject(); + $validatorArguments = ['context' => $context]; + $message = __('Sample message.'); + $messages = [$message]; + + $this->objectManagerMock->expects($this->once())->method('create') + ->with($validatorName, $validatorArguments)->willReturn($this->validatorMock); + $this->validatorMock->expects($this->once())->method('validate')->with($this->entityMock) + ->willReturn($messages); + $this->validatorResultMock->expects($this->once())->method('addMessage')->with($message); + + $expected = $this->validatorResultMock; + $actual = $this->validator->validate($this->entityMock, $validators, $context); + $this->assertEquals($expected, $actual); + } + + /** + * Test validate method + * + * @return void + * + * @throws ConfigurationMismatchException + */ + public function testValidateWithException() + { + $validatorName = 'test'; + $validators = [$validatorName]; + $this->objectManagerMock->expects($this->once())->method('create')->willReturn(null); + $this->validatorResultMock->expects($this->never())->method('addMessage'); + $this->expectException(ConfigurationMismatchException::class); + $this->validator->validate($this->entityMock, $validators); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php new file mode 100644 index 0000000000000..8890f01130c82 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Observer; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\CustomerAssignment; +use Magento\Sales\Observer\AssignOrderToCustomerObserver; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Class AssignOrderToCustomerObserverTest + */ +class AssignOrderToCustomerObserverTest extends TestCase +{ + /** @var AssignOrderToCustomerObserver */ + protected $sut; + + /** @var OrderRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ + protected $orderRepositoryMock; + + /** @var CustomerAssignment | PHPUnit_Framework_MockObject_MockObject */ + protected $assignmentMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->assignmentMock = $this->getMockBuilder(CustomerAssignment::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock, $this->assignmentMock); + } + + /** + * Test assigning order to customer after issuing guest order + * + * @dataProvider getCustomerIds + * @param null|int $customerId + * @param null|int $customerOrderId + * @return void + */ + public function testAssignOrderToCustomerAfterGuestOrder($customerId, $customerOrderId) + { + $orderId = 1; + /** @var Observer|PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + /** @var Event|PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->getMockBuilder(Event::class)->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + /** @var CustomerInterface|PHPUnit_Framework_MockObject_MockObject $customerMock */ + $customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn($customerId); + /** @var OrderInterface|PHPUnit_Framework_MockObject_MockObject $orderMock */ + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->any())->method('getData') + ->willReturnMap([ + ['delegate_data', null, ['__sales_assign_order_id' => $orderId]], + ['customer_data_object', null, $customerMock] + ]); + $orderMock->expects($this->any())->method('getCustomerId')->willReturn($customerOrderId); + $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) + ->willReturn($orderMock); + + if (!$customerOrderId && $customerId) { + $orderMock->expects($this->once())->method('setCustomerId')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerIsGuest')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerEmail')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerFirstname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerLastname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerMiddlename')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerPrefix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerSuffix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerGroupId')->willReturn($orderMock); + + $this->assignmentMock->expects($this->once())->method('execute')->with($orderMock, $customerMock); + $this->sut->execute($observerMock); + + return; + } + + $this->assignmentMock->expects($this->never())->method('execute'); + $this->sut->execute($observerMock); + } + + /** + * Customer id assigned to order + * + * @return array + */ + public function getCustomerIds(): array + { + return [ + [null, null], + [1, null], + [1, 1], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php index a6a828c888fc0..6b94605108866 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php @@ -15,12 +15,12 @@ class SubtractQtyFromQuotesObserverTest extends \PHPUnit\Framework\TestCase protected $_model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\ResourceModel\Quote|\PHPUnit_Framework_MockObject_MockObject */ protected $_quoteMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject */ protected $_observerMock; @@ -48,7 +48,7 @@ public function testSubtractQtyFromQuotes() ['getId', 'getStatus', '__wakeup'] ); $this->_eventMock->expects($this->once())->method('getProduct')->will($this->returnValue($productMock)); - $this->_quoteMock->expects($this->once())->method('substractProductFromQuotes')->with($productMock); + $this->_quoteMock->expects($this->once())->method('subtractProductFromQuotes')->with($productMock); $this->_model->execute($this->_observerMock); } } diff --git a/app/code/Magento/Sales/Test/Unit/Observer/Frontend/AddVatRequestParamsOrderCommentTest.php b/app/code/Magento/Sales/Test/Unit/Observer/Frontend/AddVatRequestParamsOrderCommentTest.php index 395b653b0be8d..45cbea7307f4d 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/Frontend/AddVatRequestParamsOrderCommentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/Frontend/AddVatRequestParamsOrderCommentTest.php @@ -84,6 +84,9 @@ public function testAddVatRequestParamsOrderComment( $this->assertNull($this->observer->execute($observer)); } + /** + * @return array + */ public function addVatRequestParamsOrderCommentDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Observer/Frontend/RestoreCustomerGroupIdTest.php b/app/code/Magento/Sales/Test/Unit/Observer/Frontend/RestoreCustomerGroupIdTest.php index f0738fbcf3129..f0845c67f1a4a 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/Frontend/RestoreCustomerGroupIdTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/Frontend/RestoreCustomerGroupIdTest.php @@ -72,6 +72,9 @@ public function testExecute($configAddressType) $this->quote->execute($observer); } + /** + * @return array + */ public function restoreCustomerGroupIdDataProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Plugin/ShippingLabelConverterTest.php b/app/code/Magento/Sales/Test/Unit/Plugin/ShippingLabelConverterTest.php new file mode 100644 index 0000000000000..7ec15075c29c9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Plugin/ShippingLabelConverterTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Plugin; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentSearchResultInterface; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\Sales\Plugin\ShippingLabelConverter; + +/** + * Unit test for plugin to convert shipping label from blob to base64encoded string. + */ +class ShippingLabelConverterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ShippingLabelConverter + */ + private $model; + + /** + * @var ShipmentRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shipmentRepositoryMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = new ShippingLabelConverter(); + $this->shipmentRepositoryMock = $this->getMockBuilder(ShipmentRepositoryInterface::class) + ->disableOriginalConstructor()->getMock(); + } + + /** + * @covers \Magento\Sales\Plugin\ShippingLabelConverter::afterGet() + * @param ShipmentInterface|\PHPUnit_Framework_MockObject_MockObject + * @return void + * @dataProvider shipmentDataProvider + */ + public function testAfterGet($shipmentMock) + { + $this->model->afterGet( + $this->shipmentRepositoryMock, + $shipmentMock + ); + } + + /** + * @covers \Magento\Sales\Plugin\ShippingLabelConverter::afterGetList() + * @param ShipmentInterface|\PHPUnit_Framework_MockObject_MockObject + * @return void + * @dataProvider shipmentDataProvider + */ + public function testAfterGetList($shipmentMock) + { + $searchResultMock = $this->getMockBuilder(ShipmentSearchResultInterface::class) + ->disableOriginalConstructor()->getMock(); + $searchResultMock->expects($this->once())->method('getItems')->willReturn([$shipmentMock]); + + $this->model->afterGetList( + $this->shipmentRepositoryMock, + $searchResultMock + ); + } + + /** + * @return array + */ + public function shipmentDataProvider() + { + return [ + ['shipmentMock' => $this->getShipmentMockWithLabel()], + ['shipmentMock' => $this->getShipmentMockWithOutLabel()], + ]; + } + + /** + * @return ShipmentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function getShipmentMockWithLabel() + { + $shippingLabel = 'shipping_label_test'; + $shippingLabelEncoded = base64_encode('shipping_label_test'); + $shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->disableOriginalConstructor()->getMock(); + $shipmentMock->expects($this->exactly(2))->method('getShippingLabel')->willReturn($shippingLabel); + $shipmentMock->expects($this->once()) + ->method('setShippingLabel') + ->with($shippingLabelEncoded) + ->willReturnSelf(); + + return $shipmentMock; + } + + /** + * @return ShipmentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function getShipmentMockWithOutLabel() + { + $shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->disableOriginalConstructor()->getMock(); + $shipmentMock->expects($this->once())->method('getShippingLabel')->willReturn(null); + $shipmentMock->expects($this->never())->method('setShippingLabel'); + + return $shipmentMock; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php index c0eba0b14138a..b2f5666803ec3 100644 --- a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php @@ -10,7 +10,7 @@ use Magento\Sales\Ui\Component\Listing\Column\Status\Options; /** - * Class OptionsTest + * Class OptionsTest for Magento\Sales\Ui\Component\Listing\Column\Status\Options. */ class OptionsTest extends \PHPUnit\Framework\TestCase { @@ -24,6 +24,9 @@ class OptionsTest extends \PHPUnit\Framework\TestCase */ protected $collectionFactoryMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); @@ -37,11 +40,31 @@ protected function setUp() ); } + /** + * Unit test for toOptionArray method. + * + * @return void + */ public function testToOptionArray() { - $collectionMock = - $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Status\Collection::class); - $options = ['options']; + $collectionMock = $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Status\Collection::class + ); + + $options = [ + [ + 'value' => '1', + 'label' => 'Label', + ], + ]; + + $expectedOptions = [ + [ + 'value' => '1', + 'label' => 'Label', + '__disableTmpl' => true, + ], + ]; $this->collectionFactoryMock->expects($this->once()) ->method('create') @@ -49,7 +72,7 @@ public function testToOptionArray() $collectionMock->expects($this->once()) ->method('toOptionArray') ->willReturn($options); - $this->assertEquals($options, $this->model->toOptionArray()); - $this->assertEquals($options, $this->model->toOptionArray()); + + $this->assertEquals($expectedOptions, $this->model->toOptionArray()); } } diff --git a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php index b435965e3396e..df78e310b5be9 100644 --- a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/ViewActionTest.php @@ -89,14 +89,28 @@ public function prepareDataSourceDataProvider() [ ['name' => 'itemName', 'config' => []], [['itemName' => '', 'entity_id' => 1]], - [['itemName' => ['view' => ['href' => 'url', 'label' => __('View')]], 'entity_id' => 1]], + [ + [ + 'itemName' => [ + 'view' => ['href' => 'url', 'label' => __('View'), '__disableTmpl' => true] + ], + 'entity_id' => 1 + ] + ], '#', ['entity_id' => 1] ], [ ['name' => 'itemName', 'config' => ['viewUrlPath' => 'url_path', 'urlEntityParamName' => 'order_id']], [['itemName' => '', 'entity_id' => 2]], - [['itemName' => ['view' => ['href' => 'url', 'label' => __('View')]], 'entity_id' => 2]], + [ + [ + 'itemName' => [ + 'view' => ['href' => 'url', 'label' => __('View'), '__disableTmpl' => true] + ], + 'entity_id' => 2 + ] + ], 'url_path', ['order_id' => 2] ] diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/CustomerGroup.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/CustomerGroup.php index 1144321ac0b56..5d95c7b94d8fa 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/CustomerGroup.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/CustomerGroup.php @@ -5,7 +5,6 @@ */ namespace Magento\Sales\Ui\Component\Listing\Column; -use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Ui\Component\Listing\Columns\Column; use Magento\Framework\View\Element\UiComponent\ContextInterface; diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php index e091d4966282a..c1e046e055468 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Ui\Component\Listing\Column\Status; use Magento\Framework\Data\OptionSourceInterface; use Magento\Sales\Model\ResourceModel\Order\Status\CollectionFactory; /** - * Class Options + * Class to transform Status options into a form of value-label pairs. */ class Options implements OptionSourceInterface { @@ -24,8 +25,6 @@ class Options implements OptionSourceInterface protected $collectionFactory; /** - * Constructor - * * @param CollectionFactory $collectionFactory */ public function __construct(CollectionFactory $collectionFactory) @@ -34,15 +33,22 @@ public function __construct(CollectionFactory $collectionFactory) } /** - * Get options + * Get options into array. * * @return array */ public function toOptionArray() { if ($this->options === null) { - $this->options = $this->collectionFactory->create()->toOptionArray(); + $options = $this->collectionFactory->create()->toOptionArray(); + + array_walk($options, function (&$option) { + $option['__disableTmpl'] = true; + }); + + $this->options = $options; } + return $this->options; } } diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/ViewAction.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/ViewAction.php index 0088c15b4734d..0248b1177e97d 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/ViewAction.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/ViewAction.php @@ -61,7 +61,8 @@ public function prepareDataSource(array $dataSource) $urlEntityParamName => $item['entity_id'] ] ), - 'label' => __('View') + 'label' => __('View'), + '__disableTmpl' => true, ] ]; } diff --git a/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php new file mode 100644 index 0000000000000..16de1412a2a45 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Sales\ViewModel\Customer; + +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer address formatter + */ +class AddressFormatter implements ArgumentInterface +{ + /** + * Customer form factory + * + * @var \Magento\Customer\Model\Metadata\FormFactory + */ + private $customerFormFactory; + + /** + * Address format helper + * + * @var \Magento\Customer\Helper\Address + */ + private $addressFormatHelper; + + /** + * Directory helper + * + * @var \Magento\Directory\Helper\Data + */ + private $directoryHelper; + + /** + * Session quote + * + * @var \Magento\Backend\Model\Session\Quote + */ + private $session; + + /** + * Json encoder + * + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $jsonEncoder; + + /** + * Customer address + * + * @param \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory + * @param \Magento\Customer\Helper\Address $addressFormatHelper + * @param \Magento\Directory\Helper\Data $directoryHelper + * @param \Magento\Backend\Model\Session\Quote $session + * @param \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + */ + public function __construct( + \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory, + \Magento\Customer\Helper\Address $addressFormatHelper, + \Magento\Directory\Helper\Data $directoryHelper, + \Magento\Backend\Model\Session\Quote $session, + \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + ) { + $this->customerFormFactory = $customerFormFactory; + $this->addressFormatHelper = $addressFormatHelper; + $this->directoryHelper = $directoryHelper; + $this->session = $session; + $this->jsonEncoder = $jsonEncoder; + } + + /** + * Return customer address array as JSON + * + * @param array $addressArray + * + * @return string + */ + public function getAddressesJson(array $addressArray) + { + $data = $this->getEmptyAddressForm(); + foreach ($addressArray as $addressId => $address) { + $addressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + $address + ); + $data[$addressId] = $addressForm->outputData( + \Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON + ); + } + + return $this->jsonEncoder->serialize($data); + } + + /** + * Represent customer address in 'online' format. + * + * @param array $address + * @return string + */ + public function getAddressAsString(array $address) + { + $formatTypeRenderer = $this->addressFormatHelper->getFormatTypeRenderer('oneline'); + $result = ''; + if ($formatTypeRenderer) { + $result = $formatTypeRenderer->renderArray($address); + } + + return $result; + } + + /** + * Return empty address address form + * + * @return array + */ + private function getEmptyAddressForm() + { + $defaultCountryId = $this->directoryHelper->getDefaultCountry($this->session->getStore()); + $emptyAddressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + [\Magento\Customer\Api\Data\AddressInterface::COUNTRY_ID => $defaultCountryId] + ); + + return [0 => $emptyAddressForm->outputData(\Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON)]; + } +} diff --git a/app/code/Magento/Sales/composer.json b/app/code/Magento/Sales/composer.json index 5468f1dad5f06..be5a0224f6081 100644 --- a/app/code/Magento/Sales/composer.json +++ b/app/code/Magento/Sales/composer.json @@ -2,9 +2,10 @@ "name": "magento/module-sales", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", + "magento/module-bundle": "100.2.*", "magento/module-customer": "101.0.*", "magento/module-authorization": "100.2.*", "magento/module-payment": "100.2.*", @@ -32,7 +33,7 @@ "magento/module-sales-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.2", + "version": "101.0.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Sales/etc/adminhtml/system.xml b/app/code/Magento/Sales/etc/adminhtml/system.xml index 8f004d9ad5968..e437918b683b2 100644 --- a/app/code/Magento/Sales/etc/adminhtml/system.xml +++ b/app/code/Magento/Sales/etc/adminhtml/system.xml @@ -27,18 +27,23 @@ <label>Checkout Totals Sort Order</label> <field id="discount" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Discount</label> + <validate>required-number validate-number</validate> </field> <field id="grand_total" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Grand Total</label> + <validate>required-number validate-number</validate> </field> <field id="shipping" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Shipping</label> + <validate>required-number validate-number</validate> </field> <field id="subtotal" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Subtotal</label> + <validate>required-number validate-number</validate> </field> <field id="tax" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Tax</label> + <validate>required-number validate-number</validate> </field> </group> <group id="reorder" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -48,6 +53,13 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> + <group id="zerograndtotal_creditmemo" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Allow Zero GrandTotal</label> + <field id="allow_zero_grandtotal" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Allow Zero GrandTotal for Creditmemo</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + </group> <group id="identity" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Invoice and Packing Slip Design</label> <field id="logo" translate="label comment" type="image" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -82,6 +94,11 @@ <label>Minimum Amount</label> <comment>Subtotal after discount</comment> </field> + <field id="include_discount_amount" translate="label" sortOrder="12" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Include Discount Amount</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Choosing yes will be used subtotal after discount, otherwise only subtotal will be used</comment> + </field> <field id="tax_including" translate="label" sortOrder="15" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Include Tax to Amount</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -106,15 +123,15 @@ <comment>We'll use the default error above if you leave this empty.</comment> </field> </group> - <group id="dashboard" translate="label,comment" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0"> + <group id="dashboard" translate="label" sortOrder="60" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Dashboard</label> - <field id="use_aggregated_data" translate="label" sortOrder="10" type="select" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="use_aggregated_data" translate="label comment" sortOrder="10" type="select" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Use Aggregated Data</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Improves dashboard performance but provides non-realtime data.</comment> </field> </group> - <group id="orders" translate="label,comment" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="orders" translate="label" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Orders Cron Settings</label> <field id="delete_pending_after" translate="label" type="text" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Pending Payment Order Lifetime (minutes)</label> @@ -132,6 +149,14 @@ <source_model>Magento\Config\Model\Config\Source\Enabledisable</source_model> <backend_model>Magento\Sales\Model\Config\Backend\Email\AsyncSending</backend_model> </field> + <field id="sending_limit" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Limit per cron run</label> + <comment>Limit how many entities (orders/shipments/etc) will be processed during one cron run.</comment> + <validate>required-number validate-number validate-greater-than-zero</validate> + <depends> + <field id="async_sending">1</field> + </depends> + </field> </group> <group id="order" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Order</label> @@ -393,7 +418,7 @@ </group> </section> <section id="rss"> - <group id="order" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="order" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Order</label> <field id="status" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Customer Order Status Notification</label> @@ -402,7 +427,7 @@ </group> </section> <section id="dev"> - <group id="grid" type="text" sortOrder="131" showInDefault="1" showInWebsite="0" showInStore="0"> + <group id="grid" translate="label" type="text" sortOrder="131" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Grid Settings</label> <field id="async_indexing" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Asynchronous indexing</label> diff --git a/app/code/Magento/Sales/etc/config.xml b/app/code/Magento/Sales/etc/config.xml index da2408416133d..2480da4ad214b 100644 --- a/app/code/Magento/Sales/etc/config.xml +++ b/app/code/Magento/Sales/etc/config.xml @@ -18,7 +18,11 @@ <reorder> <allow>1</allow> </reorder> + <zerograndtotal_creditmemo> + <allow_zero_grandtotal>1</allow_zero_grandtotal> + </zerograndtotal_creditmemo> <minimum_order> + <include_discount_amount>1</include_discount_amount> <tax_including>1</tax_including> </minimum_order> <orders> @@ -29,6 +33,7 @@ <sales_email> <general> <async_sending>0</async_sending> + <sending_limit>50</sending_limit> </general> <order> <enabled>1</enabled> diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 22ddc7b333574..6311ed60dafe7 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -65,6 +65,7 @@ <preference for="Magento\Sales\Api\OrderRepositoryInterface" type="Magento\Sales\Model\OrderRepository"/> <preference for="Magento\Sales\Api\OrderManagementInterface" type="Magento\Sales\Model\Service\OrderService"/> <preference for="Magento\Sales\Api\OrderStatusHistoryRepositoryInterface" type="Magento\Sales\Model\Order\Status\HistoryRepository"/> + <preference for="Magento\Sales\Api\PaymentFailuresInterface" type="Magento\Sales\Model\Service\PaymentFailuresService" /> <preference for="Magento\Sales\Api\ShipmentCommentRepositoryInterface" type="Magento\Sales\Model\Order\Shipment\CommentRepository"/> <preference for="Magento\Sales\Api\ShipmentItemRepositoryInterface" type="Magento\Sales\Model\Order\Shipment\ItemRepository"/> <preference for="Magento\Sales\Api\ShipmentRepositoryInterface" type="Magento\Sales\Model\Order\ShipmentRepository"/> @@ -119,6 +120,7 @@ <arguments> <argument name="providers" xsi:type="array"> <item name="default" xsi:type="string">Magento\Sales\Model\ResourceModel\Provider\UpdatedIdListProvider</item> + <item name="updated_at" xsi:type="string">Magento\Sales\Model\ResourceModel\Provider\UpdatedAtListProvider</item> </argument> </arguments> </type> @@ -322,6 +324,7 @@ <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\OrderSender</argument> <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order</argument> <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Collection</argument> + <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\OrderIdentity</argument> </arguments> </virtualType> <virtualType name="SalesOrderInvoiceSendEmails" type="Magento\Sales\Model\EmailSenderHandler"> @@ -329,6 +332,7 @@ <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\InvoiceSender</argument> <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Invoice</argument> <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Invoice\Collection</argument> + <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\InvoiceIdentity</argument> </arguments> </virtualType> <virtualType name="SalesOrderShipmentSendEmails" type="Magento\Sales\Model\EmailSenderHandler"> @@ -336,6 +340,7 @@ <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\ShipmentSender</argument> <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Shipment</argument> <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Shipment\Collection</argument> + <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\ShipmentIdentity</argument> </arguments> </virtualType> <virtualType name="SalesOrderCreditmemoSendEmails" type="Magento\Sales\Model\EmailSenderHandler"> @@ -343,48 +348,49 @@ <argument name="emailSender" xsi:type="object">Magento\Sales\Model\Order\Email\Sender\CreditmemoSender</argument> <argument name="entityResource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Creditmemo</argument> <argument name="entityCollection" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection</argument> + <argument name="identityContainer" xsi:type="object" shared="false">Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity</argument> </arguments> </virtualType> <virtualType name="SalesOrderSendEmailsObserver" type="Magento\Sales\Observer\Virtual\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesOrderInvoiceSendEmailsObserver" type="Magento\Sales\Observer\Virtual\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderInvoiceSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderInvoiceSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesOrderShipmentSendEmailsObserver" type="Magento\Sales\Observer\Virtual\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderShipmentSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderShipmentSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesOrderCreditmemoSendEmailsObserver" type="Magento\Sales\Observer\Virtual\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderCreditmemoSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderCreditmemoSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesOrderSendEmailsCron" type="Magento\Sales\Cron\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesInvoiceSendEmailsCron" type="Magento\Sales\Cron\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderInvoiceSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderInvoiceSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesShipmentSendEmailsCron" type="Magento\Sales\Cron\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderShipmentSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderShipmentSendEmails</argument> </arguments> </virtualType> <virtualType name="SalesCreditmemoSendEmailsCron" type="Magento\Sales\Cron\SendEmails"> <arguments> - <argument name="emailSenderHandler" xsi:type="object">SalesOrderCreditmemoSendEmails</argument> + <argument name="emailSenderHandler" xsi:type="object" shared="false">SalesOrderCreditmemoSendEmails</argument> </arguments> </virtualType> <type name="Magento\SalesSequence\Model\EntityPool"> @@ -686,8 +692,8 @@ <item name="billing_address" xsi:type="object">BillingAddressAggregator</item> <item name="shipping_address" xsi:type="object">ShippingAddressAggregator</item> <item name="shipping_information" xsi:type="string">sales_order.shipping_description</item> - <item name="subtotal" xsi:type="string">sales_order.base_subtotal</item> - <item name="shipping_and_handling" xsi:type="string">sales_order.base_shipping_amount</item> + <item name="subtotal" xsi:type="string">sales_invoice.base_subtotal</item> + <item name="shipping_and_handling" xsi:type="string">sales_invoice.base_shipping_amount</item> <item name="base_grand_total" xsi:type="string">sales_invoice.base_grand_total</item> <item name="grand_total" xsi:type="string">sales_invoice.grand_total</item> <item name="created_at" xsi:type="string">sales_invoice.created_at</item> @@ -740,6 +746,10 @@ <virtualType name="ShippingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_shipping_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_shipping_address</item> <item name="columnName" xsi:type="string">street</item> @@ -763,6 +773,10 @@ <virtualType name="BillingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_billing_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_billing_address</item> <item name="columnName" xsi:type="string">street</item> @@ -992,4 +1006,12 @@ </argument> </arguments> </type> + <preference + for="Magento\Sales\Api\OrderCustomerDelegateInterface" + type="Magento\Sales\Model\Order\OrderCustomerDelegate" /> + <type name="Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker"> + <arguments> + <argument name="productAvailabilityChecks" xsi:type="array" /> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/events.xml b/app/code/Magento/Sales/etc/events.xml index 9ec983acab5bd..b3a7a4ab99577 100644 --- a/app/code/Magento/Sales/etc/events.xml +++ b/app/code/Magento/Sales/etc/events.xml @@ -51,4 +51,9 @@ <event name="store_add"> <observer name="magento_sequence" instance="Magento\SalesSequence\Observer\SequenceCreatorObserver" /> </event> + <event name="customer_save_after_data_object"> + <observer + name="sales_assign_order_to_customer" + instance="Magento\Sales\Observer\AssignOrderToCustomerObserver" /> + </event> </config> diff --git a/app/code/Magento/Sales/etc/extension_attributes.xml b/app/code/Magento/Sales/etc/extension_attributes.xml index 7280a1a071548..222f61cdc7324 100644 --- a/app/code/Magento/Sales/etc/extension_attributes.xml +++ b/app/code/Magento/Sales/etc/extension_attributes.xml @@ -10,4 +10,7 @@ <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> <attribute code="shipping_assignments" type="Magento\Sales\Api\Data\ShippingAssignmentInterface[]" /> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="payment_additional_info" type="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Sales/etc/module.xml b/app/code/Magento/Sales/etc/module.xml index 4c1a534faddf7..0fd2124785ba7 100644 --- a/app/code/Magento/Sales/etc/module.xml +++ b/app/code/Magento/Sales/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Sales" setup_version="2.0.7"> + <module name="Magento_Sales" setup_version="2.0.11"> <sequence> <module name="Magento_Rule"/> <module name="Magento_Catalog"/> diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index cee245e348393..492dff8057039 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -10,271 +10,271 @@ <route url="/V1/orders/:id" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/statuses" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getStatus"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/cancel" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::cancel" /> </resources> </route> <route url="/V1/orders/:id/emails" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::emails" /> </resources> </route> <route url="/V1/orders/:id/hold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="hold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::hold" /> </resources> </route> <route url="/V1/orders/:id/unhold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="unHold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::unhold" /> </resources> </route> <route url="/V1/orders/:id/comments" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="addComment"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::comment" /> </resources> </route> <route url="/V1/orders/:id/comments" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/create" method="PUT"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/:parent_id" method="PUT"> <service class="Magento\Sales\Api\OrderAddressRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/items/:id" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/items" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/invoices/:id" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/comments" method="GET"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/emails" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/void" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setVoid"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/capture" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setCapture"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/comments" method="POST"> <service class="Magento\Sales\Api\InvoiceCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/" method="POST"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoice/:invoiceId/refund" method="POST"> <service class="Magento\Sales\Api\RefundInvoiceInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="GET"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemos" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="PUT"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/emails" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/refund" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="refund"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="POST"> <service class="Magento\Sales\Api\CreditmemoCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo" method="POST"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/order/:orderId/refund" method="POST"> <service class="Magento\Sales\Api\RefundOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::creditmemo" /> </resources> </route> <route url="/V1/shipment/:id" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipments" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="POST"> <service class="Magento\Sales\Api\ShipmentCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/emails" method="POST"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track" method="POST"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track/:id" method="DELETE"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/" method="POST"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/label" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getLabel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/order/:orderId/ship" method="POST"> <service class="Magento\Sales\Api\ShipOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::ship" /> </resources> </route> <route url="/V1/orders/" method="POST"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/transactions/:id" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/transactions" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/order/:orderId/invoice" method="POST"> <service class="Magento\Sales\Api\InvoiceOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::invoice" /> </resources> </route> </routes> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 0cfd36e219169..6435445e0ef93 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -12,4 +12,14 @@ <type name="Magento\Sales\Model\ResourceModel\Order"> <plugin name="authorization" type="Magento\Sales\Model\ResourceModel\Order\Plugin\Authorization" /> </type> + <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> + <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> + </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 0cfd36e219169..6435445e0ef93 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -12,4 +12,14 @@ <type name="Magento\Sales\Model\ResourceModel\Order"> <plugin name="authorization" type="Magento\Sales\Model\ResourceModel\Order\Plugin\Authorization" /> </type> + <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> + <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> + </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 6568284300225..62d8e53e62fa0 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -233,7 +233,6 @@ Sales,Sales "You can't create an invoice without products.","You can't create an invoice without products." "New Invoice","New Invoice" "We can't save the invoice right now.","We can't save the invoice right now." -"The invoice and the shipment have been created. The shipping label cannot be created now.","The invoice and the shipment have been created. The shipping label cannot be created now." "You created the invoice and shipment.","You created the invoice and shipment." "The invoice has been created.","The invoice has been created." "We can't send the invoice email right now.","We can't send the invoice email right now." @@ -687,17 +686,9 @@ General,General "Allow Reorder","Allow Reorder" "Invoice and Packing Slip Design","Invoice and Packing Slip Design" "Logo for PDF Print-outs (200x50)","Logo for PDF Print-outs (200x50)" -" - Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image. - "," - Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image. - " +"Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image.","Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image." "Logo for HTML Print View","Logo for HTML Print View" -" - Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png) - "," - Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png) - " +"Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png)","Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png)" Address,Address "Minimum Order Amount","Minimum Order Amount" Enable,Enable @@ -804,3 +795,6 @@ Created,Created "PDF Shipments","PDF Shipments" "PDF Creditmemos","PDF Creditmemos" Refunds,Refunds +"Shipment with requested ID %1 doesn't correspond with Order with requested ID %2.","Shipment with requested ID %1 doesn't correspond with Order with requested ID %2." +"Allow Zero GrandTotal for Creditmemo","Allow Zero GrandTotal for Creditmemo" +"Allow Zero GrandTotal","Allow Zero GrandTotal" diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml index c321bee460e46..0f5a3559f3008 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml @@ -80,13 +80,13 @@ <argument name="align" xsi:type="string">center</argument> </arguments> </block> - </block> - <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> - <arguments> - <argument name="header" xsi:type="string" translate="true">Website</argument> - <argument name="index" xsi:type="string">website_name</argument> - <argument name="align" xsi:type="string">center</argument> - </arguments> + <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> + <arguments> + <argument name="header" xsi:type="string" translate="true">Website</argument> + <argument name="index" xsi:type="string">website_name</argument> + <argument name="align" xsi:type="string">center</argument> + </arguments> + </block> </block> </block> </referenceBlock> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml index eb0a7685e5e22..3832476ff6972 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml @@ -45,8 +45,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order_create_shipping_form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index 6f0cbdb0cd43f..c52f81d5cb56d 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml index 70b5bfc298274..54348ce961c56 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml @@ -20,8 +20,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order.create.shipping.method.form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml index 56f6786397df9..559f56dcb845b 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml index 0c1b395b5116d..71490553aff17 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_new.xml @@ -22,7 +22,6 @@ <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> <block class="Magento\Framework\View\Element\Text\ListText" name="order_item_extra_info"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Totalbar" name="order_totalbar" template="Magento_Sales::order/totalbar.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Totals" name="creditmemo_totals" template="Magento_Sales::order/totals.phtml"> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Adjustments" name="adjustments" template="Magento_Sales::order/creditmemo/create/totals/adjustments.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Totals\Tax" name="tax" template="Magento_Sales::order/totals/tax.phtml"/> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml index 29a61308391c6..8375bec965794 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml @@ -13,7 +13,6 @@ <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> <block class="Magento\Framework\View\Element\Text\ListText" name="order_item_extra_info"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Totalbar" name="order_totalbar" template="Magento_Sales::order/totalbar.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Totals" name="creditmemo_totals" template="Magento_Sales::order/totals.phtml"> <block class="Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Adjustments" name="adjustments" template="Magento_Sales::order/creditmemo/create/totals/adjustments.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Totals\Tax" name="tax" template="Magento_Sales::order/totals/tax.phtml"/> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_new.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_new.xml index def5ebaf546cd..b589ac0ac793d 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_new.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_new.xml @@ -25,7 +25,6 @@ <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> <block class="Magento\Framework\View\Element\Text\ListText" name="order_item_extra_info"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Totalbar" name="order_totalbar" template="Magento_Sales::order/totalbar.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Invoice\Totals" name="invoice_totals" template="Magento_Sales::order/totals.phtml"> <block class="Magento\Sales\Block\Adminhtml\Order\Totals\Tax" name="tax" template="Magento_Sales::order/totals/tax.phtml"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_updateqty.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_updateqty.xml index 4df3f057f6a58..38e4cb50f4343 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_updateqty.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_updateqty.xml @@ -13,7 +13,6 @@ <block class="Magento\Sales\Block\Adminhtml\Items\Column\Qty" name="column_qty" template="Magento_Sales::items/column/qty.phtml" group="column"/> <block class="Magento\Sales\Block\Adminhtml\Items\Column\Name" name="column_name" template="Magento_Sales::items/column/name.phtml" group="column"/> <block class="Magento\Framework\View\Element\Text\ListText" name="order_item_extra_info"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Totalbar" name="order_totalbar" template="Magento_Sales::order/totalbar.phtml"/> <block class="Magento\Sales\Block\Adminhtml\Order\Invoice\Totals" name="invoice_totals" template="Magento_Sales::order/totals.phtml"> <block class="Magento\Sales\Block\Adminhtml\Order\Totals\Tax" name="tax" template="Magento_Sales::order/totals/tax.phtml"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index 27b8ca86b1681..aae4b0db648b3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml @@ -4,42 +4,42 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php -/** - * @see \Magento\Sales\Block\Adminhtml\Items\Column\Name - */ +/* @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name */ ?> - -<?php if ($_item = $block->getItem()): ?> - <div id="order_item_<?= /* @escapeNotVerified */ $_item->getId() ?>_title" +<?php if ($_item = $block->getItem()) : ?> + <div id="order_item_<?= (int) $_item->getId() ?>_title" class="product-title"> <?= $block->escapeHtml($_item->getName()) ?> </div> - <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($block->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU'))?>:</span> <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($block->getSku()))) ?> </div> - <?php if ($block->getOrderOptions()): ?> + <?php if ($block->getOrderOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option): ?> - <dt><?= /* @escapeNotVerified */ $_option['label'] ?>:</dt> + <?php foreach ($block->getOrderOptions() as $_option) : ?> + <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> - <?= /* @escapeNotVerified */ $block->getCustomizedOptionValue($_option) ?> - <?php else: ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?= /* @noEscape */ $block->getCustomizedOptionValue($_option) ?> + <?php else : ?> <?php $_option = $block->getFormattedOption($_option['value']); ?> - <?= /* @escapeNotVerified */ $_option['value'] ?><?php if (isset($_option['remainder']) && $_option['remainder']): ?><span id="<?= /* @escapeNotVerified */ $_dots = 'dots' . uniqid() ?>"> ...</span><span id="<?= /* @escapeNotVerified */ $_id = 'id' . uniqid() ?>"><?= /* @escapeNotVerified */ $_option['remainder'] ?></span> + <?= $block->escapeHtml($_option['value']) ?> + <?php if (isset($_option['remainder']) && $_option['remainder']) : ?> + <?php $dots = 'dots' . uniqid(); ?> + <span id="<?= /* @noEscape */ $dots; ?>"> ...</span> + <?php $id = 'id' . uniqid(); ?> + <span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($_option['remainder']) ?></span> <script> - require(['prototype'], function() { - $('<?= /* @escapeNotVerified */ $_id ?>').hide(); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_id ?>').show();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseover', function(){$('<?= /* @escapeNotVerified */ $_dots ?>').hide();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_id ?>').hide();}); - $('<?= /* @escapeNotVerified */ $_id ?>').up().observe('mouseout', function(){$('<?= /* @escapeNotVerified */ $_dots ?>').show();}); + require(['prototype'], function(){ + $('<?= /* @noEscape */ $id; ?>').hide(); + $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $id; ?>').show();}); + $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $dots; ?>').hide();}); + $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $id; ?>').hide();}); + $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $dots; ?>').show();}); }); </script> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/qty.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/qty.phtml index faa49dca3a8eb..645cdb9aac624 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/qty.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/qty.phtml @@ -3,44 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($_item = $block->getItem()): ?> -<table class="qty-table"> - <tr> - <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> - </tr> - - <?php if ((float) $_item->getQtyInvoiced()): ?> +<?php if ($item = $block->getItem()) : ?> + <table class="qty-table"> <tr> - <th><?= /* @escapeNotVerified */ __('Invoiced') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyInvoiced()*1 ?></td> + <th><?= $block->escapeHtml(__('Ordered')); ?></th> + <td><?= (int) $item->getQtyOrdered() ?></td> </tr> - <?php endif; ?> - <?php if ((float) $_item->getQtyShipped()): ?> - <tr> - <th><?= /* @escapeNotVerified */ __('Shipped') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></td> - </tr> - <?php endif; ?> + <?php if ((float)$item->getQtyInvoiced()) : ?> + <tr> + <th><?= $block->escapeHtml(__('Invoiced')); ?></th> + <td><?= (int) $item->getQtyInvoiced() ?></td> + </tr> + <?php endif; ?> - <?php if ((float) $_item->getQtyRefunded()): ?> - <tr> - <th><?= /* @escapeNotVerified */ __('Refunded') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></td> - </tr> - <?php endif; ?> + <?php if ((float)$item->getQtyShipped()) : ?> + <tr> + <th><?= $block->escapeHtml(__('Shipped')); ?></th> + <td><?= (int) $item->getQtyShipped() ?></td> + </tr> + <?php endif; ?> - <?php if ((float) $_item->getQtyCanceled()): ?> - <tr> - <th><?= /* @escapeNotVerified */ __('Canceled') ?></th> - <td><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></td> - </tr> - <?php endif; ?> + <?php if ((float)$item->getQtyRefunded()) : ?> + <tr> + <th><?= $block->escapeHtml(__('Refunded')); ?></th> + <td><?= (int) $item->getQtyRefunded() ?></td> + </tr> + <?php endif; ?> + + <?php if ((float)$item->getQtyCanceled()) : ?> + <tr> + <th><?= $block->escapeHtml(__('Canceled')); ?></th> + <td><?= (int) $item->getQtyCanceled() ?></td> + </tr> + <?php endif; ?> -</table> + </table> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/price/row.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/price/row.phtml index 54e95876d7626..fba82ce89be58 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/price/row.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/price/row.phtml @@ -3,16 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn $block */ - $_item = $block->getItem(); ?> - <div class="price-excl-tax"> - <?= /* @escapeNotVerified */ $block->displayPrices($_item->getBaseRowTotal(), $_item->getRowTotal()) ?> + <?= /* @noEscape */ $block->displayPrices($_item->getBaseRowTotal(), $_item->getRowTotal()) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/price/total.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/price/total.phtml index 46a0a0926f4c7..0bd5b8d9144de 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/price/total.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/price/total.phtml @@ -4,13 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn $block */ - $_item = $block->getItem(); ?> - -<?= /* @escapeNotVerified */ $block->displayPrices($block->getBaseTotalAmount($_item), $block->getTotalAmount($_item)) ?> +<?= /* @noEscape */ $block->displayPrices($block->getBaseTotalAmount($_item), $block->getTotalAmount($_item)) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/price/unit.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/price/unit.phtml index 444310c80884a..9146fe6713225 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/price/unit.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/price/unit.phtml @@ -3,15 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn $block */ - $_item = $block->getItem(); ?> <div class="price-excl-tax"> -<?= /* @escapeNotVerified */ $block->displayPrices($_item->getBasePrice(), $_item->getPrice()) ?> +<?= /* @noEscape */ $block->displayPrices($_item->getBasePrice(), $_item->getPrice()) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/renderer/default.phtml index 09f6ee5d6a49a..0c5b276bf382b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/renderer/default.phtml @@ -4,21 +4,19 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> - -<?= /* @escapeNotVerified */ $block->getItem()->getName() ?> -<div><strong><?= /* @escapeNotVerified */ __('SKU') ?>:</strong> <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($block->getItem()->getSku()))) ?></div> -<?php if ($block->getOrderOptions()): ?> +<?= $block->escapeHtml($block->getItem()->getName()) ?> +<div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($block->getItem()->getSku()))) ?></div> +<?php if ($block->getOrderOptions()) : ?> <ul class="item-options"> - <?php foreach ($block->getOrderOptions() as $option): ?> - <li><strong><?= /* @escapeNotVerified */ $option['label'] ?>:</strong><br /> - <?php if (is_array($option['value'])): ?> - <?php foreach ($option['value'] as $item): ?> - <?= $block->getValueHtml($item) ?><br /> - <?php endforeach; ?> - <?php else: ?> + <?php foreach ($block->getOrderOptions() as $option) : ?> + <li><strong><?= $block->escapeHtml($option['label']) ?>:</strong><br /> + <?php if (is_array($option['value'])) : ?> + <?php foreach ($option['value'] as $item) : ?> + <?= $block->getValueHtml($item) ?><br /> + <?php endforeach; ?> + <?php else : ?> <?= $block->escapeHtml($option['value']) ?> <?php endif; ?> </li> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/address/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/address/form.phtml index ff79e2f89cd20..a7f3b3c1cc8f5 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/address/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/address/form.phtml @@ -3,19 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <div class="message message-notice"> <div class="message-inner"> - <div class="message-content"><?= /* @escapeNotVerified */ __('Changing address information will not recalculate shipping, tax or other order amount.') ?></div> + <div class="message-content"><?= $block->escapeHtml(__('Changing address information will not recalculate shipping, tax or other order amount.')) ?></div> </div> </div> <fieldset class="fieldset admin__fieldset-wrapper"> <legend class="legend admin__legend"> - <span><?= /* @escapeNotVerified */ $block->getHeaderText() ?></span> + <span><?= $block->escapeHtml($block->getHeaderText()) ?></span> </legend> <br> <div class="form-inline" data-mage-init='{"Magento_Sales/order/edit/address/form":{}}'> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml index f01e1c274a1de..05e753c78f4a3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml @@ -3,16 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($_entity = $block->getEntity()): ?> +<?php if ($_entity = $block->getEntity()) : ?> <div id="comments_block" class="edit-order-comments"> <div class="order-history-block"> <div class="admin__field field-row"> <label class="admin__field-label" - for="history_comment"><?= /* @escapeNotVerified */ __('Comment Text') ?></label> + for="history_comment"><?= $block->escapeHtml(__('Comment Text')) ?></label> <div class="admin__field-control"> <textarea name="comment[comment]" class="admin__control-textarea" @@ -23,7 +20,7 @@ </div> <div class="admin__field"> <div class="order-history-comments-options"> - <?php if ($block->canSendCommentEmail()): ?> + <?php if ($block->canSendCommentEmail()) : ?> <div class="admin__field admin__field-option"> <input name="comment[is_customer_notified]" type="checkbox" @@ -31,7 +28,7 @@ id="history_notify" value="1" /> <label class="admin__field-label" - for="history_notify"><?= /* @escapeNotVerified */ __('Notify Customer by Email') ?></label> + for="history_notify"><?= $block->escapeHtml(__('Notify Customer by Email')) ?></label> </div> <?php endif; ?> <div class="admin__field admin__field-option"> @@ -41,7 +38,7 @@ class="admin__control-checkbox" value="1" /> <label class="admin__field-label" - for="history_visible"> <?= /* @escapeNotVerified */ __('Visible on Storefront') ?></label> + for="history_visible"> <?= $block->escapeHtml(__('Visible on Storefront')) ?></label> </div> </div> <div class="order-history-comments-actions"> @@ -51,16 +48,16 @@ </div> <ul class="note-list"> - <?php foreach ($_entity->getCommentsCollection(true) as $_comment): ?> + <?php foreach ($_entity->getCommentsCollection(true) as $_comment) : ?> <li> <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> <span class="note-list-customer"> - <?= /* @escapeNotVerified */ __('Customer') ?> - <?php if ($_comment->getIsCustomerNotified()): ?> - <span class="note-list-customer-notified"><?= /* @escapeNotVerified */ __('Notified') ?></span> - <?php else: ?> - <span class="note-list-customer-not-notified"><?= /* @escapeNotVerified */ __('Not Notified') ?></span> + <?= $block->escapeHtml(__('Customer')) ?> + <?php if ($_comment->getIsCustomerNotified()) : ?> + <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> + <?php else : ?> + <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> <?php endif; ?> </span> <div class="note-list-comment"><?= $block->escapeHtml($_comment->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> @@ -70,15 +67,12 @@ </div> <script> require(['prototype'], function(){ - -submitComment = function() { - submitAndReloadArea($('comments_block').parentNode, '<?= /* @escapeNotVerified */ $block->getSubmitUrl() ?>') -} - -if ($('submit_comment_button')) { - $('submit_comment_button').observe('click', submitComment); -} - + submitComment = function() { + submitAndReloadArea($('comments_block').parentNode, '<?= $block->escapeUrl($block->getSubmitUrl()) ?>') + }; + if ($('submit_comment_button')) { + $('submit_comment_button').observe('click', submitComment); + } }); </script> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/abstract.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/abstract.phtml index 13433846d6ac4..6083a37efeabc 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/abstract.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/abstract.phtml @@ -3,14 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ $block->getHeaderText() ?></span> - <?php if($block->getButtonsHtml()): ?> + <span class="title"><?= $block->escapeHtml($block->getHeaderText()) ?></span> + <?php if ($block->getButtonsHtml()) : ?> <div class="actions"><?= $block->getButtonsHtml() ?></div> <?php endif; ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml index 9dda4e7e067ed..89cc9df2e8f23 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml @@ -6,49 +6,50 @@ ?> <?php if ($block->hasMethods()) : ?> -<div id="order-billing_method_form"> - <dl class="admin__payment-methods"> - <?php - $_methods = $block->getMethods(); - $_methodsCount = count($_methods); - $_counter = 0; - ?> - <?php foreach ($_methods as $_method) : - $_code = $_method->getCode(); - $_counter++; - ?> - <dt class="admin__field-option"> - <?php if ($_methodsCount > 1) : ?> - <input id="p_method_<?= $block->escapeHtml($_code); ?>" - value="<?= $block->escapeHtml($_code); ?>" - type="radio" name="payment[method]" - title="<?= $block->escapeHtml($_method->getTitle()); ?>" - onclick="payment.switchMethod('<?= $block->escapeHtml($_code); ?>')" - <?php if ($block->getSelectedMethodCode() == $_code) : ?> - checked="checked" + <div id="order-billing_method_form"> + <dl class="admin__payment-methods"> + <?php + $_methods = $block->getMethods(); + $_methodsCount = count($_methods); + $_counter = 0; + $currentSelectedMethod = $block->getSelectedMethodCode(); + ?> + <?php foreach ($_methods as $_method) : + $_code = $_method->getCode(); + $_counter++; + ?> + <dt class="admin__field-option"> + <?php if ($_methodsCount > 1) : ?> + <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" + value="<?= $block->escapeHtmlAttr($_code); ?>" + type="radio" name="payment[method]" + title="<?= $block->escapeHtmlAttr($_method->getTitle()); ?>" + onclick="payment.switchMethod('<?= $block->escapeJs($_code); ?>')" + <?php if ($block->getSelectedMethodCode() == $_code) : ?> + checked="checked" + <?php endif; ?> + <?php $className = ($_counter == $_methodsCount) ? ' validate-one-required-by-name' : ''; ?> + class="admin__control-radio<?= $block->escapeHtmlAttr($className); ?>"/> + <?php else : ?> + <span class="no-display"> + <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" + value="<?= $block->escapeHtmlAttr($_code); ?>" + type="radio" + name="payment[method]" class="admin__control-radio" + checked="checked"/> + </span> <?php endif; ?> - <?php $className = ($_counter == $_methodsCount) ? ' validate-one-required-by-name' : ''; ?> - class="admin__control-radio<?= $block->escapeHtml($className); ?>"/> - <?php else :?> - <span class="no-display"> - <input id="p_method_<?= $block->escapeHtml($_code); ?>" - value="<?= $block->escapeHtml($_code); ?>" - type="radio" - name="payment[method]" class="admin__control-radio" - checked="checked"/> - </span> - <?php endif;?> - <label class="admin__field-label" - for="p_method_<?= $block->escapeHtml($_code); ?>"><?= $block->escapeHtml($_method->getTitle()) ?> - </label> - </dt> - <dd class="admin__payment-method-wrapper"> - <?= /* @noEscape */ $block->getChildHtml('payment.method.' . $_code) ?> - </dd> - <?php endforeach; ?> - </dl> -</div> + <label class="admin__field-label" for="p_method_<?= $block->escapeHtmlAttr($_code); ?>"> + <?= $block->escapeHtml($_method->getTitle()) ?> + </label> + </dt> + <dd class="admin__payment-method-wrapper"> + <?= /* @noEscape */ $block->getChildHtml('payment.method.' . $_code) ?> + </dd> + <?php endforeach; ?> + </dl> + </div> <script> require([ 'mage/apply/main', @@ -56,7 +57,9 @@ ], function(mage) { mage.apply(); <?php if ($_methodsCount != 1) : ?> - order.setPaymentMethod('<?= $block->escapeHtml($block->getSelectedMethodCode()); ?>'); + order.setPaymentMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); + <?php else : ?> + payment.switchMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); <?php endif; ?> }); </script> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml index 2cf09583bd902..dfa6b5e6fff79 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml @@ -4,18 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Comment $block */ +?> - ?> - -<?php /* <h4 class="icon-head fieldset-legend <?= $block->getHeaderCssClass() ?>"><?= $block->getHeaderText() ?></h4> */ ?> <div class="admin__field field-comment"> - <label for="order-comment" class="admin__field-label"><span><?= /* @escapeNotVerified */ __('Order Comments') ?></span></label> + <label for="order-comment" class="admin__field-label"><span><?= $block->escapeHtml(__('Order Comments')) ?></span></label> <div class="admin__field-control"> <textarea id="order-comment" name="order[comment][customer_note]" - class="admin__control-textarea"><?= /* @escapeNotVerified */ $block->getCommentNote() ?></textarea> + class="admin__control-textarea"><?= $block->escapeHtml($block->getCommentNote()) ?></textarea> </div> </div> <script> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml index d499df585e565..188587588897e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml @@ -3,35 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php -/** - * \Magento\Sales\Block\Adminhtml\Order\Create\Coupons - * - */ +/* @var \Magento\Sales\Block\Adminhtml\Order\Create\Coupons $block */ ?> - <div class="admin__field field-apply-coupon-code"> - <label class="admin__field-label"><span><?= /* @escapeNotVerified */ __('Apply Coupon Code') ?></span></label> + <label class="admin__field-label"><span><?= $block->escapeHtml(__('Apply Coupon Code')) ?></span></label> <div class="admin__field-control"> + <?php if (!$block->getCouponCode()) : ?> <input type="text" class="admin__control-text" id="coupons:code" value="" name="coupon_code" /> <?= $block->getButtonHtml(__('Apply'), 'order.applyCoupon($F(\'coupons:code\'))') ?> - <?php if ($block->getCouponCode()): ?> + <?php endif; ?> + <?php if ($block->getCouponCode()) : ?> <p class="added-coupon-code"> <span><?= $block->escapeHtml($block->getCouponCode()) ?></span> - <a href="#" onclick="order.applyCoupon(''); return false;" title="<?= /* @escapeNotVerified */ __('Remove Coupon Code') ?>" - class="action-remove"><span><?= /* @escapeNotVerified */ __('Remove') ?></span></a> + <a href="#" onclick="order.applyCoupon(''); return false;" title="<?= $block->escapeHtmlAttr(__('Remove Coupon Code')) ?>" + class="action-remove"><span><?= $block->escapeHtml(__('Remove')) ?></span></a> </p> <?php endif; ?> <script> - require(["Magento_Sales/order/create/form"], function(){ - - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); - + require(["Magento_Sales/order/create/form"], function() { + order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); + order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); }); </script> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml index fdbaae2347398..bd5baf397fcec 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Data $block */ ?> <div class="page-create-order"> <script> require(["Magento_Sales/order/create/form"], function(){ - order.setCurrencySymbol('<?= /* @escapeNotVerified */ $block->getCurrencySymbol($block->getCurrentCurrencyCode()) ?>') + order.setCurrencySymbol('<?= $block->escapeJs($block->getCurrencySymbol($block->getCurrentCurrencyCode())) ?>') }); </script> - <div class="order-details<?php if ($block->getCustomerId()): ?> order-details-existing-customer<?php endif; ?>"> + <div class="order-details<?php if ($block->getCustomerId()) : ?> order-details-existing-customer<?php endif; ?>"> <div id="order-additional_area" style="display: none" class="admin__page-section order-additional-area"> <?= $block->getChildHtml('additional_area') ?> @@ -35,7 +34,7 @@ <section id="order-addresses" class="admin__page-section order-addresses"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Address Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Address Information')) ?></span> </div> <div class="admin__page-section-content"> <div id="order-billing_address" class="admin__page-section-item order-billing-address"> @@ -47,21 +46,19 @@ </div> </section> - <section id="order-methods" class="admin__page-section order-methods"> - <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Information') ?></span> + <section id="shipping-methods" class="admin__page-section order-methods"> + <div id="order-shipping_method" class="admin__page-section-item order-shipping-method"> + <?= $block->getChildHtml('shipping_method') ?> </div> - <div class="admin__page-section-content"> - <div id="order-billing_method" class="admin__page-section-item order-billing-method"> - <?= $block->getChildHtml('billing_method') ?> - </div> - <div id="order-shipping_method" class="admin__page-section-item order-shipping-method"> - <?= $block->getChildHtml('shipping_method') ?> - </div> + </section> + + <section id="payment-methods" class="admin__page-section payment-methods"> + <div id="order-billing_method" class="admin__page-section-item order-billing-method"> + <?= $block->getChildHtml('billing_method') ?> </div> </section> - <?php if ($block->getChildBlock('card_validation')): ?> + <?php if ($block->getChildBlock('card_validation')) : ?> <section id="order-card_validation" class="admin__page-section order-card-validation"> <?= $block->getChildHtml('card_validation') ?> </section> @@ -71,11 +68,11 @@ <section class="admin__page-section order-summary"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')) ?></span> </div> <div class="admin__page-section-content"> <fieldset class="admin__fieldset order-history" id="order-comment"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Order History') ?></span></legend> + <legend class="admin__legend"><span><?= $block->escapeHtml(__('Order History')) ?></span></legend> <br> <?= $block->getChildHtml('comment') ?> </fieldset> @@ -86,19 +83,19 @@ </section> </div> - <?php if ($block->getCustomerId()): ?> + <?php if ($block->getCustomerId()) : ?> <div class="order-sidebar"> <div class="store-switcher order-currency"> <label class="admin__field-label" for="currency_switcher"> - <?= /* @escapeNotVerified */ __('Order Currency:') ?> + <?= $block->escapeHtml(__('Order Currency:')) ?> </label> <select id="currency_switcher" class="admin__control-select" name="order[currency]" onchange="order.setCurrencyId(this.value); order.setCurrencySymbol(this.options[this.selectedIndex].getAttribute('symbol'));"> - <?php foreach ($block->getAvailableCurrencies() as $_code): ?> - <option value="<?= /* @escapeNotVerified */ $_code ?>"<?php if ($_code == $block->getCurrentCurrencyCode()): ?> selected="selected"<?php endif; ?> symbol="<?= /* @escapeNotVerified */ $block->getCurrencySymbol($_code) ?>"> - <?= /* @escapeNotVerified */ $block->getCurrencyName($_code) ?> + <?php foreach ($block->getAvailableCurrencies() as $_code) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getCurrentCurrencyCode()) : ?> selected="selected"<?php endif; ?> symbol="<?= $block->escapeHtmlAttr($block->getCurrencySymbol($_code)) ?>"> + <?= $block->escapeHtml($block->getCurrencyName($_code)) ?> </option> <?php endforeach; ?> </select> @@ -108,5 +105,4 @@ </div> </div> <?php endif; ?> - </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml index f46e1f74ca8d5..c38acb9b79e47 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml @@ -4,22 +4,20 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form $block */ ?> -<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' data-load-base-url="<?= /* @escapeNotVerified */ $block->getLoadBlockUrl() ?>" action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" enctype="multipart/form-data"> +<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' data-load-base-url="<?= $block->escapeUrl($block->getLoadBlockUrl()) ?>" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> <div id="order-message"> <?= $block->getChildHtml('message') ?> </div> - <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector" style="display:<?= /* @escapeNotVerified */ $block->getCustomerSelectorDisplay() ?>"> + <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector" style="display:<?= /* @noEscape */ $block->getCustomerSelectorDisplay() ?>"> <?= $block->getChildHtml('customer.grid.container') ?> </div> - <div id="order-store-selector" class="fieldset-wrapper" style="display:<?= /* @escapeNotVerified */ $block->getStoreSelectorDisplay() ?>"> + <div id="order-store-selector" class="fieldset-wrapper" style="display:<?= /* @noEscape */ $block->getStoreSelectorDisplay() ?>"> <?= $block->getChildHtml('store') ?> </div> - <div id="order-data" style="display:<?= /* @escapeNotVerified */ $block->getDataSelectorDisplay() ?>"> + <div id="order-data" style="display:<?= /* @noEscape */ $block->getDataSelectorDisplay() ?>"> <?= $block->getChildHtml('data') ?> </div> </form> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml index 0d2ee1f24d5b3..f9611a02c53e4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account */ ?> -<div class="admin__page-section-title <?= /* @escapeNotVerified */ $block->getHeaderCssClass() ?>"> - <span class="title"><?= /* @escapeNotVerified */ $block->getHeaderText() ?></span> +<div class="admin__page-section-title <?= $block->escapeHtmlAttr($block->getHeaderCssClass()) ?>"> + <span class="title"><?= $block->escapeHtml($block->getHeaderText()) ?></span> <div class="actions"></div> </div> <div id="customer_account_fieds" class="admin__page-section-content"> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 686e311292ac7..606c2d00fef79 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -4,99 +4,105 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address $block */ + +/** + * @var \Magento\Customer\Model\ResourceModel\Address\Collection $addressCollection + */ +$addressCollection = $block->getData('customerAddressCollection'); + +$addressArray = []; +if ($block->getCustomerId()) : + $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray(); +endif; + +/** + * @var \Magento\Sales\ViewModel\Customer\AddressFormatter $customerAddressFormatter + */ +$customerAddressFormatter = $block->getData('customerAddressFormatter'); /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block */ -if ($block->getIsShipping()): +if ($block->getIsShipping()) : $_fieldsContainerId = 'order-shipping_address_fields'; $_addressChoiceContainerId = 'order-shipping_address_choice'; ?> <script> require(["Magento_Sales/order/create/form"], function(){ - - order.shippingAddressContainer = '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>'; - order.setAddresses(<?= /* @escapeNotVerified */ $block->getAddressCollectionJson() ?>); - + order.shippingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; + order.setAddresses(<?= /* @noEscape */ $block->getAddressCollectionJson() ?>); }); </script> <?php -else: +else : $_fieldsContainerId = 'order-billing_address_fields'; $_addressChoiceContainerId = 'order-billing_address_choice'; ?> <script> require(["Magento_Sales/order/create/form"], function(){ - order.billingAddressContainer = '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>'; + order.billingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; }); </script> <?php endif; ?> <fieldset class="admin__fieldset"> - <legend class="admin__legend <?= /* @escapeNotVerified */ $block->getHeaderCssClass() ?>"> - <span><?= /* @escapeNotVerified */ $block->getHeaderText() ?></span> + <legend class="admin__legend <?= $block->escapeHtmlAttr($block->getHeaderCssClass()) ?>"> + <span><?= $block->escapeHtml($block->getHeaderText()) ?></span> </legend><br> - <fieldset id="<?= /* @escapeNotVerified */ $_addressChoiceContainerId ?>" class="admin__fieldset order-choose-address"> - <?php if ($block->getIsShipping()): ?> + <fieldset id="<?= $block->escapeHtmlAttr($_addressChoiceContainerId) ?>" class="admin__fieldset order-choose-address"> + <?php if ($block->getIsShipping()) : ?> <div class="admin__field admin__field-option admin__field-shipping-same-as-billing"> <input type="checkbox" id="order-shipping_same_as_billing" name="shipping_same_as_billing" - onclick="order.setShippingAsBilling(this.checked)" class="admin__control-checkbox" - <?php if ($block->getIsAsBilling()): ?>checked<?php endif; ?> /> + onclick="order.setShippingAsBilling(this.checked)" class="admin__control-checkbox" <?php if ($block->getIsAsBilling()) : ?>checked<?php endif; ?> /> <label for="order-shipping_same_as_billing" class="admin__field-label"> - <?= /* @escapeNotVerified */ __('Same As Billing Address') ?> + <?= $block->escapeHtml(__('Same As Billing Address')) ?> </label> </div> <?php endif; ?> <div class="admin__field admin__field-select-from-existing-address"> - <label class="admin__field-label"><?= /* @escapeNotVerified */ __('Select from existing customer addresses:') ?></label> + <label class="admin__field-label"><?= $block->escapeHtml(__('Select from existing customer addresses:')) ?></label> <?php $_id = $block->getForm()->getHtmlIdPrefix() . 'customer_address_id' ?> <div class="admin__field-control"> - <select id="<?= /* @escapeNotVerified */ $_id ?>" - name="<?= $block->getForm()->getHtmlNamePrefix() ?>[customer_address_id]" - onchange="order.selectAddress(this, '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>')" + <select id="<?= $block->escapeHtmlAttr($_id) ?>" + name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[customer_address_id]" + onchange="order.selectAddress(this, '<?= $block->escapeJs($_fieldsContainerId) ?>')" class="admin__control-select"> - <option value=""><?= /* @escapeNotVerified */ __('Add New Address') ?></option> - <?php foreach ($block->getAddressCollection() as $_address): ?> - <?php //if($block->getAddressAsString($_address)!=$block->getAddressAsString($block->getAddress())): ?> + <option value=""><?= $block->escapeHtml(__('Add New Address')) ?></option> + <?php foreach ($addressArray as $addressId => $address) : ?> <option - value="<?= /* @escapeNotVerified */ $_address->getId() ?>"<?php if ($_address->getId() == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> - <?= /* @escapeNotVerified */ $block->getAddressAsString($_address) ?> + value="<?= $block->escapeHtmlAttr($addressId) ?>"<?php if ($addressId == $block->getAddressId()) : ?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml($customerAddressFormatter->getAddressAsString($address)) ?> </option> - <?php //endif; ?> <?php endforeach; ?> </select> </div> </div> </fieldset> - <div class="order-address admin__fieldset" id="<?= /* @escapeNotVerified */ $_fieldsContainerId ?>"> + <div class="order-address admin__fieldset" id="<?= $block->escapeHtmlAttr($_fieldsContainerId) ?>"> <?= $block->getForm()->toHtml() ?> <div class="admin__field admin__field-option order-save-in-address-book"> - <input name="<?= $block->getForm()->getHtmlNamePrefix() ?>[save_in_address_book]" type="checkbox" - id="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" - value="1" - <?php if (!$block->getDontSaveInAddressBook() && $block->getAddress()->getSaveInAddressBook()): ?> checked="checked"<?php endif; ?> - class="admin__control-checkbox"/> - <label for="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" - class="admin__field-label"><?= /* @escapeNotVerified */ __('Save in address book') ?></label> + <input name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[save_in_address_book]" type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1"<?php if (!$block->getDontSaveInAddressBook() && $block->getAddress()->getSaveInAddressBook()) : ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> + <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" + class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> </div> </div> <?php $hideElement = 'address-' . ($block->getIsShipping() ? 'shipping' : 'billing') . '-overlay'; ?> - <div style="display: none;" id="<?= /* @escapeNotVerified */ $hideElement ?>" class="order-methods-overlay"> - <span><?= /* @escapeNotVerified */ __('You don\'t need to select a shipping address.') ?></span> + <div style="display: none;" id="<?= /* @noEscape */ $hideElement ?>" class="order-methods-overlay"> + <span><?= $block->escapeHtml(__('You don\'t need to select a shipping address.')) ?></span> </div> <script> require(["Magento_Sales/order/create/form"], function(){ - order.bindAddressFields('<?= /* @escapeNotVerified */ $_fieldsContainerId ?>'); - order.bindAddressFields('<?= /* @escapeNotVerified */ $_addressChoiceContainerId ?>'); - <?php if ($block->getIsShipping() && $block->getIsAsBilling()): ?> - order.disableShippingAddress(true); - <?php endif; ?> + order.bindAddressFields('<?= $block->escapeJs($_fieldsContainerId) ?>'); + order.bindAddressFields('<?= $block->escapeJs($_addressChoiceContainerId) ?>'); + <?php if ($block->getIsShipping() && $block->getIsAsBilling()) : ?> + order.disableShippingAddress(true); + <?php endif; ?> }); </script> </fieldset> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml index 14d020c4763f5..d27782fd20b15 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml @@ -4,25 +4,25 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Giftmessage $block */ ?> -<?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> -<?php $_items = $block->getItems(); ?> -<div id="order-giftmessage" class="giftmessage-order-create"> - <fieldset class="admin__fieldset"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Gift Message for the Entire Order') ?></span></legend> - <br> - <?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> - <p><?= /* @escapeNotVerified */ __('Leave this box blank if you don\'t want to leave a gift message for the entire order.') ?></p> - <?= $block->getFormHtml($block->getQuote(), 'main') ?> - <?php endif; ?> - </fieldset> -<script> -require(['Magento_Sales/order/create/form'], function(){ - - order.giftmessageFieldsBind('order-giftmessage'); -}); -</script> -</div> +<?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> + <?php $_items = $block->getItems(); ?> + <div id="order-giftmessage" class="giftmessage-order-create"> + <fieldset class="admin__fieldset"> + <legend class="admin__legend"><span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span></legend> + <br> + <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> + <p><?= $block->escapeHtml(__('Leave this box blank if you don\'t want to leave a gift message for the entire order.')) ?></p> + <?= $block->getFormHtml($block->getQuote(), 'main') ?> + <?php endif; ?> + </fieldset> + <script> + require(['Magento_Sales/order/create/form'], function(){ + order.giftmessageFieldsBind('order-giftmessage'); + }); + </script> + </div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items.phtml index de7af269538d9..4c31e34d38654 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items.phtml @@ -4,12 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Items $block */ ?> - <div class="admin__page-section-title"> - <strong class="title"><?= /* @escapeNotVerified */ $block->getHeaderText() ?></strong> + <strong class="title"><?= $block->escapeHtml($block->getHeaderText()) ?></strong> <div class="actions"> <?= $block->getButtonsHtml() ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml index 02cf697b0e74a..d6821303d8c16 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** @@ -14,32 +13,32 @@ ?> <?php $_items = $block->getItems() ?> -<?php if (empty($_items)): ?> +<?php if (empty($_items)) : ?> <div id="order-items_grid"> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-discount"><span><?= /* @escapeNotVerified */ __('Discount') ?></span></th> - <th class="col-row-total"><span><?= /* @escapeNotVerified */ __('Row Subtotal') ?></span></th> - <th class="col-action"><span><?= /* @escapeNotVerified */ __('Action') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-discount"><span><?= $block->escapeHtml(__('Discount')) ?></span></th> + <th class="col-row-total"><span><?= $block->escapeHtml(__('Row Subtotal')) ?></span></th> + <th class="col-action"><span><?= $block->escapeHtml(__('Action')) ?></span></th> </tr> </thead> <tbody> <tr class="even"> - <td class="empty-text" colspan="100"><?= /* @escapeNotVerified */ __('No ordered items') ?></td> + <td class="empty-text" colspan="100"><?= $block->escapeHtml(__('No ordered items')) ?></td> </tr> </tbody> </table> </div> </div> -<?php else: ?> +<?php else : ?> <div class="admin__table-wrapper" id="order-items_grid"> - <?php if (count($_items)>10): ?> + <?php if (count($_items) > 10) : ?> <div class="actions update actions-update"> <?= $block->getButtonHtml(__('Update Items and Quantities'), 'order.itemsUpdate()', 'action-secondary') ?> </div> @@ -47,37 +46,34 @@ <table class="data-table admin__table-primary order-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-discount"><span><?= /* @escapeNotVerified */ __('Discount') ?></span></th> - <th class="col-row-total"><span><?= /* @escapeNotVerified */ __('Row Subtotal') ?></span></th> - <th class="col-action"><span><?= /* @escapeNotVerified */ __('Action') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-discount"><span><?= $block->escapeHtml(__('Discount')) ?></span></th> + <th class="col-row-total"><span><?= $block->escapeHtml(__('Row Subtotal')) ?></span></th> + <th class="col-action"><span><?= $block->escapeHtml(__('Action')) ?></span></th> </tr> </thead> <tfoot> <tr> - <td class="col-total"><?= /* @escapeNotVerified */ __('Total %1 product(s)', count($_items)) ?></td> - <td colspan="2" class="col-subtotal"><?= /* @escapeNotVerified */ __('Subtotal:') ?></td> - <td class="col-price"><strong><?= /* @escapeNotVerified */ $block->formatPrice($block->getSubtotal()) ?></strong></td> - <td class="col-price"><strong><?= /* @escapeNotVerified */ $block->formatPrice($block->getDiscountAmount()) ?></strong></td> - <td class="col-price"><strong> - <?php - /* @escapeNotVerified */ echo $block->formatPrice($block->getSubtotalWithDiscount()); - ?></strong></td> + <td class="col-total"><?= $block->escapeHtml(__('Total %1 product(s)', count($_items))) ?></td> + <td colspan="2" class="col-subtotal"><?= $block->escapeHtml(__('Subtotal:')) ?></td> + <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotal()) ?></strong></td> + <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getDiscountAmount()) ?></strong></td> + <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotalWithDiscount()); ?></strong></td> <td colspan="2"> </td> </tr> </tfoot> <?php $i = 0 ?> - <?php foreach ($_items as $_item):$i++ ?> - <tbody class="<?= /* @escapeNotVerified */ ($i%2) ? 'even' : 'odd' ?>"> + <?php foreach ($_items as $_item) : $i++ ?> + <tbody class="<?= /* @noEscape */ ($i%2) ? 'even' : 'odd' ?>"> <tr> <td class="col-product"> - <span id="order_item_<?= /* @escapeNotVerified */ $_item->getId() ?>_title"><?= $block->escapeHtml($_item->getName()) ?></span> + <span id="order_item_<?= (int) $_item->getId() ?>_title"><?= $block->escapeHtml($_item->getName()) ?></span> <div class="product-sku-block"> - <span><?= /* @escapeNotVerified */ __('SKU') ?>:</span> - <?= implode('<br />', $this->helper('Magento\Catalog\Helper\Data')->splitSku($block->escapeHtml($_item->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU')) ?>:</span> + <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($_item->getSku()))) ?> </div> <div class="product-configure-block"> <?= $block->getConfigureButtonHtml($_item) ?> @@ -88,56 +84,56 @@ <?= $block->getItemUnitPriceHtml($_item) ?> <?php $_isCustomPrice = $block->usedCustomPriceForItem($_item) ?> - <?php if ($_tier = $block->getTierHtml($_item)): ?> - <div id="item_tier_block_<?= /* @escapeNotVerified */ $_item->getId() ?>"<?php if ($_isCustomPrice): ?> style="display:none"<?php endif; ?>> - <a href="#" onclick="$('item_tier_<?= /* @escapeNotVerified */ $_item->getId() ?>').toggle();return false;"><?= /* @escapeNotVerified */ __('Tier Pricing') ?></a> - <div style="display:none" id="item_tier_<?= /* @escapeNotVerified */ $_item->getId() ?>"><?= /* @escapeNotVerified */ $_tier ?></div> + <?php if ($_tier = $block->getTierHtml($_item)) : ?> + <div id="item_tier_block_<?= (int) $_item->getId() ?>"<?php if ($_isCustomPrice) : ?> style="display:none"<?php endif; ?>> + <a href="#" onclick="$('item_tier_<?= (int) $_item->getId() ?>').toggle();return false;"><?= $block->escapeHtml(__('Tier Pricing')) ?></a> + <div style="display:none" id="item_tier_<?= (int) $_item->getId() ?>"><?= /* @noEscape */ $_tier ?></div> </div> <?php endif; ?> - <?php if ($block->canApplyCustomPrice($_item)): ?> + <?php if ($block->canApplyCustomPrice($_item)) : ?> <div class="custom-price-block"> <input type="checkbox" - class="admin__control-checkbox" - id="item_use_custom_price_<?= /* @escapeNotVerified */ $_item->getId() ?>" - <?php if ($_isCustomPrice): ?> checked="checked"<?php endif; ?> - onclick="order.toggleCustomPrice(this, 'item_custom_price_<?= /* @escapeNotVerified */ $_item->getId() ?>', 'item_tier_block_<?= /* @escapeNotVerified */ $_item->getId() ?>');"/> + class="admin__control-checkbox" + id="item_use_custom_price_<?= (int) $_item->getId() ?>" + <?php if ($_isCustomPrice) : ?> checked="checked"<?php endif; ?> + onclick="order.toggleCustomPrice(this, 'item_custom_price_<?= (int) $_item->getId() ?>', 'item_tier_block_<?= (int) $_item->getId() ?>');"/> <label class="normal admin__field-label" - for="item_use_custom_price_<?= /* @escapeNotVerified */ $_item->getId() ?>"> - <span><?= /* @escapeNotVerified */ __('Custom Price') ?>*</span></label> + for="item_use_custom_price_<?= (int) $_item->getId() ?>"> + <span><?= $block->escapeHtml(__('Custom Price')) ?>*</span></label> </div> <?php endif; ?> - <input id="item_custom_price_<?= /* @escapeNotVerified */ $_item->getId() ?>" - name="item[<?= /* @escapeNotVerified */ $_item->getId() ?>][custom_price]" - value="<?= /* @escapeNotVerified */ sprintf("%.2f", $block->getOriginalEditablePrice($_item)) ?>" - <?php if (!$_isCustomPrice): ?> - style="display:none" - disabled="disabled" - <?php endif; ?> - class="input-text item-price admin__control-text"/> + <input id="item_custom_price_<?= (int) $_item->getId() ?>" + name="item[<?= (int) $_item->getId() ?>][custom_price]" + value="<?= /* @noEscape */ sprintf("%.2f", $block->getOriginalEditablePrice($_item)) ?>" + <?php if (!$_isCustomPrice) : ?> + style="display:none" + disabled="disabled" + <?php endif; ?> + class="input-text item-price admin__control-text"/> </td> <td class="col-qty"> - <input name="item[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" + <input name="item[<?= (int) $_item->getId() ?>][qty]" class="input-text item-qty admin__control-text" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" + value="<?= (int) $_item->getQty() ?>" maxlength="12" /> </td> <td class="col-subtotal col-price"> <?= $block->getItemRowTotalHtml($_item) ?> </td> <td class="col-discount col-price"> - <?= /* @escapeNotVerified */ $block->formatPrice(-$_item->getTotalDiscountAmount()) ?> + <?= /* @noEscape */ $block->formatPrice(-$_item->getTotalDiscountAmount()) ?> <div class="discount-price-block"> - <input id="item_use_discount_<?= /* @escapeNotVerified */ $_item->getId() ?>" - class="admin__control-checkbox" - name="item[<?= /* @escapeNotVerified */ $_item->getId() ?>][use_discount]" - <?php if (!$_item->getNoDiscount()): ?>checked="checked"<?php endif; ?> - value="1" - type="checkbox" /> + <input id="item_use_discount_<?= (int) $_item->getId() ?>" + class="admin__control-checkbox" + name="item[<?= (int) $_item->getId() ?>][use_discount]" + <?php if (!$_item->getNoDiscount()) : ?>checked="checked"<?php endif; ?> + value="1" + type="checkbox" /> <label - for="item_use_discount_<?= /* @escapeNotVerified */ $_item->getId() ?>" + for="item_use_discount_<?= (int) $_item->getId() ?>" class="normal admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Apply') ?></span></label> + <span><?= $block->escapeHtml(__('Apply')) ?></span></label> </div> </td> @@ -145,19 +141,19 @@ <?= $block->getItemRowTotalWithDiscountHtml($_item) ?> </td> <td class="col-actions last"> - <select class="admin__control-select" name="item[<?= /* @escapeNotVerified */ $_item->getId() ?>][action]"> - <option value=""><?= /* @escapeNotVerified */ __('Please select') ?></option> - <option value="remove"><?= /* @escapeNotVerified */ __('Remove') ?></option> - <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()): ?> - <option value="cart"><?= /* @escapeNotVerified */ __('Move to Shopping Cart') ?></option> - <?php if ($block->isMoveToWishlistAllowed($_item)): ?> + <select class="admin__control-select" name="item[<?= (int) $_item->getId() ?>][action]"> + <option value=""><?= $block->escapeHtml(__('Please select')) ?></option> + <option value="remove"><?= $block->escapeHtml(__('Remove')) ?></option> + <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()) : ?> + <option value="cart"><?= $block->escapeHtml(__('Move to Shopping Cart')) ?></option> + <?php if ($block->isMoveToWishlistAllowed($_item)) : ?> <?php $wishlists = $block->getCustomerWishlists();?> - <?php if (count($wishlists) <= 1):?> - <option value="wishlist"><?= /* @escapeNotVerified */ __('Move to Wish List') ?></option> - <?php else: ?> - <optgroup label="<?= /* @escapeNotVerified */ __('Move to Wish List') ?>"> - <?php foreach ($wishlists as $wishlist):?> - <option value="wishlist_<?= /* @escapeNotVerified */ $wishlist->getId() ?>"><?= $block->escapeHtml($wishlist->getName()) ?></option> + <?php if (count($wishlists) <= 1) : ?> + <option value="wishlist"><?= $block->escapeHtml(__('Move to Wish List')) ?></option> + <?php else : ?> + <optgroup label="<?= $block->escapeHtml(__('Move to Wish List')) ?>"> + <?php foreach ($wishlists as $wishlist) :?> + <option value="wishlist_<?= (int) $wishlist->getId() ?>"><?= $block->escapeHtml($wishlist->getName()) ?></option> <?php endforeach;?> </optgroup> <?php endif; ?> @@ -168,22 +164,21 @@ </tr> <?php $hasMessageError = false; ?> - <?php foreach ($_item->getMessage(false) as $messageError):?> - <?php if (!empty($messageError)) { + <?php foreach ($_item->getMessage(false) as $messageError) : ?> + <?php if (!empty($messageError)) : $hasMessageError = true; - } - ?> + endif; ?> <?php endforeach; ?> - <?php if ($hasMessageError):?> + <?php if ($hasMessageError) : ?> <tr class="row-messages-error"> <td colspan="100"> <!-- ToDo UI: remove the 100 --> - <?php foreach ($_item->getMessage(false) as $message): + <?php foreach ($_item->getMessage(false) as $message) : if (empty($message)) { continue; } ?> - <div class="message <?php if ($_item->getHasError()): ?>message-error<?php else: ?>message-notice<?php endif; ?>"> + <div class="message <?php if ($_item->getHasError()) : ?>message-error<?php else : ?>message-notice<?php endif; ?>"> <?= $block->escapeHtml($message) ?> </div> <?php endforeach; ?> @@ -195,7 +190,7 @@ </tbody> <?php endforeach; ?> </table> - <p><small><?= /* @escapeNotVerified */ $block->getInclExclTaxMessage() ?></small></p> + <p><small><?= $block->escapeHtml($block->getInclExclTaxMessage()) ?></small></p> </div> <div class="order-discounts"> @@ -210,43 +205,42 @@ order.itemsOnchangeBind() }); </script> + <?php if ($block->isGiftMessagesAvailable()) : ?> + <script> + require([ + "prototype", + "Magento_Sales/order/giftoptions_tooltip" + ], function(){ -<?php if ($block->isGiftMessagesAvailable()) : ?> -<script> -require([ - "prototype", - "Magento_Sales/order/giftoptions_tooltip" -], function(){ - -//<![CDATA[ - /** - * Retrieve gift options tooltip content - */ - function getGiftOptionsTooltipContent(itemId) { - var contentLines = []; - var headerLine = null; - var contentLine = null; - - $$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { - if (element.down(0)) { - headerLine = element.down(0).innerHTML; - contentLine = element.down(0).next().innerHTML; - if (contentLine.length > 30) { - contentLine = contentLine.slice(0,30) + '...'; - } - contentLines.push(headerLine + ' ' + contentLine); + //<![CDATA[ + /** + * Retrieve gift options tooltip content + */ + function getGiftOptionsTooltipContent(itemId) { + var contentLines = []; + var headerLine = null; + var contentLine = null; + + $$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { + if (element.down(0)) { + headerLine = element.down(0).innerHTML; + contentLine = element.down(0).next().innerHTML; + if (contentLine.length > 30) { + contentLine = contentLine.slice(0,30) + '...'; + } + contentLines.push(headerLine + ' ' + contentLine); + } + }); + return contentLines.join('<br/>'); } - }); - return contentLines.join('<br/>'); - } - giftOptionsTooltip.setTooltipContentLoaderFunction(getGiftOptionsTooltipContent); + giftOptionsTooltip.setTooltipContentLoaderFunction(getGiftOptionsTooltipContent); - window.getGiftOptionsTooltipContent = getGiftOptionsTooltipContent; + window.getGiftOptionsTooltipContent = getGiftOptionsTooltipContent; -//]]> + //]]> -}); -</script> -<?php endif; ?> + }); + </script> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/row.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/row.phtml index 2e1f058e17128..6a0715a056558 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/row.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/row.phtml @@ -3,16 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid $block */ - $_item = $block->getItem(); ?> - <div class="price-excl-tax"> - <?= /* @escapeNotVerified */ $block->formatPrice($_item->getRowTotal()) ?> + <?= /* @noEscape */ $block->formatPrice($_item->getRowTotal()) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/total.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/total.phtml index aafd3afd2783c..ae4bcd26e7c1e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/total.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/total.phtml @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid $block */ - $_item = $block->getItem(); ?> - <?php $_rowTotalWithoutDiscount = $_item->getRowTotal() - $_item->getTotalDiscountAmount(); ?> -<?= /* @escapeNotVerified */ $block->formatPrice(max(0, $_rowTotalWithoutDiscount)) ?> +<?= /* @noEscape */ $block->formatPrice(max(0, $_rowTotalWithoutDiscount)) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/unit.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/unit.phtml index 0ecefcf5273f1..b5871518cf596 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/unit.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/price/unit.phtml @@ -3,15 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid $block */ - $_item = $block->getItem(); ?> <div class="price-excl-tax"> -<?= /* @escapeNotVerified */ $block->formatPrice($_item->getCalculationPrice()) ?> +<?= /* @noEscape */ $block->formatPrice($_item->getCalculationPrice()) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml index 374684d351b49..eb39f71265cd6 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +?> <script> require([ "prototype", @@ -13,16 +11,14 @@ require([ "Magento_Catalog/catalog/product/composite/configure", "domReady!" ], function(){ - order.sidebarHide(); if (window.productConfigure) { productConfigure.addListType('product_to_add', { - urlFetch: '<?= /* @escapeNotVerified */ $block->getUrl('sales/order_create/configureProductToAdd') ?>' + urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>' }); productConfigure.addListType('quote_items', { - urlFetch: '<?= /* @escapeNotVerified */ $block->getUrl('sales/order_create/configureQuoteItems') ?>' + urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureQuoteItems'))) ?>' }); } - }); </script> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml index 973758a66947c..1dcf57d879543 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml @@ -3,8 +3,5 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<input type="checkbox" name="newsletter:subscribe"> <label for="newsletter:subscribe" style="width: 90%; float: none;"><?= /* @escapeNotVerified */ __('Subscribe to Newsletter') ?></label><br/> +<input type="checkbox" name="newsletter:subscribe"> <label for="newsletter:subscribe" style="width: 90%; float: none;"><?= $block->escapeHtml(__('Subscribe to Newsletter')) ?></label><br/> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml index 9e0394f6430bd..baaf4c078f2c7 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml @@ -4,46 +4,45 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form */ ?> <?php $_shippingRateGroups = $block->getShippingRates(); ?> -<?php if ($_shippingRateGroups): ?> +<?php if ($_shippingRateGroups) : ?> <div id="order-shipping-method-choose" class="control" style="display:none"> <dl class="admin__order-shipment-methods"> - <?php foreach ($_shippingRateGroups as $code => $_rates): ?> + <?php foreach ($_shippingRateGroups as $code => $_rates) : ?> <dt class="admin__order-shipment-methods-title"><?= $block->escapeHtml($block->getCarrierName($code)) ?></dt> <dd class="admin__order-shipment-methods-options"> <ul class="admin__order-shipment-methods-options-list"> - <?php foreach ($_rates as $_rate): ?> + <?php foreach ($_rates as $_rate) : ?> <?php $_radioProperty = 'name="order[shipping_method]" type="radio" onclick="order.setShippingMethod(this.value)"' ?> <?php $_code = $_rate->getCode() ?> <li class="admin__field-option"> - <?php if ($_rate->getErrorMessage()): ?> - <div class="messages"> + <?php if ($_rate->getErrorMessage()) : ?> + <div class="messages"> <div class="message message-error error"> <div><?= $block->escapeHtml($_rate->getErrorMessage()) ?></div> </div> - </div> - <?php else: ?> + </div> + <?php else : ?> <?php $_checked = $block->isMethodActive($_code) ? 'checked="checked"' : '' ?> - <input <?= /* @escapeNotVerified */ $_radioProperty ?> value="<?= /* @escapeNotVerified */ $_code ?>" - id="s_method_<?= /* @escapeNotVerified */ $_code ?>" <?= /* @escapeNotVerified */ $_checked ?> + <input <?= /* @noEscape */ $_radioProperty ?> value="<?= $block->escapeHtmlAttr($_code) ?>" + id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> class="admin__control-radio required-entry"/> - <label class="admin__field-label" for="s_method_<?= /* @escapeNotVerified */ $_code ?>"> + <label class="admin__field-label" for="s_method_<?= $block->escapeHtmlAttr($_code) ?>"> <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> </label> - <?php endif ?> + <?php endif; ?> </li> <?php endforeach; ?> </ul> @@ -51,7 +50,7 @@ <?php endforeach; ?> </dl> </div> - <?php if ($_rate = $block->getActiveMethodRate()): ?> + <?php if ($_rate = $block->getActiveMethodRate()) : ?> <div id="order-shipping-method-info" class="order-shipping-method-info"> <dl class="admin__order-shipment-methods"> <dt class="admin__order-shipment-methods-title"> @@ -60,12 +59,12 @@ <dd class="admin__order-shipment-methods-options"> <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> </dd> @@ -73,39 +72,35 @@ <a href="#" onclick="$('order-shipping-method-info').hide();$('order-shipping-method-choose').show();return false" class="action-default"> - <span><?= /* @escapeNotVerified */ __('Click to change shipping method') ?></span> + <span><?= $block->escapeHtml(__('Click to change shipping method')) ?></span> </a> </div> - <?php else: ?> + <?php else : ?> <script> require(['prototype'], function(){ $('order-shipping-method-choose').show(); }); </script> <?php endif; ?> -<?php elseif ($block->getIsRateRequest()): ?> +<?php elseif ($block->getIsRateRequest()) : ?> <div class="order-shipping-method-summary"> - <strong class="order-shipping-method-not-available"><?= /* @escapeNotVerified */ __('Sorry, no quotes are available for this order.') ?></strong> + <strong class="order-shipping-method-not-available"><?= $block->escapeHtml(__('Sorry, no quotes are available for this order.')) ?></strong> </div> -<?php else: ?> +<?php else : ?> <div id="order-shipping-method-summary" class="order-shipping-method-summary"> <a href="#" onclick="order.loadShippingRates();return false" class="action-default"> - <span><?= /* @escapeNotVerified */ __('Get shipping methods and rates') ?></span> + <span><?= $block->escapeHtml(__('Get shipping methods and rates')) ?></span> </a> <input type="hidden" name="order[has_shipping]" value="" class="required-entry" /> </div> <?php endif; ?> <div style="display: none;" id="shipping-method-overlay" class="order-methods-overlay"> - <span><?= /* @escapeNotVerified */ __('You don\'t need to select a shipping method.') ?></span> + <span><?= $block->escapeHtml(__('You don\'t need to select a shipping method.')) ?></span> </div> <script> require(["Magento_Sales/order/create/form"], function(){ - - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); - - <?php if ($block->getQuote()->isVirtual()): ?> - order.isOnlyVirtualProduct = true; - <?php endif; ?> + order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); + order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); + order.isOnlyVirtualProduct = <?= /* @noEscape */ $block->getQuote()->isVirtual() ? 'true' : 'false'; ?>; }); </script> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml index 48823de3f4db2..d4dea4eb85a57 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml @@ -4,18 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - -/** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar */ +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar $block */ ?> <div class="customer-current-activity-inner"> - <h4 class="customer-activity-title"><?= /* @escapeNotVerified */ __('Customer\'s Activities') ?></h4> + <h4 class="customer-activity-title"><?= $block->escapeHtml(__('Customer\'s Activities')) ?></h4> <div class="create-order-sidebar-container"> <?= $block->getChildHtml('top_button') ?> - <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child): ?> - <?php if ($_alias != 'top_button' && $_alias != 'bottom_button'): ?> - <?php if ($block->canDisplay($_child)): ?> - <div class="order-sidebar-block" id="order-sidebar_<?= /* @escapeNotVerified */ $_alias ?>"> + <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child) : ?> + <?php if ($_alias != 'top_button' && $_alias != 'bottom_button') : ?> + <?php if ($block->canDisplay($_child)) : ?> + <div class="order-sidebar-block" id="order-sidebar_<?= $block->escapeHtmlAttr($_alias) ?>"> <?= $block->getChildHtml($_alias) ?> </div> <?php endif; ?> @@ -32,12 +30,12 @@ require([ function addSidebarCompositeListType() { productConfigure.addListType('sidebar', { - urlFetch: '<?= /* @escapeNotVerified */ $block->getUrl('sales/order_create/configureProductToAdd') ?>', - urlConfirm: '<?= /* @escapeNotVerified */ $block->getUrl('sales/order_create/addConfigured') ?>' + urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>', + urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' }); productConfigure.addListType('sidebar_wishlist', { - urlFetch: '<?= /* @escapeNotVerified */ $block->getUrl('customer/wishlist_product_composite_wishlist/configure') ?>', - urlConfirm: '<?= /* @escapeNotVerified */ $block->getUrl('sales/order_create/addConfigured') ?>' + urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))) ?>', + urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' }); } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml index 2dbf717f73439..a323bc9aa5c2f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml @@ -3,44 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\AbstractSidebar */ ?> -<div class="create-order-sidebar-block" id="sidebar_data_<?= /* @escapeNotVerified */ $block->getDataId() ?>"> +<div class="create-order-sidebar-block" id="sidebar_data_<?= $block->escapeHtmlAttr($block->getDataId()) ?>"> <div class="head sidebar-title-block"> <a href="#" class="action-refresh" title="<?= $block->escapeHtml(__('Refresh')) ?>" - onclick="order.loadArea('sidebar_<?= /* @escapeNotVerified */ $block->getDataId() ?>', 'sidebar_data_<?= /* @escapeNotVerified */ $block->getDataId() ?>');return false;"> - <span><?= /* @escapeNotVerified */ __('Refresh') ?></span> + onclick="order.loadArea('sidebar_<?= $block->escapeJs($block->getDataId()) ?>', 'sidebar_data_<?= $block->escapeJs($block->getDataId()) ?>');return false;"> + <span><?= $block->escapeHtml(__('Refresh')) ?></span> </a> <h5 class="create-order-sidebar-label"> - <?= /* @escapeNotVerified */ $block->getHeaderText() ?> - <span class="normal">(<?= /* @escapeNotVerified */ $block->getItemCount() ?>)</span> + <?= $block->escapeHtml($block->getHeaderText()) ?> + <span class="normal">(<?= $block->escapeHtml($block->getItemCount()) ?>)</span> </h5> </div> <div class="content"> <div class="auto-scroll"> - <?php if ($block->getItemCount()): ?> + <?php if ($block->getItemCount()) : ?> <table class="admin__table-primary"> <thead> <tr> - <th class="col-item"><?= /* @escapeNotVerified */ __('Item') ?></th> + <th class="col-item"><?= $block->escapeHtml(__('Item')) ?></th> - <?php if ($block->canDisplayItemQty()): ?> - <th class="col-qty"><?= /* @escapeNotVerified */ __('Qty') ?></th> + <?php if ($block->canDisplayItemQty()) : ?> + <th class="col-qty"><?= $block->escapeHtml(__('Qty')) ?></th> <?php endif; ?> - <?php if ($block->canDisplayPrice()): ?> - <th class="col-price"><?= /* @escapeNotVerified */ __('Price') ?></th> + <?php if ($block->canDisplayPrice()) : ?> + <th class="col-price"><?= $block->escapeHtml(__('Price')) ?></th> <?php endif; ?> - <?php if ($block->canRemoveItems()): ?> + <?php if ($block->canRemoveItems()) : ?> <th class="col-remove"> <span title="<?= $block->escapeHtml(__('Remove')) ?>" class="icon icon-remove"> - <span><?= /* @escapeNotVerified */ __('Remove') ?></span> + <span><?= $block->escapeHtml(__('Remove')) ?></span> </span> </th> <?php endif; ?> @@ -48,40 +45,40 @@ <th class="col-add"> <span title="<?= $block->escapeHtml(__('Add To Order')) ?>" class="icon icon-add"> - <span><?= /* @escapeNotVerified */ __('Add To Order') ?></span> + <span><?= $block->escapeHtml(__('Add To Order')) ?></span> </span> </th> </tr> </thead> <tbody> - <?php foreach ($block->getItems() as $_item): ?> + <?php foreach ($block->getItems() as $_item) : ?> <tr> <td class="col-item"><?= $block->escapeHtml($_item->getName()) ?></td> - <?php if ($block->canDisplayItemQty()): ?> + <?php if ($block->canDisplayItemQty()) : ?> <td class="col-qty"> - <?= /* @escapeNotVerified */ $block->getItemQty($_item) ?> + <?= (int) $block->getItemQty($_item) ?> </td> <?php endif; ?> - <?php if ($block->canDisplayPrice()): ?> + <?php if ($block->canDisplayPrice()) : ?> <td class="col-price"> <?= /* @noEscape */ $block->getItemPrice($block->getProduct($_item)) ?> </td> <?php endif; ?> - <?php if ($block->canRemoveItems()): ?> + <?php if ($block->canRemoveItems()) : ?> <td class="col-remove"> <div class="admin__field-option"> - <input id="sidebar-remove-<?= /* @escapeNotVerified */ $block->getSidebarStorageAction() ?>-<?= /* @escapeNotVerified */ $block->getItemId($_item) ?>" + <input id="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>" type="checkbox" class="admin__control-checkbox" - name="sidebar[remove][<?= /* @escapeNotVerified */ $block->getItemId($_item) ?>]" - value="<?= /* @escapeNotVerified */ $block->getDataId() ?>" + name="sidebar[remove][<?= (int) $block->getItemId($_item) ?>]" + value="<?= $block->escapeHtmlAttr($block->getDataId()) ?>" title="<?= $block->escapeHtml(__('Remove')) ?>" /> <label class="admin__field-label" - for="sidebar-remove-<?= /* @escapeNotVerified */ $block->getSidebarStorageAction() ?>-<?= /* @escapeNotVerified */ $block->getItemId($_item) ?>"> + for="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>"> </label> </div> </td> @@ -89,29 +86,29 @@ <td class="col-add"> <div class="admin__field-option"> - <?php if ($block->isConfigurationRequired($_item->getTypeId()) && $block->getDataId() == 'wishlist'): ?> + <?php if ($block->isConfigurationRequired($_item->getTypeId()) && $block->getDataId() == 'wishlist') : ?> <a href="#" class="icon icon-configure" title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('<?= 'sidebar_wishlist' ?>', <?= /* @escapeNotVerified */ $block->getProductId($_item) ?>, <?= /* @escapeNotVerified */ $block->getItemId($_item) ?>); return false;"> - <span><?= /* @escapeNotVerified */ __('Configure and Add to Order') ?></span> + onclick="order.sidebarConfigureProduct('sidebar_wishlist', <?= (int) $block->getProductId($_item) ?>, <?= (int) $block->getItemId($_item) ?>); return false;"> + <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php elseif ($block->isConfigurationRequired($_item->getTypeId())): ?> + <?php elseif ($block->isConfigurationRequired($_item->getTypeId())) : ?> <a href="#" class="icon icon-configure" title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('<?= 'sidebar' ?>', <?= /* @escapeNotVerified */ $block->getProductId($_item) ?>); return false;"> - <span><?= /* @escapeNotVerified */ __('Configure and Add to Order') ?></span> + onclick="order.sidebarConfigureProduct('sidebar', <?= (int) $block->getProductId($_item) ?>); return false;"> + <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php else: ?> - <input id="sidebar-<?= /* @escapeNotVerified */ $block->getSidebarStorageAction() ?>-<?= /* @escapeNotVerified */ $block->getIdentifierId($_item) ?>" + <?php else : ?> + <input id="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>" type="checkbox" class="admin__control-checkbox" - name="sidebar[<?= /* @escapeNotVerified */ $block->getSidebarStorageAction() ?>][<?= /* @escapeNotVerified */ $block->getIdentifierId($_item) ?>]" - value="<?= /* @escapeNotVerified */ $block->canDisplayItemQty() ? $_item->getQty()*1 : 1 ?>" + name="sidebar[<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>][<?= (int) $block->getIdentifierId($_item) ?>]" + value="<?= /* @noEscape */ $block->canDisplayItemQty() ? (int) $_item->getQty() : 1 ?>" title="<?= $block->escapeHtml(__('Add To Order')) ?>"/> <label class="admin__field-label" - for="sidebar-<?= /* @escapeNotVerified */ $block->getSidebarStorageAction() ?>-<?= /* @escapeNotVerified */ $block->getIdentifierId($_item) ?>"> + for="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>"> </label> <?php endif; ?> </div> @@ -120,11 +117,11 @@ <?php endforeach; ?> </tbody> </table> - <?php else: ?> - <span class="no-items"><?= /* @escapeNotVerified */ __('No items') ?></span> + <?php else : ?> + <span class="no-items"><?= $block->escapeHtml(__('No items')) ?></span> <?php endif ?> </div> - <?php if ($block->getItemCount() && $block->canRemoveItems()): ?> + <?php if ($block->getItemCount() && $block->canRemoveItems()) : ?> <?= $block->getChildHtml('empty_customer_cart_button') ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml index 1de681bdb2084..407bd0272e9fd 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml @@ -3,20 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Store\Select */ ?> <div class="store-scope form-inline"> <div class="admin__fieldset tree-store-scope"> <?php $showHelpHint = 0; ?> - <?php foreach ($block->getWebsiteCollection() as $_website): ?> + <?php foreach ($block->getWebsiteCollection() as $_website) : ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group): ?> + <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store): ?> - <?php if ($showWebsite == false): ?> + <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> + <?php if ($showWebsite == false) : ?> <?php $showWebsite = true; ?> <div class="admin__field field-website_label"> <label class="admin__field-label" for=""> @@ -24,7 +21,7 @@ </label> <div class="admin__field-control"> <div class="admin__field admin__field-option"> - <?php if ($showHelpHint == 0): + <?php if ($showHelpHint == 0) : echo $block->getHintHtml(); $showHelpHint = 1; endif; ?> @@ -33,7 +30,7 @@ </div> <?php endif; ?> - <?php if ($showGroup == false): ?> + <?php if ($showGroup == false) : ?> <?php $showGroup = true; ?> <div class="admin__field field-group_label"> <label class="admin__field-label" for=""><span><?= $block->escapeHtml($_group->getName()) ?></span></label> @@ -46,8 +43,8 @@ <div class="admin__field-control"> <div class="nested"> <div class="admin__field admin__field-option"> - <input type="radio" id="store_<?= /* @escapeNotVerified */ $_store->getId() ?>" class="admin__control-radio" onclick="order.setStoreId('<?= /* @escapeNotVerified */ $_store->getId() ?>')"/> - <label class="admin__field-label" for="store_<?= /* @escapeNotVerified */ $_store->getId() ?>"> + <input type="radio" id="store_<?= (int) $_store->getId() ?>" class="admin__control-radio" onclick="order.setStoreId('<?= (int) $_store->getId() ?>')"/> + <label class="admin__field-label" for="store_<?= (int) $_store->getId() ?>"> <?= $block->escapeHtml($_store->getName()) ?> </label> </div> @@ -55,7 +52,7 @@ </div> </div> <?php endforeach; ?> - <?php if ($showGroup): ?> + <?php if ($showGroup) : ?> <?php endif; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml index 02433edf82346..9a901d99ae8f8 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml @@ -4,31 +4,30 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals $block */ ?> -<legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Order Totals') ?></span></legend> +<legend class="admin__legend"><span><?= $block->escapeHtml(__('Order Totals')) ?></span></legend> <br> <table class="admin__table-secondary data-table"> <tbody> - <?= /* @escapeNotVerified */ $block->renderTotals() ?> - <?= /* @escapeNotVerified */ $block->renderTotals('footer') ?> + <?= /* @noEscape */ $block->renderTotals() ?> + <?= /* @noEscape */ $block->renderTotals('footer') ?> </tbody> </table> <div class="order-totals-actions"> <div class="admin__field admin__field-option field-append-comments"> <input type="checkbox" id="notify_customer" name="order[comment][customer_note_notify]" - value="1"<?php if ($block->getNoteNotify()): ?> checked="checked"<?php endif; ?> + value="1"<?php if ($block->getNoteNotify()) : ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> - <label for="notify_customer" class="admin__field-label"><?= /* @escapeNotVerified */ __('Append Comments') ?></label> + <label for="notify_customer" class="admin__field-label"><?= $block->escapeHtml(__('Append Comments')) ?></label> </div> - <?php if ($block->canSendNewOrderConfirmationEmail()): ?> + <?php if ($block->canSendNewOrderConfirmationEmail()) : ?> <div class="admin__field admin__field-option field-email-order-confirmation"> <input type="checkbox" id="send_confirmation" name="order[send_confirmation]" value="1" checked="checked" class="admin__control-checkbox"/> - <label for="send_confirmation" class="admin__field-label"><?= /* @escapeNotVerified */ __('Email Order Confirmation') ?></label> + <label for="send_confirmation" class="admin__field-label"><?= $block->escapeHtml(__('Email Order Confirmation')) ?></label> </div> <?php endif; ?> <div class="actions"> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml index 9ab8a6552539d..4a55eb609924f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml @@ -3,19 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<tr class="<?= /* @escapeNotVerified */ $block->getTotal()->getCode() ?> row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> +<tr class="<?= $block->escapeHtmlAttr($block->getTotal()->getCode()) ?> row-totals"> + <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValue()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> + <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> + <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml index 7486edd732706..4c4f94b5b3bb1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml @@ -4,38 +4,36 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\Tax\Block\Checkout\Grandtotal * @see \Magento\Tax\Block\Checkout\Grandtotal */ ?> -<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0):?> -<tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <strong><?= /* @escapeNotVerified */ __('Grand Total Excl. Tax') ?></strong> - </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <strong><?= /* @escapeNotVerified */ $block->formatPrice($block->getTotalExclTax()) ?></strong> - </td> -</tr> -<?= /* @escapeNotVerified */ $block->renderTotals('taxes', $block->getColspan()) ?> -<tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <strong><?= /* @escapeNotVerified */ __('Grand Total Incl. Tax') ?></strong> - </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <strong><?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> - </td> -</tr> -<?php else:?> -<tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <strong><?= /* @escapeNotVerified */ $block->getTotal()->getTitle() ?></strong> - </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <strong><?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> - </td> -</tr> +<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0) : ?> + <tr class="row-totals"> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <strong><?= $block->escapeHtml(__('Grand Total Excl. Tax')) ?></strong> + </td> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getTotalExclTax()) ?></strong> + </td> + </tr> + <?= /* @noEscape */ $block->renderTotals('taxes', $block->getColspan()) ?> + <tr class="row-totals"> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <strong><?= $block->escapeHtml(__('Grand Total Incl. Tax')) ?></strong> + </td> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> + </td> + </tr> + <?php else : ?> + <tr class="row-totals"> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <strong><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></strong> + </td> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> + </td> + </tr> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml index 2235a428e1e75..db204a46f1f94 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml @@ -4,46 +4,44 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\Tax\Block\Checkout\Shipping * @see \Magento\Tax\Block\Checkout\Shipping */ ?> -<?php if ($block->displayBoth()):?> +<?php if ($block->displayBoth()) :?> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ $block->getExcludeTaxLabel() ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml($block->getExcludeTaxLabel()) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getShippingExcludeTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ $block->getIncludeTaxLabel() ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml($block->getIncludeTaxLabel()) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getShippingIncludeTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> <?php elseif ($block->displayIncludeTax()) : ?> <tr> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ $block->getTotal()->getTitle() ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getShippingIncludeTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> -<?php else:?> +<?php else : ?> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getShippingExcludeTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml index 380a7dc6fcbf9..a63458491baea 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml @@ -4,37 +4,35 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal * @see \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal */ ?> -<?php if ($block->displayBoth()):?> +<?php if ($block->displayBoth()) : ?> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ __('Subtotal (Excl. Tax)') ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml(__('Subtotal (Excl. Tax)')) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValueExclTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueExclTax()) ?> </td> </tr> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ __('Subtotal (Incl. Tax)') ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml(__('Subtotal (Incl. Tax)')) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValueInclTax()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueInclTax()) ?> </td> </tr> <?php else : ?> <tr class="row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?= /* @escapeNotVerified */ $block->getTotal()->getTitle() ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= /* @escapeNotVerified */ $block->getStyle() ?>" class="admin__total-amount"> - <?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValue()) ?> + <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 92139896273da..042b2f5113cac 100755 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -4,56 +4,59 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Squiz.PHP.GlobalKeyword.NotAllowed + +/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax $block */ $taxAmount = $block->getTotal()->getValue(); ?> -<?php if (($taxAmount == 0 && $this->helper('Magento\Tax\Helper\Data')->displayZeroTax()) || ($taxAmount > 0)): ?> -<?php global $taxIter; $taxIter++; ?> -<?php $class = "{$block->getTotal()->getCode()} " . ($this->helper('Magento\Tax\Helper\Data')->displayFullSummary() ? 'summary-total' : ''); ?> -<tr<?php if ($this->helper('Magento\Tax\Helper\Data')->displayFullSummary()): ?> - onclick="expandDetails(this, '.summary-details-<?= /* @escapeNotVerified */ $taxIter ?>')" -<?php endif; ?> - class="<?= /* @escapeNotVerified */ $class ?> row-totals"> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-mark" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayFullSummary()): ?> - <div class="summary-collapse"><?= /* @escapeNotVerified */ $block->getTotal()->getTitle() ?></div> - <?php else: ?> - <?= /* @escapeNotVerified */ $block->getTotal()->getTitle() ?> - <?php endif;?> - </td> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount"><?= /* @escapeNotVerified */ $block->formatPrice($block->getTotal()->getValue()) ?></td> -</tr> -<?php if ($this->helper('Magento\Tax\Helper\Data')->displayFullSummary()): ?> +<?php if (($taxAmount == 0 && $this->helper(\Magento\Tax\Helper\Data::class)->displayZeroTax()) || ($taxAmount > 0)) : + global $taxIter; + $taxIter++; + ?> + <?php $class = $block->escapeHtmlAttr("{$block->getTotal()->getCode()} " . ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() ? 'summary-total' : '')); ?> + <tr<?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + onclick="expandDetails(this, '.summary-details-<?= $block->escapeJs($taxIter) ?>')" + <?php endif; ?> + class="<?= /* @noEscape */ $class ?> row-totals"> + <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <div class="summary-collapse"><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></div> + <?php else : ?> + <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> + <?php endif;?> + </td> + <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> + </td> + </tr> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> <?php $isTop = 1; ?> - <?php foreach ($block->getTotal()->getFullInfo() as $info): ?> - <?php if (isset($info['hidden']) && $info['hidden']) { - continue; - } ?> - <?php $percent = $info['percent']; ?> - <?php $amount = $info['amount']; ?> - <?php $rates = $info['rates']; ?> - <?php $isFirst = 1; ?> + <?php foreach ($block->getTotal()->getFullInfo() as $info) : ?> + <?php if (isset($info['hidden']) && $info['hidden']) : + continue; + endif; ?> + <?php $percent = $info['percent']; ?> + <?php $amount = $info['amount']; ?> + <?php $rates = $info['rates']; ?> - <?php foreach ($rates as $rate): ?> - <tr class="summary-details-<?= /* @escapeNotVerified */ $taxIter ?> summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> - <td class="admin__total-mark" style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" colspan="<?= /* @escapeNotVerified */ $block->getColspan() ?>"> + <?php foreach ($rates as $rate) : ?> + <tr class="summary-details-<?= $block->escapeHtmlAttr($taxIter) ?> summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> + <td class="admin__total-mark" style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($rate['title']) ?> - <?php if (!is_null($rate['percent'])): ?> - (<?= (float)$rate['percent'] ?>%) + <?php if ($rate['percent'] !== null) : ?> + (<?= (float) $rate['percent'] ?>%) <?php endif; ?> <br /> </td> - <?php if ($isFirst): ?> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount" rowspan="<?= count($rates) ?>"> - <?= /* @escapeNotVerified */ $block->formatPrice($amount) ?> - </td> - <?php endif; ?> + <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <?= /* @noEscape */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> + </td> </tr> - <?php $isFirst = 0; ?> <?php $isTop = 0; ?> - <?php endforeach; ?> + <?php endforeach; ?> <?php endforeach; ?> -<?php endif;?> + <?php endif;?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/form.phtml index 2f32483e11a50..050098078b3f3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/form.phtml @@ -4,10 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Form $block */ ?> -<form id="edit_form" method="post" action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>"> +<form id="edit_form" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> <?php $_order = $block->getCreditmemo()->getOrder() ?> @@ -15,23 +16,23 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="admin__page-section-item order-payment-method"> - <?php else: ?> + <?php else : ?> <div class="admin__page-section-item order-payment-method order-payment-method-virtual"> <?php endif; ?> <?php /* Billing Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> <div class="order-payment-currency"> - <?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> </div> <div class="order-payment-additional"> <?= $block->getChildHtml('order_payment_additional') ?> @@ -39,27 +40,27 @@ </div> </div> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="admin__page-section-item order-shipping-address"> <?php /* Shipping Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping Information')) ?></span> </div> <div class="admin__page-section-item-content shipping-description-wrapper"> <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> <div class="shipping-description-content"> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displaySalesPriceInclTax($block->getSource()->getStoreId())): ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displaySalesPriceInclTax($block->getSource()->getStoreId())) : ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> + <?php else : ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displaySalesBothPrices($block->getSource()->getStoreId()) && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displaySalesBothPrices($block->getSource()->getStoreId()) && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index fcf4ccad7060b..893634635f32b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items $block */ ?> <?php $_items = $block->getCreditmemo()->getAllItems() ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items to Refund') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items to Refund')) ?></span> </div> <?php if (count($_items)) : ?> @@ -19,20 +18,20 @@ <table class="data-table admin__table-primary order-creditmemo-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-ordered-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-ordered-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> <?php if ($block->canReturnToStock()) : ?> - <th class="col-return-to-stock"><span><?= /* @escapeNotVerified */ __('Return to Stock') ?></span></th> + <th class="col-return-to-stock"><span><?= $block->escapeHtml(__('Return to Stock')) ?></span></th> <?php endif; ?> - <th class="col-refund"><span><?= /* @escapeNotVerified */ __('Qty to Refund') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-tax-amount"><span><?= /* @escapeNotVerified */ __('Tax Amount') ?></span></th> - <th class="col-discont"><span><?= /* @escapeNotVerified */ __('Discount Amount') ?></span></th> - <th class="col-total last"><span><?= /* @escapeNotVerified */ __('Row Total') ?></span></th> + <th class="col-refund"><span><?= $block->escapeHtml(__('Qty to Refund')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-tax-amount"><span><?= $block->escapeHtml(__('Tax Amount')) ?></span></th> + <th class="col-discont"><span><?= $block->escapeHtml(__('Discount Amount')) ?></span></th> + <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> - <?php if ($block->canEditQty()): ?> + <?php if ($block->canEditQty()) : ?> <tfoot> <tr> <td colspan="3"> </td> @@ -43,13 +42,13 @@ </tr> </tfoot> <?php endif; ?> - <?php $i = 0; foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; - } else { - $i++; - } ?> - <tbody class="<?= /* @escapeNotVerified */ $i%2 ? 'even' : 'odd' ?>"> + <?php $i = 0; foreach ($_items as $_item) : ?> + <?php if ($_item->getOrderItem()->getParentItem()) : + continue; + else : + $i++; + endif; ?> + <tbody class="<?= /* @noEscape */ $i%2 ? 'even' : 'odd' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> </tbody> @@ -58,49 +57,50 @@ </div> <?php else : ?> <div class="no-items"> - <?= /* @escapeNotVerified */ __('No Items To Refund') ?> + <?= $block->escapeHtml(__('No Items To Refund')) ?> </div> <?php endif; ?> </section> <?php $orderTotalBar = $block->getChildHtml('order_totalbar'); ?> -<?php if (!empty($orderTotalBar)): ?> +<?php if (!empty($orderTotalBar)) : ?> <section class="fieldset-wrapper"> - <?= /* @escapeNotVerified */ $orderTotalBar ?> + <?= /* @noEscape */ $orderTotalBar ?> </section> <?php endif; ?> <section class="admin__page-section"> <input type="hidden" name="creditmemo[do_offline]" id="creditmemo_do_offline" value="0" /> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Credit Memo Comments') ?></span> + <span class="title"><?= $block->escapeHtml(__('Credit Memo Comments')) ?></span> </div> <div id="history_form" class="admin__fieldset-wrapper-content"> <div class="admin__field"> <label class="normal admin__field-label" for="creditmemo_comment_text"> - <span><?= /* @escapeNotVerified */ __('Comment Text') ?></span></label> + <span><?= $block->escapeHtml(__('Comment Text')) ?></span></label> <div class="admin__field-control"> <textarea id="creditmemo_comment_text" class="admin__control-textarea" name="creditmemo[comment_text]" rows="3" - cols="5"><?= /* @escapeNotVerified */ $block->getCreditmemo()->getCommentText() ?></textarea> + cols="5"><?= $block->escapeHtml($block->getCreditmemo()->getCommentText()) ?></textarea> </div> </div> </div> </div> <div class="admin__page-section-item order-totals creditmemo-totals"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Refund Totals') ?></span> + <span class="title"><?= $block->escapeHtml(__('Refund Totals')) ?></span> </div> <?= $block->getChildHtml('creditmemo_totals') ?> + <div class="totals-actions"><?= $block->getUpdateTotalsButtonHtml() ?></div> <div class="order-totals-actions"> <div class="field choice admin__field admin__field-option field-append-comments"> <input id="notify_customer" @@ -109,10 +109,10 @@ value="1" type="checkbox" /> <label for="notify_customer" class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Append Comments') ?></span> + <span><?= $block->escapeHtml(__('Append Comments')) ?></span> </label> </div> - <?php if ($block->canSendCreditmemoEmail()):?> + <?php if ($block->canSendCreditmemoEmail()) :?> <div class="field choice admin__field admin__field-option field-email-copy"> <input id="send_email" class="admin__control-checkbox" @@ -120,7 +120,7 @@ value="1" type="checkbox" /> <label for="send_email" class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Email Copy of Credit Memo') ?></span> + <span><?= $block->escapeHtml(__('Email Copy of Credit Memo')) ?></span> </label> </div> <?php endif; ?> @@ -136,67 +136,76 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); -var fields = $$('.qty-input'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button,.update-totals-button'); +var fields = jQuery('.qty-input,.order-subtotal-table input[type="text"]'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +}; -for(var i=0;i<fields.length;i++){ - fields[i].observe('change', checkButtonsRelation) - fields[i].baseValue = fields[i].value; -} +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); +}; + +disableButtons(updateButtons); + +fields.on('change', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + enableButtons(submitButtons); + disableButtons(updateButtons); } } submitCreditMemo = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=0; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 0); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); } submitCreditMemoOffline = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=1; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 1); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); } -var sendEmailCheckbox = $('send_email'); - -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var creditmemoCommentText = $('creditmemo_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } -function bindSendEmail() -{ - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //creditmemoCommentText.disabled = false; +function bindSendEmail() { + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //creditmemoCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items/renderer/default.phtml index b27a07c4bf824..65c28578b68b3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items/renderer/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> @@ -21,8 +18,8 @@ <?php if ($block->canReturnItemToStock($_item)) : ?> <input type="checkbox" class="admin__control-checkbox" - name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][back_to_stock]" - value="1"<?php if ($_item->getBackToStock()):?> checked<?php endif;?>/> + name="creditmemo[items][<?= (int) $_item->getOrderItemId() ?>][back_to_stock]" + value="1"<?php if ($_item->getBackToStock()) : ?> checked<?php endif; ?>/> <label class="admin__field-label"></label> <?php endif; ?> </td> @@ -31,17 +28,17 @@ <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" - name="creditmemo[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>][qty]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>"/> + name="creditmemo[items][<?= (int) $_item->getOrderItemId() ?>][qty]" + value="<?= (int) $_item->getQty() ?>"/> <?php else : ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> + <?= (int) $_item->getQty() ?> <?php endif; ?> </td> <td class="col-subtotal"> <?= $block->getColumnHtml($_item, 'subtotal') ?> </td> - <td class="col-tax-amount"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?></td> - <td class="col-discont"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?></td> + <td class="col-tax-amount"><?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?></td> + <td class="col-discont"><?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?></td> <td class="col-total last"> <?= $block->getColumnHtml($_item, 'total') ?> </td> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml index 6401c3207ce40..fd2d737251c79 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml @@ -4,37 +4,36 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Adjustments $block */ ?> <?php $_source = $block->getSource() ?> -<?php if ($_source): ?> +<?php if ($_source) : ?> <tr> - <td class="label"><?= /* @escapeNotVerified */ $block->getShippingLabel() ?><div id="shipping_amount_adv"></div></td> + <td class="label"><?= $block->escapeHtml($block->getShippingLabel()) ?><div id="shipping_amount_adv"></div></td> <td> <input type="text" name="creditmemo[shipping_amount]" - value="<?= /* @escapeNotVerified */ $block->getShippingAmount() ?>" + value="<?= $block->escapeHtmlAttr($block->getShippingAmount()) ?>" class="input-text admin__control-text not-negative-amount" id="shipping_amount" /> </td> </tr> <tr> - <td class="label"><?= /* @escapeNotVerified */ __('Adjustment Refund') ?><div id="adjustment_positive_adv"></div></td> + <td class="label"><?= $block->escapeHtml(__('Adjustment Refund')) ?><div id="adjustment_positive_adv"></div></td> <td> <input type="text" name="creditmemo[adjustment_positive]" - value="<?= /* @escapeNotVerified */ $_source->getBaseAdjustmentFeePositive()*1 ?>" + value="<?= $block->escapeHtmlAttr($_source->getBaseAdjustmentPositive()) ?>" class="input-text admin__control-text not-negative-amount" id="adjustment_positive" /> </td> </tr> <tr> - <td class="label"><?= /* @escapeNotVerified */ __('Adjustment Fee') ?><div id="adjustment_negative_adv"></div></td> + <td class="label"><?= $block->escapeHtml(__('Adjustment Fee')) ?><div id="adjustment_negative_adv"></div></td> <td> <input type="text" name="creditmemo[adjustment_negative]" - value="<?= /* @escapeNotVerified */ $_source->getBaseAdjustmentFeeNegative()*1 ?>" + value="<?= $block->escapeHtmlAttr($_source->getBaseAdjustmentNegative()) ?>" class="input-text admin__control-text not-negative-amount" id="adjustment_negative"/> <script> @@ -42,7 +41,7 @@ //<![CDATA[ Validation.addAllThese([ - ['not-negative-amount', '<?= /* @escapeNotVerified */ __('Please enter a positive number in this field.') ?>', function(v) { + ['not-negative-amount', '<?= $block->escapeJs(__('Please enter a positive number in this field.')) ?>', function(v) { if(v.length) return /^\s*\d+([,.]\d+)*\s*%?\s*$/.test(v); else @@ -69,6 +68,9 @@ enableElements('submit-button'); } }); + $(id).observe('change', function (event) { + enableElements('submit-button'); + }); } //]]> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml index 7f5c54a3236e0..7e3dc7ea0be0e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml @@ -4,54 +4,55 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\View\Form $block */ ?> <?php $_order = $block->getCreditmemo()->getOrder() ?> <?= $block->getChildHtml('order_info') ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="admin__page-section-item order-payment-method"> - <?php else: ?> + <?php else : ?> <div class="admin__page-section-item order-payment-method order-payment-method-virtual"> <?php endif; ?> <?php /* Billing Address */?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> - <div class="order-payment-currency"><?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?></div> + <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> <div class="order-payment-additional"><?= $block->getChildHtml('order_payment_additional') ?></div> </div> </div> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="admin__page-section-item order-shipping-address"> <?php /* Shipping Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping Information')) ?></span> </div> <div class="shipping-description-wrapper admin__page-section-item-content"> <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> <div class="shipping-description-content"> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> + <?php else : ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </div> </div> @@ -61,33 +62,33 @@ </section> <?php $_items = $block->getCreditmemo()->getAllItems() ?> -<?php if (count($_items)): ?> +<?php if (count($_items)) : ?> <div id="creditmemo_items_container"> <?= $block->getChildHtml('creditmemo_items') ?> </div> -<?php else: ?> +<?php else : ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items Refunded') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items Refunded')) ?></span> </div> - <div class="no-items admin__page-section-content"><?= /* @escapeNotVerified */ __('No Items') ?></div> + <div class="no-items admin__page-section-content"><?= $block->escapeHtml(__('No Items')) ?></div> </section> <?php endif; ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Memo Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Memo Total')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Credit Memo History') ?></span> + <span class="title"><?= $block->escapeHtml(__('Credit Memo History')) ?></span> </div> <div class="admin__page-section-item-content"><?= $block->getChildHtml('order_comments') ?></div> </div> <div class="admin__page-section-item order-totals" id="history_form"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Credit Memo Totals') ?></span> + <span class="title"><?= $block->escapeHtml(__('Credit Memo Totals')) ?></span> </div> <div class="admin__page-section-content"><?= $block->getChildHtml('creditmemo_totals') ?></div> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items.phtml index d77f2fbde22e4..1471197982898 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items.phtml @@ -4,35 +4,34 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\View\Items $block */ ?> <?php $_items = $block->getCreditmemo()->getAllItems() ?> <div class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items Refunded') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items Refunded')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-creditmemo-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-tax"><span><?= /* @escapeNotVerified */ __('Tax Amount') ?></span></th> - <th class="col-discount"><span><?= /* @escapeNotVerified */ __('Discount Amount') ?></span></th> - <th class="col-total last"><span><?= /* @escapeNotVerified */ __('Row Total') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-tax"><span><?= $block->escapeHtml(__('Tax Amount')) ?></span></th> + <th class="col-discount"><span><?= $block->escapeHtml(__('Discount Amount')) ?></span></th> + <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> - <?php $i = 0; foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { + <?php $i = 0; foreach ($_items as $_item) : ?> + <?php if ($_item->getOrderItem()->getParentItem()) : continue; - } else { + else : $i++; - } ?> - <tbody class="<?= /* @escapeNotVerified */ $i%2 ? 'even' : 'odd' ?>"> + endif; ?> + <tbody class="<?= /* @noEscape */ $i%2 ? 'even' : 'odd' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> </tbody> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items/renderer/default.phtml index d39d41fe9c392..80ad5302392ae 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/items/renderer/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> @@ -16,12 +13,12 @@ <td class="col-price"> <?= $block->getColumnHtml($_item, 'price') ?> </td> - <td class="col-qty"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></td> + <td class="col-qty"><?= (int) $_item->getQty() ?></td> <td class="col-subtotal"> <?= $block->getColumnHtml($_item, 'subtotal') ?> </td> - <td class="col-tax"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?></td> - <td class="col-discount"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?></td> + <td class="col-tax"><?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?></td> + <td class="col-discount"><?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?></td> <td class="col-total last"> <?= $block->getColumnHtml($_item, 'total') ?> </td> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml index 2372087de5d79..961baf5499652 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml @@ -4,18 +4,22 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /* store view name = $_order->getStore()->getName() web site name = $_order->getStore()->getWebsite()->getName() store name = $_order->getStore()->getGroup()->getName() */ + +/* @var \Magento\Sales\Block\Adminhtml\Order\Details $block */ ?> -<?php $_order = $block->getOrder() ?> +<?php +/* @var \Magento\Sales\Model\Order $_order */ +$_order = $block->getOrder() ?> <div> -<?= __('Customer Name: %1', $block->escapeHtml($_order->getCustomerFirstname() ? $_order->getCustomerName() : $_order->getBillingAddress()->getName())) ?><br /> -<?= __('Purchased From: %1', $block->escapeHtml($_order->getStore()->getGroup()->getName())) ?><br /> +<?= $block->escapeHtml(__('Customer Name: %1', $_order->getCustomerFirstname() ? $_order->getCustomerName() : $_order->getBillingAddress()->getName())) ?><br /> +<?= $block->escapeHtml(__('Purchased From: %1', $_order->getStore()->getGroup()->getName())) ?><br /> </div> <table cellpadding="0" border="0" width="100%" style="border:1px solid #bebcb7; background:#f8f7f5;"> <thead> @@ -27,58 +31,58 @@ store name = $_order->getStore()->getGroup()->getName() </thead> <tbody> -<?php $i = 0; foreach ($_order->getAllItems() as $_item): $i++ ?> +<?php $i = 0; foreach ($_order->getAllItems() as $_item) : $i++ ?> <tr <?= $i%2 ? 'bgcolor="#eeeded"' : '' ?>> <td align="left" valign="top" style="padding:3px 9px"><strong><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_item->getGiftMessageId())): ?> - <br /><strong><?= /* @escapeNotVerified */ __('Gift Message') ?></strong> - <br /><?= /* @escapeNotVerified */ __('From:') ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> - <br /><?= /* @escapeNotVerified */ __('To:') ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> - <br /><?= /* @escapeNotVerified */ __('Message:') ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> + <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_item->getGiftMessageId())) : ?> + <br /><strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> + <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> + <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> + <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> <?php endif; ?> </td> - <td align="center" valign="top" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></td> - <td align="right" valign="top" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_order->formatPrice($_item->getRowTotal()) ?></td> + <td align="center" valign="top" style="padding:3px 9px"><?= (int) $_item->getQtyOrdered() ?></td> + <td align="right" valign="top" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?></td> </tr> -<?php endforeach ?> +<?php endforeach; ?> </tbody> <tfoot> - <?php if ($_order->getGiftMessageId() && $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_order->getGiftMessageId())): ?> + <?php if ($_order->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_order->getGiftMessageId())) : ?> <tr> <td colspan="3" align="left" style="padding:3px 9px"> - <strong><?= /* @escapeNotVerified */ __('Gift Message') ?></strong> - <br /><?= /* @escapeNotVerified */ __('From:') ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> - <br /><?= /* @escapeNotVerified */ __('To:') ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> - <br /><?= /* @escapeNotVerified */ __('Message:') ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> + <strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> + <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> + <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> + <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> - <?php endif; ?> + <?php endif; ?> <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ __('Subtotal') ?></td> - <td align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getSubtotal()) ?></td> + <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Subtotal')) ?></td> + <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getSubtotal()) ?></td> </tr> - <?php if ($_order->getDiscountAmount() > 0): ?> + <?php if ($_order->getDiscountAmount() > 0) : ?> <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ (($_order->getCouponCode()) ? __('Discount (%1)', $_order->getCouponCode()) : __('Discount')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> + <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml((($_order->getCouponCode()) ? __('Discount (%1)', $_order->getCouponCode()) : __('Discount'))) ?></td> + <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> </tr> <?php endif; ?> <?php if ($_order->getShippingAmount() || $_order->getShippingDescription()) : ?> <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ __('Shipping & Handling') ?></td> - <td align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getShippingAmount()) ?></td> + <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> + <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getShippingAmount()) ?></td> </tr> <?php endif; ?> - <?php if ($_order->getTaxAmount() > 0): ?> + <?php if ($_order->getTaxAmount() > 0) : ?> <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ __('Tax') ?></td> - <td align="right" style="padding:3px 9px"><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getTaxAmount()) ?></td> + <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Tax')) ?></td> + <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getTaxAmount()) ?></td> </tr> <?php endif; ?> <tr bgcolor="#DEE5E8"> - <td colspan="2" align="right" style="padding:3px 9px"><strong><big><?= /* @escapeNotVerified */ __('Grand Total') ?></big></strong></td> - <td align="right" style="padding:6px 9px"><strong><big><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getGrandTotal()) ?></big></strong></td> + <td colspan="2" align="right" style="padding:3px 9px"><strong><big><?= $block->escapeHtml(__('Grand Total')) ?></big></strong></td> + <td align="right" style="padding:6px 9px"><strong><big><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></big></strong></td> </tr> </tfoot> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/giftoptions.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/giftoptions.phtml index acce4571be005..1010eb339e26d 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/giftoptions.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/giftoptions.phtml @@ -3,13 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($block->getChildHtml()): ?> +<?php if ($block->getChildHtml()) : ?> <section class="admin__page-section order-gift-options"> - <div class="admin__page-section-title"><strong class="title"><?= /* @escapeNotVerified */ __('Gift Options') ?></strong></div> + <div class="admin__page-section-title"><strong class="title"><?= $block->escapeHtml(__('Gift Options')) ?></strong></div> <?= $block->getChildHtml() ?> </section> -<?php endif ?> +<?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml index cabbb8df8573c..da9f0d273af24 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml @@ -4,68 +4,69 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Form $block */ ?> -<form id="edit_form" class="order-invoice-edit" method="post" action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>"> +<form id="edit_form" class="order-invoice-edit" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> <?php $_order = $block->getInvoice()->getOrder() ?> <?= $block->getChildHtml('order_info') ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()): ?> order-payment-method-virtual<?php endif; ?>"> + <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()) : ?> order-payment-method-virtual<?php endif; ?>"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> - <div class="order-payment-currency"><?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?></div> + <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> <div class="order-payment-additional"><?= $block->getChildHtml('order_payment_additional') ?></div> </div> </div> - <?php if (!$_order->getIsVirtual()): ?> - <div class="admin__page-section-item order-shipping-address"> - <?php /*Shipping Address */ ?> - <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping Information') ?></span> - </div> - <div class="admin__page-section-item-content"> - <div class="shipping-description-wrapper"> - <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> - <div class="shipping-description-content"> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?php if (!$_order->getIsVirtual()) : ?> + <div class="admin__page-section-item order-shipping-address"> + <?php /*Shipping Address */ ?> + <div class="admin__page-section-item-title"> + <span class="title"><?= $block->escapeHtml(__('Shipping Information')) ?></span> + </div> + <div class="admin__page-section-item-content"> + <div class="shipping-description-wrapper"> + <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> + <div class="shipping-description-content"> + <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> - <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> - <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> - <?php endif; ?> - <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> + <?php else : ?> + <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> + <?php endif; ?> + <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) - <?php endif; ?> + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) + <?php endif; ?> + </div> </div> - </div> - <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()): ?> - <div class="admin__field admin__field-option"> - <input type="checkbox" name="invoice[do_shipment]" id="invoice_do_shipment" value="1" - class="admin__control-checkbox" <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> - <label for="invoice_do_shipment" - class="admin__field-label"><span><?= /* @escapeNotVerified */ __('Create Shipment') ?></span></label> - </div> - <?php if ($block->hasInvoiceShipmentTypeMismatch()): ?> - <small><?= /* @escapeNotVerified */ __('Invoice and shipment types do not match for some items on this order. You can create a shipment only after creating the invoice.') ?></small> + <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()) : ?> + <div class="admin__field admin__field-option"> + <input type="checkbox" name="invoice[do_shipment]" id="invoice_do_shipment" value="1" + class="admin__control-checkbox" <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> + <label for="invoice_do_shipment" + class="admin__field-label"><span><?= $block->escapeHtml(__('Create Shipment')) ?></span></label> + </div> + <?php if ($block->hasInvoiceShipmentTypeMismatch()) : ?> + <small><?= $block->escapeHtml(__('Invoice and shipment types do not match for some items on this order. You can create a shipment only after creating the invoice.')) ?></small> + <?php endif; ?> <?php endif; ?> - <?php endif; ?> - <div id="tracking" style="display:none;"><?= $block->getChildHtml('tracking', false) ?></div> + <div id="tracking" style="display:none;"><?= $block->getChildHtml('tracking', false) ?></div> + </div> </div> - </div> <?php endif; ?> </div> </section> @@ -90,7 +91,7 @@ require(['prototype'], function(){ } /*forced creating of shipment*/ - var forcedShipmentCreate = <?= /* @escapeNotVerified */ $block->getForcedShipmentCreate() ?>; + var forcedShipmentCreate = <?= (int) $block->getForcedShipmentCreate() ?>; var shipmentElement = $('invoice_do_shipment'); if (forcedShipmentCreate && shipmentElement) { shipmentElement.checked = true; diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index fa5ea0568011b..c54efa43e47e6 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -3,48 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> - <section class="admin__page-section"> <div class="admin__page-section-title"> <?php $_itemsGridLabel = $block->getForcedShipmentCreate() ? 'Items to Invoice and Ship' : 'Items to Invoice'; ?> - <span class="title"><?= /* @escapeNotVerified */ __('%1', $_itemsGridLabel) ?></span> + <span class="title"><?= $block->escapeHtml(__('%1', $_itemsGridLabel)) ?></span> </div> <div class="admin__page-section-content grid"> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-invoice-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-ordered-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-qty-invoice"><span><?= /* @escapeNotVerified */ __('Qty to Invoice') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-tax"><span><?= /* @escapeNotVerified */ __('Tax Amount') ?></span></th> - <th class="col-discount"><span><?= /* @escapeNotVerified */ __('Discount Amount') ?></span></th> - <th class="col-total last"><span><?= /* @escapeNotVerified */ __('Row Total') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-ordered-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-qty-invoice"><span><?= $block->escapeHtml(__('Qty to Invoice')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-tax"><span><?= $block->escapeHtml(__('Tax Amount')) ?></span></th> + <th class="col-discount"><span><?= $block->escapeHtml(__('Discount Amount')) ?></span></th> + <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> - <?php if ($block->canEditQty()): ?> - <tfoot> - <tr> - <td colspan="2"> </td> - <td colspan="3"><?= $block->getUpdateButtonHtml() ?></td> - <td colspan="3"> </td> - </tr> - </tfoot> + <?php if ($block->canEditQty()) : ?> + <tfoot> + <tr> + <td colspan="3"> </td> + <td><?= $block->getUpdateButtonHtml() ?></td> + <td colspan="4"> </td> + </tr> + </tfoot> <?php endif; ?> <?php $_items = $block->getInvoice()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; - } else { - $_i++; - } ?> - <tbody class="<?= /* @escapeNotVerified */ $_i%2 ? 'even' : 'odd' ?>"> + <?php $_i = 0; foreach ($_items as $_item) : ?> + <?php if ($_item->getOrderItem()->getParentItem()) : + continue; + else : + $_i++; + endif; ?> + <tbody class="<?= /* @noEscape */ $_i%2 ? 'even' : 'odd' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> </tbody> @@ -56,29 +52,29 @@ <?php $orderTotalBar = $block->getChildHtml('order_totalbar'); ?> -<?php if (!empty($orderTotalBar)): ?> +<?php if (!empty($orderTotalBar)) : ?> <section class="admin__page-section"> - <?= /* @escapeNotVerified */ $orderTotalBar ?> + <?= /* @noEscape */ $orderTotalBar ?> </section> <?php endif; ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Invoice History') ?></span> + <span class="title"><?= $block->escapeHtml(__('Invoice History')) ?></span> </div> <div id="history_form" class="admin__page-section-item-content order-history-form"> <div class="admin__field"> <label for="invoice_comment_text" class="admin__field-label"> - <span><?= /* @escapeNotVerified */ __('Invoice Comments') ?></span> + <span><?= $block->escapeHtml(__('Invoice Comments')) ?></span> </label> <div class="admin__field-control"> <textarea id="invoice_comment_text" name="invoice[comment_text]" class="admin__control-textarea" - rows="3" cols="5"><?= /* @escapeNotVerified */ $block->getInvoice()->getCommentText() ?></textarea> + rows="3" cols="5"><?= $block->escapeHtml($block->getInvoice()->getCommentText()) ?></textarea> </div> </div> </div> @@ -86,41 +82,41 @@ <div id="invoice_totals" class="admin__page-section-item order-totals"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Invoice Totals') ?></span> + <span class="title"><?= $block->escapeHtml(__('Invoice Totals')) ?></span> </div> <div class="admin__page-section-item-content order-totals-actions"> <?= $block->getChildHtml('invoice_totals') ?> - <?php if ($block->isCaptureAllowed()): ?> - <?php if ($block->canCapture()):?> - <div class="admin__field"> - <?php - /* - <label for="invoice_do_capture" class="normal"><?= __('Capture Amount') ?></label> - <input type="checkbox" name="invoice[do_capture]" id="invoice_do_capture" value="1" checked/> - */ - ?> - <label for="invoice_do_capture" class="admin__field-label"><?= /* @escapeNotVerified */ __('Amount') ?></label> - <select class="admin__control-select" name="invoice[capture_case]"> - <option value="online"><?= /* @escapeNotVerified */ __('Capture Online') ?></option> - <option value="offline"><?= /* @escapeNotVerified */ __('Capture Offline') ?></option> - <option value="not_capture"><?= /* @escapeNotVerified */ __('Not Capture') ?></option> - </select> - </div> - <?php elseif ($block->isGatewayUsed()):?> - <input type="hidden" name="invoice[capture_case]" value="offline"/> - <div><?= /* @escapeNotVerified */ __('The invoice will be created offline without the payment gateway.') ?></div> - <?php endif?> + <?php if ($block->isCaptureAllowed()) : ?> + <?php if ($block->canCapture()) : ?> + <div class="admin__field"> + <?php + /* + <label for="invoice_do_capture" class="normal"><?= __('Capture Amount') ?></label> + <input type="checkbox" name="invoice[do_capture]" id="invoice_do_capture" value="1" checked/> + */ + ?> + <label for="invoice_do_capture" class="admin__field-label"><?= $block->escapeHtml(__('Amount')) ?></label> + <select class="admin__control-select" name="invoice[capture_case]"> + <option value="online"><?= $block->escapeHtml(__('Capture Online')) ?></option> + <option value="offline"><?= $block->escapeHtml(__('Capture Offline')) ?></option> + <option value="not_capture"><?= $block->escapeHtml(__('Not Capture')) ?></option> + </select> + </div> + <?php elseif ($block->isGatewayUsed()) :?> + <input type="hidden" name="invoice[capture_case]" value="offline"/> + <div><?= $block->escapeHtml(__('The invoice will be created offline without the payment gateway.')) ?></div> + <?php endif; ?> <?php endif; ?> <div class="admin__field admin__field-option field-append"> <input id="notify_customer" name="invoice[comment_customer_notify]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="notify_customer"><?= /* @escapeNotVerified */ __('Append Comments') ?></label> + <label class="admin__field-label" for="notify_customer"><?= $block->escapeHtml(__('Append Comments')) ?></label> </div> - <?php if ($block->canSendInvoiceEmail()): ?> + <?php if ($block->canSendInvoiceEmail()) : ?> <div class="admin__field admin__field-option field-email"> <input id="send_email" name="invoice[send_email]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="send_email"><?= /* @escapeNotVerified */ __('Email Copy of Invoice') ?></label> + <label class="admin__field-label" for="send_email"><?= $block->escapeHtml(__('Email Copy of Invoice')) ?></label> </div> <?php endif; ?> <?= $block->getChildHtml('submit_before') ?> @@ -134,56 +130,61 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; -var fields = $$('.qty-input'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +}; -for(var i=0;i<fields.length;i++){ - jQuery(fields[i]).on('keyup', checkButtonsRelation); - fields[i].baseValue = fields[i].value; -} +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); +}; + +disableButtons(updateButtons); + +fields.on('keyup', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { if (enableSubmitButtons) { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + enableButtons(submitButtons); } - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + disableButtons(updateButtons); } } -var sendEmailCheckbox = $('send_email'); -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var invoiceCommentText = $('invoice_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } function bindSendEmail() { - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //invoiceCommentText.disabled = false; + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //invoiceCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items/renderer/default.phtml index 5bfb5f130cedb..98ae143e30c0b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items/renderer/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> @@ -18,17 +15,17 @@ <td class="col-qty-invoice"> <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" - name="invoice[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>"/> + name="invoice[items][<?= (int) $_item->getOrderItemId() ?>]" + value="<?= (int) $_item->getQty() ?>"/> <?php else : ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> + <?= (int) $_item->getQty() ?> <?php endif; ?> </td> <td class="col-subtotal"> <?= $block->getColumnHtml($_item, 'subtotal') ?> </td> - <td class="col-tax"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?></td> - <td class="col-discount"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?></td> + <td class="col-tax"><?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?></td> + <td class="col-discount"><?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?></td> <td class="col-total last"> <?= $block->getColumnHtml($_item, 'total') ?> </td> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml index e86a87f089897..784d3f892f2c4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml @@ -4,8 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\View\Form $block */ ?> <?php $_invoice = $block->getInvoice() ?> <?php $_order = $_invoice->getOrder() ?> @@ -13,46 +14,46 @@ <section class="admin__page-section order-view-billing-shipping"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()): ?> order-payment-method-virtual<?php endif; ?> admin__fieldset-wrapper"> + <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()) : ?> order-payment-method-virtual<?php endif; ?> admin__fieldset-wrapper"> <?php /*Billing Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> <div class="order-payment-currency"> - <?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> </div> <div class="order-payment-additional"><?= $block->getChildHtml('order_payment_additional') ?></div> </div> </div> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="admin__page-section-item order-shipping-address"> <?php /*Shipping Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping Information')) ?></span> </div> <div class="admin__page-section-item-content shipping-description-wrapper"> <div class="shipping-description-title"> <?= $block->escapeHtml($_order->getShippingDescription()) ?> </div> <div class="shipping-description-content"> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> + <?php else : ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> <div><?= $block->getChildHtml('shipment_tracking') ?></div> </div> @@ -65,7 +66,7 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items Invoiced') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items Invoiced')) ?></span> </div> <div id="invoice_item_container" class="admin__page-section-content"> @@ -75,12 +76,12 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Invoice History') ?></span> + <span class="title"><?= $block->escapeHtml(__('Invoice History')) ?></span> </div> <div class="admin__page-section-item-content"> <?= $block->getChildHtml('order_comments') ?> @@ -89,7 +90,7 @@ <div id="history_form" class="admin__page-section-item order-totals"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Invoice Totals') ?></span> + <span class="title"><?= $block->escapeHtml(__('Invoice Totals')) ?></span> </div> <?= $block->getChildHtml('invoice_totals') ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items.phtml index 63f33dbb74bad..6dbfd19e9a2c7 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items.phtml @@ -4,30 +4,29 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\View\Items $block */ ?> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-invoice-tables"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-price"><span><?= /* @escapeNotVerified */ __('Price') ?></span></th> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-subtotal"><span><?= /* @escapeNotVerified */ __('Subtotal') ?></span></th> - <th class="col-tax"><span><?= /* @escapeNotVerified */ __('Tax Amount') ?></span></th> - <th class="col-discount"><span><?= /* @escapeNotVerified */ __('Discount Amount') ?></span></th> - <th class="col-total last"><span><?= /* @escapeNotVerified */ __('Row Total') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-price"><span><?= $block->escapeHtml(__('Price')) ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-subtotal"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> + <th class="col-tax"><span><?= $block->escapeHtml(__('Tax Amount')) ?></span></th> + <th class="col-discount"><span><?= $block->escapeHtml(__('Discount Amount')) ?></span></th> + <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> <?php $_items = $block->getInvoice()->getAllItems() ?> - <?php $i = 0; foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; - } else { - $i++; - } ?> - <tbody class="<?= /* @escapeNotVerified */ $i%2 ? 'even' : 'odd' ?>"> + <?php $i = 0; foreach ($_items as $_item) : ?> + <?php if ($_item->getOrderItem()->getParentItem()) : + continue; + else : + $i++; + endif; ?> + <tbody class="<?= /* @noEscape */ $i%2 ? 'even' : 'odd' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> </tbody> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items/renderer/default.phtml index af9e5b3bb702d..59301931c73c7 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/items/renderer/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> @@ -16,12 +13,12 @@ <td class="col-price"> <?= $block->getColumnHtml($_item, 'price') ?> </td> - <td class="col-qty"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></td> + <td class="col-qty"><?= (int) $_item->getQty() ?></td> <td class="col-subtotal"> <?= $block->getColumnHtml($_item, 'subtotal') ?> </td> - <td class="col-tax"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('tax_amount') ?></td> - <td class="col-discount"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?></td> + <td class="col-tax"><?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?></td> + <td class="col-discount"><?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?></td> <td class="col-total last"> <?= $block->getColumnHtml($_item, 'total') ?> </td> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml index b038c72199c7f..819718af2dde2 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// @deprecated +$totals = $block->getTotals(); ?> -<?php if (sizeof($block->getTotals()) > 0): ?> +<?php if (count($block->getTotals()) > 0) : ?> <table class="items-to-invoice"> <tr> - <?php foreach ($block->getTotals() as $_total): ?> - <td <?php if ($_total['grand']): ?>class="grand-total"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_total['label'] ?><br /> - <?= /* @escapeNotVerified */ $_total['value'] ?> + <?php foreach ($block->getTotals() as $_total) : ?> + <td <?php if ($_total['grand']) : ?>class="grand-total"<?php endif; ?>> + <?= $block->escapeHtml($_total['label']) ?><br /> + <?= $block->escapeHtml($_total['value']) ?> </td> <?php endforeach; ?> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals.phtml index d23ebac84e6c2..1f495b9c4a573 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals.phtml @@ -4,67 +4,58 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Order\Totals $block */ ?> -<?php /*$_source = $block->getSource(); ?> -<?php $block->setPriceDataObject($_source) ?> -<?php if ($_source): ?> -<table width="100%"> - <?= $block->getChildHtml('main') ?> - <?= $block->getChildHtml('footer') ?> -</table> -<?php endif;*/ ?> <table class="data-table admin__table-secondary order-subtotal-table"> - <?php $_totals = $block->getTotals('footer')?> + <?php $_totals = $block->getTotals('footer') ?> - <?php if ($_totals):?> + <?php if ($_totals) : ?> <tfoot> - <?php foreach ($block->getTotals('footer') as $_code => $_total): ?> - <?php if ($_total->getBlockName()): ?> + <?php foreach ($block->getTotals('footer') as $_code => $_total) : ?> + <?php if ($_total->getBlockName()) : ?> <?= $block->getChildHtml($_total->getBlockName(), false) ?> - <?php else:?> - <tr class="col-<?= /* @escapeNotVerified */ $_code ?>"> - <td <?= /* @escapeNotVerified */ $block->getLabelProperties() ?> class="label"> + <?php else : ?> + <tr class="col-<?= $block->escapeHtmlAttr($_code) ?>"> + <td <?= /* @noEscape */ $block->getLabelProperties() ?> class="label"> <strong><?= $block->escapeHtml($_total->getLabel()) ?></strong> </td> - <td <?= /* @escapeNotVerified */ $block->getValueProperties() ?>> - <strong><?= /* @escapeNotVerified */ $block->formatValue($_total) ?></strong> + <td <?= /* @noEscape */ $block->getValueProperties() ?>> + <strong><?= /* @noEscape */ $block->formatValue($_total) ?></strong> </td> </tr> - <?php endif?> - <?php endforeach?> + <?php endif; ?> + <?php endforeach; ?> </tfoot> - <?php endif?> + <?php endif; ?> <?php $_totals = $block->getTotals('')?> - <?php if ($_totals):?> + <?php if ($_totals) : ?> <tbody> - <?php foreach ($_totals as $_code => $_total): ?> - <?php if ($_total->getBlockName()): ?> + <?php foreach ($_totals as $_code => $_total) : ?> + <?php if ($_total->getBlockName()) : ?> <?= $block->getChildHtml($_total->getBlockName(), false) ?> - <?php else:?> - <tr class="col-<?= /* @escapeNotVerified */ $_code ?>"> - <td <?= /* @escapeNotVerified */ $block->getLabelProperties() ?> class="label"> - <?php if ($_total->getStrong()):?> + <?php else : ?> + <tr class="col-<?= $block->escapeHtmlAttr($_code) ?>"> + <td <?= /* @noEscape */ $block->getLabelProperties() ?> class="label"> + <?php if ($_total->getStrong()) : ?> <strong><?= $block->escapeHtml($_total->getLabel()) ?></strong> - <?php else:?> + <?php else : ?> <?= $block->escapeHtml($_total->getLabel()) ?> <?php endif?> </td> - <?php if ($_total->getStrong()):?> - <td <?= /* @escapeNotVerified */ $block->getValueProperties() ?>> - <strong><?= /* @escapeNotVerified */ $block->formatValue($_total) ?></strong> + <?php if ($_total->getStrong()) : ?> + <td <?= /* @noEscape */ $block->getValueProperties() ?>> + <strong><?= /* @noEscape */ $block->formatValue($_total) ?></strong> </td> - <?php else:?> - <td <?= /* @escapeNotVerified */ $block->getValueProperties() ?>> - <span><?= /* @escapeNotVerified */ $block->formatValue($_total) ?></span> + <?php else : ?> + <td <?= /* @noEscape */ $block->getValueProperties() ?>> + <span><?= /* @noEscape */ $block->formatValue($_total) ?></span> </td> - <?php endif?> + <?php endif; ?> </tr> - <?php endif?> - <?php endforeach?> + <?php endif; ?> + <?php endforeach; ?> </tbody> - <?php endif?> + <?php endif; ?> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/discount.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/discount.phtml index db2567a5f3dbf..9ff3a91eab7ce 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/discount.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/discount.phtml @@ -3,23 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_source = $block->getSource() ?> <?php $_order = $block->getOrder() ?> <?php $block->setPriceDataObject($_source) ?> -<?php if ((float) $_source->getDiscountAmount()): ?> +<?php if ((float) $_source->getDiscountAmount()) : ?> <tr> <td class="label"> - <?php if ($_order->getCouponCode()): ?> - <?= /* @escapeNotVerified */ __('Discount (%1)', $_order->getCouponCode()) ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('Discount') ?> + <?php if ($_order->getCouponCode()) : ?> + <?= $block->escapeHtml(__('Discount (%1)', $_order->getCouponCode())) ?> + <?php else : ?> + <?= $block->escapeHtml(__('Discount')) ?> <?php endif; ?> </td> - <td><?= /* @escapeNotVerified */ $block->displayPriceAttribute('discount_amount') ?></td> + <td><?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml index 81402e37288ef..87d7c85c2d9ed 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml @@ -3,13 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($block->getCanDisplayTotalDue()): ?> +<?php if ($block->getCanDisplayTotalDue()) : ?> <tr> - <td class="label"><big><strong><?= /* @escapeNotVerified */ __('Total Due') ?></strong></big></td> - <td class="emph"><big><?= /* @escapeNotVerified */ $block->displayPriceAttribute('total_due', true) ?></big></td> + <td class="label"><big><strong><?= $block->escapeHtml(__('Total Due')) ?></strong></big></td> + <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></big></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml index 519aa660fbf12..dc76799251c7a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_source = $block->getSource() ?> <?php $block->setPriceDataObject($_source) ?> @@ -13,12 +10,12 @@ <tr> <td class="label"> <strong><big> - <?php if ($block->getGrandTotalTitle()): ?> - <?= /* @escapeNotVerified */ $block->getGrandTotalTitle() ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('Grand Total') ?> + <?php if ($block->getGrandTotalTitle()) : ?> + <?= $block->escapeHtml($block->getGrandTotalTitle()) ?> + <?php else : ?> + <?= $block->escapeHtml(__('Grand Total')) ?> <?php endif; ?> </big></strong> </td> - <td class="emph"><big><?= /* @escapeNotVerified */ $block->displayPriceAttribute('grand_total', true) ?></big></td> + <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></big></td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/item.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/item.phtml index 6c44e6ec632a7..f3590654181c2 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/item.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/item.phtml @@ -3,16 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php $_source = $block->getSource() ?> +<?php $_source = $block->getSource() ?> <?php $block->setPriceDataObject($_source) ?> -<?php if ((float) $_source->getDataUsingMethod($block->getSourceField())): ?> +<?php if ((float) $_source->getDataUsingMethod($block->getSourceField())) : ?> <tr> - <td class="label"><?php if ($block->getStrong()) : ?><strong><?php endif; ?><?= /* @escapeNotVerified */ __($block->getLabel()) ?><?php if ($block->getStrong()) : ?></strong><?php endif; ?></td> - <td <?= $block->getHtmlClass() ? ('class="' . $block->getHtmlClass() . '"') : '' ?>><?php if ($block->getStrong()) : ?><strong><?php endif; ?><?= /* @escapeNotVerified */ $block->displayPriceAttribute($block->getSourceField()) ?><?php if ($block->getStrong()) : ?></strong><?php endif; ?></td> + <td class="label"> + <?php if ($block->getStrong()) : ?> + <strong> + <?php endif; ?> + <?= $block->escapeHtml(__($block->getLabel())) ?> + <?php if ($block->getStrong()) : ?> + </strong> + <?php endif; ?> + </td> + <td <?= $block->getHtmlClass() ? ('class="' . $block->escapeHtmlAttr($block->getHtmlClass()) . '"') : '' ?>> + <?php if ($block->getStrong()) : ?><strong><?php endif; ?> + <?= /* @noEscape */ $block->displayPriceAttribute($block->getSourceField()) ?> + <?php if ($block->getStrong()) : ?></strong><?php endif; ?> + </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/paid.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/paid.phtml index 8befc7cc0ccb4..9ede6e21e78af 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/paid.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/paid.phtml @@ -3,14 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($block->getCanDisplayTotalPaid()): ?> +<?php if ($block->getCanDisplayTotalPaid()) : ?> <tr> - <td class="label"><strong><?= /* @escapeNotVerified */ __('Total Paid') ?></strong></td> - <td class="emph"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('total_paid', true) ?> + <td class="label"><strong><?= $block->escapeHtml(__('Total Paid')) ?></strong></td> + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('total_paid', true) ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/refunded.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/refunded.phtml index e579578f711ae..1e7087c7222c3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/refunded.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/refunded.phtml @@ -3,13 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if ($block->getCanDisplayTotalRefunded()): ?> +<?php if ($block->getCanDisplayTotalRefunded()) : ?> <tr> - <td class="label"><strong><?= /* @escapeNotVerified */ __('Total Refunded') ?></strong></td> - <td class="emph"><?= /* @escapeNotVerified */ $block->displayPriceAttribute('total_refunded', true) ?></td> + <td class="label"><strong><?= $block->escapeHtml(__('Total Refunded')) ?></strong></td> + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('total_refunded', true) ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/shipping.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/shipping.phtml index 47304bf7f4a7e..4250dc1d047ba 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/shipping.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/shipping.phtml @@ -3,16 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_source = $block->getSource() ?> <?php $block->setPriceDataObject($_source) ?> -<?php if ((float) $_source->getShippingAmount() || $_source->getShippingDescription()): ?> +<?php if ((float) $_source->getShippingAmount() || $_source->getShippingDescription()) : ?> <tr> - <td class="label"><?= /* @escapeNotVerified */ __('Shipping & Handling') ?></td> - <td><?= /* @escapeNotVerified */ $block->displayPriceAttribute('shipping_amount') ?></td> + <td class="label"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> + <td><?= /* @noEscape */ $block->displayPriceAttribute('shipping_amount') ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml index 284597027468d..a68fb09fd2058 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml @@ -4,42 +4,41 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Sales\Block\Adminhtml\Order\Totals\Tax */ -?> -<?php + /** @var $_source \Magento\Sales\Model\Order\Invoice */ $_source = $block->getSource(); $_order = $block->getOrder(); $_fullInfo = $block->getFullTaxInfo(); ?> -<?php if ($block->displayFullSummary() && $_fullInfo): ?> +<?php if ($block->displayFullSummary() && $_fullInfo) : ?> <tr class="summary-total" onclick="expandDetails(this, '.summary-details')"> -<?php else: ?> +<?php else : ?> <tr> <?php endif; ?> <td class="label"> <div class="summary-collapse" tabindex="0"> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayFullSummary()): ?> - <?= /* @escapeNotVerified */ __('Total Tax') ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('Tax') ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <?= $block->escapeHtml(__('Total Tax')) ?> + <?php else : ?> + <?= $block->escapeHtml(__('Tax')) ?> <?php endif;?> </div> </td> <td> - <?= /* @escapeNotVerified */ $block->displayAmount($_source->getTaxAmount(), $_source->getBaseTaxAmount()) ?> + <?= /* @noEscape */ $block->displayAmount($_source->getTaxAmount(), $_source->getBaseTaxAmount()) ?> </td> </tr> -<?php if ($block->displayFullSummary()): ?> +<?php if ($block->displayFullSummary()) : ?> <?php $isTop = 1; ?> - <?php if (isset($_fullInfo[0]['rates'])): ?> - <?php foreach ($_fullInfo as $info): ?> - <?php if (isset($info['hidden']) && $info['hidden']) { + <?php if (isset($_fullInfo[0]['rates'])) : ?> + <?php foreach ($_fullInfo as $info) : ?> + <?php if (isset($info['hidden']) && $info['hidden']) : continue; - } ?> + endif; ?> <?php $percent = $info['percent']; $amount = $info['amount']; @@ -48,39 +47,38 @@ $_fullInfo = $block->getFullTaxInfo(); $isFirst = 1; ?> - <?php foreach ($rates as $rate): ?> - <tr class="summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> - <?php if (!is_null($rate['percent'])): ?> - <td class="admin__total-mark"><?= /* @escapeNotVerified */ $rate['title'] ?> (<?= (float)$rate['percent'] ?>%)<br /></td> - <?php else: ?> - <td class="admin__total-mark"><?= /* @escapeNotVerified */ $rate['title'] ?><br /></td> - <?php endif; ?> - <?php if ($isFirst): ?> - <td rowspan="<?= count($rates) ?>"><?= /* @escapeNotVerified */ $block->displayAmount($amount, $baseAmount) ?></td> - <?php endif; ?> - </tr> - <?php + <?php foreach ($rates as $rate) : ?> + <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> + <?php if ($rate['percent'] !== null) : ?> + <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?> (<?= (float)$rate['percent'] ?>%)<br /></td> + <?php else : ?> + <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?><br /></td> + <?php endif; ?> + <?php if ($isFirst) : ?> + <td rowspan="<?= count($rates) ?>"><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> + <?php endif; ?> + </tr> + <?php $isFirst = 0; $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> - <?php else: ?> - <?php foreach ($_fullInfo as $info): ?> + <?php else : ?> + <?php foreach ($_fullInfo as $info) : ?> <?php $percent = $info['percent']; $amount = $info['tax_amount']; $baseAmount = $info['base_tax_amount']; $isFirst = 1; ?> - - <tr class="summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> - <?php if (!is_null($info['percent'])): ?> + <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> + <?php if ($info['percent'] !== null) : ?> <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?> (<?= (float)$info['percent'] ?>%)<br /></td> - <?php else: ?> + <?php else : ?> <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?><br /></td> <?php endif; ?> - <td><?= /* @escapeNotVerified */ $block->displayAmount($amount, $baseAmount) ?></td> + <td><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> </tr> <?php $isFirst = 0; diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/giftmessage.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/giftmessage.phtml index 393af3e40d6eb..bf80f5df00b5e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/giftmessage.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/giftmessage.phtml @@ -4,61 +4,60 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Sales\Block\Adminhtml\Order\View\Giftmessage */ ?> -<?php if ($block->canDisplayGiftmessage()): ?> -<?php $_required = $block->getMessage()->getMessage() != ''?> -<div id="<?= $block->getHtmlId() ?>" class="admin__page-section-content giftmessage-whole-order-container"> - <form class="entry-edit form-inline" id="<?= /* @escapeNotVerified */ $block->getFieldId('form') ?>" action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>"> - <fieldset class="admin__fieldset"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Gift Message for the Entire Order') ?></span></legend> - <br/> - - <input type="hidden" id="<?= /* @escapeNotVerified */ $block->getFieldId('type') ?>" - name="<?= /* @escapeNotVerified */ $block->getFieldName('type') ?>" - value="order"/> - - <div class="admin__field field-from-name<?= $_required ? ' required' : '' ?>"> - <label class="admin__field-label" - for="<?= /* @escapeNotVerified */ $block->getFieldId('sender') ?>"><span><?= /* @escapeNotVerified */ __('From Name') ?></span></label> - - <div class="admin__field-control"> - <input class="admin__control-text <?= $_required ? 'required-entry' : '' ?>" type="text" - id="<?= /* @escapeNotVerified */ $block->getFieldId('sender') ?>" - name="<?= /* @escapeNotVerified */ $block->getFieldName('sender') ?>" - value="<?= $block->escapeHtml($block->getMessage()->getSender()) ?>"/> +<?php if ($block->canDisplayGiftmessage()) : ?> + <?php $_required = $block->getMessage()->getMessage() != '' ?> + <div id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>" class="admin__page-section-content giftmessage-whole-order-container"> + <form class="entry-edit form-inline" id="<?= $block->escapeHtmlAttr($block->getFieldId('form')) ?>" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> + <fieldset class="admin__fieldset"> + <legend class="admin__legend"><span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span></legend> + <br/> + + <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId('type')) ?>" + name="<?= $block->escapeHtmlAttr($block->getFieldName('type')) ?>" + value="order"/> + + <div class="admin__field field-from-name<?= $_required ? ' required' : '' ?>"> + <label class="admin__field-label" + for="<?= $block->escapeHtmlAttr($block->getFieldId('sender')) ?>"><span><?= $block->escapeHtml(__('From Name')) ?></span></label> + + <div class="admin__field-control"> + <input class="admin__control-text <?= $_required ? 'required-entry' : '' ?>" type="text" + id="<?= $block->escapeHtmlAttr($block->getFieldId('sender')) ?>" + name="<?= $block->escapeHtmlAttr($block->getFieldName('sender')) ?>" + value="<?= $block->escapeHtml($block->getMessage()->getSender()) ?>"/> + </div> </div> - </div> - <div class="admin__field field-to-name<?= $_required ? ' required' : '' ?>"> - <label class="admin__field-label" - for="<?= /* @escapeNotVerified */ $block->getFieldId('recipient') ?>"><span><?= /* @escapeNotVerified */ __('To Name') ?></span></label> + <div class="admin__field field-to-name<?= $_required ? ' required' : '' ?>"> + <label class="admin__field-label" + for="<?= $block->escapeHtmlAttr($block->getFieldId('recipient')) ?>"><span><?= $block->escapeHtml(__('To Name')) ?></span></label> - <div class="admin__field-control"> - <input class="admin__control-text <?= $_required ? 'required-entry' : '' ?>" type="text" - id="<?= /* @escapeNotVerified */ $block->getFieldId('recipient') ?>" - name="<?= /* @escapeNotVerified */ $block->getFieldName('recipient') ?>" - value="<?= $block->escapeHtml($block->getMessage()->getRecipient()) ?>"/> + <div class="admin__field-control"> + <input class="admin__control-text <?= $_required ? 'required-entry' : '' ?>" type="text" + id="<?= $block->escapeHtmlAttr($block->getFieldId('recipient')) ?>" + name="<?= $block->escapeHtmlAttr($block->getFieldName('recipient')) ?>" + value="<?= $block->escapeHtml($block->getMessage()->getRecipient()) ?>"/> + </div> </div> - </div> - <div class="admin__field field-gift-message"> - <label class="admin__field-label" - for="<?= /* @escapeNotVerified */ $block->getFieldId('message') ?>"><span><?= /* @escapeNotVerified */ __('Gift Message') ?></span></label> - <div class="admin__field-control"> - <textarea id="<?= /* @escapeNotVerified */ $block->getFieldId('message') ?>" - name="<?= /* @escapeNotVerified */ $block->getFieldName('message') ?>" - class="admin__control-textarea" - rows="2" - cols="15"><?= $block->escapeHtml($block->getMessage()->getMessage()) ?></textarea> + <div class="admin__field field-gift-message"> + <label class="admin__field-label" + for="<?= $block->escapeHtmlAttr($block->getFieldId('message')) ?>"><span><?= $block->escapeHtml(__('Gift Message')) ?></span></label> + <div class="admin__field-control"> + <textarea id="<?= $block->escapeHtmlAttr($block->getFieldId('message')) ?>" + name="<?= $block->escapeHtmlAttr($block->getFieldName('message')) ?>" + class="admin__control-textarea" + rows="2" + cols="15"><?= $block->escapeHtml($block->getMessage()->getMessage()) ?></textarea> + </div> </div> - </div> - <div class="actions"> - <?= $block->getSaveButtonHtml() ?> - </div> - </fieldset> - </form> -</div> + <div class="actions"> + <?= $block->getSaveButtonHtml() ?> + </div> + </fieldset> + </form> + </div> <?php endif ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml index 6ac6e13a873ed..16643a29a7fbe 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml @@ -4,19 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile /** @var \Magento\Sales\Block\Adminhtml\Order\View\History $block */ ?> <div id="order_history_block" class="edit-order-comments"> - <?php if ($block->canAddComment()):?> + <?php if ($block->canAddComment()) : ?> <div class="order-history-block" id="history_form"> <div class="admin__field"> - <label for="history_status" class="admin__field-label"><?= /* @noEscape */ __('Status') ?></label> + <label for="history_status" class="admin__field-label"><?= $block->escapeHtml(__('Status')) ?></label> <div class="admin__field-control"> <select name="history[status]" id="history_status" class="admin__control-select"> - <?php foreach ($block->getStatuses() as $_code => $_label): ?> - <option value="<?= $block->escapeHtml($_code) ?>"<?php if ($_code == $block->getOrder()->getStatus()): ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> + <?php foreach ($block->getStatuses() as $_code => $_label) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getOrder()->getStatus()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> <?php endforeach; ?> </select> </div> @@ -24,7 +23,7 @@ <div class="admin__field"> <label for="history_comment" class="admin__field-label"> - <?= /* @noEscape */ __('Comment') ?> + <?= $block->escapeHtml(__('Comment')) ?> </label> <div class="admin__field-control"> <textarea name="history[comment]" @@ -38,14 +37,14 @@ <div class="admin__field"> <div class="order-history-comments-options"> <div class="admin__field admin__field-option"> - <?php if ($block->canSendCommentEmail()): ?> + <?php if ($block->canSendCommentEmail()) : ?> <input name="history[is_customer_notified]" type="checkbox" id="history_notify" class="admin__control-checkbox" value="1" /> <label class="admin__field-label" for="history_notify"> - <?= /* @noEscape */ __('Notify Customer by Email') ?> + <?= $block->escapeHtml(__('Notify Customer by Email')) ?> </label> <?php endif; ?> </div> @@ -57,7 +56,7 @@ class="admin__control-checkbox" value="1" /> <label class="admin__field-label" for="history_visible"> - <?= /* @noEscape */ __('Visible on Storefront') ?> + <?= $block->escapeHtml(__('Visible on Storefront')) ?> </label> </div> </div> @@ -70,32 +69,30 @@ <?php endif;?> <ul class="note-list"> - <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item): ?> + <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item) : ?> <li class="note-list-item"> <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> <span class="note-list-status"><?= $block->escapeHtml($_item->getStatusLabel()) ?></span> <span class="note-list-customer"> - <?= /* @noEscape */ __('Customer') ?> - <?php if ($block->isCustomerNotificationNotApplicable($_item)): ?> - <span class="note-list-customer-notapplicable"><?= /* @noEscape */ __('Notification Not Applicable') ?></span> - <?php elseif ($_item->getIsCustomerNotified()): ?> - <span class="note-list-customer-notified"><?= /* @noEscape */ __('Notified') ?></span> - <?php else: ?> - <span class="note-list-customer-not-notified"><?= /* @noEscape */ __('Not Notified') ?></span> + <?= $block->escapeHtml(__('Customer')) ?> + <?php if ($block->isCustomerNotificationNotApplicable($_item)) : ?> + <span class="note-list-customer-notapplicable"><?= $block->escapeHtml(__('Notification Not Applicable')) ?></span> + <?php elseif ($_item->getIsCustomerNotified()) : ?> + <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> + <?php else : ?> + <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> <?php endif; ?> </span> - <?php if ($_item->getComment()): ?> + <?php if ($_item->getComment()) : ?> <div class="note-list-comment"><?= $block->escapeHtml($_item->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> <?php endif; ?> </li> <?php endforeach; ?> </ul> <script> -require(['prototype'], function(){ - - if($('order_status'))$('order_status').update('<?= $block->escapeJs($block->escapeHtml($block->getOrder()->getStatusLabel())) ?>'); - -}); -</script> + require(['prototype'], function(){ + if($('order_status'))$('order_status').update('<?= $block->escapeJs($block->escapeHtml($block->getOrder()->getStatusLabel())) ?>'); + }); + </script> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index 56c1a99e66ade..ab5cd49449ece 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -7,9 +7,6 @@ /** * @var \Magento\Sales\Block\Adminhtml\Order\View\Info $block */ - -// @codingStandardsIgnoreFile - $order = $block->getOrder(); $orderAdminDate = $block->formatDate( @@ -24,6 +21,9 @@ $orderStoreDate = $block->formatDate( true, $block->getTimezoneForStore($order->getStore()) ); + +$customerUrl = $block->getCustomerViewUrl(); +$allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> <section class="admin__page-section order-view-account-information"> @@ -36,10 +36,10 @@ $orderStoreDate = $block->formatDate( <?php $confirmationEmailStatusMessage = $order->getEmailSent() ? __('The order confirmation email was sent') : __('The order confirmation email is not sent'); ?> <div class="admin__page-section-item-title"> <span class="title"> - <?php if ($block->getNoUseOrderLink()): ?> + <?php if ($block->getNoUseOrderLink()) : ?> <?= $block->escapeHtml(__('Order # %1', $order->getRealOrderId())) ?> (<span><?= $block->escapeHtml($confirmationEmailStatusMessage) ?></span>) - <?php else: ?> - <a href="<?= $block->escapeHtml($block->getViewUrl($order->getId())) ?>"><?= $block->escapeHtml(__('Order # %1', $order->getRealOrderId())) ?></a> + <?php else : ?> + <a href="<?= $block->escapeUrl($block->getViewUrl($order->getId())) ?>"><?= $block->escapeHtml(__('Order # %1', $order->getRealOrderId())) ?></a> <span>(<?= $block->escapeHtml($confirmationEmailStatusMessage) ?>)</span> <?php endif; ?> </span> @@ -50,7 +50,7 @@ $orderStoreDate = $block->formatDate( <th><?= $block->escapeHtml(__('Order Date')) ?></th> <td><?= $block->escapeHtml($orderAdminDate) ?></td> </tr> - <?php if ($orderAdminDate != $orderStoreDate):?> + <?php if ($orderAdminDate != $orderStoreDate) : ?> <tr> <th><?= $block->escapeHtml(__('Order Date (%1)', $block->getTimezoneForStore($order->getStore()))) ?></th> <td><?= $block->escapeHtml($orderStoreDate) ?></td> @@ -61,48 +61,48 @@ $orderStoreDate = $block->formatDate( <td><span id="order_status"><?= $block->escapeHtml($order->getStatusLabel()) ?></span></td> </tr> <?= $block->getChildHtml() ?> - <?php if ($block->isSingleStoreMode() == false):?> + <?php if ($block->isSingleStoreMode() == false) : ?> <tr> <th><?= $block->escapeHtml(__('Purchased From')) ?></th> <td><?= $block->escapeHtml($block->getOrderStoreName(), ['br']) ?></td> </tr> <?php endif; ?> - <?php if ($order->getRelationChildId()): ?> + <?php if ($order->getRelationChildId()) : ?> <tr> <th><?= $block->escapeHtml(__('Link to the New Order')) ?></th> <td> - <a href="<?= $block->escapeHtml($block->getViewUrl($order->getRelationChildId())) ?>"> + <a href="<?= $block->escapeUrl($block->getViewUrl($order->getRelationChildId())) ?>"> <?= $block->escapeHtml($order->getRelationChildRealId()) ?> </a> </td> </tr> <?php endif; ?> - <?php if ($order->getRelationParentId()): ?> + <?php if ($order->getRelationParentId()) : ?> <tr> <th><?= $block->escapeHtml(__('Link to the Previous Order')) ?></th> <td> - <a href="<?= $block->escapeHtml($block->getViewUrl($order->getRelationParentId())) ?>"> + <a href="<?= $block->escapeUrl($block->getViewUrl($order->getRelationParentId())) ?>"> <?= $block->escapeHtml($order->getRelationParentRealId()) ?> </a> </td> </tr> <?php endif; ?> - <?php if ($order->getRemoteIp() && $block->shouldDisplayCustomerIp()): ?> + <?php if ($order->getRemoteIp() && $block->shouldDisplayCustomerIp()) : ?> <tr> <th><?= $block->escapeHtml(__('Placed from IP')) ?></th> - <td><?= $block->escapeHtml($order->getRemoteIp()); echo $order->getXForwardedFor() ? ' (' . $block->escapeHtml($order->getXForwardedFor()) . ')' : ''; ?></td> + <td><?= $block->escapeHtml($order->getRemoteIp()); ?><?= $order->getXForwardedFor() ? ' (' . $block->escapeHtml($order->getXForwardedFor()) . ')' : ''; ?></td> </tr> <?php endif; ?> - <?php if ($order->getGlobalCurrencyCode() != $order->getBaseCurrencyCode()): ?> + <?php if ($order->getGlobalCurrencyCode() != $order->getBaseCurrencyCode()) : ?> <tr> <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getGlobalCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> <td><?= $block->escapeHtml($order->getBaseToGlobalRate()) ?></td> </tr> <?php endif; ?> - <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()): ?> + <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()) : ?> <tr> <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> - <th><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></th> + <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> </table> @@ -114,8 +114,8 @@ $orderStoreDate = $block->formatDate( <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Account Information')) ?></span> <div class="actions"> - <?php if ($customerUrl = $block->getCustomerViewUrl()) : ?> - <a href="<?= /* @noEscape */ $block->getCustomerViewUrl() ?>" target="_blank"> + <?php if ($customerUrl) : ?> + <a href="<?= /* @noEscape */ $customerUrl ?>" target="_blank"> <?= $block->escapeHtml(__('Edit Customer')) ?> </a> <?php endif; ?> @@ -126,18 +126,18 @@ $orderStoreDate = $block->formatDate( <tr> <th><?= $block->escapeHtml(__('Customer Name')) ?></th> <td> - <?php if ($customerUrl = $block->getCustomerViewUrl()): ?> + <?php if ($customerUrl) : ?> <a href="<?= $block->escapeUrl($customerUrl) ?>" target="_blank"> <span><?= $block->escapeHtml($order->getCustomerName()) ?></span> </a> - <?php else: ?> + <?php else : ?> <?= $block->escapeHtml($order->getCustomerName()) ?> <?php endif; ?> </td> </tr> <tr> <th><?= $block->escapeHtml(__('Email')) ?></th> - <td><a href="mailto:<?php echo $block->escapeHtml($order->getCustomerEmail()) ?>"><?php echo $block->escapeHtml($order->getCustomerEmail()) ?></a></td> + <td><a href="mailto:<?= $block->escapeHtmlAttr($order->getCustomerEmail()) ?>"><?= $block->escapeHtml($order->getCustomerEmail()) ?></a></td> </tr> <?php if ($groupName = $block->getCustomerGroupName()) : ?> <tr> @@ -145,7 +145,7 @@ $orderStoreDate = $block->formatDate( <td><?= $block->escapeHtml($groupName) ?></td> </tr> <?php endif; ?> - <?php foreach ($block->getCustomerAccountData() as $data):?> + <?php foreach ($block->getCustomerAccountData() as $data) : ?> <tr> <th><?= $block->escapeHtml($data['label']) ?></th> <td><?= $block->escapeHtml($data['value'], ['br']) ?></td> @@ -169,16 +169,16 @@ $orderStoreDate = $block->formatDate( <span class="title"><?= $block->escapeHtml(__('Billing Address')) ?></span> <div class="actions"><?= /* @noEscape */ $block->getAddressEditLink($order->getBillingAddress()); ?></div> </div> - <address class="admin__page-section-item-content"><?= /* @noEscape */ $block->getFormattedAddress($order->getBillingAddress()); ?></address> + <address class="admin__page-section-item-content"><?= $block->escapeHtml($block->getFormattedAddress($order->getBillingAddress()), $allowedAddressHtmlTags); ?></address> </div> - <?php if (!$block->getOrder()->getIsVirtual()): ?> + <?php if (!$block->getOrder()->getIsVirtual()) : ?> <div class="admin__page-section-item order-shipping-address"> <?php /* Shipping Address */ ?> <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Shipping Address')) ?></span> <div class="actions"><?= /* @noEscape */ $block->getAddressEditLink($order->getShippingAddress()); ?></div> </div> - <address class="admin__page-section-item-content"><?= /* @noEscape */ $block->getFormattedAddress($order->getShippingAddress()); ?></address> + <address class="admin__page-section-item-content"><?= $block->escapeHtml($block->getFormattedAddress($order->getShippingAddress()), $allowedAddressHtmlTags); ?></address> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/items.phtml index dc62bce78ea1e..734ad24b02fcf 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/items.phtml @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile -?> -<?php /** * @var \Magento\Sales\Block\Adminhtml\Order\View\Items $block */ @@ -18,19 +15,19 @@ $_order = $block->getOrder() ?> <?php $i = 0; $columns = $block->getColumns(); $lastItemNumber = count($columns) ?> - <?php foreach ($columns as $columnName => $columnTitle):?> + <?php foreach ($columns as $columnName => $columnTitle) : ?> <?php $i++; ?> - <th class="col-<?= /* @noEscape */ $columnName ?><?= /* @noEscape */ ($i === $lastItemNumber ? ' last' : '') ?>"><span><?= /* @noEscape */ $columnTitle ?></span></th> + <th class="col-<?= $block->escapeHtmlAttr($columnName) ?><?= /* @noEscape */ ($i === $lastItemNumber ? ' last' : '') ?>"><span><?= $block->escapeHtml($columnTitle) ?></span></th> <?php endforeach; ?> </tr> </thead> <?php $_items = $block->getItemsCollection();?> - <?php $i = 0; foreach ($_items as $_item):?> - <?php if ($_item->getParentItem()) { + <?php $i = 0; foreach ($_items as $_item) : ?> + <?php if ($_item->getParentItem()) : continue; - } else { + else : $i++; - }?> + endif; ?> <tbody class="<?= /* @noEscape */ $i%2 ? 'even' : 'odd' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/items/renderer/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/items/renderer/default.phtml index 387aab3d9616f..c54e23d141e0d 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/items/renderer/default.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var \Magento\Sales\Block\Adminhtml\Order\View\Items\Renderer\DefaultRenderer $block */ ?> <?php $_item = $block->getItem() ?> @@ -14,8 +11,10 @@ <?php $i = 0; $columns = $block->getColumns(); $lastItemNumber = count($columns) ?> - <?php foreach ($columns as $columnName => $columnClass):?> + <?php foreach ($columns as $columnName => $columnClass) : ?> <?php $i++; ?> - <td class="<?= /* @noEscape */ $columnClass ?><?= /* @noEscape */ ($i === $lastItemNumber ? ' last' : '') ?>"><?= /* @escapeNotVerified */ $block->getColumnHtml($_item, $columnName) ?></td> + <td class="<?= /* @noEscape */ $columnClass ?><?= /* @noEscape */ ($i === $lastItemNumber ? ' last' : '') ?>"> + <?= $block->getColumnHtml($_item, $columnName) ?> + </td> <?php endforeach; ?> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/history.phtml index 87feaa857ef0e..0a93d25a7a021 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/history.phtml @@ -4,25 +4,24 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var \Magento\Sales\Block\Adminhtml\Order\View\Tab\History $block */ ?> <section class="admin__page-section edit-order-comments"> <ul class="note-list"> - <?php foreach ($block->getFullHistory() as $_item): ?> + <?php foreach ($block->getFullHistory() as $_item) : ?> <li class="note-list-item"> - <span class="note-list-date"><?= /* @escapeNotVerified */ $block->getItemCreatedAt($_item) ?></span> - <span class="note-list-time"><?= /* @escapeNotVerified */ $block->getItemCreatedAt($_item, 'time') ?></span> - <span class="note-list-status"><?= /* @escapeNotVerified */ $block->getItemTitle($_item) ?></span> - <?php if ($block->isItemNotified($_item, false)): ?> + <span class="note-list-date"><?= /* @noEscape */ $block->getItemCreatedAt($_item) ?></span> + <span class="note-list-time"><?= /* @noEscape */ $block->getItemCreatedAt($_item, 'time') ?></span> + <span class="note-list-status"><?= /* @noEscape */ $block->getItemTitle($_item) ?></span> + <?php if ($block->isItemNotified($_item, false)) : ?> <span class="note-list-customer"> - <?= /* @escapeNotVerified */ __('Customer') ?> - <?php if ($block->isCustomerNotificationNotApplicable($_item)): ?> - <span class="note-list-customer-notapplicable"><?= /* @escapeNotVerified */ __('Notification Not Applicable') ?></span> - <?php elseif ($block->isItemNotified($_item)): ?> - <span class="note-list-customer-notified"><?= /* @escapeNotVerified */ __('Notified') ?></span> - <?php else: ?> - <span class="note-list-customer-not-notified"><?= /* @escapeNotVerified */ __('Not Notified') ?></span> + <?= $block->escapeHtml(__('Customer')) ?> + <?php if ($block->isCustomerNotificationNotApplicable($_item)) : ?> + <span class="note-list-customer-notapplicable"><?= $block->escapeHtml(__('Notification Not Applicable')) ?></span> + <?php elseif ($block->isItemNotified($_item)) : ?> + <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> + <?php else : ?> + <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> <?php endif; ?> </span> <?php endif; ?> @@ -31,18 +30,18 @@ </ul> <div class="edit-order-comments-block"> <div class="edit-order-comments-block-title"> - <?= /* @escapeNotVerified */ __('Notes for this Order') ?> + <?= $block->escapeHtml(__('Notes for this Order')) ?> </div> - <?php foreach ($block->getFullHistory() as $_item): ?> - <?php if ($_comment = $block->getItemComment($_item)): ?> + <?php foreach ($block->getFullHistory() as $_item) : ?> + <?php if ($_comment = $block->getItemComment($_item)) : ?> <div class="comments-block-item"> <div class="comments-block-item-comment"> - <?= /* @escapeNotVerified */ $_comment ?> + <?= /* @noEscape */ $_comment ?> </div> <span class="comments-block-item-date-time"> - <?= /* @escapeNotVerified */ __('Comment added') ?> - <?= /* @escapeNotVerified */ $block->getItemCreatedAt($_item) ?> - <?= /* @escapeNotVerified */ $block->getItemCreatedAt($_item, 'time') ?> + <?= $block->escapeHtml(__('Comment added')) ?> + <?= /* @noEscape */ $block->getItemCreatedAt($_item) ?> + <?= /* @noEscape */ $block->getItemCreatedAt($_item, 'time') ?> </span> </div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml index 434ca127832a1..390adb7d5cfce 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/** @var $block \Magento\Sales\Block\Adminhtml\Order\View\Tab\Info */ ?> -<?php /** @var $block \Magento\Sales\Block\Adminhtml\Order\View\Tab\Info */ ?> <?php $_order = $block->getOrder() ?> <div id="order-messages"> @@ -15,21 +13,21 @@ </div> <?= $block->getChildHtml('order_info') ?> -<input type="hidden" name="order_id" value="<?= /* @escapeNotVerified */ $_order->getId() ?>"/> +<input type="hidden" name="order_id" value="<?= (int) $_order->getId() ?>"/> <section class="admin__page-section order-view-billing-shipping"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()): ?> order-payment-method-virtual<?php endif; ?>"> + <div class="admin__page-section-item order-payment-method<?= ($_order->getIsVirtual() ? ' order-payment-method-virtual' : '') ?>"> <?php /* Payment Method */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?></div> + <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> <div class="order-payment-additional"> <?= $block->getChildHtml('order_payment_additional') ?> <?= $block->getChildHtml('payment_additional_info') ?> @@ -46,26 +44,26 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items Ordered') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items Ordered')) ?></span> </div> <?= $block->getItemsHtml() ?> </section> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Notes for this Order') ?></span> + <span class="title"><?= $block->escapeHtml(__('Notes for this Order')) ?></span> </div> <?= $block->getChildHtml('order_history') ?> </div> <div class="admin__page-section-item order-totals"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Totals') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Totals')) ?></span> </div> <?= $block->getChildHtml('order_totals') ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/page/js/components.phtml b/app/code/Magento/Sales/view/adminhtml/templates/page/js/components.phtml index 1194e72e27c62..13f44b97fc789 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/page/js/components.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/page/js/components.phtml @@ -1,12 +1,9 @@ <?php /** - * @category design - * @package default_default * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/rss/order/grid/link.phtml b/app/code/Magento/Sales/view/adminhtml/templates/rss/order/grid/link.phtml index 872f70d1d693a..231eb274290cf 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/rss/order/grid/link.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/rss/order/grid/link.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Sales\Block\Adminhtml\Rss\Order\Grid\Link */ ?> -<?php if ($block->isRssAllowed() && $block->getLink()): ?> -<a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="link-feed"><?= /* @escapeNotVerified */ $block->getLabel() ?></a> +<?php if ($block->isRssAllowed() && $block->getLink()) : ?> +<a href="<?= $block->escapeUrl($block->getLink()) ?>" class="link-feed"><?= $block->escapeHtml($block->getLabel()) ?></a> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/transactions/detail.phtml b/app/code/Magento/Sales/view/adminhtml/templates/transactions/detail.phtml index b3708edf1d098..01b7548be15cd 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/transactions/detail.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/transactions/detail.phtml @@ -4,35 +4,34 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +/* @var \Magento\Sales\Block\Adminhtml\Transactions\Detail $block */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getButtonsHtml() ?></div> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Transaction Data') ?></span> + <span class="title"><?= $block->escapeHtml(__('Transaction Data')) ?></span> </div> <div id="log_details_fieldset" class="admin__page-section-content log-details"> <table class="log-info data-table admin__table-secondary"> <tbody> <tr> - <th><?= /* @escapeNotVerified */ __('Transaction ID') ?></th> + <th><?= $block->escapeHtml(__('Transaction ID')) ?></th> <td><?= $block->getTxnIdHtml() ?></td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Parent Transaction ID') ?></th> + <th><?= $block->escapeHtml(__('Parent Transaction ID')) ?></th> <td> - <?php if ($block->getParentTxnIdHtml()): ?> + <?php if ($block->getParentTxnIdHtml()) : ?> <a href="<?= $block->getParentTxnIdUrlHtml() ?>"> <?= $block->getParentTxnIdHtml() ?> </a> <?php else : ?> - <?= /* @escapeNotVerified */ __('N/A') ?> + <?= $block->escapeHtml(__('N/A')) ?> <?php endif; ?> </td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Order ID') ?></th> + <th><?= $block->escapeHtml(__('Order ID')) ?></th> <td> <a href="<?= $block->getOrderIdUrlHtml() ?>"> <?= $block->getOrderIncrementIdHtml() ?> @@ -40,15 +39,15 @@ </td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Transaction Type') ?></th> + <th><?= $block->escapeHtml(__('Transaction Type')) ?></th> <td><?= $block->getTxnTypeHtml() ?></td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Is Closed') ?></th> + <th><?= $block->escapeHtml(__('Is Closed')) ?></th> <td><?= $block->getIsClosedHtml() ?></td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Created At') ?></th> + <th><?= $block->escapeHtml(__('Created At')) ?></th> <td><?= $block->getCreatedAtHtml() ?></td> </tr> </tbody> @@ -58,7 +57,7 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title""><?= /* @escapeNotVerified */ __('Child Transactions') ?></span> + <span class="title"><?= $block->escapeHtml(__('Child Transactions')) ?></span> </div> <div class="admin__page-section-content log-details-grid"> <?= $block->getChildHtml('child_grid') ?> @@ -67,7 +66,7 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Transaction Details') ?></span> + <span class="title"><?= $block->escapeHtml(__('Transaction Details')) ?></span> </div> <div class="admin__page-section-content log-details-grid"> <?= $block->getChildHtml('detail_grid') ?> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml index ecc2b5beee321..10b7b1c028c66 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml @@ -35,7 +35,13 @@ <listingToolbar name="listing_top"> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> - <exportButton name="export_button"/> + <exportButton name="export_button"> + <settings> + <additionalParams> + <param xsi:type="string" active="true" name="order_id">*</param> + </additionalParams> + </settings> + </exportButton> <filterSearch name="fulltext"/> <filters name="listing_filters"> <filterSelect name="store_id" provider="${ $.parentName }"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml index 3ec450a570b46..ac1233c5e4961 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml @@ -35,7 +35,13 @@ <listingToolbar name="listing_top"> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> - <exportButton name="export_button"/> + <exportButton name="export_button"> + <settings> + <additionalParams> + <param xsi:type="string" active="true" name="order_id">*</param> + </additionalParams> + </settings> + </exportButton> <filterSearch name="fulltext"/> <filters name="listing_filters"> <filterSelect name="store_id" provider="${ $.parentName }"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml index 27cef50742163..6db77a79b8c14 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml @@ -35,7 +35,13 @@ <listingToolbar name="listing_top"> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> - <exportButton name="export_button"/> + <exportButton name="export_button"> + <settings> + <additionalParams> + <param xsi:type="string" active="true" name="order_id">*</param> + </additionalParams> + </settings> + </exportButton> <filterSearch name="fulltext"/> <filters name="listing_filters"> <filterSelect name="store_id" provider="${ $.parentName }"> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 02529cee5c0ff..efc946077c1aa 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -4,14 +4,17 @@ */ define([ - "jquery", + 'jquery', 'Magento_Ui/js/modal/confirm', 'Magento_Ui/js/modal/alert', - "mage/translate", - "prototype", - "Magento_Catalog/catalog/product/composite/configure", + 'mage/template', + 'text!Magento_Sales/templates/order/create/shipping/reload.html', + 'text!Magento_Sales/templates/order/create/payment/reload.html', + 'mage/translate', + 'prototype', + 'Magento_Catalog/catalog/product/composite/configure', 'Magento_Ui/js/lib/view/utils/async' -], function(jQuery, confirm, alert){ +], function (jQuery, confirm, alert, template, shippingTemplate, paymentTemplate) { window.AdminOrder = new Class.create(); @@ -21,6 +24,7 @@ define([ this.loadBaseUrl = false; this.customerId = data.customer_id ? data.customer_id : false; this.storeId = data.store_id ? data.store_id : false; + this.quoteId = data['quote_id'] ? data['quote_id'] : false; this.currencyId = false; this.currencySymbol = data.currency_symbol ? data.currency_symbol : ''; this.addresses = data.addresses ? data.addresses : $H({}); @@ -28,7 +32,7 @@ define([ this.gridProducts = $H({}); this.gridProductsGift = $H({}); this.billingAddressContainer = ''; - this.shippingAddressContainer= ''; + this.shippingAddressContainer = ''; this.isShippingMethodReseted = data.shipping_method_reseted ? data.shipping_method_reseted : false; this.overlayData = $H({}); this.giftMessageDataChanged = false; @@ -38,6 +42,20 @@ define([ this.isOnlyVirtualProduct = false; this.excludedPaymentMethods = []; this.summarizePrice = true; + this.selectAddressEvent = false; + this.shippingTemplate = template(shippingTemplate, { + data: { + title: jQuery.mage.__('Shipping Method'), + linkText: jQuery.mage.__('Get shipping methods and rates') + } + }); + this.paymentTemplate = template(paymentTemplate, { + data: { + title: jQuery.mage.__('Payment Method'), + linkText: jQuery.mage.__('Get available payment methods') + } + }); + jQuery.async('#order-items', (function(){ this.dataArea = new OrderFormArea('data', $(this.getAreaId('data')), this); this.itemsArea = Object.extend(new OrderFormArea('items', $(this.getAreaId('items')), this), { @@ -46,8 +64,8 @@ define([ if (typeof controlButtonArea != 'undefined') { var buttons = controlButtonArea.childElements(); for (var i = 0; i < buttons.length; i++) { - if (buttons[i].innerHTML.include(button.label)) { - return ; + if (buttons[i].innerHTML.include(button.getLabel())) { + return; } } button.insertIn(controlButtonArea, 'top'); @@ -152,50 +170,74 @@ define([ }, selectAddress : function(el, container){ + id = el.value; if (id.length == 0) { id = '0'; } - if(this.addresses[id]){ - this.fillAddressFields(container, this.addresses[id]); - } - else{ + this.selectAddressEvent = true; + if (this.addresses[id]) { + this.fillAddressFields(container, this.addresses[id]); + } else { this.fillAddressFields(container, {}); } + this.selectAddressEvent = false; var data = this.serializeData(container); data[el.name] = id; - if(this.isShippingField(container) && !this.isShippingMethodReseted){ + + this.resetPaymentMethod(); + if (this.isShippingField(container) && !this.isShippingMethodReseted) { this.resetShippingMethod(data); - } - else{ + } else{ this.saveData(data); } + }, - isShippingField : function(fieldId){ - if(this.shippingAsBilling){ + /** + * Checks if the field belongs to the shipping address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isShippingField: function (fieldId) { + if (this.shippingAsBilling) { return fieldId.include('billing'); } + return fieldId.include('shipping'); }, - isBillingField : function(fieldId){ + /** + * Checks if the field belongs to the billing address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isBillingField: function (fieldId) { return fieldId.include('billing'); }, - bindAddressFields : function(container) { - var fields = $(container).select('input', 'select', 'textarea'); - for(var i=0;i<fields.length;i++){ - Event.observe(fields[i], 'change', this.changeAddressField.bind(this)); + /** + * Binds events on container form fields. + * + * @param {String} container + */ + bindAddressFields: function (container) { + var fields = $(container).select('input', 'select', 'textarea'), + i; + + for (i = 0; i < fields.length; i++) { + jQuery(fields[i]).change(this.changeAddressField.bind(this)); } }, /** * Triggers on each form's element changes. * - * @param {Object} event + * @param {Event} event */ changeAddressField: function (event) { var field = Event.element(event), @@ -203,7 +245,8 @@ define([ matchRes = field.name.match(re), type, name, - data; + data, + resetShipping = false; if (!matchRes) { return; @@ -219,20 +262,35 @@ define([ } data = data.toObject(); - if (type === 'billing' && this.shippingAsBilling || type === 'shipping' && !this.shippingAsBilling) { + if (type === 'billing' && this.shippingAsBilling) { + this.syncAddressField(this.shippingAddressContainer, field.name, field.value); + resetShipping = true; + } + + if (type === 'shipping' && !this.shippingAsBilling) { + resetShipping = true; + } + + if (resetShipping) { data['reset_shipping'] = true; } data['order[' + type + '_address][customer_address_id]'] = null; - data['shipping_as_billing'] = jQuery('[name="shipping_same_as_billing"]').is(':checked') ? 1 : 0; + data['shipping_as_billing'] = +this.shippingAsBilling; if (name === 'customer_address_id') { data['order[' + type + '_address][customer_address_id]'] = $('order-' + type + '_address_customer_address_id').value; } + if (name === 'country_id' && this.selectAddressEvent === false) { + $('order-' + type + '_address_customer_address_id').value = ''; + } + + this.resetPaymentMethod(); + if (data['reset_shipping']) { - this.resetShippingMethod(data); + this.resetShippingMethod(); } else { this.saveData(data); @@ -242,7 +300,28 @@ define([ } }, - fillAddressFields : function(container, data){ + /** + * Set address container form field value. + * + * @param {String} container - container ID + * @param {String} fieldName - form field name + * @param {*} fieldValue - form field value + */ + syncAddressField: function (container, fieldName, fieldValue) { + var syncName; + + if (this.isBillingField(fieldName)) { + syncName = fieldName.replace('billing', 'shipping'); + } + + $(container).select('[name="' + syncName + '"]').each(function (element) { + if (~['input', 'textarea', 'select'].indexOf(element.tagName.toLowerCase())) { + element.value = fieldValue; + } + }); + }, + + fillAddressFields: function(container, data){ var regionIdElem = false; var regionIdElemValue = false; @@ -283,10 +362,15 @@ define([ fields[i].setValue(data[name] ? data[name] : ''); } - if (fields[i].changeUpdater) fields[i].changeUpdater(); + if (fields[i].changeUpdater) { + fields[i].changeUpdater(); + } + if (name == 'region' && data['region_id'] && !data['region']){ fields[i].value = data['region_id']; } + + jQuery(fields[i]).trigger('change'); } }, @@ -317,46 +401,83 @@ define([ } }, - setShippingAsBilling : function(flag){ - var data; - var areasToLoad = ['billing_method', 'shipping_address', 'totals', 'giftmessage']; + /** + * Equals shipping and billing addresses. + * + * @param {Boolean} flag + */ + setShippingAsBilling: function (flag) { + var data, + areasToLoad = ['billing_method', 'shipping_address', 'shipping_method', 'totals', 'giftmessage']; + this.disableShippingAddress(flag); - if(flag){ - data = this.serializeData(this.billingAddressContainer); - } else { - data = this.serializeData(this.shippingAddressContainer); - areasToLoad.push('shipping_method'); - } + data = this.serializeData(flag ? this.billingAddressContainer : this.shippingAddressContainer); data = data.toObject(); data['shipping_as_billing'] = flag ? 1 : 0; data['reset_shipping'] = 1; - this.loadArea( areasToLoad, true, data); + this.loadArea(areasToLoad, true, data); }, - resetShippingMethod : function(data){ - var areasToLoad = ['billing_method', 'shipping_address', 'totals', 'giftmessage', 'items']; - if(!this.isOnlyVirtualProduct) { - areasToLoad.push('shipping_method'); - areasToLoad.push('shipping_address'); + /** + * Replace shipping method area. + */ + resetShippingMethod: function () { + if (!this.isOnlyVirtualProduct) { + $(this.getAreaId('shipping_method')).update(this.shippingTemplate); } + }, - data['reset_shipping'] = 1; - this.isShippingMethodReseted = true; - this.loadArea(areasToLoad, true, data); + /** + * Replace payment method area. + */ + resetPaymentMethod: function () { + $(this.getAreaId('billing_method')).update(this.paymentTemplate); }, - loadShippingRates : function(){ + /** + * Loads shipping options according to address data. + * + * @return {Boolean} + */ + loadShippingRates: function () { + var addressContainer = this.shippingAsBilling ? + 'billingAddressContainer' : + 'shippingAddressContainer', + data = this.serializeData(this[addressContainer]).toObject(); + + data['collect_shipping_rates'] = 1; this.isShippingMethodReseted = false; - this.loadArea(['shipping_method', 'totals'], true, {collect_shipping_rates: 1}); + this.loadArea(['shipping_method', 'totals'], true, data); + + return false; }, - setShippingMethod : function(method){ + setShippingMethod: function(method) { var data = {}; + data['order[shipping_method]'] = method; - this.loadArea(['shipping_method', 'totals', 'billing_method'], true, data); + this.loadArea([ + 'shipping_method', + 'totals', + 'billing_method' + ], true, data); + }, + + /** + * Updates available payment + * methods list according to order data. + * + * @return boolean + */ + loadPaymentMethods: function() { + var data = this.serializeData(this.billingAddressContainer).toObject(); + + this.loadArea(['billing_method','totals'], true, data); + + return false; }, - switchPaymentMethod : function(method){ + switchPaymentMethod: function(method){ jQuery('#edit_form') .off('submitOrder') .on('submitOrder', function(){ @@ -448,6 +569,9 @@ define([ applyCoupon : function(code){ this.loadArea(['items', 'shipping_method', 'totals', 'billing_method'], true, {'order[coupon][code]':code, reset_shipping: 0}); this.orderItemChanged = false; + jQuery('html, body').animate({ + scrollTop: 0 + }); }, addProduct : function(id){ @@ -618,7 +742,7 @@ define([ } else if (((elms[i].type == 'checkbox' || elms[i].type == 'radio') && elms[i].checked) || ((elms[i].type == 'file' || elms[i].type == 'text' || elms[i].type == 'textarea' || elms[i].type == 'hidden') - && Form.Element.getValue(elms[i])) + && Form.Element.getValue(elms[i])) ) { if (this._isSummarizePrice(elms[i])) { productPrice += getPrice(elms[i]); @@ -906,6 +1030,7 @@ define([ qtyElement.value = confirmedCurrentQty.value; } this.productConfigureAddFields['item['+itemId+'][configured]'] = 1; + this.itemsUpdate(); }.bind(this)); productConfigure.setShowWindowCallback(listType, function() { @@ -1130,12 +1255,18 @@ define([ */ isPaymentValidationAvailable : function(){ return ((typeof this.paymentMethod) == 'undefined' - || this.excludedPaymentMethods.indexOf(this.paymentMethod) == -1); + || this.excludedPaymentMethods.indexOf(this.paymentMethod) == -1); }, - serializeData : function(container){ - var fields = $(container).select('input', 'select', 'textarea'); - var data = Form.serializeElements(fields, true); + /** + * Serializes container form elements data. + * + * @param {String} container + * @return {Object} + */ + serializeData: function (container) { + var fields = $(container).select('input', 'select', 'textarea'), + data = Form.serializeElements(fields, true); return $H(data); }, @@ -1155,8 +1286,12 @@ define([ submit : function() { - jQuery('#edit_form').trigger('processStart'); - jQuery('#edit_form').trigger('submitOrder'); + var $editForm = jQuery('#edit_form'); + + if ($editForm.valid()) { + $editForm.trigger('processStart'); + $editForm.trigger('submitOrder'); + } }, _realSubmit: function () { @@ -1416,8 +1551,10 @@ define([ node.update('<span>' + this._label + '</span>'); content[position] = node; Element.insert(element, content); + }, + + getLabel: function(){ + return this._label; } }; - }); - diff --git a/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html new file mode 100644 index 0000000000000..c503f3c678ab6 --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html @@ -0,0 +1,18 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="admin__page-section-title"> + <span class="title"><%- data.title %></span> +</div> +<div id="order-billing_method_summary" + class="order-billing-method-summary"> + <a href="#" + onclick="return order.loadPaymentMethods();" + class="action-default"> + <span><%- data.linkText %></span> + </a> +</div> diff --git a/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html new file mode 100644 index 0000000000000..6b191ee81a45a --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html @@ -0,0 +1,19 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="admin__page-section-title"> + <span class="title"><%- data.title %></span> +</div> +<div id="order-shipping-method-summary" + class="order-shipping-method-summary"> + <a href="#" + onclick="return order.loadShippingRates();" + class="action-default"> + <span><%- data.linkText %></span> + </a> + <input type="hidden" name="order[has_shipping]" value="" class="required-entry" /> +</div> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html index ca89446a2f7c0..5ae6f5f9d82c7 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -56,7 +62,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html index b21f659814368..657de2aae2045 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Credit memo for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.getName()":"Guest Customer Name (Billing)", +"var billing.name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var creditmemo":"Credit Memo", +"var order":"Order", +"var order_data.is_not_virtual":"Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -54,7 +60,7 @@ <h1>{{trans "Your Credit Memo #%creditmemo_id for Order #%order_id" creditmemo_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -66,10 +72,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html index 3a4aab19e9e7c..7e7930f33f1b9 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index bc7c079d7f21b..ed8f592b59638 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name credit memo" store_name=$store.frontend_name}} @--> <!--@vars { -"var comment":"Credit Memo Comment", +"var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new.html b/app/code/Magento/Sales/view/frontend/email/invoice_new.html index ca5f7ee632e22..68773ee9d7570 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout area=\"frontend\" handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -56,7 +62,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html index c93df9f9e8efb..5053ccc2ac635 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var order.shipping_description":"Shipping Description" +"var order.shipping_description":"Shipping Description", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var invoice": "Invoice", +"var order": "Order", +"var order_data.is_not_virtual": "Order Type" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -54,7 +60,7 @@ <h1>{{trans "Your Invoice #%invoice_id for Order #%order_id" invoice_id=$invoice <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -66,10 +72,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update.html b/app/code/Magento/Sales/view/frontend/email/invoice_update.html index cafdd65ff5208..a8f98a238e314 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Invoice Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index fafb533301efb..289c5113fe285 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Invoice Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_new.html b/app/code/Magento/Sales/view/frontend/email/order_new.html index 370bdb0f2f336..13c436b131b82 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new.html @@ -4,16 +4,25 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order area=\"frontend\"":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var created_at_formatted":"Order Created At (datetime)", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.is_not_virtual":"Order Type", +"var order":"Order", +"var order_data.customer_name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} @@ -21,9 +30,9 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%customer_name," customer_name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%customer_name," customer_name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> @@ -38,16 +47,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(2) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -58,7 +67,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -70,10 +79,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html index cfd99e5b0936e..866a1ad87f9b1 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html @@ -4,27 +4,33 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var order.getEmailCustomerNote()":"Email Order Note", -"var order.getBillingAddress().getName()":"Guest Customer Name", -"var order.getCreatedAtFormatted(2)":"Order Created At (datetime)", +"var order_data.email_customer_note|escape|nl2br":"Email Order Note", +"var order.billing_address.name":"Guest Customer Name", +"var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", -"var order.getShippingDescription()":"Shipping Description", -"var shipping_msg":"Shipping message" +"var order.shipping_description":"Shipping Description", +"var shipping_msg":"Shipping message", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual":"Order Type", +"var order":"Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getBillingAddress().getName()}}</p> + <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send an email with a link to track your order."}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -36,16 +42,16 @@ <tr class="email-summary"> <td> <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_id=$order.increment_id |raw}}</h1> - <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$order.getCreatedAtFormatted(2) |raw}}</p> + <p>{{trans 'Placed on <span class="no-link">%created_at</span>' created_at=$created_at_formatted |raw}}</p> </td> </tr> <tr class="email-information"> <td> - {{depend order.getEmailCustomerNote()}} + {{depend order_data.email_customer_note}} <table class="message-info"> <tr> <td> - {{var order.getEmailCustomerNote()|escape|nl2br}} + {{var order_data.email_customer_note|escape|nl2br}} </td> </tr> </table> @@ -56,7 +62,7 @@ <h1>{{trans 'Your Order <span class="no-link">#%increment_id</span>' increment_i <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -68,10 +74,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> {{if shipping_msg}} <p>{{var shipping_msg}}</p> {{/if}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_update.html b/app/code/Magento/Sales/view/frontend/email/order_update.html index a709a9ed8a7f1..b2c4e86654f6f 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 5a39b01810c18..1ce0d162ed76e 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -4,25 +4,29 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name order" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order_data.frontend_status_label":"Order Status", +"var store.frontend_name":"Store Frontend Name", +"var store_email":"Store Email", +"var store_phone":"Store Phone", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new.html b/app/code/Magento/Sales/view/frontend/email/shipment_new.html index 8af49f322c682..39823a0c9d80b 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new.html @@ -4,29 +4,35 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} @@ -53,14 +59,14 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -72,10 +78,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index df1677f56a500..ed2f52ed85066 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -4,28 +4,34 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.getName()":"Guest Customer Name", +"var billing.name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", -"var comment":"Shipment Comment", +"var comment|escape|nl2br":"Shipment Comment", "var shipment.increment_id":"Shipment Id", "layout handle=\"sales_email_order_shipment_items\" shipment=$shipment order=$order":"Shipment Items Grid", "block class='Magento\\\\Framework\\\\View\\\\Element\\\\Template' area='frontend' template='Magento_Sales::email\/shipment\/track.phtml' shipment=$shipment order=$order":"Shipment Track Details", "var formattedShippingAddress|raw":"Shipping Address", "var order.shipping_description":"Shipping Description", -"var order.getShippingDescription()":"Shipping Description" +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours", +"var order_data.is_not_virtual": "Order Type", +"var shipment": "Shipment", +"var order": "Order" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> - {{trans "Thank you for your order from %store_name." store_name=$store.getFrontendName()}} + {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. {{depend store_hours}} {{trans 'Our hours are <span class="no-link">%store_hours</span>.' store_hours=$store_hours |raw}} @@ -51,14 +57,14 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> <h3>{{trans "Billing Info"}}</h3> <p>{{var formattedBillingAddress|raw}}</p> </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="address-details"> <h3>{{trans "Shipping Info"}}</h3> <p>{{var formattedShippingAddress|raw}}</p> @@ -70,10 +76,10 @@ <h3>{{trans "Shipping Info"}}</h3> <h3>{{trans "Payment Method"}}</h3> {{var payment_html|raw}} </td> - {{depend order.getIsNotVirtual()}} + {{depend order_data.is_not_virtual}} <td class="method-info"> <h3>{{trans "Shipping Method"}}</h3> - <p>{{var order.getShippingDescription()}}</p> + <p>{{var order.shipping_description}}</p> </td> {{/depend}} </tr> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update.html b/app/code/Magento/Sales/view/frontend/email/shipment_update.html index 6d9efc37004bc..9d0057f78df7f 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update.html @@ -4,27 +4,31 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var this.getUrl($store, 'customer/account/')":"Customer Account URL", -"var order.getCustomerName()":"Customer Name", -"var comment":"Order Comment", +"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL", +"var order_data.customer_name":"Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.getCustomerName()}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 4896a00b7bc5a..087cb0ddbf5bc 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -4,26 +4,30 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.getFrontendName()}} @--> +<!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.getName()":"Guest Customer Name", -"var comment":"Order Comment", +"var billing.name":"Guest Customer Name", +"var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", -"var shipment.increment_id":"Shipment Id" +"var order_data.frontend_status_label":"Order Status", +"var shipment.increment_id":"Shipment Id", +"var store.frontend_name":"Store Frontend Name", +"var store_phone":"Store Phone", +"var store_email":"Store Email", +"var store_hours":"Store Hours" } @--> {{template config_path="design/email/header_template"}} <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.getName()}}</p> + <p class="greeting">{{trans "%name," name=$billing.name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order_data.frontend_status_label |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml new file mode 100644 index 0000000000000..bbc7f04ce94fd --- /dev/null +++ b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="sales_email_order_shipment_renderers"/> + <body> + <block class="Magento\Framework\View\Element\Template" name="sales.order.email.shipment.track" template="Magento_Sales::email/shipment/track.phtml"> + <arguments> + <argument name="tracking_url" xsi:type="object">Magento\Sales\Block\DataProviders\Email\Shipment\TrackingUrl</argument> + </arguments> + </block> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml b/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml index 0e2689e3e2191..99af16ce629d0 100644 --- a/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml +++ b/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml @@ -12,11 +12,20 @@ <body> <attribute name="class" value="sales-guest-view"/> <referenceContainer name="page.main.title"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.status" template="Magento_Sales::order/order_status.phtml" /> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.date" template="Magento_Sales::order/order_date.phtml" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.status" + template="Magento_Sales::order/order_status.phtml" + cacheable="false" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.date" + template="Magento_Sales::order/order_date.phtml" + cacheable="false" /> </referenceContainer> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="sales.order.print" template="Magento_Sales::order/view.phtml"> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="sales.order.print" + template="Magento_Sales::order/view.phtml" + cacheable="false"> <block class="Magento\Sales\Block\Order\PrintShipment" name="order_items" template="Magento_Sales::order/items.phtml"> <block class="Magento\Framework\View\Element\RendererList" name="sales.order.print.renderers" as="renderer.list" /> <block class="Magento\Sales\Block\Order\Totals" name="order_totals" template="Magento_Sales::order/totals.phtml"> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml b/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml index 50ffe979651ea..4410a6fc4a9a2 100644 --- a/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml +++ b/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml @@ -12,12 +12,21 @@ <body> <attribute name="class" value="account"/> <referenceContainer name="page.main.title"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.status" template="Magento_Sales::order/order_status.phtml" /> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.date" template="Magento_Sales::order/order_date.phtml" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.status" + template="Magento_Sales::order/order_status.phtml" + cacheable="false" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.date" + template="Magento_Sales::order/order_date.phtml" + cacheable="false" /> </referenceContainer> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="sales.order.print" template="Magento_Sales::order/view.phtml"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order_items" template="Magento_Sales::order/items.phtml"> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="sales.order.print" + template="Magento_Sales::order/view.phtml" + cacheable="false"> + <block class="Magento\Sales\Block\Order\Items" name="order_items" template="Magento_Sales::order/items.phtml"> <block class="Magento\Framework\View\Element\RendererList" name="sales.order.print.renderers" as="renderer.list" /> <block class="Magento\Sales\Block\Order\Totals" name="order_totals" template="Magento_Sales::order/totals.phtml"> <arguments> diff --git a/app/code/Magento/Sales/view/frontend/requirejs-config.js b/app/code/Magento/Sales/view/frontend/requirejs-config.js index 04778765f3c97..4d323684afff6 100644 --- a/app/code/Magento/Sales/view/frontend/requirejs-config.js +++ b/app/code/Magento/Sales/view/frontend/requirejs-config.js @@ -6,8 +6,10 @@ var config = { map: { '*': { - giftMessage: 'Magento_Sales/gift-message', - ordersReturns: 'Magento_Sales/orders-returns' + giftMessage: 'Magento_Sales/js/gift-message', + ordersReturns: 'Magento_Sales/js/orders-returns', + 'Magento_Sales/gift-message': 'Magento_Sales/js/gift-message', + 'Magento_Sales/orders-returns': 'Magento_Sales/js/orders-returns' } } }; diff --git a/app/code/Magento/Sales/view/frontend/templates/email/creditmemo/items.phtml b/app/code/Magento/Sales/view/frontend/templates/email/creditmemo/items.phtml index 297e31d6d2c98..90c3ddeee5a30 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/creditmemo/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/creditmemo/items.phtml @@ -4,35 +4,30 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php $_creditmemo = $block->getCreditmemo() ?> <?php $_order = $block->getOrder() ?> -<?php if ($_creditmemo && $_order): ?> +<?php if ($_creditmemo && $_order) : ?> <table class="email-items"> <thead> <tr> <th class="item-info"> - <?= /* @escapeNotVerified */ __('Items') ?> + <?= $block->escapeHtml(__('Items')) ?> </th> <th class="item-qty"> - <?= /* @escapeNotVerified */ __('Qty') ?> + <?= $block->escapeHtml(__('Qty')) ?> </th> <th class="item-subtotal"> - <?= /* @escapeNotVerified */ __('Subtotal') ?> + <?= $block->escapeHtml(__('Subtotal')) ?> </th> </tr> </thead> - <?php foreach ($_creditmemo->getAllItems() as $_item): ?> - <?php - if ($_item->getOrderItem()->getParentItem()) { - continue; - } - ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_creditmemo->getAllItems() as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> <tfoot class="order-totals"> <?= $block->getChildHtml('creditmemo_totals') ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/invoice/items.phtml b/app/code/Magento/Sales/view/frontend/templates/email/invoice/items.phtml index 10e07d1365470..e2efd650295d4 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/invoice/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/invoice/items.phtml @@ -4,35 +4,30 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php $_invoice = $block->getInvoice() ?> <?php $_order = $block->getOrder() ?> -<?php if ($_invoice && $_order): ?> +<?php if ($_invoice && $_order) : ?> <table class="email-items"> <thead> <tr> <th class="item-info"> - <?= /* @escapeNotVerified */ __('Items') ?> + <?= $block->escapeHtml(__('Items')) ?> </th> <th class="item-qty"> - <?= /* @escapeNotVerified */ __('Qty') ?> + <?= $block->escapeHtml(__('Qty')) ?> </th> <th class="item-subtotal"> - <?= /* @escapeNotVerified */ __('Subtotal') ?> + <?= $block->escapeHtml(__('Subtotal')) ?> </th> </tr> </thead> - <?php foreach ($_invoice->getAllItems() as $_item): ?> - <?php - if ($_item->getOrderItem()->getParentItem()) { - continue; - } - ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_invoice->getAllItems() as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> <tfoot class="order-totals"> <?= $block->getChildHtml('invoice_totals') ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items.phtml index 358264463d49a..1bba8166762c7 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items.phtml @@ -4,51 +4,53 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var $block \Magento\Sales\Block\Order\Email\Items */ ?> <?php $_order = $block->getOrder() ?> -<?php if ($_order): ?> +<?php if ($_order) : ?> <?php $_items = $_order->getAllItems(); ?> <table class="email-items"> <thead> <tr> <th class="item-info"> - <?= /* @escapeNotVerified */ __('Items') ?> + <?= $block->escapeHtml(__('Items')) ?> </th> <th class="item-qty"> - <?= /* @escapeNotVerified */ __('Qty') ?> + <?= $block->escapeHtml(__('Qty')) ?> </th> <th class="item-price"> - <?= /* @escapeNotVerified */ __('Price') ?> + <?= $block->escapeHtml(__('Price')) ?> </th> </tr> </thead> - <?php foreach ($_items as $_item): ?> - <?php - if ($_item->getParentItem()) { - continue; - } - ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> <tfoot class="order-totals"> <?= $block->getChildHtml('order_totals') ?> </tfoot> </table> - <?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order', $_order, $_order->getStore()) && $_order->getGiftMessageId()): ?> - <?php $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_order->getGiftMessageId()); ?> - <?php if ($_giftMessage): ?> + <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class) + ->isMessagesAllowed('order', $_order, $_order->getStore()) + && $_order->getGiftMessageId() + ) : ?> + <?php $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class) + ->getGiftMessage($_order->getGiftMessageId()); ?> + <?php if ($_giftMessage) : ?> <br /> <table class="message-gift"> <tr> <td> - <h3><?= /* @escapeNotVerified */ __('Gift Message for this Order') ?></h3> - <strong><?= /* @escapeNotVerified */ __('From:') ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('To:') ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('Message:') ?></strong> + <h3><?= $block->escapeHtml(__('Gift Message for this Order')) ?></h3> + <strong><?= $block->escapeHtml(__('From:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> + <br /><strong><?= $block->escapeHtml(__('To:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> + <br /><strong><?= $block->escapeHtml(__('Message:')) ?></strong> <br /><?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml index 20c2c1869fedb..1066a8dc9b1be 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml @@ -4,21 +4,19 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php $_item = $block->getItem() ?> <?php $_order = $block->getItem()->getOrder(); ?> <tr> - <td class="item-info<?php if ($block->getItemOptions()): ?> has-extra<?php endif; ?>"> + <td class="item-info<?= ($block->getItemOptions() ? ' has-extra' : '') ?>"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - <?php if ($block->getItemOptions()): ?> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> <dd> - <?= /* @escapeNotVerified */ nl2br($option['value']) ?> + <?= /* @noEscape */ nl2br($block->escapeHtml($option['value'])) ?> </dd> <?php endforeach; ?> </dl> @@ -29,8 +27,8 @@ <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQty() * 1 ?></td> + <td class="item-qty"><?= (int) $_item->getQty() ?></td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> + <?= /* @noEscape */ $block->getItemPrice($_item) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/invoice/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/invoice/default.phtml index 1fca65932b0b0..e0a25ce52068d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/invoice/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/invoice/default.phtml @@ -3,34 +3,31 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_item = $block->getItem() ?> <?php $_order = $block->getItem()->getOrder(); ?> <tr> - <td class="item-info<?php if ($block->getItemOptions()): ?> has-extra<?php endif; ?>"> + <td class="item-info<?= ($block->getItemOptions() ? ' has-extra' : '') ?>"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - <?php if ($block->getItemOptions()): ?> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <?php if ($block->getItemOptions()) : ?> <dl> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> <dd> - <?= /* @escapeNotVerified */ nl2br($option['value']) ?> + <?= /* @noEscape */ nl2br($block->escapeHtml($option['value'])) ?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock) : ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQty() * 1 ?></td> + <td class="item-qty"><?= (int) $_item->getQty() ?></td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item->getOrderItem()) ?> + <?= /* @noEscape */ $block->getItemPrice($_item->getOrderItem()) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/order/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/order/default.phtml index 2974e4cd7ad80..a39442136e2f0 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/order/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/order/default.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var $block \Magento\Sales\Block\Order\Email\Items\DefaultItems */ @@ -13,15 +13,15 @@ $_item = $block->getItem(); $_order = $_item->getOrder(); ?> <tr> - <td class="item-info<?php if ($block->getItemOptions()): ?> has-extra<?php endif; ?>"> + <td class="item-info<?= ($block->getItemOptions() ? ' has-extra' : '') ?>"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - <?php if ($block->getItemOptions()): ?> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <?php if ($block->getItemOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> <dd> - <?= /* @escapeNotVerified */ nl2br($option['value']) ?> + <?= /* @noEscape */ nl2br($block->escapeHtml($option['value'])) ?> </dd> <?php endforeach; ?> </dl> @@ -32,21 +32,24 @@ $_order = $_item->getOrder(); <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQtyOrdered() * 1 ?></td> + <td class="item-qty"><?= (int) $_item->getQtyOrdered() ?></td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> + <?= /* @noEscape */ $block->getItemPrice($_item) ?> </td> </tr> -<?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessage($_item->getGiftMessageId())): ?> -<tr> +<?php if ($_item->getGiftMessageId() + && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class) + ->getGiftMessage($_item->getGiftMessageId()) +) : ?> + <tr> <td colspan="3" class="item-extra"> <table class="message-gift"> <tr> <td> - <h3><?= /* @escapeNotVerified */ __('Gift Message') ?></h3> - <strong><?= /* @escapeNotVerified */ __('From:') ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('To:') ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> - <br /><strong><?= /* @escapeNotVerified */ __('Message:') ?></strong> + <h3><?= $block->escapeHtml(__('Gift Message')) ?></h3> + <strong><?= $block->escapeHtml(__('From:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getSender()) ?> + <br /><strong><?= $block->escapeHtml(__('To:')) ?></strong> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> + <br /><strong><?= $block->escapeHtml(__('Message:')) ?></strong> <br /><?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/price/row.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/price/row.phtml index 106aeb16c2897..bafae71ba75b4 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/price/row.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/price/row.phtml @@ -4,8 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?php /** @var \Magento\Sales\Block\Order\Email\Items\DefaultItems $block */ @@ -16,4 +15,4 @@ $_item = $block->getItem(); $_order = $_item->getOrder(); ?> -<?= /* @escapeNotVerified */ $_order->formatPrice($_item->getRowTotal()) ?> +<?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/shipment/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/shipment/default.phtml index f41a09f5da0f3..3cab6ae1ed993 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/shipment/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/shipment/default.phtml @@ -4,29 +4,27 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $_item \Magento\Sales\Model\Order\Item */ $_item = $block->getItem() ?> <tr> - <td class="item-info<?php if ($block->getItemOptions()): ?> has-extra<?php endif; ?>"> + <td class="item-info<?= ($block->getItemOptions() ? ' has-extra' : '') ?>"> <p class="product-name"><?= $block->escapeHtml($_item->getName()) ?></p> - <p class="sku"><?= /* @escapeNotVerified */ __('SKU') ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> - <?php if ($block->getItemOptions()): ?> + <p class="sku"><?= $block->escapeHtml(__('SKU')) ?>: <?= $block->escapeHtml($block->getSku($_item)) ?></p> + <?php if ($block->getItemOptions()) : ?> <dl class="item-options"> - <?php foreach ($block->getItemOptions() as $option): ?> - <dt><strong><em><?= /* @escapeNotVerified */ $option['label'] ?></em></strong></dt> + <?php foreach ($block->getItemOptions() as $option) : ?> + <dt><strong><em><?= $block->escapeHtml($option['label']) ?></em></strong></dt> <dd> - <?= /* @escapeNotVerified */ nl2br($option['value']) ?> + <?= /* @noEscape */ nl2br($option['value']) ?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock) : ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQty() * 1 ?></td> + <td class="item-qty"><?= (int) $_item->getQty() ?></td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/shipment/items.phtml b/app/code/Magento/Sales/view/frontend/templates/email/shipment/items.phtml index 2d2b7b2c2b26e..956705fb7b55d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/shipment/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/shipment/items.phtml @@ -4,32 +4,27 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> <?php $_shipment = $block->getShipment() ?> <?php $_order = $block->getOrder() ?> -<?php if ($_shipment && $_order): ?> +<?php if ($_shipment && $_order) : ?> <table class="email-items"> <thead> <tr> <th class="item-info"> - <?= /* @escapeNotVerified */ __('Items') ?> + <?= $block->escapeHtml(__('Items')) ?> </th> <th class="item-qty"> - <?= /* @escapeNotVerified */ __('Qty') ?> + <?= $block->escapeHtml(__('Qty')) ?> </th> </tr> </thead> - <?php foreach ($_shipment->getAllItems() as $_item): ?> - <?php - if ($_item->getOrderItem()->getParentItem()) { - continue; - } - ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_shipment->getAllItems() as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> </table> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml index 9f7146ab084df..5349ae5f1b1a8 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml @@ -3,28 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> +<?php /* @var \Magento\Framework\View\Element\Template $block */ ?> <?php $_shipment = $block->getShipment() ?> -<?php $_order = $block->getOrder() ?> -<?php if ($_shipment && $_order && $_shipment->getAllTracks()): ?> -<br /> -<table class="shipment-track"> - <thead> - <tr> - <th><?= /* @escapeNotVerified */ __('Shipped By') ?></th> - <th><?= /* @escapeNotVerified */ __('Tracking Number') ?></th> - </tr> - </thead> - <tbody> - <?php foreach ($_shipment->getAllTracks() as $_item): ?> - <tr> - <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> - <td><?= $block->escapeHtml($_item->getNumber()) ?></td> - </tr> - <?php endforeach ?> - </tbody> -</table> +<?php +/* @var \Magento\Sales\Model\Order $_order */ +$_order = $block->getOrder() ?> +<?php if ($_shipment && $_order) : ?> + <?php $trackCollection = $_order->getTracksCollection($_shipment->getId()) ?> + <?php if ($trackCollection) : ?> + <br /> + <table class="shipment-track"> + <thead> + <tr> + <th><?= $block->escapeHtml(__('Shipped By')) ?></th> + <th><?= $block->escapeHtml(__('Tracking Number')) ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($trackCollection as $_item) : ?> + <tr> + <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> + <td> + <a href="<?= $block->escapeUrl($block->getTrackingUrl()->getUrl($_item)) ?>" target="_blank"> + <?= $block->escapeHtml($_item->getNumber()) ?> + </a> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml index 3ebca4d08b349..c6c1910c68d07 100644 --- a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml @@ -4,17 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - ?> -<form class="form form-orders-search" id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{}, "validation":{}}' action="<?= /* @escapeNotVerified */ $block->getActionUrl() ?>" +<form class="form form-orders-search" + id="oar-widget-orders-and-returns-form" + data-mage-init='{"ordersReturns":{}, "validation":{}}' + action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" name="guest_post"> <fieldset class="fieldset"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Order Information') ?></span></legend> + <legend class="legend"><span><?= $block->escapeHtml(__('Order Information')) ?></span></legend> <br> <div class="field id required"> - <label class="label" for="oar-order-id"><span><?= /* @escapeNotVerified */ __('Order ID') ?></span></label> + <label class="label" for="oar-order-id"><span><?= $block->escapeHtml(__('Order ID')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" @@ -22,7 +23,7 @@ </div> </div> <div class="field lastname required"> - <label class="label" for="oar-billing-lastname"><span><?= /* @escapeNotVerified */ __('Billing Last Name') ?></span></label> + <label class="label" for="oar-billing-lastname"><span><?= $block->escapeHtml(__('Billing Last Name')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar-billing-lastname" name="oar_billing_lastname" @@ -30,17 +31,17 @@ </div> </div> <div class="field find required"> - <label class="label" for="quick-search-type-id"><span><?= /* @escapeNotVerified */ __('Find Order By') ?></span></label> + <label class="label" for="quick-search-type-id"><span><?= $block->escapeHtml(__('Find Order By')) ?></span></label> <div class="control"> <select name="oar_type" id="quick-search-type-id" class="select"> - <option value="email"><?= /* @escapeNotVerified */ __('Email') ?></option> - <option value="zip"><?= /* @escapeNotVerified */ __('ZIP Code') ?></option> + <option value="email"><?= $block->escapeHtml(__('Email')) ?></option> + <option value="zip"><?= $block->escapeHtml(__('ZIP Code')) ?></option> </select> </div> </div> <div id="oar-email" class="field email required"> - <label class="label" for="oar_email"><span><?= /* @escapeNotVerified */ __('Email') ?></span></label> + <label class="label" for="oar_email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input type="email" class="input-text" id="oar_email" name="oar_email" @@ -48,7 +49,7 @@ </div> </div> <div id="oar-zip" class="field zip required"> - <label class="label" for="oar_zip"><span><?= /* @escapeNotVerified */ __('Billing ZIP Code') ?></span></label> + <label class="label" for="oar_zip"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" data-validate="{required:true}"/> @@ -56,9 +57,10 @@ </div> </fieldset> <div class="actions-toolbar"> + <?= $block->getBlockHtml('formkey')?> <div class="primary"> - <button type="submit" title="<?= /* @escapeNotVerified */ __('Continue') ?>" class="action submit primary"> - <span><?= /* @escapeNotVerified */ __('Continue') ?></span> + <button type="submit" title="<?= $block->escapeHtml(__('Continue')) ?>" class="action submit primary"> + <span><?= $block->escapeHtml(__('Continue')) ?></span> </button> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/items/price/row.phtml b/app/code/Magento/Sales/view/frontend/templates/items/price/row.phtml index bcf740307af15..73350c5b0c47f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/items/price/row.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/items/price/row.phtml @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer $block */ $_item = $block->getItem(); ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <span class="cart-price"> - <?= /* @escapeNotVerified */ $block->getOrder()->formatPrice($block->getItem()->getRowTotal()) ?> + <?= /* @noEscape */ $block->getOrder()->formatPrice($block->getItem()->getRowTotal()) ?> </span> </span> diff --git a/app/code/Magento/Sales/view/frontend/templates/items/price/total_after_discount.phtml b/app/code/Magento/Sales/view/frontend/templates/items/price/total_after_discount.phtml index dfb476ee381c1..458c6d1e32fa8 100644 --- a/app/code/Magento/Sales/view/frontend/templates/items/price/total_after_discount.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/items/price/total_after_discount.phtml @@ -4,10 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable PSR2.Files.ClosingTag /** @var \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer $block */ $_item = $block->getItem(); ?> -<?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> -<?= /* @escapeNotVerified */ $_order->formatPrice($block->getTotalAmount($_item)) ?> +<?php +/** @var \Magento\Sales\Model\Order $_order */ +$_order = $block->getItem()->getOrderItem()->getOrder() ?> +<?= /* @noEscape */ $_order->formatPrice($block->getTotalAmount($_item)) ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/items/price/unit.phtml b/app/code/Magento/Sales/view/frontend/templates/items/price/unit.phtml index 7a484fed719f8..1b2f15133da8c 100644 --- a/app/code/Magento/Sales/view/frontend/templates/items/price/unit.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/items/price/unit.phtml @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer $block */ $_item = $block->getItem(); ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>"> <span class="cart-price"> - <?= /* @escapeNotVerified */ $block->getOrder()->formatPrice($block->getItem()->getPrice()) ?> + <?= /* @noEscape */ $block->getOrder()->formatPrice($block->getItem()->getPrice()) ?> </span> </span> diff --git a/app/code/Magento/Sales/view/frontend/templates/js/components.phtml b/app/code/Magento/Sales/view/frontend/templates/js/components.phtml index bad5acc209b5f..13f44b97fc789 100644 --- a/app/code/Magento/Sales/view/frontend/templates/js/components.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/js/components.phtml @@ -4,7 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable PSR2.Files.ClosingTag ?> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/comments.phtml b/app/code/Magento/Sales/view/frontend/templates/order/comments.phtml index 589b857239971..448ced1d35f8f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/comments.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/comments.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @@ -13,12 +10,15 @@ * @see \Magento\Sales\Block\Order\Comments */ ?> -<?php if ($block->hasComments()):?> +<?php if ($block->hasComments()) : ?> <div class="order additional details comments"> - <h3 class="subtitle"><?= /* @escapeNotVerified */ $block->getTitle() ?></h3> + <h3 class="subtitle"><?= $block->escapeHtml($block->getTitle()) ?></h3> <dl class="order comments"> - <?php foreach ($block->getComments() as $_commentItem): ?> - <dt class="comment date"><?= /* @escapeNotVerified */ $block->formatDate($_commentItem->getCreatedAt(), \IntlDateFormatter::MEDIUM, true) ?></dt> + <?php foreach ($block->getComments() as $_commentItem) : ?> + <dt class="comment date"> + <?= /* @noEscape */ + $block->formatDate($_commentItem->getCreatedAt(), \IntlDateFormatter::MEDIUM, true) ?> + </dt> <dd class="comment text"><?= $block->escapeHtml($_commentItem->getComment()) ?></dd> <?php endforeach; ?> </dl> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo.phtml index a6a9b64d2c784..3d8853bbe0366 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo.phtml @@ -3,13 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Order\Creditmemo */ ?> <div class="order-details-items creditmemo"> <?= $block->getChildHtml('creditmemo_items') ?> <div class="actions-toolbar"> <div class="secondary"> - <a href="<?= /* @escapeNotVerified */ $block->getBackUrl() ?>" class="action back"> - <span><?= /* @escapeNotVerified */ $block->getBackTitle() ?></span> + <a href="<?= $block->escapeUrl($block->getBackUrl()) ?>" class="action back"> + <span><?= $block->escapeHtml($block->getBackTitle()) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml index a90300efaf5c9..019baeea54e23 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml @@ -3,56 +3,51 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> - <a href="<?= /* @escapeNotVerified */ $block->getPrintAllCreditmemosUrl($_order) ?>" + <a href="<?= $block->escapeUrl($block->getPrintAllCreditmemosUrl($_order)) ?>" onclick="this.target='_blank'" class="action print"> - <span><?= /* @escapeNotVerified */ __('Print All Refunds') ?></span> - </a> -</div> -<?php foreach ($_order->getCreditmemosCollection() as $_creditmemo): ?> -<div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Refund #') ?><?= /* @escapeNotVerified */ $_creditmemo->getIncrementId() ?> </strong> - <a href="<?= /* @escapeNotVerified */ $block->getPrintCreditmemoUrl($_creditmemo) ?>" - onclick="this.target='_blank'" - class="action print"> - <span><?= /* @escapeNotVerified */ __('Print Refund') ?></span> + <span><?= $block->escapeHtml(__('Print All Refunds')) ?></span> </a> </div> +<?php foreach ($_order->getCreditmemosCollection() as $_creditmemo) : ?> + <div class="order-title"> + <strong><?= $block->escapeHtml(__('Refund #')) ?><?= $block->escapeHtml($_creditmemo->getIncrementId()) ?> </strong> + <a href="<?= $block->escapeUrl($block->getPrintCreditmemoUrl($_creditmemo)) ?>" + onclick="this.target='_blank'" + class="action print"> + <span><?= $block->escapeHtml(__('Print Refund')) ?></span> + </a> + </div> -<div class="table-wrapper order-items-creditmemo"> - <table class="data table table-order-items creditmemo" id="my-refund-table-<?= /* @escapeNotVerified */ $_creditmemo->getId() ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Refunded') ?></caption> - <thead> - <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty') ?></th> - <th class="col subtotal"><?= /* @escapeNotVerified */ __('Subtotal') ?></th> - <th class="col discount"><?= /* @escapeNotVerified */ __('Discount Amount') ?></th> - <th class="col total"><?= /* @escapeNotVerified */ __('Row Total') ?></th> - </tr> - </thead> - <?php $_items = $_creditmemo->getAllItems(); ?> - <?php $_count = count($_items) ?> - <?php foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; -} ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> - <?php endforeach; ?> - <tfoot> - <?= $block->getTotalsHtml($_creditmemo) ?> - </tfoot> - </table> -</div> -<?= $block->getCommentsHtml($_creditmemo) ?> + <div class="table-wrapper order-items-creditmemo"> + <table class="data table table-order-items creditmemo" id="my-refund-table-<?= (int) $_creditmemo->getId() ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Refunded')) ?></caption> + <thead> + <tr> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty')) ?></th> + <th class="col subtotal"><?= $block->escapeHtml(__('Subtotal')) ?></th> + <th class="col discount"><?= $block->escapeHtml(__('Discount Amount')) ?></th> + <th class="col total"><?= $block->escapeHtml(__('Row Total')) ?></th> + </tr> + </thead> + <?php $_items = $_creditmemo->getAllItems(); ?> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> + <?php endforeach; ?> + <tfoot> + <?= $block->getTotalsHtml($_creditmemo) ?> + </tfoot> + </table> + </div> + <?= $block->getCommentsHtml($_creditmemo) ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index 5542439da17fd..27e7715c2dc33 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -3,45 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>"> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> +<tr id="order-item-row-<?= (int) $_item->getId() ?>"> + <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()): ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> - <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> - </dl> + <?php if ($_options = $block->getItemOptions()) : ?> + <dl class="item-options"> + <?php foreach ($_options as $_option) : ?> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php if (!$block->getPrintStatus()) : ?> + <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> + <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> + <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <div class="tooltip content"> + <dl class="item options"> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dd><?= $block->escapeHtml($_formatedOptionValue['full_view']) ?></dd> + </dl> + </div> + <?php endif; ?> + </dd> + <?php else : ?> + <dd> + <?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?> + </dd> + <?php endif; ?> + <?php endforeach; ?> + </dl> <?php endif; ?> <?php /* downloadable */ ?> - <?php if ($links = $block->getLinks()): ?> + <?php if ($links = $block->getLinks()) : ?> <dl class="item options"> - <dt><?= /* @escapeNotVerified */ $block->getLinksTitle() ?></dt> - <?php foreach ($links->getPurchasedItems() as $link): ?> + <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> + <?php foreach ($links->getPurchasedItems() as $link) : ?> <dd><?= $block->escapeHtml($link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> @@ -49,20 +48,20 @@ <?php /* EOF downloadable */ ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock) : ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></td> + <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"><?= (int) $_item->getQty() ?></td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> <?= $block->getItemRowTotalHtml() ?> </td> - <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"><?= /* @escapeNotVerified */ $_order->formatPrice(-$_item->getDiscountAmount()) ?></td> + <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"><?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?></td> <td class="col total" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> <?= $block->getItemRowTotalAfterDiscountHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml index 1c02a5c31ea6b..ef56ad69dcb1b 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml @@ -4,49 +4,50 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var \Magento\Sales\Block\Order\History $block */ ?> <?php $_orders = $block->getOrders(); ?> <?= $block->getChildHtml('info') ?> -<?php if ($_orders && count($_orders)): ?> +<?php if ($_orders && count($_orders)) : ?> <div class="table-wrapper orders-history"> <table class="data table table-order-items history" id="my-orders-table"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Orders') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('Orders')) ?></caption> <thead> <tr> - <th scope="col" class="col id"><?= /* @escapeNotVerified */ __('Order #') ?></th> - <th scope="col" class="col date"><?= /* @escapeNotVerified */ __('Date') ?></th> - <?= /* @noEscape */ $block->getChildHtml('extra.column.header') ?> - <th scope="col" class="col shipping"><?= /* @escapeNotVerified */ __('Ship To') ?></th> - <th scope="col" class="col total"><?= /* @escapeNotVerified */ __('Order Total') ?></th> - <th scope="col" class="col status"><?= /* @escapeNotVerified */ __('Status') ?></th> - <th scope="col" class="col actions"><?= /* @escapeNotVerified */ __('Action') ?></th> + <th scope="col" class="col id"><?= $block->escapeHtml(__('Order #')) ?></th> + <th scope="col" class="col date"><?= $block->escapeHtml(__('Date')) ?></th> + <?= $block->getChildHtml('extra.column.header') ?> + <th scope="col" class="col shipping"><?= $block->escapeHtml(__('Ship To')) ?></th> + <th scope="col" class="col total"><?= $block->escapeHtml(__('Order Total')) ?></th> + <th scope="col" class="col status"><?= $block->escapeHtml(__('Status')) ?></th> + <th scope="col" class="col actions"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> <tbody> - <?php foreach ($_orders as $_order): ?> + <?php foreach ($_orders as $_order) : ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= /* @escapeNotVerified */ $_order->getRealOrderId() ?></td> - <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= /* @escapeNotVerified */ $block->formatDate($_order->getCreatedAt()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= $block->escapeHtml($_order->getRealOrderId()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= /* @noEscape */ $block->formatDate($_order->getCreatedAt()) ?></td> <?php $extra = $block->getChildBlock('extra.container'); ?> - <?php if ($extra): ?> + <?php if ($extra) : ?> <?php $extra->setOrder($_order); ?> - <?= /* @noEscape */ $extra->getChildHtml() ?> + <?= $extra->getChildHtml() ?> <?php endif; ?> <td data-th="<?= $block->escapeHtml(__('Ship To')) ?>" class="col shipping"><?= $_order->getShippingAddress() ? $block->escapeHtml($_order->getShippingAddress()->getName()) : ' ' ?></td> - <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getGrandTotal()) ?></td> - <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= /* @escapeNotVerified */ $_order->getStatusLabel() ?></td> + <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= $block->escapeHtml($_order->getStatusLabel()) ?></td> <td data-th="<?= $block->escapeHtml(__('Actions')) ?>" class="col actions"> - <a href="<?= /* @escapeNotVerified */ $block->getViewUrl($_order) ?>" class="action view"> - <span><?= /* @escapeNotVerified */ __('View Order') ?></span> + <a href="<?= $block->escapeUrl($block->getViewUrl($_order)) ?>" class="action view"> + <span><?= $block->escapeHtml(__('View Order')) ?></span> </a> - <?php if ($this->helper('Magento\Sales\Helper\Reorder')->canReorder($_order->getEntityId())) : ?> - <a href="#" data-post='<?php /* @escapeNotVerified */ echo + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class)->canReorder($_order->getEntityId())) : ?> + <a href="#" data-post='<?= /* @noEscape */ $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) ->getPostData($block->getReorderUrl($_order)) ?>' class="action order"> - <span><?= /* @escapeNotVerified */ __('Reorder') ?></span> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> </a> <?php endif ?> </td> @@ -55,9 +56,9 @@ </tbody> </table> </div> - <?php if ($block->getPagerHtml()): ?> + <?php if ($block->getPagerHtml()) : ?> <div class="order-products-toolbar toolbar bottom"><?= $block->getPagerHtml() ?></div> <?php endif ?> -<?php else: ?> - <div class="message info empty"><span><?= /* @escapeNotVerified */ __('You have placed no orders.') ?></span></div> +<?php else : ?> + <div class="message info empty"><span><?= $block->escapeHtml(__('You have placed no orders.')) ?></span></div> <?php endif ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/info.phtml b/app/code/Magento/Sales/view/frontend/templates/order/info.phtml index 2a31814d9a054..1bf05f4fed7f7 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/info.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/info.phtml @@ -3,50 +3,47 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Order\Info */ ?> <?php $_order = $block->getOrder() ?> <div class="block block-order-details-view"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Order Information') ?></strong> + <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> </div> <div class="block-content"> - <?php if (!$_order->getIsVirtual()): ?> - <div class="box box-order-shipping-address"> - <strong class="box-title"><span><?= /* @escapeNotVerified */ __('Shipping Address') ?></span></strong> - <div class="box-content"> - <address><?= /* @escapeNotVerified */ $block->getFormattedAddress($_order->getShippingAddress()) ?></address> + <?php if (!$_order->getIsVirtual()) : ?> + <div class="box box-order-shipping-address"> + <strong class="box-title"><span><?= $block->escapeHtml(__('Shipping Address')) ?></span></strong> + <div class="box-content"> + <address><?= /* @noEscape */ $block->getFormattedAddress($_order->getShippingAddress()) ?></address> + </div> </div> - </div> - <div class="box box-order-shipping-method"> - <strong class="box-title"> - <span><?= /* @escapeNotVerified */ __('Shipping Method') ?></span> - </strong> - <div class="box-content"> - <?php if ($_order->getShippingDescription()): ?> - <?= $block->escapeHtml($_order->getShippingDescription()) ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('No shipping information available') ?> - <?php endif; ?> + <div class="box box-order-shipping-method"> + <strong class="box-title"> + <span><?= $block->escapeHtml(__('Shipping Method')) ?></span> + </strong> + <div class="box-content"> + <?php if ($_order->getShippingDescription()) : ?> + <?= $block->escapeHtml($_order->getShippingDescription()) ?> + <?php else : ?> + <?= $block->escapeHtml(__('No shipping information available')) ?> + <?php endif; ?> + </div> </div> - </div> - <?php endif; ?> + <?php endif; ?> <div class="box box-order-billing-address"> <strong class="box-title"> - <span><?= /* @escapeNotVerified */ __('Billing Address') ?></span> + <span><?= $block->escapeHtml(__('Billing Address')) ?></span> </strong> <div class="box-content"> - <address><?= /* @escapeNotVerified */ $block->getFormattedAddress($_order->getBillingAddress()) ?></address> + <address><?= /* @noEscape */ $block->getFormattedAddress($_order->getBillingAddress()) ?></address> </div> </div> <div class="box box-order-billing-method"> <strong class="box-title"> - <span><?= /* @escapeNotVerified */ __('Payment Method') ?></span> + <span><?= $block->escapeHtml(__('Payment Method')) ?></span> </strong> <div class="box-content"> <?= $block->getPaymentInfoHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml index fa3f63b61cd07..6b87d3c22331c 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml @@ -4,23 +4,22 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <div class="actions"> - <?php $_order = $block->getOrder() ?> - <?php if ($this->helper('Magento\Sales\Helper\Reorder')->canReorder($_order->getEntityId())) : ?> - <a href="#" data-post='<?php /* @escapeNotVerified */ echo - $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + <?php $_order = $block->getOrder() ?> + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class)->canReorder($_order->getEntityId())) : ?> + <a href="#" data-post='<?= + /* @noEscape */ $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) ->getPostData($block->getReorderUrl($_order)) ?>' class="action order"> - <span><?= /* @escapeNotVerified */ __('Reorder') ?></span> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> </a> <?php endif ?> <a class="action print" - href="<?= /* @escapeNotVerified */ $block->getPrintUrl($_order) ?>" + href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" onclick="this.target='_blank';"> - <span><?= /* @escapeNotVerified */ __('Print Order') ?></span> + <span><?= $block->escapeHtml(__('Print Order')) ?></span> </a> <?= $block->getChildHtml() ?> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons/rss.phtml b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons/rss.phtml index 3ea1bf6452c03..e29d1aa1f849f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons/rss.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons/rss.phtml @@ -4,12 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Sales\Block\Order\Info\Buttons\Rss */ ?> -<?php if ($block->isRssAllowed() && $block->getLink()): ?> -<a href="<?= /* @escapeNotVerified */ $block->getLink() ?>" class="action rss"> - <span><?= /* @escapeNotVerified */ $block->getLabel() ?></span> -</a> +<?php if ($block->isRssAllowed() && $block->getLink()) : ?> + <a href="<?= $block->escapeHtmlAttr($block->getLink()) ?>" class="action rss"> + <span><?= $block->escapeHtml($block->getLabel()) ?></span> + </a> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice.phtml index 699067dc9612c..017331ad72b76 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice.phtml @@ -3,13 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Order\Invoice */ ?> <div class="order-details-items invoice"> <?= $block->getChildHtml('invoice_items') ?> <div class="actions-toolbar"> <div class="secondary"> - <a href="<?= /* @escapeNotVerified */ $block->getBackUrl() ?>" class="action back"> - <span><?= /* @escapeNotVerified */ $block->getBackTitle() ?></span> + <a href="<?= $block->escapeUrl($block->getBackUrl()) ?>" class="action back"> + <span><?= $block->escapeHtml($block->getBackTitle()) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml index 980710569fa3b..419060bfba713 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml @@ -3,53 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> - <a href="<?= /* @escapeNotVerified */ $block->getPrintAllInvoicesUrl($_order) ?>" + <a href="<?= $block->escapeUrl($block->getPrintAllInvoicesUrl($_order)) ?>" target="_blank" class="action print"> - <span><?= /* @escapeNotVerified */ __('Print All Invoices') ?></span> + <span><?= $block->escapeHtml(__('Print All Invoices')) ?></span> </a> </div> -<?php foreach ($_order->getInvoiceCollection() as $_invoice): ?> -<div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Invoice #') ?><?= /* @escapeNotVerified */ $_invoice->getIncrementId() ?></strong> - <a href="<?= /* @escapeNotVerified */ $block->getPrintInvoiceUrl($_invoice) ?>" - onclick="this.target='_blank'" - class="action print"> - <span><?= /* @escapeNotVerified */ __('Print Invoice') ?></span> - </a> -</div> -<div class="table-wrapper table-order-items invoice"> - <table class="data table table-order-items invoice" id="my-invoice-table-<?= /* @escapeNotVerified */ $_invoice->getId() ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Invoiced') ?></caption> - <thead> - <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty Invoiced') ?></th> - <th class="col subtotal"><?= /* @escapeNotVerified */ __('Subtotal') ?></th> - </tr> - </thead> - <?php $_items = $_invoice->getAllItems(); ?> - <?php $_count = count($_items) ?> - <?php foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; -} ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> - <?php endforeach; ?> - <tfoot> - <?= $block->getInvoiceTotalsHtml($_invoice) ?> - </tfoot> - </table> -</div> -<?= $block->getInvoiceCommentsHtml($_invoice) ?> +<?php foreach ($_order->getInvoiceCollection() as $_invoice) : ?> + <div class="order-title"> + <strong><?= $block->escapeHtml(__('Invoice #')) ?><?= $block->escapeHtml($_invoice->getIncrementId()) ?></strong> + <a href="<?= $block->escapeUrl($block->getPrintInvoiceUrl($_invoice)) ?>" + onclick="this.target='_blank'" + class="action print"> + <span><?= $block->escapeHtml(__('Print Invoice')) ?></span> + </a> + </div> + <div class="table-wrapper table-order-items invoice"> + <table class="data table table-order-items invoice" id="my-invoice-table-<?= (int) $_invoice->getId() ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Invoiced')) ?></caption> + <thead> + <tr> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty Invoiced')) ?></th> + <th class="col subtotal"><?= $block->escapeHtml(__('Subtotal')) ?></th> + </tr> + </thead> + <?php $_items = $_invoice->getAllItems(); ?> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> + <?php endforeach; ?> + <tfoot> + <?= $block->getInvoiceTotalsHtml($_invoice) ?> + </tfoot> + </table> + </div> + <?= $block->getInvoiceCommentsHtml($_invoice) ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index 4f95f3d93e2c2..ece72e9119d1e 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -3,38 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>"> +<tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()): ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> - <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> - </dl> + <?php if ($_options = $block->getItemOptions()) : ?> + <dl class="item-options"> + <?php foreach ($_options as $_option) : ?> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php if (!$block->getPrintStatus()) : ?> + <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> + <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> + <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <div class="tooltip content"> + <dl class="item options"> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dd><?= $block->escapeHtml($_formatedOptionValue['full_view']) ?></dd> + </dl> + </div> + <?php endif; ?> + </dd> + <?php else : ?> + <dd><?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?></dd> + <?php endif; ?> + <?php endforeach; ?> + </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> <?php if ($addInfoBlock) :?> @@ -42,12 +39,12 @@ <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Invoiced')) ?>"> - <span class="qty summary"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></span> + <span class="qty summary"><?= (int) $_item->getQty() ?></span> </td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> <?= $block->getItemRowTotalHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml index e43d32760febb..a99d90c8c310d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate /** @var \Magento\Sales\Block\Order\Items $block */ ?> <div class="table-wrapper order-items"> - <table class="data table table-order-items" id="my-orders-table" summary="<?= /* @escapeNotVerified */ __('Items Ordered') ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Ordered') ?></caption> + <table class="data table table-order-items" id="my-orders-table" summary="<?= $block->escapeHtmlAttr(__('Items Ordered')) ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Ordered')) ?></caption> <thead> - <?php if($block->isPagerDisplayed()): ?> + <?php if ($block->isPagerDisplayed()) : ?> <tr> <td colspan="5" data-block="order-items-pager-top" class="order-pager-wrapper order-pager-wrapper-top"> <?= $block->getPagerHtml() ?> @@ -20,52 +20,55 @@ </tr> <?php endif ?> <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty') ?></th> - <th class="col subtotal"><?= /* @escapeNotVerified */ __('Subtotal') ?></th> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty')) ?></th> + <th class="col subtotal"><?= $block->escapeHtml(__('Subtotal')) ?></th> </tr> </thead> <?php $items = $block->getItems(); ?> <?php $giftMessage = ''?> - <?php foreach ($items as $item): ?> - <?php if ($item->getParentItem()) continue; ?> - <tbody> + <tbody> + <?php foreach ($items as $item) : + if ($item->getParentItem()) : + continue; + endif; + ?> <?= $block->getItemHtml($item) ?> - <?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $item) && $item->getGiftMessageId()): ?> - <?php $giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($item); ?> + <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order_item', $item) && $item->getGiftMessageId()) : ?> + <?php $giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessageForEntity($item); ?> <tr> <td class="col options" colspan="5"> <a href="#" - id="order-item-gift-message-link-<?= /* @escapeNotVerified */ $item->getId() ?>" + id="order-item-gift-message-link-<?= (int) $item->getId() ?>" class="action show" - aria-controls="order-item-gift-message-<?= /* @escapeNotVerified */ $item->getId() ?>" - data-item-id="<?= /* @escapeNotVerified */ $item->getId() ?>"> - <?= /* @escapeNotVerified */ __('Gift Message') ?> + aria-controls="order-item-gift-message-<?= (int) $item->getId() ?>" + data-item-id="<?= (int) $item->getId() ?>"> + <?= $block->escapeHtml(__('Gift Message')) ?> </a> - <?php $giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($item); ?> - <div class="order-gift-message" id="order-item-gift-message-<?= /* @escapeNotVerified */ $item->getId() ?>" role="region" aria-expanded="false" tabindex="-1"> + <?php $giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessageForEntity($item); ?> + <div class="order-gift-message" id="order-item-gift-message-<?= (int) $item->getId() ?>" role="region" aria-expanded="false" tabindex="-1"> <a href="#" - title="<?= /* @escapeNotVerified */ __('Close') ?>" - aria-controls="order-item-gift-message-<?= /* @escapeNotVerified */ $item->getId() ?>" - data-item-id="<?= /* @escapeNotVerified */ $item->getId() ?>" + title="<?= $block->escapeHtml(__('Close')) ?>" + aria-controls="order-item-gift-message-<?= (int) $item->getId() ?>" + data-item-id="<?= (int) $item->getId() ?>" class="action close"> - <?= /* @escapeNotVerified */ __('Close') ?> + <?= $block->escapeHtml(__('Close')) ?> </a> <dl class="item-options"> - <dt class="item-sender"><strong class="label"><?= /* @escapeNotVerified */ __('From') ?></strong><?= $block->escapeHtml($giftMessage->getSender()) ?></dt> - <dt class="item-recipient"><strong class="label"><?= /* @escapeNotVerified */ __('To') ?></strong><?= $block->escapeHtml($giftMessage->getRecipient()) ?></dt> - <dd class="item-message"><?= /* @escapeNotVerified */ $this->helper('Magento\GiftMessage\Helper\Message')->getEscapedGiftMessage($item) ?></dd> + <dt class="item-sender"><strong class="label"><?= $block->escapeHtml(__('From')) ?></strong><?= $block->escapeHtml($giftMessage->getSender()) ?></dt> + <dt class="item-recipient"><strong class="label"><?= $block->escapeHtml(__('To')) ?></strong><?= $block->escapeHtml($giftMessage->getRecipient()) ?></dt> + <dd class="item-message"><?= /* @noEscape */ $this->helper(\Magento\GiftMessage\Helper\Message::class)->getEscapedGiftMessage($item) ?></dd> </dl> </div> </td> </tr> <?php endif ?> - </tbody> <?php endforeach; ?> + </tbody> <tfoot> - <?php if($block->isPagerDisplayed()): ?> + <?php if ($block->isPagerDisplayed()) : ?> <tr> <td colspan="5" data-block="order-items-pager-bottom" class="order-pager-wrapper order-pager-wrapper-bottom"> <?= $block->getPagerHtml() ?> @@ -76,7 +79,7 @@ </tfoot> </table> </div> -<?php if ($giftMessage): ?> +<?php if ($giftMessage) : ?> <script type="text/x-magento-init"> { "a.action.show, a.action.close": { diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index d4550dd4f01c3..aec0ec6b4fe2d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -4,69 +4,67 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer */ $_item = $block->getItem(); ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>"> +<tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()): ?> - <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> - <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php endif; ?> - </dd> - <?php else: ?> - <dd> - <?= nl2br($block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value']))) ?> - </dd> - <?php endif; ?> - <?php endforeach; ?> - </dl> + <?php if ($_options = $block->getItemOptions()) : ?> + <dl class="item-options"> + <?php foreach ($_options as $_option) : ?> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php if (!$block->getPrintStatus()) : ?> + <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> + <dd> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['full_view'], ['a']) ?> + <?php else : ?> + <?=$block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php endif; ?> + </dd> + <?php else : ?> + <dd> + <?= /* @noEscape */ nl2br($block->escapeHtml($_option['print_value'] ?? $_option['value'])) ?> + </dd> + <?php endif; ?> + <?php endforeach; ?> + </dl> <?php endif; ?> <?php $addtInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addtInfoBlock) :?> + <?php if ($addtInfoBlock) : ?> <?= $addtInfoBlock->setItem($_item)->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <ul class="items-qty"> - <?php if ($block->getItem()->getQtyOrdered() > 0): ?> + <?php if ($block->getItem()->getQtyOrdered() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Ordered') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $block->getItem()->getQtyOrdered()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Ordered')) ?></span> + <span class="content"><?= (int) $block->getItem()->getQtyOrdered() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyShipped() > 0): ?> + <?php if ($block->getItem()->getQtyShipped() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $block->getItem()->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')) ?></span> + <span class="content"><?= (int) $block->getItem()->getQtyShipped() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyCanceled() > 0): ?> + <?php if ($block->getItem()->getQtyCanceled() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Canceled') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $block->getItem()->getQtyCanceled()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Canceled')) ?></span> + <span class="content"><?= (int) $block->getItem()->getQtyCanceled() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyRefunded() > 0): ?> + <?php if ($block->getItem()->getQtyRefunded() > 0) : ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Refunded') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $block->getItem()->getQtyRefunded()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Refunded')) ?></span> + <span class="content"><?= (int) $block->getItem()->getQtyRefunded() ?></span> </li> <?php endif; ?> </ul> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml b/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml index 8a8e7b1194caa..6f2d8d87ade86 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/order_comments.phtml @@ -3,24 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Sales\Block\Order\View*/?> <?php $_history = $block->getOrder()->getVisibleStatusHistory() ?> -<?php if (count($_history)): ?> +<?php if (!empty($_history)) : ?> <div class="block block-order-details-comments"> - <div class="block-title"><strong><?= /* @escapeNotVerified */ __('About Your Order') ?></strong></div> + <div class="block-title"><strong><?= $block->escapeHtml(__('About Your Order')) ?></strong></div> <div class="block-content"> <dl class="order-comments"> - <?php foreach ($_history as $_historyItem): ?> - <dt class="comment-date"><?= /* @escapeNotVerified */ $block->formatDate($_historyItem->getCreatedAt(), \IntlDateFormatter::MEDIUM, true) ?></dt> + <?php foreach ($_history as $_historyItem) : ?> + <dt class="comment-date"> + <?= /* @noEscape */ + $block->formatDate($_historyItem->getCreatedAt(), \IntlDateFormatter::MEDIUM, true) ?> + </dt> <dd class="comment-content"><?= $block->escapeHtml($_historyItem->getComment()) ?></dd> <?php endforeach; ?> </dl> - </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/order_date.phtml b/app/code/Magento/Sales/view/frontend/templates/order/order_date.phtml index bcd7cc6e0260d..80a0ea02499cc 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/order_date.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/order_date.phtml @@ -3,11 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> - <div class="order-date"> - <?= /* @escapeNotVerified */ __('<span class="label">Order Date:</span> %1', '<date>' . $block->formatDate($block->getOrder()->getCreatedAt(), \IntlDateFormatter::LONG) . '</date>') ?> + <?= $block->escapeHtml(__('<span class="label">Order Date:</span> %1', '<date>' . $block->formatDate($block->getOrder()->getCreatedAt(), \IntlDateFormatter::LONG) . '</date>'), ['span', 'date']) ?> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/order_status.phtml b/app/code/Magento/Sales/view/frontend/templates/order/order_status.phtml index c77014d1bf8c9..7a3ff4398d983 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/order_status.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/order_status.phtml @@ -6,4 +6,4 @@ ?> <?php /** @var $block \Magento\Sales\Block\Order\Info */ ?> -<span class="order-status"><?= /* @escapeNotVerified */ $block->getOrder()->getStatusLabel() ?></span> +<span class="order-status"><?= $block->escapeHtml($block->getOrder()->getStatusLabel()) ?></span> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/print/creditmemo.phtml b/app/code/Magento/Sales/view/frontend/templates/order/print/creditmemo.phtml index d68b4cdb62fcc..05c6048ee15ae 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/print/creditmemo.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/print/creditmemo.phtml @@ -3,45 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_order = $block->getOrder() ?> <?php $_creditmemo = $block->getCreditmemo() ?> -<?php if ($_creditmemo): ?> +<?php if ($_creditmemo) : ?> <?php $_creditmemos = [$_creditmemo]; ?> -<?php else: ?> +<?php else : ?> <?php $_creditmemos = $_order->getCreditmemosCollection() ?> <?php endif; ?> -<?php foreach ($_creditmemos as $_creditmemo): ?> +<?php foreach ($_creditmemos as $_creditmemo) : ?> <div class="order-details-items creditmemo"> <div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Refund #%1', $_creditmemo->getIncrementId()) ?></strong> + <strong><?= $block->escapeHtml(__('Refund #%1', $_creditmemo->getIncrementId())) ?></strong> </div> <div class="table-wrapper order-items-creditmemo"> - <table class="data table table-order-items creditmemo" id="my-refund-table-<?= /* @escapeNotVerified */ $_creditmemo->getId() ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Refunded') ?></caption> + <table class="data table table-order-items creditmemo" id="my-refund-table-<?= (int) $_creditmemo->getId() ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Refunded')) ?></caption> <thead> <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty') ?></th> - <th class="col subtotal"><?= /* @escapeNotVerified */ __('Subtotal') ?></th> - <th class="col discount"><?= /* @escapeNotVerified */ __('Discount Amount') ?></th> - <th class="col rowtotal"><?= /* @escapeNotVerified */ __('Row Total') ?></th> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty')) ?></th> + <th class="col subtotal"><?= $block->escapeHtml(__('Subtotal')) ?></th> + <th class="col discount"><?= $block->escapeHtml(__('Discount Amount')) ?></th> + <th class="col rowtotal"><?= $block->escapeHtml(__('Row Total')) ?></th> </tr> </thead> <?php $_items = $_creditmemo->getAllItems(); ?> - <?php $_count = count($_items); ?> - <?php foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; -} ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> <tfoot> <?= $block->getTotalsHtml($_creditmemo) ?> @@ -50,22 +45,22 @@ </div> <div class="block block-order-details-view"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Order Information') ?></strong> + <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> </div> <div class="block-content"> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="box box-order-shipping-address"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Address') ?></strong> + <strong><?= $block->escapeHtml(__('Shipping Address')) ?></strong> </div> <div class="box-content"> <?php $_shipping = $_creditmemo->getShippingAddress() ?> - <address><?= /* @escapeNotVerified */ $block->formatAddress($_shipping, 'html') ?></address> + <address><?= /* @noEscape */ $block->formatAddress($_shipping, 'html') ?></address> </div> </div> <div class="box box-order-shipping-method"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Method') ?></strong> + <strong><?= $block->escapeHtml(__('Shipping Method')) ?></strong> </div> <div class="box-content"> <?= $block->escapeHtml($_order->getShippingDescription()) ?> @@ -74,16 +69,16 @@ <?php endif; ?> <div class="box box-order-billing-address"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Billing Address') ?></strong> + <strong><?= $block->escapeHtml(__('Billing Address')) ?></strong> </div> <div class="box-content"> <?php $_billing = $_creditmemo->getbillingAddress() ?> - <address><?= /* @escapeNotVerified */ $block->formatAddress($_order->getBillingAddress(), 'html') ?></address> + <address><?= /* @noEscape */ $block->formatAddress($_order->getBillingAddress(), 'html') ?></address> </div> </div> <div class="box box-order-billing-method"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Payment Method') ?></strong> + <strong><?= $block->escapeHtml(__('Payment Method')) ?></strong> </div> <div class="box-content"> <?= $block->getPaymentInfoHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/print/invoice.phtml b/app/code/Magento/Sales/view/frontend/templates/order/print/invoice.phtml index 5aa1cab686e2c..3c0196d59329c 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/print/invoice.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/print/invoice.phtml @@ -3,43 +3,38 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_order = $block->getOrder() ?> <?php $_invoice = $block->getInvoice() ?> -<?php if ($_invoice): ?> -<?php $_invoices = [$_invoice]; ?> -<?php else: ?> -<?php $_invoices = $_order->getInvoiceCollection() ?> +<?php if ($_invoice) : ?> + <?php $_invoices = [$_invoice]; ?> +<?php else : ?> + <?php $_invoices = $_order->getInvoiceCollection() ?> <?php endif; ?> -<?php foreach ($_invoices as $_invoice): ?> +<?php foreach ($_invoices as $_invoice) : ?> <div class="order-details-items invoice"> <div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Invoice #') ?><?= /* @escapeNotVerified */ $_invoice->getIncrementId() ?></strong> + <strong><?= $block->escapeHtml(__('Invoice #')) ?><?= (int) $_invoice->getIncrementId() ?></strong> </div> <div class="table-wrapper table-order-items invoice"> - <table class="data table table-order-items invoice" id="my-invoice-table-<?= /* @escapeNotVerified */ $_invoice->getId() ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Invoiced') ?></caption> + <table class="data table table-order-items invoice" id="my-invoice-table-<?= (int) $_invoice->getId() ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Invoiced')) ?></caption> <thead> <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Price') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty Invoiced') ?></th> - <th class="col subtotal"><?= /* @escapeNotVerified */ __('Subtotal') ?></th> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Price')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty Invoiced')) ?></th> + <th class="col subtotal"><?= $block->escapeHtml(__('Subtotal')) ?></th> </tr> </thead> <?php $_items = $_invoice->getItemsCollection(); ?> - <?php $_count = $_items->count(); ?> - <?php foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; -} ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> <tfoot> <?= $block->getInvoiceTotalsHtml($_invoice) ?> @@ -48,46 +43,46 @@ </div> <div class="block block-order-details-view"> <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Order Information') ?></strong> + <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> </div> <div class="block-content"> - <?php if (!$_order->getIsVirtual()): ?> + <?php if (!$_order->getIsVirtual()) : ?> <div class="box box-order-shipping-address"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Address') ?></strong> + <strong><?= $block->escapeHtml(__('Shipping Address')) ?></strong> </div> <div class="box-content"> <?php $_shipping = $_invoice->getShippingAddress() ?> - <address><?= /* @escapeNotVerified */ $block->formatAddress($_shipping, 'html') ?></address> + <address><?= /* @noEscape */ $block->formatAddress($_shipping, 'html') ?></address> </div> </div> <div class="box box-order-shipping-method"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Method') ?></strong> + <strong><?= $block->escapeHtml(__('Shipping Method')) ?></strong> </div> <div class="box-content"> - <?php if ($_order->getShippingDescription()): ?> + <?php if ($_order->getShippingDescription()) : ?> <?= $block->escapeHtml($_order->getShippingDescription()) ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('No shipping information available') ?> + <?php else : ?> + <?= $block->escapeHtml(__('No shipping information available')) ?> <?php endif; ?> </div> </div> <?php endif; ?> <div class="box box-order-billing-address"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Billing Address') ?></strong> + <strong><?= $block->escapeHtml(__('Billing Address')) ?></strong> </div> <div class="box-content"> <?php $_billing = $_invoice->getbillingAddress() ?> - <address><?= /* @escapeNotVerified */ $block->formatAddress($_order->getBillingAddress(), 'html') ?></address> + <address><?= /* @noEscape */ $block->formatAddress($_order->getBillingAddress(), 'html') ?></address> </div> </div> <div class="box box-order-billing-method"> <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Payment Method') ?></strong> + <strong><?= $block->escapeHtml(__('Payment Method')) ?></strong> </div> <div class="box-content"> <?= $block->getPaymentInfoHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml index d1110d1a8d380..3f12ca1f7b270 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml @@ -3,86 +3,83 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /* @var $block \Magento\Sales\Block\Order\PrintOrder\Shipment */ ?> <?php $order = $block->getOrder(); ?> -<?php if (!$block->getObjectData($order, 'is_virtual')): ?> -<?php foreach ($block->getShipmentsCollection() as $shipment): ?> - <div class="order-details-items shipments"> - <div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Shipment #%1', $block->getObjectData($shipment, 'increment_id')) ?></strong> - </div> - <div class="table-wrapper order-items-shipment"> - <table class="data table table-order-items shipment" id="my-shipment-table-<?= /* @escapeNotVerified */ $block->getObjectData($shipment, 'id') ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Invoiced') ?></caption> - <thead> - <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col price"><?= /* @escapeNotVerified */ __('Qty Shipped') ?></th> - </tr> - </thead> - <?php foreach ($block->getShipmentItems($shipment) as $item): ?> - <tbody> - <?= $block->getItemHtml($item) ?> - </tbody> - <?php endforeach; ?> - </table> - </div> - <div class="block block-order-details-view"> - <div class="block-title"> - <strong><?= /* @escapeNotVerified */ __('Order Information') ?></strong> +<?php if (!$block->getObjectData($order, 'is_virtual')) : ?> + <?php foreach ($block->getShipmentsCollection() as $shipment) : ?> + <div class="order-details-items shipments"> + <div class="order-title"> + <strong><?= $block->escapeHtml(__('Shipment #%1', $block->getObjectData($shipment, 'increment_id'))) ?></strong> </div> - <div class="block-content"> - <div class="box box-order-shipping-address"> - <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Address') ?></strong> - </div> - <div class="box-content"> - <address><?= $block->getShipmentAddressFormattedHtml($shipment) ?></address> - </div> + <div class="table-wrapper order-items-shipment"> + <table class="data table table-order-items shipment" id="my-shipment-table-<?= (int) $block->getObjectData($shipment, 'id') ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Invoiced')) ?></caption> + <thead> + <tr> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $block->escapeHtml(__('Qty Shipped')) ?></th> + </tr> + </thead> + <?php foreach ($block->getShipmentItems($shipment) as $item) : ?> + <tbody> + <?= $block->getItemHtml($item) ?> + </tbody> + <?php endforeach; ?> + </table> + </div> + <div class="block block-order-details-view"> + <div class="block-title"> + <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> </div> - - <div class="box box-order-shipping-method"> - <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Shipping Method') ?></strong> + <div class="block-content"> + <div class="box box-order-shipping-address"> + <div class="box-title"> + <strong><?= $block->escapeHtml(__('Shipping Address')) ?></strong> + </div> + <div class="box-content"> + <address><?= $block->getShipmentAddressFormattedHtml($shipment) ?></address> + </div> </div> - <div class="box-content"> - <?= $block->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> - <?php $tracks = $block->getShipmentTracks($shipment); - if ($tracks): ?> - <dl class="order-tracking"> - <?php foreach ($tracks as $track): ?> - <dt class="tracking-title"><?= $block->escapeHtml($block->getObjectData($track, 'title')) ?></dt> - <dd class="tracking-content"><?= $block->escapeHtml($block->getObjectData($track, 'number')) ?></dd> + + <div class="box box-order-shipping-method"> + <div class="box-title"> + <strong><?= $block->escapeHtml(__('Shipping Method')) ?></strong> + </div> + <div class="box-content"> + <?= $block->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> + <?php $tracks = $block->getShipmentTracks($shipment); + if ($tracks) : ?> + <dl class="order-tracking"> + <?php foreach ($tracks as $track) : ?> + <dt class="tracking-title"><?= $block->escapeHtml($block->getObjectData($track, 'title')) ?></dt> + <dd class="tracking-content"><?= $block->escapeHtml($block->getObjectData($track, 'number')) ?></dd> <?php endforeach; ?> - </dl> - <?php endif; ?> + </dl> + <?php endif; ?> + </div> </div> - </div> - <div class="box box-order-billing-method"> - <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Billing Address') ?></strong> + <div class="box box-order-billing-method"> + <div class="box-title"> + <strong><?= $block->escapeHtml(__('Billing Address')) ?></strong> + </div> + <div class="box-content"> + <address><?= $block->getBillingAddressFormattedHtml($order) ?></address> + </div> </div> - <div class="box-content"> - <address><?= $block->getBillingAddressFormattedHtml($order) ?></address> - </div> - </div> - <div class="box box-order-billing-method"> - <div class="box-title"> - <strong><?= /* @escapeNotVerified */ __('Payment Method') ?></strong> - </div> - <div class="box-content"> - <?= $block->getPaymentInfoHtml() ?> + <div class="box box-order-billing-method"> + <div class="box-title"> + <strong><?= $block->escapeHtml(__('Payment Method')) ?></strong> + </div> + <div class="box-content"> + <?= $block->getPaymentInfoHtml() ?> + </div> </div> </div> </div> </div> - </div> <?php endforeach; ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/recent.phtml b/app/code/Magento/Sales/view/frontend/templates/order/recent.phtml index df7e6bf334d9a..e87b33b39da2f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/recent.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/recent.phtml @@ -4,53 +4,59 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var $block \Magento\Sales\Block\Order\Recent */ ?> <div class="block block-dashboard-orders"> -<?php $_orders = $block->getOrders(); ?> +<?php + $_orders = $block->getOrders(); + $count = count($_orders); +?> <div class="block-title order"> - <strong><?= /* @escapeNotVerified */ __('Recent Orders') ?></strong> - <?php if (sizeof($_orders->getItems()) > 0): ?> - <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('sales/order/history') ?>"> - <span><?= /* @escapeNotVerified */ __('View All') ?></span> + <strong><?= $block->escapeHtml(__('Recent Orders')) ?></strong> + <?php if ($count > 0) : ?> + <a class="action view" href="<?= $block->escapeUrl($block->getUrl('sales/order/history')) ?>"> + <span><?= $block->escapeHtml(__('View All')) ?></span> </a> <?php endif; ?> </div> <div class="block-content"> <?= $block->getChildHtml() ?> - <?php if (sizeof($_orders->getItems()) > 0): ?> + <?php if ($count > 0) : ?> <div class="table-wrapper orders-recent"> <table class="data table table-order-items recent" id="my-orders-table"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Recent Orders') ?></caption> + <caption class="table-caption"><?= $block->escapeHtml(__('Recent Orders')) ?></caption> <thead> <tr> - <th scope="col" class="col id"><?= /* @escapeNotVerified */ __('Order #') ?></th> - <th scope="col" class="col date"><?= /* @escapeNotVerified */ __('Date') ?></th> - <th scope="col" class="col shipping"><?= /* @escapeNotVerified */ __('Ship To') ?></th> - <th scope="col" class="col total"><?= /* @escapeNotVerified */ __('Order Total') ?></th> - <th scope="col" class="col status"><?= /* @escapeNotVerified */ __('Status') ?></th> - <th scope="col" class="col actions"><?= /* @escapeNotVerified */ __('Action') ?></th> + <th scope="col" class="col id"><?= $block->escapeHtml(__('Order #')) ?></th> + <th scope="col" class="col date"><?= $block->escapeHtml(__('Date')) ?></th> + <th scope="col" class="col shipping"><?= $block->escapeHtml(__('Ship To')) ?></th> + <th scope="col" class="col total"><?= $block->escapeHtml(__('Order Total')) ?></th> + <th scope="col" class="col status"><?= $block->escapeHtml(__('Status')) ?></th> + <th scope="col" class="col actions"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> <tbody> - <?php foreach ($_orders as $_order): ?> + <?php foreach ($_orders as $_order) : ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= /* @escapeNotVerified */ $_order->getRealOrderId() ?></td> - <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= /* @escapeNotVerified */ $block->formatDate($_order->getCreatedAt()) ?></td> - <td data-th="<?= $block->escapeHtml(__('Ship To')) ?>" class="col shipping"><?= $_order->getShippingAddress() ? $block->escapeHtml($_order->getShippingAddress()->getName()) : ' ' ?></td> - <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @escapeNotVerified */ $_order->formatPrice($_order->getGrandTotal()) ?></td> - <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= /* @escapeNotVerified */ $_order->getStatusLabel() ?></td> + <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= $block->escapeHtml($_order->getRealOrderId()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= /* @noEscape */ $block->formatDate($_order->getCreatedAt()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Ship To')) ?>" class="col shipping"><?= $_order->getShippingAddress() ? $block->escapeHtml($_order->getShippingAddress()->getName()) : " " ?></td> + <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= $block->escapeHtml($_order->getStatusLabel()) ?></td> <td data-th="<?= $block->escapeHtml(__('Actions')) ?>" class="col actions"> - <a href="<?= /* @escapeNotVerified */ $block->getViewUrl($_order) ?>" class="action view"> - <span><?= /* @escapeNotVerified */ __('View Order') ?></span> + <a href="<?= $block->escapeUrl($block->getViewUrl($_order)) ?>" class="action view"> + <span><?= $block->escapeHtml(__('View Order')) ?></span> </a> - <?php if ($this->helper('Magento\Sales\Helper\Reorder')->canReorder($_order->getEntityId())) : ?> - <a href="#" data-post='<?php /* @escapeNotVerified */ echo + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class) + ->canReorder($_order->getEntityId()) + ) : ?> + <a href="#" data-post='<?= /* @noEscape */ $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) ->getPostData($block->getReorderUrl($_order)) ?>' class="action order"> - <span><?= /* @escapeNotVerified */ __('Reorder') ?></span> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> </a> <?php endif ?> </td> @@ -59,8 +65,8 @@ </tbody> </table> </div> - <?php else: ?> - <div class="message info empty"><span><?= /* @escapeNotVerified */ __('You have placed no orders.') ?></span></div> + <?php else : ?> + <div class="message info empty"><span><?= $block->escapeHtml(__('You have placed no orders.')) ?></span></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index b46e8d115480e..57aeffb26f823 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -3,44 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_item = $block->getItem() ?> <?php $_order = $block->getItem()->getOrderItem()->getOrder() ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>"> +<tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()): ?> - <dl class="item options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> - <div class="tooltip content"> - <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> - </dl> - </div> - <?php endif; ?> - </dd> - <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> - <?php endif; ?> - <?php endforeach; ?> - </dl> + <?php if ($_options = $block->getItemOptions()) : ?> + <dl class="item options"> + <?php foreach ($_options as $_option) : ?> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php if (!$block->getPrintStatus()) : ?> + <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> + <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> + <?= $block->escapeHtml($_formatedOptionValue['value']) ?> + <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <div class="tooltip content"> + <dl class="item options"> + <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dd><?= $block->escapeHtml($_formatedOptionValue['full_view']) ?></dd> + </dl> + </div> + <?php endif; ?> + </dd> + <?php else : ?> + <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php endif; ?> + <?php endforeach; ?> + </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock) : ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($block->getSku()) ?></td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"><?= (int) $_item->getQty() ?></td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/totals.phtml b/app/code/Magento/Sales/view/frontend/templates/order/totals.phtml index 8f0e884a22953..7a88f14eb0715 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/totals.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/totals.phtml @@ -4,32 +4,30 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * @var $block \Magento\Sales\Block\Order\Totals * @see \Magento\Sales\Block\Order\Totals */ ?> -<?php foreach ($block->getTotals() as $_code => $_total): ?> - <?php if ($_total->getBlockName()): ?> +<?php foreach ($block->getTotals() as $_code => $_total) : ?> + <?php if ($_total->getBlockName()) : ?> <?= $block->getChildHtml($_total->getBlockName(), false) ?> - <?php else:?> - <tr class="<?= /* @escapeNotVerified */ $_code ?>"> - <th <?= /* @escapeNotVerified */ $block->getLabelProperties() ?> scope="row"> - <?php if ($_total->getStrong()):?> - <strong><?= $block->escapeHtml($_total->getLabel()) ?></strong> - <?php else:?> - <?= $block->escapeHtml($_total->getLabel()) ?> - <?php endif?> + <?php else :?> + <tr class="<?= $block->escapeHtmlAttr($_code) ?>"> + <th <?= /* @noEscape */ $block->getLabelProperties() ?> scope="row"> + <?php if ($_total->getStrong()) : ?> + <strong><?= $block->escapeHtml($_total->getLabel()) ?></strong> + <?php else : ?> + <?= $block->escapeHtml($_total->getLabel()) ?> + <?php endif ?> </th> - <td <?= /* @escapeNotVerified */ $block->getValueProperties() ?> data-th="<?= $block->escapeHtml($_total->getLabel()) ?>"> - <?php if ($_total->getStrong()):?> - <strong><?= /* @escapeNotVerified */ $block->formatValue($_total) ?></strong> - <?php else:?> - <?= /* @escapeNotVerified */ $block->formatValue($_total) ?> + <td <?= /* @noEscape */ $block->getValueProperties() ?> data-th="<?= $block->escapeHtmlAttr($_total->getLabel()) ?>"> + <?php if ($_total->getStrong()) : ?> + <strong><?= /* @noEscape */ $block->formatValue($_total) ?></strong> + <?php else : ?> + <?= /* @noEscape */ $block->formatValue($_total) ?> <?php endif?> </td> </tr> - <?php endif?> + <?php endif; ?> <?php endforeach?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/view.phtml b/app/code/Magento/Sales/view/frontend/templates/order/view.phtml index 1cb8533763759..273554667ffe4 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/view.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/view.phtml @@ -4,31 +4,37 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** @var $block \Magento\Sales\Block\Order\View*/?> <div class="order-details-items ordered"> <?php $_order = $block->getOrder() ?> <div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Items Ordered') ?></strong> - <?php if ($_order->getTracksCollection()->count()) : ?> + <strong><?= $block->escapeHtml(__('Items Ordered')) ?></strong> + <?php if (!empty($_order->getTracksCollection()->getItems())) : ?> <?= $block->getChildHtml('tracking-info-link') ?> <?php endif; ?> </div> <?= $block->getChildHtml('order_items') ?> - <?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order', $_order) && $_order->getGiftMessageId()): ?> + <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('order', $_order) + && $_order->getGiftMessageId() + ) : ?> <div class="block block-order-details-gift-message"> - <div class="block-title"><strong><?= /* @escapeNotVerified */ __('Gift Message for This Order') ?></strong></div> - <?php $_giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($_order); ?> + <div class="block-title"><strong><?= $block->escapeHtml(__('Gift Message for This Order')) ?></strong></div> + <?php + $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessageForEntity($_order); + ?> <div class="block-content"> <dl class="item-options"> - <dt class="item-sender"><strong class="label"><?= /* @escapeNotVerified */ __('From') ?></strong><?= $block->escapeHtml($_giftMessage->getSender()) ?></dt> - <dt class="item-recipient"><strong class="label"><?= /* @escapeNotVerified */ __('To') ?></strong><?= $block->escapeHtml($_giftMessage->getRecipient()) ?></dt> - <dd class="item-message"><?= /* @escapeNotVerified */ $this->helper('Magento\GiftMessage\Helper\Message')->getEscapedGiftMessage($_order) ?></dd> + <dt class="item-sender"><strong class="label"><?= $block->escapeHtml(__('From')) ?></strong><?= $block->escapeHtml($_giftMessage->getSender()) ?></dt> + <dt class="item-recipient"><strong class="label"><?= $block->escapeHtml(__('To')) ?></strong><?= $block->escapeHtml($_giftMessage->getRecipient()) ?></dt> + <dd class="item-message"> + <?= /* @noEscape */ + $this->helper(\Magento\GiftMessage\Helper\Message::class)->getEscapedGiftMessage($_order) ?> + </dd> </dl> </div> </div> @@ -36,8 +42,8 @@ <div class="actions-toolbar"> <div class="secondary"> - <a class="action back" href="<?= /* @escapeNotVerified */ $block->getBackUrl() ?>"> - <span><?= /* @escapeNotVerified */ $block->getBackTitle() ?></span> + <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"> + <span><?= $block->escapeHtml($block->getBackTitle()) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index 5ecf1ebe893bc..ba1204fac8ec5 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Last ordered items sidebar * @@ -15,25 +13,29 @@ <div class="block block-reorder" data-bind="scope: 'lastOrderedItems'"> <div class="block-title no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}"> - <strong id="block-reorder-heading" role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Recently Ordered') ?></strong> + <strong id="block-reorder-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Recently Ordered')) ?></strong> </div> <div class="block-content no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}" aria-labelledby="block-reorder-heading"> <form method="post" class="form reorder" - action="<?= /* @escapeNotVerified */ $block->getFormActionUrl() ?>" id="reorder-validate-detail"> - <strong class="subtitle"><?= /* @escapeNotVerified */ __('Last Ordered Items') ?></strong> + action="<?= $block->escapeUrl($block->getFormActionUrl()) ?>" id="reorder-validate-detail"> + <strong class="subtitle"><?= $block->escapeHtml(__('Last Ordered Items')) ?></strong> <ol id="cart-sidebar-reorder" class="product-items product-items-names" data-bind="foreach: lastOrderedItems().items"> <li class="product-item"> - <div class="field item choice no-display" data-bind="css: {'no-display': !is_saleable}"> + <div class="field item choice"> <label class="label" data-bind="attr: {'for': 'reorder-item-' + id}"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </label> <div class="control"> <input type="checkbox" name="order_items[]" - data-bind="attr: {id: 'reorder-item-' + id, value: id}" - title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" + data-bind="attr: { + id: 'reorder-item-' + id, + value: id, + title: is_saleable ? '<?= $block->escapeHtml(__('Add to Cart')) ?>' : '<?= $block->escapeHtml(__('Product is not salable.')) ?>' + }, + disable: !is_saleable" class="checkbox" data-validate='{"validate-one-checkbox-required-by-name": true}'/> </div> </div> @@ -46,15 +48,15 @@ </ol> <div id="cart-sidebar-reorder-advice-container"></div> <div class="actions-toolbar"> - <div class="primary no-display" - data-bind="css: {'no-display': !lastOrderedItems().isShowAddToCart}"> - <button type="submit" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" class="action tocart primary"> - <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + <div class="primary" + data-bind="visible: isShowAddToCart"> + <button type="submit" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" class="action tocart primary"> + <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </div> <div class="secondary"> - <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>"> - <span><?= /* @escapeNotVerified */ __('View All') ?></span> + <a class="action view" href="<?= $block->escapeUrl($block->getUrl('customer/account')) ?>#my-orders-table"> + <span><?= $block->escapeHtml(__('View All')) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml index ac428283dfcb0..25926688c6f47 100644 --- a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml @@ -4,31 +4,29 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Sales\Block\Widget\Guest\Form */ ?> -<?php if ($block->isEnable()): ?> +<?php if ($block->isEnable()) : ?> <div class="widget block block-orders-returns"> <div class="block-title"> - <strong role="heading" aria-level="2"><?= /* @escapeNotVerified */ __('Orders and Returns') ?></strong> + <strong role="heading" aria-level="2"><?= $block->escapeHtml(__('Orders and Returns')) ?></strong> </div> <div class="block-content"> - <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' action="<?= /* @escapeNotVerified */ $block->getActionUrl() ?>" method="post" + <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" class="form form-orders-search" name="guest_post"> <fieldset class="fieldset"> <div class="field find required"> - <label class="label"><span><?= /* @escapeNotVerified */ __('Find Order By') ?></span></label> + <label class="label"><span><?= $block->escapeHtml(__('Find Order By')) ?></span></label> <div class="control"> <select name="oar_type" id="quick-search-type-id" class="select" title=""> - <option value="email"><?= /* @escapeNotVerified */ __('Email') ?></option> - <option value="zip"><?= /* @escapeNotVerified */ __('ZIP Code') ?></option> + <option value="email"><?= $block->escapeHtml(__('Email')) ?></option> + <option value="zip"><?= $block->escapeHtml(__('ZIP Code')) ?></option> </select> </div> </div> <div class="field id required"> - <label for="oar-order-id" class="label"><span><?= /* @escapeNotVerified */ __('Order ID') ?></span></label> + <label for="oar-order-id" class="label"><span><?= $block->escapeHtml(__('Order ID')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" autocomplete="off" @@ -37,7 +35,7 @@ </div> <div class="field lastname required"> <label for="oar-billing-lastname" - class="label"><span><?= /* @escapeNotVerified */ __('Billing Last Name') ?></span></label> + class="label"><span><?= $block->escapeHtml(__('Billing Last Name')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar-billing-lastname" name="oar_billing_lastname" @@ -45,7 +43,7 @@ </div> </div> <div id="oar-email" class="field email required"> - <label for="oar_email" class="label"><span><?= /* @escapeNotVerified */ __('Email') ?></span></label> + <label for="oar_email" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input type="email" class="input-text" id="oar_email" name="oar_email" autocomplete="off" @@ -53,7 +51,7 @@ </div> </div> <div id="oar-zip" style="display: none;" class="field zip required"> - <label for="oar_zip" class="label"><span><?= /* @escapeNotVerified */ __('Billing ZIP Code') ?></span></label> + <label for="oar_zip" class="label"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" @@ -64,7 +62,7 @@ <div class="actions-toolbar"> <div class="primary"> <button type="submit" title="<?= $block->escapeHtml(__('Search')) ?>" class="action search"> - <span><?= /* @escapeNotVerified */ __('Search') ?></span> + <span><?= $block->escapeHtml(__('Search')) ?></span> </button> </div> </div> diff --git a/app/code/Magento/Sales/view/frontend/web/gift-message.js b/app/code/Magento/Sales/view/frontend/web/js/gift-message.js similarity index 100% rename from app/code/Magento/Sales/view/frontend/web/gift-message.js rename to app/code/Magento/Sales/view/frontend/web/js/gift-message.js diff --git a/app/code/Magento/Sales/view/frontend/web/orders-returns.js b/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js similarity index 100% rename from app/code/Magento/Sales/view/frontend/web/orders-returns.js rename to app/code/Magento/Sales/view/frontend/web/js/orders-returns.js diff --git a/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js b/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js index f393cc3fcd3bc..17e61a77d98a3 100644 --- a/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js +++ b/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js @@ -5,27 +5,43 @@ define([ 'uiComponent', - 'Magento_Customer/js/customer-data' -], function (Component, customerData) { + 'Magento_Customer/js/customer-data', + 'underscore' +], function (Component, customerData, _) { 'use strict'; return Component.extend({ + defaults: { + isShowAddToCart: false + }, + /** @inheritdoc */ initialize: function () { - var isShowAddToCart = false, - item; - this._super(); this.lastOrderedItems = customerData.get('last-ordered-items'); + this.lastOrderedItems.subscribe(this.checkSalableItems.bind(this)); + this.checkSalableItems(); + + return this; + }, + + /** @inheritdoc */ + initObservable: function () { + this._super() + .observe('isShowAddToCart'); + + return this; + }, - for (item in this.lastOrderedItems.items) { - if (item['is_saleable']) { - isShowAddToCart = true; - break; - } - } + /** + * Check if items is_saleable and change add to cart button visibility. + */ + checkSalableItems: function () { + var isShowAddToCart = _.some(this.lastOrderedItems().items, { + 'is_saleable': true + }); - this.lastOrderedItems.isShowAddToCart = isShowAddToCart; + this.isShowAddToCart(isShowAddToCart); } }); }); diff --git a/app/code/Magento/SalesAnalytics/Test/Mftf/LICENSE.txt b/app/code/Magento/SalesAnalytics/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/SalesAnalytics/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 <insert your license name here>" 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/app/code/Magento/SalesAnalytics/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/SalesAnalytics/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/SalesAnalytics/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 <insert your license name here>" 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/app/code/Magento/SalesAnalytics/Test/Mftf/README.md b/app/code/Magento/SalesAnalytics/Test/Mftf/README.md new file mode 100644 index 0000000000000..00e1b136c5c26 --- /dev/null +++ b/app/code/Magento/SalesAnalytics/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sales Analytics Functional Tests + +The Functional Test Module for **Magento Sales Analytics** module. diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json index e1e60bed3bd53..f0e9147dd67af 100644 --- a/app/code/Magento/SalesAnalytics/composer.json +++ b/app/code/Magento/SalesAnalytics/composer.json @@ -2,12 +2,12 @@ "name": "magento/module-sales-analytics", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-sales": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SalesInventory/Test/Mftf/LICENSE.txt b/app/code/Magento/SalesInventory/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/SalesInventory/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 <insert your license name here>" 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/app/code/Magento/SalesInventory/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/SalesInventory/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/SalesInventory/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 <insert your license name here>" 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/app/code/Magento/SalesInventory/Test/Mftf/README.md b/app/code/Magento/SalesInventory/Test/Mftf/README.md new file mode 100644 index 0000000000000..5d52679cbb173 --- /dev/null +++ b/app/code/Magento/SalesInventory/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sales Inventory Functional Tests + +The Functional Test Module for **Magento Sales Inventory** module. diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php index 3d811363156d7..32eb810c7a16a 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Order/ReturnValidatorTest.php @@ -121,6 +121,9 @@ public function testValidationWithWrongOrderItems() ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php index 2ee8f81a2aa88..0fea8f4210f8a 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Model/Plugin/Order/Validation/InvoiceRefundCreationArgumentsTest.php @@ -140,6 +140,9 @@ public function testAfterValidation($erroMessage) ); } + /** + * @return array + */ public function dataProvider() { return [ diff --git a/app/code/Magento/SalesInventory/Test/Unit/Observer/RefundOrderInventoryObserverTest.php b/app/code/Magento/SalesInventory/Test/Unit/Observer/RefundOrderInventoryObserverTest.php index c2e2d4710f96c..759320106ea1b 100644 --- a/app/code/Magento/SalesInventory/Test/Unit/Observer/RefundOrderInventoryObserverTest.php +++ b/app/code/Magento/SalesInventory/Test/Unit/Observer/RefundOrderInventoryObserverTest.php @@ -173,6 +173,11 @@ public function testRefundOrderInventory() $this->observer->execute($this->eventObserver); } + /** + * @param $productId + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ private function getCreditMemoItem($productId) { $backToStock = true; diff --git a/app/code/Magento/SalesInventory/composer.json b/app/code/Magento/SalesInventory/composer.json index 1b429807e0fb8..719c91441a956 100644 --- a/app/code/Magento/SalesInventory/composer.json +++ b/app/code/Magento/SalesInventory/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-sales-inventory", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog-inventory": "100.2.*", "magento/module-sales": "101.0.*", "magento/module-store": "100.2.*", @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php index 0cb286056d825..bee7573c1fe2a 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php @@ -26,7 +26,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to delete this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php index 23100d672d92c..6138e6691837b 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/Tab/Coupons/Grid.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SalesRule\Block\Adminhtml\Promo\Quote\Edit\Tab\Coupons; +use Magento\SalesRule\Block\Adminhtml\Promo\Quote\Edit\Tab\Coupons\Grid\Column\Renderer\Used; + /** * Coupon codes grid * @@ -48,9 +48,7 @@ public function __construct( } /** - * Constructor - * - * @return void + * @inheritdoc */ protected function _construct() { @@ -60,9 +58,7 @@ protected function _construct() } /** - * Prepare collection for grid - * - * @return $this + * @inheritdoc */ protected function _prepareCollection() { @@ -73,15 +69,20 @@ protected function _prepareCollection() */ $collection = $this->_salesRuleCoupon->create()->addRuleToFilter($priceRule)->addGeneratedCouponsFilter(); + if ($this->_isExport && $this->getMassactionBlock()->isAvailable()) { + $itemIds = $this->getMassactionBlock()->getSelected(); + if (!empty($itemIds)) { + $collection->addFieldToFilter('coupon_id', ['in' => $itemIds]); + } + } + $this->setCollection($collection); return parent::_prepareCollection(); } /** - * Define grid columns - * - * @return $this + * @inheritdoc */ protected function _prepareColumns() { @@ -106,7 +107,7 @@ protected function _prepareColumns() 'width' => '100', 'type' => 'options', 'options' => [__('No'), __('Yes')], - 'renderer' => \Magento\SalesRule\Block\Adminhtml\Promo\Quote\Edit\Tab\Coupons\Grid\Column\Renderer\Used::class, + 'renderer' => Used::class, 'filter_condition_callback' => [$this->_salesRuleCoupon->create(), 'addIsUsedFilterCallback'] ] ); @@ -122,9 +123,7 @@ protected function _prepareColumns() } /** - * Configure grid mass actions - * - * @return $this + * @inheritdoc */ protected function _prepareMassaction() { @@ -147,9 +146,7 @@ protected function _prepareMassaction() } /** - * Get grid url - * - * @return string + * @inheritdoc */ public function getGridUrl() { diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php index b4be904223aac..6a54404e1865e 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php @@ -87,7 +87,7 @@ public function prepareElementHtml(\Magento\Framework\Data\Form\Element\Abstract if ($element->getValue()) { $rule = $this->ruleFactory->create()->load((int)$element->getValue()); if ($rule->getId()) { - $chooser->setLabel($rule->getName()); + $chooser->setLabel($this->escapeHtml($rule->getName())); } } diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote.php index 15f87931886c8..55e1d319cc776 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote.php @@ -74,7 +74,7 @@ protected function _initRule() /** * Initiate action * - * @return this + * @return $this */ protected function _initAction() { diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php index dcbc290f98579..a55d4c86cfef7 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php @@ -12,9 +12,14 @@ class CouponsMassDelete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Qu * Coupons mass delete action * * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $this->_initRule(); $rule = $this->_coreRegistry->registry(\Magento\SalesRule\Model\RegistryConstants::CURRENT_SALES_RULE); diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php index 9adb62583985d..b505fd1d6a0fb 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php @@ -6,28 +6,35 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote { /** * Delete promo quote action * * @return void + * @throws NotFoundException */ public function execute() { - $id = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $id = (int)$this->getRequest()->getParam('id'); if ($id) { try { $model = $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class); $model->load($id); $model->delete(); - $this->messageManager->addSuccess(__('You deleted the rule.')); + $this->messageManager->addSuccessMessage(__('You deleted the rule.')); $this->_redirect('sales_rule/*/'); return; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('We can\'t delete the rule right now. Please review the log and try again.') ); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -35,7 +42,7 @@ public function execute() return; } } - $this->messageManager->addError(__('We can\'t find a rule to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a rule to delete.')); $this->_redirect('sales_rule/*/'); } } diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php new file mode 100644 index 0000000000000..aef60f54670f9 --- /dev/null +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Widget; + +/** + * Class for generation of JSON for building tree catalog. + * + * Examples of use: + * \Magento\Catalog\Block\Adminhtml\Category\Tree::getLoadTreeUrl + * \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser::getLoadTreeUrl + */ +class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Widget\CategoriesJson +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_SalesRule::quote'; +} diff --git a/app/code/Magento/SalesRule/Helper/Coupon.php b/app/code/Magento/SalesRule/Helper/Coupon.php index 628107945414c..8d5dd69ccf45f 100644 --- a/app/code/Magento/SalesRule/Helper/Coupon.php +++ b/app/code/Magento/SalesRule/Helper/Coupon.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SalesRule\Helper; /** @@ -81,7 +79,10 @@ public function getFormatsList() */ public function getDefaultLength() { - return (int)$this->scopeConfig->getValue(self::XML_PATH_SALES_RULE_COUPON_LENGTH, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return (int)$this->scopeConfig->getValue( + self::XML_PATH_SALES_RULE_COUPON_LENGTH, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** @@ -91,7 +92,10 @@ public function getDefaultLength() */ public function getDefaultFormat() { - return $this->scopeConfig->getValue(self::XML_PATH_SALES_RULE_COUPON_FORMAT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return $this->scopeConfig->getValue( + self::XML_PATH_SALES_RULE_COUPON_FORMAT, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** @@ -101,7 +105,10 @@ public function getDefaultFormat() */ public function getDefaultPrefix() { - return $this->scopeConfig->getValue(self::XML_PATH_SALES_RULE_COUPON_PREFIX, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return $this->scopeConfig->getValue( + self::XML_PATH_SALES_RULE_COUPON_PREFIX, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** @@ -111,7 +118,10 @@ public function getDefaultPrefix() */ public function getDefaultSuffix() { - return $this->scopeConfig->getValue(self::XML_PATH_SALES_RULE_COUPON_SUFFIX, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return $this->scopeConfig->getValue( + self::XML_PATH_SALES_RULE_COUPON_SUFFIX, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** @@ -121,7 +131,10 @@ public function getDefaultSuffix() */ public function getDefaultDashInterval() { - return (int)$this->scopeConfig->getValue(self::XML_PATH_SALES_RULE_COUPON_DASH_INTERVAL, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + return (int)$this->scopeConfig->getValue( + self::XML_PATH_SALES_RULE_COUPON_DASH_INTERVAL, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); } /** diff --git a/app/code/Magento/SalesRule/Model/Coupon/Codegenerator.php b/app/code/Magento/SalesRule/Model/Coupon/Codegenerator.php index 1d86ec2b01b35..c89677f0e9097 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Codegenerator.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Codegenerator.php @@ -38,7 +38,7 @@ public function generateCode() $length = $this->getActualLength(); $code = ''; for ($i = 0, $indexMax = strlen($alphabet) - 1; $i < $length; ++$i) { - $code .= substr($alphabet, mt_rand(0, $indexMax), 1); + $code .= substr($alphabet, random_int(0, $indexMax), 1); } return $code; @@ -54,7 +54,7 @@ protected function getActualLength() $lengthMin = $this->getLengthMin() ? $this->getLengthMin() : static::DEFAULT_LENGTH_MIN; $lengthMax = $this->getLengthMax() ? $this->getLengthMax() : static::DEFAULT_LENGTH_MAX; - return $this->getLength() ? $this->getLength() : mt_rand($lengthMin, $lengthMax); + return $this->getLength() ? $this->getLength() : random_int($lengthMin, $lengthMax); } /** diff --git a/app/code/Magento/SalesRule/Model/Coupon/Massgenerator.php b/app/code/Magento/SalesRule/Model/Coupon/Massgenerator.php index 453a022d31397..b9cac227be127 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Massgenerator.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Massgenerator.php @@ -168,16 +168,10 @@ public function generatePool() ++$attempt; } while ($this->getResource()->exists($code)); - $expirationDate = $this->getToDate(); - if ($expirationDate instanceof \DateTimeInterface) { - $expirationDate = $expirationDate->format('Y-m-d H:i:s'); - } - $coupon->setId(null) ->setRuleId($this->getRuleId()) ->setUsageLimit($this->getUsesPerCoupon()) ->setUsagePerCustomer($this->getUsagePerCustomer()) - ->setExpirationDate($expirationDate) ->setCreatedAt($nowTimestamp) ->setType(\Magento\SalesRule\Helper\Coupon::COUPON_TYPE_SPECIFIC_AUTOGENERATED) ->setCode($code) diff --git a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php new file mode 100644 index 0000000000000..292d04c232e6f --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Sales\Model\Order; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; +use Magento\SalesRule\Model\Rule\CustomerFactory; +use Magento\SalesRule\Model\RuleFactory; + +/** + * Updates the coupon usages. + */ +class UpdateCouponUsages +{ + /** + * @var RuleFactory + */ + private $ruleFactory; + + /** + * @var RuleFactory + */ + private $ruleCustomerFactory; + + /** + * @var Coupon + */ + private $coupon; + + /** + * @var Usage + */ + private $couponUsage; + + /** + * @param RuleFactory $ruleFactory + * @param CustomerFactory $ruleCustomerFactory + * @param Coupon $coupon + * @param Usage $couponUsage + */ + public function __construct( + RuleFactory $ruleFactory, + CustomerFactory $ruleCustomerFactory, + Coupon $coupon, + Usage $couponUsage + ) { + $this->ruleFactory = $ruleFactory; + $this->ruleCustomerFactory = $ruleCustomerFactory; + $this->coupon = $coupon; + $this->couponUsage = $couponUsage; + } + + /** + * Executes the current command. + * + * @param Order $subject + * @param bool $increment + * @return Order + */ + public function execute(Order $subject, bool $increment) + { + if (!$subject || !$subject->getAppliedRuleIds()) { + return $subject; + } + // lookup rule ids + $ruleIds = explode(',', $subject->getAppliedRuleIds()); + $ruleIds = array_unique($ruleIds); + $customerId = (int)$subject->getCustomerId(); + // use each rule (and apply to customer, if applicable) + foreach ($ruleIds as $ruleId) { + if (!$ruleId) { + continue; + } + $this->updateRuleUsages($increment, (int)$ruleId, $customerId); + } + $this->updateCouponUsages($subject, $increment, $customerId); + + return $subject; + } + + /** + * Update the number of rule usages. + * + * @param bool $increment + * @param int $ruleId + * @param int $customerId + */ + private function updateRuleUsages(bool $increment, int $ruleId, int $customerId) + { + /** @var \Magento\SalesRule\Model\Rule $rule */ + $rule = $this->ruleFactory->create(); + $rule->load($ruleId); + if ($rule->getId()) { + $rule->loadCouponCode(); + if ($increment || $rule->getTimesUsed() > 0) { + $rule->setTimesUsed($rule->getTimesUsed() + ($increment ? 1 : -1)); + $rule->save(); + } + if ($customerId) { + $this->updateCustomerRuleUsages($increment, $ruleId, $customerId); + } + } + } + + /** + * Update the number of rule usages per customer. + * + * @param bool $increment + * @param int $ruleId + * @param int $customerId + */ + private function updateCustomerRuleUsages(bool $increment, int $ruleId, int $customerId) + { + /** @var \Magento\SalesRule\Model\Rule\Customer $ruleCustomer */ + $ruleCustomer = $this->ruleCustomerFactory->create(); + $ruleCustomer->loadByCustomerRule($customerId, $ruleId); + if ($ruleCustomer->getId()) { + if ($increment || $ruleCustomer->getTimesUsed() > 0) { + $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + ($increment ? 1 : -1)); + } + } elseif ($increment) { + $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); + } + $ruleCustomer->save(); + } + + /** + * Update the number of coupon usages. + * + * @param Order $subject + * @param bool $increment + * @param int $customerId + */ + private function updateCouponUsages(Order $subject, bool $increment, int $customerId) + { + $this->coupon->load($subject->getCouponCode(), 'code'); + if ($this->coupon->getId()) { + if ($increment || $this->coupon->getTimesUsed() > 0) { + $this->coupon->setTimesUsed($this->coupon->getTimesUsed() + ($increment ? 1 : -1)); + $this->coupon->save(); + } + if ($customerId) { + $this->couponUsage->updateCustomerCouponTimesUsed($customerId, $this->coupon->getId(), $increment); + } + } + } +} diff --git a/app/code/Magento/SalesRule/Model/CouponRepository.php b/app/code/Magento/SalesRule/Model/CouponRepository.php index 99d6727a8be29..5aa66dd1e8ac5 100644 --- a/app/code/Magento/SalesRule/Model/CouponRepository.php +++ b/app/code/Magento/SalesRule/Model/CouponRepository.php @@ -119,7 +119,6 @@ public function save(\Magento\SalesRule\Api\Data\CouponInterface $coupon) __('Specified rule does not allow auto generated coupons') ); } - $coupon->setExpirationDate($rule->getToDate()); $coupon->setUsageLimit($rule->getUsesPerCoupon()); $coupon->setUsagePerCustomer($rule->getUsesPerCustomer()); } catch (\Exception $e) { diff --git a/app/code/Magento/SalesRule/Model/DeltaPriceRound.php b/app/code/Magento/SalesRule/Model/DeltaPriceRound.php new file mode 100644 index 0000000000000..294b618b5819a --- /dev/null +++ b/app/code/Magento/SalesRule/Model/DeltaPriceRound.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SalesRule\Model; + +use Magento\Framework\Pricing\PriceCurrencyInterface; + +/** + * Round price and save rounding operation delta. + */ +class DeltaPriceRound +{ + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + + /** + * @var float[] + */ + private $roundingDeltas; + + /** + * @param PriceCurrencyInterface $priceCurrency + */ + public function __construct(PriceCurrencyInterface $priceCurrency) + { + $this->priceCurrency = $priceCurrency; + } + + /** + * Round price based on previous rounding operation delta. + * + * @param float $price + * @param string $type + * @return float + */ + public function round($price, $type) + { + if ($price) { + // initialize the delta to a small number to avoid non-deterministic behavior with rounding of 0.5 + $delta = isset($this->roundingDeltas[$type]) ? $this->roundingDeltas[$type] : 0.000001; + $price += $delta; + $roundPrice = $this->priceCurrency->round($price); + $this->roundingDeltas[$type] = $price - $roundPrice; + $price = $roundPrice; + } + + return $price; + } + + /** + * Reset all deltas. + * + * @return void + */ + public function resetAll() + { + $this->roundingDeltas = []; + } + + /** + * Reset deltas by type. + * + * @param string $type + * @return void + */ + public function reset($type) + { + if (isset($this->roundingDeltas[$type])) { + unset($this->roundingDeltas[$type]); + } + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php b/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php new file mode 100644 index 0000000000000..57e195ad518e3 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Quote/ChildrenValidationLocator.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SalesRule\Model\Quote; + +use \Magento\Quote\Model\Quote\Item\AbstractItem as QuoteItem; + +/** + * Class ChildrenValidationLocator + * + * Used to determine necessity to validate rule on item's children that may depends on product type + */ +class ChildrenValidationLocator +{ + /** + * @var array + */ + private $productTypeChildrenValidationMap; + + /** + * @param array $productTypeChildrenValidationMap + * <pre> + * [ + * 'ProductType1' => true, + * 'ProductType2' => false + * ] + * </pre> + */ + public function __construct( + array $productTypeChildrenValidationMap = [] + ) { + $this->productTypeChildrenValidationMap = $productTypeChildrenValidationMap; + } + + /** + * Checks necessity to validate rule on item's children + * + * @param QuoteItem $item + * @return bool + */ + public function isChildrenValidationRequired(QuoteItem $item): bool + { + $type = $item->getProduct()->getTypeId(); + if (isset($this->productTypeChildrenValidationMap[$type])) { + return (bool)$this->productTypeChildrenValidationMap[$type]; + } + return true; + } +} diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php index 5068f53f23bc9..a544355536d6f 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php @@ -34,14 +34,6 @@ protected function _construct() */ public function _beforeSave(AbstractModel $object) { - if (!$object->getExpirationDate()) { - $object->setExpirationDate(null); - } elseif ($object->getExpirationDate() instanceof \DateTimeInterface) { - $object->setExpirationDate( - $object->getExpirationDate()->format('Y-m-d H:i:s') - ); - } - // maintain single primary coupon per rule $object->setIsPrimary($object->getIsPrimary() ? 1 : null); @@ -106,7 +98,7 @@ public function exists($code) } /** - * Update auto generated Specific Coupon if it's rule changed + * Update auto generated Specific Coupon if its rule changed * * @param \Magento\SalesRule\Model\Rule $rule * @return $this @@ -126,13 +118,6 @@ public function updateSpecificCoupons(\Magento\SalesRule\Model\Rule $rule) $updateArray['usage_per_customer'] = $rule->getUsesPerCustomer(); } - $ruleNewDate = new \DateTime($rule->getToDate()); - $ruleOldDate = new \DateTime($rule->getOrigData('to_date')); - - if ($ruleNewDate != $ruleOldDate) { - $updateArray['expiration_date'] = $rule->getToDate(); - } - if (!empty($updateArray)) { $this->getConnection()->update( $this->getTable('salesrule_coupon'), diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php index 6407f04611a36..8680a91a9acc4 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php @@ -27,9 +27,10 @@ protected function _construct() * * @param int $customerId * @param mixed $couponId + * @param bool $increment * @return void */ - public function updateCustomerCouponTimesUsed($customerId, $couponId) + public function updateCustomerCouponTimesUsed($customerId, $couponId, $increment = true) { $connection = $this->getConnection(); $select = $connection->select(); @@ -44,13 +45,13 @@ public function updateCustomerCouponTimesUsed($customerId, $couponId) $timesUsed = $connection->fetchOne($select, [':coupon_id' => $couponId, ':customer_id' => $customerId]); - if ($timesUsed > 0) { + if ($timesUsed !== false) { $this->getConnection()->update( $this->getMainTable(), - ['times_used' => $timesUsed + 1], + ['times_used' => $timesUsed + ($increment ? 1 : -1)], ['coupon_id = ?' => $couponId, 'customer_id = ?' => $customerId] ); - } else { + } elseif ($increment) { $this->getConnection()->insert( $this->getMainTable(), ['coupon_id' => $couponId, 'customer_id' => $customerId, 'times_used' => 1] @@ -73,11 +74,11 @@ public function loadByCustomerCoupon(\Magento\Framework\DataObject $object, $cus $select = $connection->select()->from( $this->getMainTable() )->where( - 'customer_id =:customet_id' + 'customer_id =:customer_id' )->where( 'coupon_id = :coupon_id' ); - $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customet_id' => $customerId]); + $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customer_id' => $customerId]); if ($data) { $object->setData($data); } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php index 540ba7e2ea55a..4ebf01d145fe9 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SalesRule\Model\ResourceModel\Report\Rule; /** @@ -95,7 +93,8 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'base_subtotal_canceled', 0 ) . ' - ' . $connection->getIfNullSql( - 'ABS(base_discount_amount) - ABS(' . $connection->getIfNullSql('base_discount_canceled', 0) . ')', + 'ABS(base_discount_amount) - ABS(' + . $connection->getIfNullSql('base_discount_canceled', 0) . ')', 0 ) . ' + ' . $connection->getIfNullSql( 'base_tax_amount - ' . $connection->getIfNullSql('base_tax_canceled', 0), @@ -124,7 +123,8 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'base_subtotal_refunded', 0 ) . ' - ' . $connection->getIfNullSql( - 'ABS(base_discount_invoiced) - ABS(' . $connection->getIfNullSql('base_discount_refunded', 0) . ')', + 'ABS(base_discount_invoiced) - ABS(' + . $connection->getIfNullSql('base_discount_refunded', 0) . ')', 0 ) . ' + ' . $connection->getIfNullSql( 'base_tax_invoiced - ' . $connection->getIfNullSql('base_tax_refunded', 0), diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php index 794fc94d6a2a8..3a5ed16fdd2fd 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php @@ -239,7 +239,7 @@ public function saveStoreLabels($ruleId, $labels) $connection->delete($table, ['rule_id=?' => $ruleId, 'store_id IN (?)' => $deleteByStoreIds]); } } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); throw $e; } $connection->commit(); diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 54b50dbdf38db..ea6b34faaa570 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -6,8 +6,12 @@ namespace Magento\SalesRule\Model\ResourceModel\Rule; +use Magento\Framework\DB\Select; use Magento\Framework\Serialize\Serializer\Json; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Api\Data\CouponInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; /** * Sales Rules resource collection model. @@ -79,6 +83,8 @@ protected function _construct() } /** + * Map data for associated entities + * * @param string $entityType * @param string $objectField * @throws \Magento\Framework\Exception\LocalizedException @@ -104,15 +110,20 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** + * Add website ids and customer group ids to rules data + * * @return $this * @throws \Exception * @since 100.1.0 @@ -136,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -148,75 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ), - ['code'] - ); - - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ? ', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - $noCouponWhereCondition . ' OR ((' . $orWhereCondition . ') AND ' . $andWhereCondition . ')' - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -224,6 +185,96 @@ public function setValidationFilter( return $this; } + /** + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. + * + * @param int $websiteId + * @param int $customerGroupId + * @param string $now + */ + private function prepareSelect($websiteId, $customerGroupId, $now) + { + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON + ); + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$isAutogenerated OR $isValidSpecific", + null, + Select::TYPE_CONDITION + ); + + return $couponSelect; + } + + /** + * Join coupon table to select. + * + * @param string $couponCode + * @param Select $couponSelect + */ + private function joinCouponTable($couponCode, Select $couponSelect) + { + $couponJoinCondition = + 'main_table.rule_id = rule_coupons.rule_id' + . ' AND ' . + $this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode); + + $couponSelect->joinInner( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $couponJoinCondition, + [Coupon::KEY_CODE] + ); + } + /** * Filter collection by website(s), customer group(s) and date. * Filter collection to only active rules. @@ -239,7 +290,7 @@ public function addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now = n { if (!$this->getFlag('website_group_date_filter')) { if ($now === null) { - $now = $this->_date->date()->format('Y-m-d'); + $now = $this->_date->date(null, null, false, false)->format('Y-m-d'); } $this->addWebsiteFilter($websiteId); @@ -320,7 +371,7 @@ public function addAttributeInConditionFilter($attributeCode) $this->getSelect()->where( sprintf('(%s OR %s)', $cCond, $aCond), null, - \Magento\Framework\DB\Select::TYPE_CONDITION + Select::TYPE_CONDITION ); return $this; @@ -363,6 +414,8 @@ public function addCustomerGroupFilter($customerGroupId) } /** + * Getter for _associatedEntitiesMap property + * * @return array * @deprecated 100.1.0 */ @@ -377,6 +430,8 @@ private function getAssociatedEntitiesMap() } /** + * Getter for dateApplier property + * * @return DateApplier * @deprecated 100.1.0 */ diff --git a/app/code/Magento/SalesRule/Model/Rule.php b/app/code/Magento/SalesRule/Model/Rule.php index f4469213bd96e..b9342fe7b35b7 100644 --- a/app/code/Magento/SalesRule/Model/Rule.php +++ b/app/code/Magento/SalesRule/Model/Rule.php @@ -296,8 +296,6 @@ public function afterSave() $this->getUsesPerCoupon() ? $this->getUsesPerCoupon() : null )->setUsagePerCustomer( $this->getUsesPerCustomer() ? $this->getUsesPerCustomer() : null - )->setExpirationDate( - $this->getToDate() )->save(); } else { $this->getPrimaryCoupon()->delete(); @@ -499,8 +497,8 @@ public function acquireCoupon($saveNewlyCreated = true, $saveAttemptCount = 10) $this->getUsesPerCoupon() ? $this->getUsesPerCoupon() : null )->setUsagePerCustomer( $this->getUsesPerCustomer() ? $this->getUsesPerCustomer() : null - )->setExpirationDate( - $this->getToDate() + )->setType( + \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED ); $couponCode = self::getCouponCodeGenerator()->generateCode(); @@ -521,7 +519,7 @@ public function acquireCoupon($saveNewlyCreated = true, $saveAttemptCount = 10) $coupon->setCode( $couponCode . self::getCouponCodeGenerator()->getDelimiter() . sprintf( '%04u', - rand(0, 9999) + random_int(0, 9999) ) ); continue; diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index caa938322617d..c53e23321b905 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -5,6 +5,14 @@ */ namespace Magento\SalesRule\Model\Rule\Action\Discount; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\SalesRule\Model\DeltaPriceRound; +use Magento\SalesRule\Model\Validator; + +/** + * Calculates discount for cart item if fixed discount applied on whole cart. + */ class CartFixed extends AbstractDiscount { /** @@ -14,6 +22,33 @@ class CartFixed extends AbstractDiscount */ protected $_cartFixedRuleUsedForAddress = []; + /** + * @var DeltaPriceRound + */ + private $deltaPriceRound; + + /** + * @var string + */ + private static $discountType = 'CartFixed'; + + /** + * @param Validator $validator + * @param DataFactory $discountDataFactory + * @param PriceCurrencyInterface $priceCurrency + * @param DeltaPriceRound $deltaPriceRound + */ + public function __construct( + Validator $validator, + DataFactory $discountDataFactory, + PriceCurrencyInterface $priceCurrency, + DeltaPriceRound $deltaPriceRound = null + ) { + $this->deltaPriceRound = $deltaPriceRound ?: ObjectManager::getInstance()->get(DeltaPriceRound::class); + + parent::__construct($validator, $discountDataFactory, $priceCurrency); + } + /** * @param \Magento\SalesRule\Model\Rule $rule * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item @@ -51,14 +86,22 @@ public function calculate($rule, $item, $qty) $cartRules[$rule->getId()] = $rule->getDiscountAmount(); } - if ($cartRules[$rule->getId()] > 0) { + $availableDiscountAmount = (float)$cartRules[$rule->getId()]; + $discountType = self::$discountType . $rule->getId(); + + if ($availableDiscountAmount > 0) { $store = $quote->getStore(); if ($ruleTotals['items_count'] <= 1) { - $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $cartRules[$rule->getId()]); + $quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store); + $baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount); + $this->deltaPriceRound->reset($discountType); } else { - $discountRate = $baseItemPrice * $qty / $ruleTotals['base_items_price']; - $maximumItemDiscount = $rule->getDiscountAmount() * $discountRate; + $ratio = $baseItemPrice * $qty / $ruleTotals['base_items_price']; + $maximumItemDiscount = $this->deltaPriceRound->round( + $rule->getDiscountAmount() * $ratio, + $discountType + ); + $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); @@ -67,7 +110,11 @@ public function calculate($rule, $item, $qty) $baseDiscountAmount = $this->priceCurrency->round($baseDiscountAmount); - $cartRules[$rule->getId()] -= $baseDiscountAmount; + $availableDiscountAmount -= $baseDiscountAmount; + $cartRules[$rule->getId()] = $availableDiscountAmount; + if ($availableDiscountAmount <= 0) { + $this->deltaPriceRound->reset($discountType); + } $discountData->setAmount($this->priceCurrency->round(min($itemPrice * $qty, $quoteAmount))); $discountData->setBaseAmount($baseDiscountAmount); diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index fd5953697c7db..96867222d170a 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -58,6 +58,7 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php index 499e35db9dfd6..059177b4066f3 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\SalesRule\Model\Rule\Condition; /** * Product rule condition data model * * @author Magento Core Team <core@magentocommerce.com> + * + * @method string getAttribute() */ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct { @@ -24,13 +27,145 @@ protected function _addSpecialAttributes(array &$attributes) $attributes['quote_item_qty'] = __('Quantity in cart'); $attributes['quote_item_price'] = __('Price in cart'); $attributes['quote_item_row_total'] = __('Row total in cart'); + + $attributes['parent::category_ids'] = __('Category (Parent only)'); + $attributes['children::category_ids'] = __('Category (Children Only)'); + } + + /** + * Retrieve attribute + * + * @return string + */ + public function getAttribute(): string + { + $attribute = $this->getData('attribute'); + if (strpos($attribute, '::') !== false) { + list(, $attribute) = explode('::', $attribute); + } + + return $attribute; + } + + /** + * @inheritdoc + */ + public function getAttributeName() + { + $attribute = $this->getAttribute(); + if ($this->getAttributeScope()) { + $attribute = $this->getAttributeScope() . '::' . $attribute; + } + + return $this->getAttributeOption($attribute); + } + + /** + * @inheritdoc + */ + public function loadAttributeOptions() + { + $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode(); + + $attributes = []; + foreach ($productAttributes as $attribute) { + /* @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + if (!$attribute->isAllowedForRuleCondition() + || !$attribute->getDataUsingMethod($this->_isUsedForRuleProperty) + ) { + continue; + } + $frontLabel = $attribute->getFrontendLabel(); + $attributes[$attribute->getAttributeCode()] = $frontLabel; + $attributes['parent::' . $attribute->getAttributeCode()] = $frontLabel . __('(Parent Only)'); + $attributes['children::' . $attribute->getAttributeCode()] = $frontLabel . __('(Children Only)'); + } + + $this->_addSpecialAttributes($attributes); + + asort($attributes); + $this->setAttributeOption($attributes); + + return $this; + } + + /** + * @inheritdoc + */ + public function getAttributeElementHtml() + { + $html = parent::getAttributeElementHtml() . + $this->getAttributeScopeElement()->getHtml(); + + return $html; + } + + /** + * Retrieve form element for scope element + * + * @return \Magento\Framework\Data\Form\Element\AbstractElement + */ + private function getAttributeScopeElement(): \Magento\Framework\Data\Form\Element\AbstractElement + { + return $this->getForm()->addField( + $this->getPrefix() . '__' . $this->getId() . '__attribute_scope', + 'hidden', + [ + 'name' => $this->elementName . '[' . $this->getPrefix() . '][' . $this->getId() . '][attribute_scope]', + 'value' => $this->getAttributeScope(), + 'no_span' => true, + 'class' => 'hidden', + 'data-form-part' => $this->getFormName(), + ] + ); + } + + /** + * Set attribute value + * + * @param string $value + * @return void + */ + public function setAttribute(string $value) + { + if (strpos($value, '::') !== false) { + list($scope, $attribute) = explode('::', $value); + $this->setData('attribute_scope', $scope); + $this->setData('attribute', $attribute); + } else { + $this->setData('attribute', $value); + } + } + + /** + * @inheritdoc + */ + public function loadArray($arr) + { + parent::loadArray($arr); + $this->setAttributeScope($arr['attribute_scope'] ?? null); + + return $this; + } + + /** + * @inheritdoc + */ + public function asArray(array $arrAttributes = []) + { + $out = parent::asArray($arrAttributes); + $out['attribute_scope'] = $this->getAttributeScope(); + + return $out; } /** * Validate Product Rule Condition * * @param \Magento\Framework\Model\AbstractModel $model + * * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function validate(\Magento\Framework\Model\AbstractModel $model) { @@ -51,10 +186,17 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) $attrCode = $this->getAttribute(); - if ('category_ids' == $attrCode) { + if ($attrCode === 'category_ids') { return $this->validateAttribute($this->_getAvailableInCategories($product->getId())); } + if ($attrCode === 'quote_item_price') { + $numericOperations = $this->getDefaultOperatorInputByType()['numeric']; + if (in_array($this->getOperator(), $numericOperations)) { + $this->setData('value', $this->getFormattedPrice($this->getValue())); + } + } + return parent::validate($product); } @@ -79,4 +221,23 @@ public function getValueElementChooserUrl() } return $url !== false ? $this->_backendData->getUrl($url) : ''; } + + /** + * @param string $value + * @return float|null + */ + private function getFormattedPrice($value) + { + $value = preg_replace('/[^0-9^\^.,-]/m', '', $value); + + /** + * If the comma is the third symbol in the number, we consider it to be a decimal separator + */ + $separatorComa = strpos($value, ','); + $separatorDot = strpos($value, '.'); + if ($separatorComa !== false && $separatorDot === false && preg_match('/,\d{3}$/m', $value) === 1) { + $value .= '.00'; + } + return $this->_localeFormat->getNumber($value); + } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php index b5ac02e67b1e1..2277240eb8aad 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php @@ -8,6 +8,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; /** + * Combine conditions for product. + * * @api * @since 100.0.2 */ @@ -85,4 +87,76 @@ public function collectValidatedAttributes($productCollection) } return $this; } + + /** + * @inheritdoc + */ + protected function _isValid($entity) + { + if (!$this->getConditions()) { + return true; + } + + $all = $this->getAggregator() === 'all'; + $true = (bool)$this->getValue(); + + foreach ($this->getConditions() as $cond) { + if ($entity instanceof \Magento\Framework\Model\AbstractModel) { + $validated = $this->validateEntity($entity, $cond); + } else { + $validated = $cond->validateByEntityId($entity); + } + if ($all && $validated !== $true) { + return false; + } elseif (!$all && $validated === $true) { + return true; + } + } + + return $all ? true : false; + } + + /** + * Validate entity. + * + * @param \Magento\Framework\Model\AbstractModel $entity + * @param mixed $cond + * @return bool + */ + private function validateEntity(\Magento\Framework\Model\AbstractModel $entity, $cond): bool + { + $true = (bool)$this->getValue(); + $validated = !$true; + foreach ($this->retrieveValidateEntities($entity, $cond->getAttributeScope()) as $validateEntity) { + $validated = $cond->validate($validateEntity); + if ($validated === $true) { + break; + } + } + + return $validated; + } + + /** + * Retrieve entities for validation by attribute scope + * + * @param \Magento\Framework\Model\AbstractModel $entity + * @param string|null $attributeScope + * @return \Magento\Framework\Model\AbstractModel[] + */ + private function retrieveValidateEntities( + \Magento\Framework\Model\AbstractModel $entity, + $attributeScope + ): array { + if ($attributeScope === 'parent') { + $validateEntities = [$entity]; + } elseif ($attributeScope === 'children') { + $validateEntities = $entity->getChildren() ?: [$entity]; + } else { + $validateEntities = $entity->getChildren() ?: []; + $validateEntities[] = $entity; + } + + return $validateEntities; + } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php index 1e8fbf43ec3bc..b3a44fcc56011 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php @@ -5,6 +5,9 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +/** + * Subselect conditions for product. + */ class Subselect extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -161,9 +164,12 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) } } if ($hasValidChild || parent::validate($item)) { - $total += (($hasValidChild && $useChildrenTotal) ? $childrenAttrTotal : $item->getData($attr)); + $total += ($hasValidChild && $useChildrenTotal) + ? $childrenAttrTotal * $item->getQty() + : $item->getData($attr); } } + return $this->validateAttribute($total); } } diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index 06a4e252bf60e..4262ce90c8667 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -6,6 +6,8 @@ namespace Magento\SalesRule\Model; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; +use Magento\Framework\App\ObjectManager; /** * Class RulesApplier @@ -25,19 +27,33 @@ class RulesApplier */ protected $validatorUtility; + /** + * @var ChildrenValidationLocator + */ + private $childrenValidationLocator; + + /** + * @var \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory + */ + private $calculatorFactory; + /** * @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\SalesRule\Model\Utility $utility + * @param ChildrenValidationLocator $childrenValidationLocator */ public function __construct( \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\SalesRule\Model\Utility $utility + \Magento\SalesRule\Model\Utility $utility, + ChildrenValidationLocator $childrenValidationLocator = null ) { $this->calculatorFactory = $calculatorFactory; $this->validatorUtility = $utility; $this->_eventManager = $eventManager; + $this->childrenValidationLocator = $childrenValidationLocator + ?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class); } /** @@ -61,6 +77,9 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) } if (!$skipValidation && !$rule->getActions()->validate($item)) { + if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { + continue; + } $childItems = $item->getChildren(); $isContinue = true; if (!empty($childItems)) { diff --git a/app/code/Magento/SalesRule/Model/Utility.php b/app/code/Magento/SalesRule/Model/Utility.php index a3876a9d7e046..45c5dc3da0ebd 100644 --- a/app/code/Magento/SalesRule/Model/Utility.php +++ b/app/code/Magento/SalesRule/Model/Utility.php @@ -189,6 +189,8 @@ public function deltaRoundingFix( ) { $discountAmount = $discountData->getAmount(); $baseDiscountAmount = $discountData->getBaseAmount(); + $rowTotalInclTax = $item->getRowTotalInclTax(); + $baseRowTotalInclTax = $item->getBaseRowTotalInclTax(); //TODO Seems \Magento\Quote\Model\Quote\Item\AbstractItem::getDiscountPercent() returns float value //that can not be used as array index @@ -205,6 +207,23 @@ public function deltaRoundingFix( - $this->priceCurrency->round($baseDiscountAmount); } + /** + * When we have 100% discount check if totals will not be negative + */ + + if ($percentKey == 100) { + $discountDelta = $rowTotalInclTax - $discountAmount; + $baseDiscountDelta = $baseRowTotalInclTax - $baseDiscountAmount; + + if ($discountDelta < 0) { + $discountAmount += $discountDelta; + } + + if ($baseDiscountDelta < 0) { + $baseDiscountAmount += $baseDiscountDelta; + } + } + $discountData->setAmount($this->priceCurrency->round($discountAmount)); $discountData->setBaseAmount($this->priceCurrency->round($baseDiscountAmount)); diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 197b7366a8b8d..ea0221d8f072d 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SalesRule\Model; use Magento\Quote\Model\Quote\Address; @@ -184,6 +182,8 @@ protected function _getRules(Address $address = null) } /** + * Address id getter. + * * @param Address $address * @return string */ @@ -329,21 +329,7 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $rule->getDiscountAmount(); break; case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: - $cartRules = $address->getCartFixedRules(); - if (!isset($cartRules[$rule->getId()])) { - $cartRules[$rule->getId()] = $rule->getDiscountAmount(); - } - if ($cartRules[$rule->getId()] > 0) { - $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore()); - $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount); - $baseDiscountAmount = min( - $baseShippingAmount - $address->getBaseShippingDiscountAmount(), - $cartRules[$rule->getId()] - ); - $cartRules[$rule->getId()] -= $baseDiscountAmount; - } - - $address->setCartFixedRules($cartRules); + // Shouldn't be proceed according to MAGETWO-96403 break; } @@ -508,7 +494,7 @@ public function sortItemsByPriority($items, Address $address = null) foreach ($items as $itemKey => $itemValue) { if ($rule->getActions()->validate($itemValue)) { unset($items[$itemKey]); - array_push($itemsSorted, $itemValue); + $itemsSorted[] = $itemValue; } } } @@ -521,6 +507,8 @@ public function sortItemsByPriority($items, Address $address = null) } /** + * Rule total items getter. + * * @param int $key * @return array * @throws \Magento\Framework\Exception\LocalizedException @@ -535,6 +523,8 @@ public function getRuleItemTotalsInfo($key) } /** + * Decrease rule items count. + * * @param int $key * @return $this */ diff --git a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php new file mode 100644 index 0000000000000..d9699d334ff6a --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php @@ -0,0 +1,52 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Observer; + +use Magento\Framework\Event\Observer; +use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Framework\Event\ObserverInterface; + +class AssignCouponDataAfterOrderCustomerAssignObserver implements ObserverInterface +{ + const EVENT_KEY_CUSTOMER = 'customer'; + + const EVENT_KEY_ORDER = 'order'; + + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * AssignCouponDataAfterOrderCustomerAssign constructor. + * + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct( + UpdateCouponUsages $updateCouponUsages + ) { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * @inheritDoc + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + /** @var OrderInterface $order */ + $order = $event->getData(self::EVENT_KEY_ORDER); + + if ($order->getCustomerId()) { + $this->updateCouponUsages->execute($order, true); + } + } +} diff --git a/app/code/Magento/SalesRule/Observer/SalesOrderAfterPlaceObserver.php b/app/code/Magento/SalesRule/Observer/SalesOrderAfterPlaceObserver.php deleted file mode 100644 index a49802d4c304a..0000000000000 --- a/app/code/Magento/SalesRule/Observer/SalesOrderAfterPlaceObserver.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SalesRule\Observer; - -use Magento\Framework\Event\Observer as EventObserver; -use Magento\Framework\Event\ObserverInterface; - -class SalesOrderAfterPlaceObserver implements ObserverInterface -{ - /** - * @var \Magento\SalesRule\Model\RuleFactory - */ - protected $_ruleFactory; - - /** - * @var \Magento\SalesRule\Model\RuleFactory - */ - protected $_ruleCustomerFactory; - - /** - * @var \Magento\SalesRule\Model\Coupon - */ - protected $_coupon; - - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Usage - */ - protected $_couponUsage; - - /** - * @param \Magento\SalesRule\Model\RuleFactory $ruleFactory - * @param \Magento\SalesRule\Model\Rule\CustomerFactory $ruleCustomerFactory - * @param \Magento\SalesRule\Model\Coupon $coupon - * @param \Magento\SalesRule\Model\ResourceModel\Coupon\Usage $couponUsage - */ - public function __construct( - \Magento\SalesRule\Model\RuleFactory $ruleFactory, - \Magento\SalesRule\Model\Rule\CustomerFactory $ruleCustomerFactory, - \Magento\SalesRule\Model\Coupon $coupon, - \Magento\SalesRule\Model\ResourceModel\Coupon\Usage $couponUsage - ) { - $this->_ruleFactory = $ruleFactory; - $this->_ruleCustomerFactory = $ruleCustomerFactory; - $this->_coupon = $coupon; - $this->_couponUsage = $couponUsage; - } - - /** - * @param EventObserver $observer - * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function execute(EventObserver $observer) - { - $order = $observer->getEvent()->getOrder(); - - if (!$order || !$order->getAppliedRuleIds()) { - return $this; - } - - // lookup rule ids - $ruleIds = explode(',', $order->getAppliedRuleIds()); - $ruleIds = array_unique($ruleIds); - - $ruleCustomer = null; - $customerId = $order->getCustomerId(); - - // use each rule (and apply to customer, if applicable) - foreach ($ruleIds as $ruleId) { - if (!$ruleId) { - continue; - } - /** @var \Magento\SalesRule\Model\Rule $rule */ - $rule = $this->_ruleFactory->create(); - $rule->load($ruleId); - if ($rule->getId()) { - $rule->loadCouponCode(); - $rule->setTimesUsed($rule->getTimesUsed() + 1); - $rule->save(); - - if ($customerId) { - /** @var \Magento\SalesRule\Model\Rule\Customer $ruleCustomer */ - $ruleCustomer = $this->_ruleCustomerFactory->create(); - $ruleCustomer->loadByCustomerRule($customerId, $ruleId); - - if ($ruleCustomer->getId()) { - $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + 1); - } else { - $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); - } - $ruleCustomer->save(); - } - } - } - - $this->_coupon->load($order->getCouponCode(), 'code'); - if ($this->_coupon->getId()) { - $this->_coupon->setTimesUsed($this->_coupon->getTimesUsed() + 1); - $this->_coupon->save(); - if ($customerId) { - $this->_couponUsage->updateCustomerCouponTimesUsed($customerId, $this->_coupon->getId()); - } - } - - return $this; - } -} diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php new file mode 100644 index 0000000000000..f62e2e0be4d0c --- /dev/null +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Plugin; + +use Magento\Sales\Model\Order; +use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; + +class CouponUsagesDecrement +{ + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * CouponUsagesDecrement constructor. + * + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct( + UpdateCouponUsages $updateCouponUsages + ) { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * Decrements number of coupon usages after cancelling order. + * + * @param Order $subject + * @param callable $proceed + * @return Order + */ + public function aroundCancel(Order $subject, callable $proceed) + { + $canCancel = $subject->canCancel(); + $returnValue = $proceed(); + if ($canCancel) { + $returnValue = $this->updateCouponUsages->execute($returnValue, false); + } + + return $returnValue; + } +} diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php new file mode 100644 index 0000000000000..8810b4630f51a --- /dev/null +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Plugin; + +use Magento\Sales\Model\Order; +use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; + +class CouponUsagesIncrement +{ + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * CouponUsagesIncrement constructor. + * + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct( + UpdateCouponUsages $updateCouponUsages + ) { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * Increments number of coupon usages after placing order. + * + * @param Order $subject + * @param Order $result + * @return Order + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterPlace(Order $subject, Order $result) + { + $this->updateCouponUsages->execute($subject, true); + + return $subject; + } +} diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..10783c1f0caeb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteCartPriceRuleByName"> + <arguments> + <argument name="ruleName" type="string"/> + </arguments> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> + <click selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="clickDeleteButton"/> + <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> + </actionGroup> + <actionGroup name="selectNotLoggedInCustomerGroup"> + <!-- This actionGroup was created to be merged from B2B because B2B has a very different form control here --> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + </actionGroup> + + <!--Set Subtotal condition for Customer Segment--> + <actionGroup name="SetCartAttributeConditionForCartPriceRuleActionGroup"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="operatorType" defaultValue="is" type="string"/> + <argument name="value" type="string"/> + </arguments> + <scrollTo selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="scrollToActionTab"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.conditionsHeaderOpen}}" + visible="false" stepKey="openActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="applyRuleForConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{attributeName}}" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickToChooseOption"/> + <selectOption userInput="{{operatorType}}" selector="{{AdminCartPriceRulesFormSection.conditionsOperator}}" stepKey="setOperatorType"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption1"/> + <fillField userInput="{{value}}" selector="{{AdminCartPriceRulesFormSection.conditionsValue}}" stepKey="fillActionValue"/> + <click selector="{{AdminMainActionsSection.saveAndContinue}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="SetConditionForActionsInCartPriceRuleActionGroup"> + <arguments> + <argument name="actionsAggregator" type="string" defaultValue="ANY"/> + <argument name="actionsValue" type="string" defaultValue="FALSE"/> + <argument name="childAttribute" type="string" defaultValue="Category"/> + <argument name="actionOperator" type="string" defaultValue="is"/> + <argument name="actionValue" type="string"/> + </arguments> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickOnActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('ALL')}}" stepKey="clickToChooseOption"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsAggregator}}" userInput="{{actionsAggregator}}" stepKey="selectCondition"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('TRUE')}}" stepKey="clickToChooseOption2"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsValue}}" userInput="{{actionsValue}}" stepKey="selectCondition2"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="selectActionConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{childAttribute}}" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForAttributeSelected"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickOnOperator"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.operator}}" userInput="{{actionOperator}}" stepKey="selectOperator"/> + <!-- In case we are choosing already selected value - select is not closed automatically --> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.condition('...')}}" dependentSelector="{{AdminCartPriceRulesFormSection.operator}}" visible="true" stepKey="closeSelect"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption3"/> + <fillField selector="{{AdminCartPriceRulesFormSection.actionValue}}" userInput="{{actionValue}}" stepKey="fillActionValue"/> + <click selector="{{AdminCartPriceRulesFormSection.applyAction}}" stepKey="applyAction"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="deleteAllCartPriceRule"> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceRuleList"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters"/> + <executeInSelenium + function=" + function ($webdriver) use ($I) { + $rows = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)')); + while(!empty($rows)) { + $rows[0]->click(); + $I->waitForPageLoad(30); + $I->click('#delete'); + $I->waitForPageLoad(30); + $I->waitForElementVisible('aside.confirm .modal-footer button.action-accept', 10); + $I->waitForPageLoad(60); + $I->click('aside.confirm .modal-footer button.action-accept'); + $I->waitForPageLoad(60); + $I->waitForLoadingMaskToDisappear(); + $I->waitForElementVisible('#messages div.message-success', 10); + $I->see('You deleted the rule.', '#messages div.message-success'); + $rows = $webdriver->findElements(\Facebook\WebDriver\WebDriverBy::cssSelector('table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)')); + } + }" + stepKey="deleteAllCartPriceRulesOneByOne"/> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..c5f35aef6f480 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="rule"/> + </arguments> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPriceList"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{rule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{rule.websites}}" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" parameterArray="[{{rule.customerGroups}}]" stepKey="selectCustomerGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="{{rule.apply}}" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{rule.discountAmount}}" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="AdminCreateCartPriceRuleWithProductSubselectionCondition" extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="condition" type="string" defaultValue="Category" /> + <argument name="operation" type="string" defaultValue="equals or greater than" /> + <argument name="totalQuantity" type="string" defaultValue="2" /> + <argument name="value" type="string" defaultValue="_defaultCategory.name" /> + </arguments> + <!--Go to Conditions section--> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" after="selectActionType" stepKey="openConditionsSection"/> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1')}}" after="openConditionsSection" stepKey="addFirstCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1')}}" userInput="Products subselection" after="addFirstCondition" stepKey="selectRule"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('is')}}" after="selectRule" stepKey="waitForFirstRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('is')}}" after="waitForFirstRuleElement" stepKey="clickToChangeRule"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleOperatorSelect('1--1')}}" userInput="{{operation}}" after="clickToChangeRule" stepKey="selectRule1"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="selectRule1" stepKey="waitForSecondRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="waitForSecondRuleElement" stepKey="clickToChangeRule1"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleValueInput('1--1')}}" userInput="{{totalQuantity}}" after="clickToChangeRule1" stepKey="fillRule"/> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1--1')}}" after="fillRule" stepKey="addSecondCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1--1')}}" userInput="{{condition}}" after="addSecondCondition" stepKey="selectSecondCondition"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="selectSecondCondition" stepKey="waitForThirdRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter('...')}}" after="waitForThirdRuleElement" stepKey="addThirdCondition"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.chooseValue('1--1--1')}}" after="addThirdCondition" stepKey="waitForForthRuleElement"/> + <click selector="{{AdminCartPriceRulesFormSection.chooseValue('1--1--1')}}" after="waitForForthRuleElement" stepKey="chooseValue"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(value)}}" after="chooseValue" stepKey="waitForCategoryVisible"/> + <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(value)}}" after="waitForCategoryVisible" stepKey="checkCategoryName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleSpecificCouponActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleSpecificCouponActionGroup.xml new file mode 100644 index 0000000000000..4801a033dfc6e --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleSpecificCouponActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCartPriceRuleSpecificCouponActionGroup" + extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="couponCode" type="string" defaultValue="{{_defaultCoupon.code}}"/> + <argument name="userPerCoupon" type="string" defaultValue="1"/> + </arguments> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" before="clickToExpandActions" + userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{couponCode}}" + after="selectCouponType" stepKey="fillCouponCode"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" after="fillCouponCode" + userInput="{{userPerCoupon}}" stepKey="fillUserPerCoupon"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..0f18819e3ed1e --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminEditCartPriceRuleActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChangeCartPriceRuleWebsiteActionGroup"> + <arguments> + <argument name="websiteLabel" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForPriceList"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{websiteLabel}}" stepKey="selectWebsites"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..2c44fdf3e900f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Search grid with keyword search--> + <actionGroup name="AdminFilterCartPriceRuleActionGroup"> + <arguments> + <argument name="ruleName"/> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml new file mode 100644 index 0000000000000..4cd0637e83b77 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ApplyCartRuleOnStorefrontActionGroup"> + <arguments> + <argument name="product"/> + <argument name="couponCode" type="string"/> + </arguments> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> + <waitForText userInput="You added {{product.name}} to your shopping cart." stepKey="waitForProductAddToShoppingCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageIsOpened"/> + <click selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" stepKey="clickToDiscountTab"/> + <fillField selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" userInput="{{couponCode}}" + stepKey="fillCouponCode"/> + <click selector="{{AdminCartPriceRuleDiscountSection.applyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForApplyCode"/> + </actionGroup> + + <!-- Apply Sales Rule Coupon to the cart --> + <actionGroup name="StorefrontApplyCouponActionGroup"> + <arguments> + <argument name="couponCode" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{StorefrontDiscountSection.discountTab}}" dependentSelector="{{StorefrontDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{StorefrontDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <fillField userInput="{{couponCode}}" selector="{{StorefrontDiscountSection.couponInput}}" stepKey="fillCouponField"/> + <click selector="{{StorefrontDiscountSection.applyCodeBtn}}" stepKey="clickApplyButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage" /> + <see userInput='You used coupon code "{{couponCode}}".' selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Apply Sales Rule Coupon to the cart --> + <actionGroup name="StorefrontTryingToApplyCouponActionGroup" extends="StorefrontApplyCouponActionGroup"> + <remove keyForRemoval="waitForSuccessMessage"/> + <remove keyForRemoval="seeSuccessMessage"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.error}}" stepKey="waitError"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{{couponCode}}" is not valid.' + stepKey="seeErrorMessages"/> + </actionGroup> + + <!-- Cancel Sales Rule Coupon applied to the cart --> + <actionGroup name="StorefrontCancelCouponActionGroup"> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{StorefrontDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{StorefrontDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <click selector="{{StorefrontDiscountSection.cancelCoupon}}" stepKey="clickCancelButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see userInput="You canceled the coupon code." selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="StorefrontApplyCouponOnCheckoutActionGroup" extends="StorefrontApplyCouponActionGroup"> + <arguments> + <argument name="successMessage" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountInput}}" stepKey="waitForCouponField"/> + <fillField userInput="{{couponCode}}" selector="{{StorefrontDiscountSection.discountInput}}" stepKey="fillCouponField"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage" /> + <see userInput='{{successMessage}}' selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml new file mode 100644 index 0000000000000..87f50ed0e92cb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Apply discount code during checkout --> + <actionGroup name="StorefrontApplyDiscountCodeActionGroup"> + <arguments> + <argument name="discountCode" type="string"/> + </arguments> + <click selector="{{StorefrontDiscountSection.discountTab}}" stepKey="clickToAddDiscount"/> + <fillField selector="{{StorefrontDiscountSection.discountInput}}" userInput="{{discountCode}}" stepKey="fillFieldDiscountCode"/> + <click selector="{{StorefrontDiscountSection.applyCodeBtn}}" stepKey="clickToApplyDiscount"/> + <waitForPageLoad stepKey="waitForDiscountToBeAdded"/> + <see selector="{{StorefrontDiscountSection.discountVerificationMsg}}" userInput="Your coupon was successfully applied" stepKey="assertDiscountApplyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml new file mode 100644 index 0000000000000..35feabc8d9fbe --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyDiscountAmount"> + <arguments> + <argument name="expectedDiscount" type="string"/> + </arguments> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" userInput="{{expectedDiscount}}" stepKey="seeDiscountTotal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml new file mode 100644 index 0000000000000..8810157092d97 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/CouponData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultCoupon" type="coupon"> + <data key="rule_id">4</data> + <data key="code" unique="suffix">FREESHIPPING123</data> + <data key="times_used">0</data> + <data key="is_primary">false</data> + </entity> + + <entity name="SimpleSalesRuleCoupon" type="coupon"> + <var key="rule_id" entityKey="rule_id" entityType="SalesRule"/> + <data key="code" unique="suffix">couponCode</data> + <data key="is_primary">1</data> + <data key="times_used">0</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesCouponData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesCouponData.xml new file mode 100644 index 0000000000000..2e56367fe9d56 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesCouponData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiSalesRuleCoupon" type="SalesRuleCoupon"> + <data key="code" unique="suffix">salesCoupon</data> + <data key="times_used">0</data> + <data key="is_primary">1</data> + <data key="type">0</data> + <var key="rule_id" entityType="SalesRule" entityKey="rule_id"/> + </entity> + <entity name="SimpleSalesRuleCoupon" type="coupon"> + <var key="rule_id" entityKey="rule_id" entityType="SalesRule"/> + <data key="code" unique="suffix">Code</data> + <data key="is_primary">1</data> + <data key="times_used">0</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml new file mode 100644 index 0000000000000..cc695b347c4fb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleAddressConditions" type="SalesRuleConditionAttribute"> + <data key="subtotal">Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal</data> + <data key="totalItemsQty">Magento\SalesRule\Model\Rule\Condition\Address|total_qty</data> + <data key="totalWeight">Magento\SalesRule\Model\Rule\Condition\Address|weight</data> + <data key="shippingMethod">Magento\SalesRule\Model\Rule\Condition\Address|shipping_method</data> + <data key="shippingPostCode">Magento\SalesRule\Model\Rule\Condition\Address|postcode</data> + <data key="shippingRegion">Magento\SalesRule\Model\Rule\Condition\Address|region</data> + <data key="shippingState">Magento\SalesRule\Model\Rule\Condition\Address|region_id</data> + <data key="shippingCountry">Magento\SalesRule\Model\Rule\Condition\Address|country_id</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml new file mode 100644 index 0000000000000..8637b79421fde --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -0,0 +1,268 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiSalesRule" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">2</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">2</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="discount_qty">2</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">1</data> + <data key="is_rss">true</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + <requiredEntity type="SalesRuleLabel">SalesRuleLabelDefault</requiredEntity> + <requiredEntity type="SalesRuleLabel">SalesRuleLabelStore1</requiredEntity> + </entity> + <entity name="TestSalesRule" type="SalesRule"> + <data key="name" unique="suffix">TestSalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Percent of product price discount</data> + <data key="discountAmount">50</data> + </entity> + <entity name="SalesRuleWithSkuInActions" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN'</data> + <data key="is_active">true</data> + <data key="coupon_type">Specific Coupon</data> + <data key="coupon_code">ABCD</data> + <data key="discount_amount">70</data> + </entity> + <entity name="SalesRuleSpecificCoupon" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">1</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">2</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="discount_qty">1</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">1</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">true</data> + <data key="uses_per_coupon">2</data> + <data key="simple_free_shipping">1</data> + </entity> + <entity name="SalesRule100PercentDiscount" extends="TestSalesRule" type="SalesRule"> + <data key="discountAmount">100</data> + </entity> + <entity name="SaleRule50PercentDiscountNoCoupon" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + </entity> + <entity name="ApiCartRule" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + </entity> + <entity name="SalesRuleSpecificCouponWithFixedDiscount" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>1</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">1</data> + <data key="simple_action">cart_fixed</data> + <data key="discount_amount">10</data> + <data key="discount_qty">10</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">1</data> + </entity> + <entity name="CartPriceRuleWithRewardPointsOnly" type="SalesRule"> + <data key="name" unique="suffix">SalesRuleReward</data> + <data key="description">Sales Rule with Reward Point</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">0</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">0</data> + <requiredEntity type="sales-rule-extension-attribute">SalesRuleExtensionAttribute</requiredEntity> + </entity> + + <entity name="PriceRuleWithCondition" type="SalesRule"> + <data key="name" unique="suffix">SalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Fixed amount discount for whole cart</data> + <data key="discountAmount">0</data> + </entity> + + <entity name="SalesRuleNoCouponWithFixedDiscount" extends="ApiCartRule"> + <data key="simple_action">by_fixed</data> + </entity> + + <entity name="SalesRuleSpecificCouponWithPercentDiscount" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>1</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">1</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="discount_qty">10</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">1</data> + </entity> + + <entity name="ActiveSalesRuleForNotLoggedIn" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + <requiredEntity type="SalesRuleLabel">SalesRuleLabelDefault</requiredEntity> + <requiredEntity type="SalesRuleLabel">SalesRuleLabelStore1</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml new file mode 100644 index 0000000000000..43ff4d897c143 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleExtensionAttribute" type="sales-rule-extension-attribute"> + <data key="reward_points_delta">200</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml new file mode 100644 index 0000000000000..8af7ac0fdd99a --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleProductConditions" type="SalesRuleConditionAttribute"> + <data key="priceInCart" >Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price</data> + <data key="quantityInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_qty</data> + <data key="rowTotalInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_row_total</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/LICENSE.txt b/app/code/Magento/SalesRule/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/SalesRule/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 <insert your license name here>" 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/app/code/Magento/SalesRule/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/SalesRule/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/SalesRule/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 <insert your license name here>" 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/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml new file mode 100644 index 0000000000000..9bec2ecd006cf --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/coupon-meta.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCoupon" dataType="coupon" type="create" auth="adminOauth" url="/V1/coupons" method="POST"> + <contentType>application/json</contentType> + <object key="coupon" dataType="coupon"> + <field key="rule_id" required="true">integer</field> + <field key="times_used" required="true">integer</field> + <field key="is_primary" required="true">boolean</field> + <field key="code">string</field> + <field key="usage_limit">integer</field> + <field key="usage_per_customer">integer</field> + <field key="expiration_date">string</field> + <field key="created_at">string</field> + <field key="type">integer</field> + <field key="extension_attributes">empty_extension_attribute</field> + </object> + </operation> + + <operation name="DeleteCoupon" dataType="coupon" type="delete" auth="adminOauth" url="/rest/V1/coupons/{coupon_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml new file mode 100644 index 0000000000000..51c4ac24a7426 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml @@ -0,0 +1,12 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="SetSalesRuleExtensionAttribute" dataType="sales-rule-extension-attribute" type="create"> + <field key="reward_points_delta">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml new file mode 100644 index 0000000000000..38009c510d2be --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSalesRule" dataType="SalesRule" type="create" auth="adminOauth" url="/V1/salesRules" method="POST"> + <contentType>application/json</contentType> + <object key="rule" dataType="SalesRule"> + <field key="name" required="true">string</field> + <field key="description">string</field> + <field key="is_active">boolean</field> + <field key="from_date">string</field> + <field key="to_date">string</field> + <field key="uses_per_customer">integer</field> + <field key="sort_order">integer</field> + <field key="simple_action">string</field> + <field key="discount_amount">integer</field> + <field key="discount_qty">integer</field> + <field key="discount_step">integer</field> + <field key="times_used">integer</field> + <field key="uses_per_coupon">integer</field> + <field key="apply_to_shipping">boolean</field> + <field key="is_rss">boolean</field> + <field key="use_auto_generation">boolean</field> + <field key="coupon_type">string</field> + <field key="simple_free_shipping">string</field> + <field key="stop_rules_processing">boolean</field> + <field key="is_advanced">boolean</field> + <field key="extension_attributes">sales-rule-extension-attribute</field> + <array key="store_labels"> + <!-- specify object name as array value --> + <value>SalesRuleStoreLabel</value> + <!-- alternatively, define object embedded in array directly --> + <!--object dataType="SalesRuleStoreLabel" key="store_labels"> + <field key="store_id">integer</field> + <field key="store_label">string</field> + </object--> + </array> + <array key="product_ids"> + <value>integer</value> + </array> + <array key="customer_group_ids"> + <value>integer</value> + </array> + <array key="website_ids"> + <value>integer</value> + </array> + <object dataType="RuleCondition" key="condition"> + <field key="condition_type">string</field> + <array key="conditions"> + <value>integer</value> + </array> + <field key="aggregator_type">string</field> + <field key="operator">string</field> + <field key="attribute_name">string</field> + <field key="value">string</field> + <field key="extension_attributes">empty_extension_attribute</field> + </object> + <object dataType="ActionCondition" key="action_condition"> + <field key="condition_type">string</field> + <field key="aggregator_type">string</field> + <field key="operator">string</field> + <field key="attribute_name">string</field> + <field key="value">string</field> + <field key="extension_attributes">empty_extension_attribute</field> + </object> + </object> + </operation> + <operation name="DeleteSalesRule" dataType="SalesRule" type="delete" auth="adminOauth" url="/V1/salesRules/{rule_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule_store_label-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule_store_label-meta.xml new file mode 100644 index 0000000000000..2c27f09b83c31 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule_store_label-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSalesRuleStoreLabel" dataType="SalesRuleStoreLabel" type="create"> + <field key="store_id">integer</field> + <field key="store_label">string</field> + </operation> +</operations> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml new file mode 100644 index 0000000000000..faed9d42bcdec --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCartPriceRuleEditPage" area="admin" url="sales_rule/promo_quote/edit/id/{{salesRuleId}}" module="Magento_SalesRule" parameterized="true"> + <section name="AdminCartPriceRulesFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml new file mode 100644 index 0000000000000..3cd2563ddf183 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesLimitPage.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/PageObject.xsd"> + <page name="AdminCartPriceRulesLimitPage" url="sales_rule/promo_quote/index/limit/{{count}}" area="admin" module="Magento_SalesRule" parameterized="true"/> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesPage.xml new file mode 100644 index 0000000000000..6e4b2be8420b8 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRulesPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCartPriceRulesPage" url="sales_rule/promo_quote/" area="admin" module="Magento_SalesRule"> + <section name="AdminCartPriceRulesSection"/> + <section name="AdminCartPriceRulesFormSection"/> + <section name="AdminCartPriceRuleDiscountSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.xml new file mode 100644 index 0000000000000..83ec6095d57ee --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CheckoutCartPage" url="/checkout/cart" area="storefront" module="Magento_Checkout"> + <section name="StorefrontDiscountSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/PriceRuleNewPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/PriceRuleNewPage.xml new file mode 100644 index 0000000000000..f584d433cf1eb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/PriceRuleNewPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="PriceRuleNewPage" url="sales_rule/promo_quote/new/" area="admin" module="Magento_SalesRule"> + <section name="PriceRuleConditionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/README.md b/app/code/Magento/SalesRule/Test/Mftf/README.md new file mode 100644 index 0000000000000..7b65abe671e29 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sales Rule Functional Tests + +The Functional Test Module for **Magento Sales Rule** module. diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml new file mode 100644 index 0000000000000..d388cc4b61628 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRuleDiscountSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCartPriceRuleDiscountSection"> + <element name="discountTab" type="button" selector="#block-discount-heading"/> + <element name="couponInput" type="input" selector="#coupon_code"/> + <element name="applyCodeBtn" type="button" selector="#discount-coupon-form button[class*='apply']" timeout="30"/> + <element name="discountBlockActive" type="text" selector=".block.discount.active"/> + <element name="cancelButton" type="text" selector="#discount-form .action-cancel" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml new file mode 100644 index 0000000000000..e1dd048e4a9e4 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCartPriceRulesFormSection"> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> + <element name="delete" type="button" selector="#delete" timeout="30"/> + <element name="modalAcceptButton" type="button" selector="button.action-accept" timeout="30"/> + + <!-- Rule Information (the main form on the page) --> + <element name="ruleName" type="input" selector="input[name='name']"/> + <element name="websites" type="multiselect" selector="select[name='website_ids']"/> + <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> + <element name="coupon" type="select" selector="select[name='coupon_type']"/> + <element name="couponCode" type="input" selector="input[name='coupon_code']"/> + <element name="useAutoGeneration" type="checkbox" selector="input[name='use_auto_generation']"/> + <element name="userPerCoupon" type="input" selector="//input[@name='uses_per_coupon']"/> + <element name="userPerCustomer" type="input" selector="//input[@name='uses_per_customer']"/> + <element name="priority" type="input" selector="//*[@name='sort_order']"/> + + <!-- Conditions sub-form --> + <element name="conditionsHeader" type="button" selector="div[data-index='conditions']" timeout="30"/> + <element name="conditionsHeaderOpen" type="button" selector="div[data-index='conditions'] div[data-state-collapsible='open']" timeout="30"/> + <element name="addCondition" type="button" selector="//*[@id='conditions__{{arg}}__children']//span" parameterized="true"/> + <element name="ruleCondition" type="select" selector="rule[conditions][{{arg}}][new_child]" parameterized="true"/> + <element name="ruleParameter" type="text" selector="//span[@class='rule-param']/a[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="ruleOperatorSelect" type="select" selector="rule[conditions][{{arg}}][operator]" parameterized="true"/> + <element name="ruleValueInput" type="input" selector="rule[conditions][{{arg}}][value]" parameterized="true"/> + <element name="chooseValue" type="button" selector="label[for='conditions__{{arg}}__value']" parameterized="true"/> + <element name="categoryCheckbox" type="checkbox" selector="//span[contains(text(), '{{arg}}')]/parent::a/preceding-sibling::input[@type='checkbox']" parameterized="true"/> + <element name="conditionsValue" type="input" selector=".rule-param-edit input"/> + <element name="conditionsOperator" type="select" selector=".rule-param-edit select"/> + + <!-- Actions sub-form --> + <element name="actionsHeader" type="button" selector="div[data-index='actions']" timeout="30"/> + <element name="actionsHeaderOpen" type="button" selector="div[data-index='actions'] div[data-state-collapsible='open']" timeout="30"/> + <element name="apply" type="select" selector="select[name='simple_action']"/> + <element name="applyDiscountToShipping" type="checkbox" selector="input[name='apply_to_shipping']"/> + <element name="applyDiscountToShippingLabel" type="checkbox" selector="input[name='apply_to_shipping']+label"/> + <element name="discountAmount" type="input" selector="input[name='discount_amount']"/> + <element name="discountStep" type="input" selector="input[name='discount_step']"/> + <element name="freeShipping" type="select" selector="select[name='simple_free_shipping']"/> + <element name="conditions" type="button" selector=".rule-param.rule-param-new-child > a"/> + <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{arg}}']" parameterized="true"/> + <element name="actionsAggregator" type="select" selector="#actions__1__aggregator"/> + <element name="actionsValue" type="select" selector="#actions__1__value"/> + <element name="operator" type="select" selector="select[name*='[operator]']"/> + <element name="childAttribute" type="select" selector="select[name*='new_child']"/> + <element name="optionInput" type="input" selector="ul[class*='rule-param-children'] input[name*='[value]']"/> + <element name="actionValue" type="input" selector=".rule-param-edit input"/> + <element name="applyAction" type="text" selector=".rule-param-apply" timeout="30"/> + <element name="actionOperator" type="select" selector=".rule-param-edit select"/> + + <!-- Manage Coupon Codes sub-form --> + <element name="manageCouponCodesHeader" type="button" selector="div[data-index='manage_coupon_codes']" timeout="30"/> + <element name="successMessage" type="text" selector="div.message.message-success.success"/> + <element name="couponQty" type="input" selector="#coupons_qty"/> + <element name="generateCouponsButton" type="button" selector="#coupons_generate_button" timeout="30"/> + <element name="generatedCouponByIndex" type="text" selector="#couponCodesGrid_table > tbody > tr:nth-child({{var}}) > td.col-code" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml new file mode 100644 index 0000000000000..0aa01e06c44c7 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/SectionObject.xsd"> + <section name="AdminCartPriceRulesSection"> + <element name="addNewRuleButton" type="button" selector="#add" timeout="30"/> + <element name="messages" type="text" selector=".messages"/> + <element name="filterByNameInput" type="input" selector="input[name='name']"/> + <element name="searchButton" type="button" selector="#promo_quote_grid button[title='Search']" timeout="30"/> + <element name="rowByIndex" type="text" selector="tr[data-role='row']:nth-of-type({{var1}})" parameterized="true" timeout="30"/> + <element name="nameColumns" type="text" selector="td[data-column='name']"/> + <element name="rowContainingText" type="text" selector="//*[@id='promo_quote_grid_table']/tbody/tr[td//text()[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> + <element name="rulesRow" type="text" selector="//tr[@data-role='row']"/> + <element name="pageCurrent" type="text" selector="//label[@for='promo_quote_grid_page-current']"/> + <element name="countPages" type="text" selector="//label[@for='promo_quote_grid_page-current']//span"/> + <element name="totalCount" type="text" selector="span[data-ui-id*='grid-total-count']"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml new file mode 100644 index 0000000000000..3e5755cebdfe8 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="PriceRuleConditionsSection"> + <element name="conditionsTab" type="text" selector="//div[@data-index='conditions']//span[contains(.,'Conditions')][1]"/> + <element name="createNewRule" type="text" selector="span.rule-param.rule-param-new-child"/> + <element name="rulesDropdown" type="select" selector="select[data-form-part='sales_rule_form'][data-ui-id='newchild-0-select-rule-conditions-1-new-child']"/> + <element name="addProductAttributesButton" type="text" selector="#conditions__1--1__children>li>span>a"/> + <element name="productAttributesDropdown" type="select" selector="#conditions__1--1__new_child"/> + <element name="changeCategoriesButton" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>a"/> + <element name="categoriesChooser" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>span>label>a"/> + <element name="treeRoot" type="text" selector=".x-tree-root-ct.x-tree-lines"/> + <element name="lastTreeNode" type="text" selector=".x-tree-root-ct.x-tree-lines > div > li > ul > li:last-child div img.x-tree-elbow-end-plus"/> + <element name="subcategory4level" type="text" selector=".x-tree-root-ct.x-tree-lines > div > li > ul > li > ul > li > ul > li > ul > li > div img.x-tree-elbow-end-plus"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml new file mode 100644 index 0000000000000..26b52d4610c37 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCartSummarySection"> + <element name="discountLabel" type="text" selector="//*[@id='cart-totals']//tr[.//th//span[contains(@class, 'discount coupon')]]"/> + <element name="discountTotal" type="text" selector="//*[@id='cart-totals']//tr[.//th//span[contains(@class, 'discount coupon')]]//td//span//span[@class='price']"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml new file mode 100644 index 0000000000000..cab8f9f710c6e --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontDiscountSection"> + <element name="discountTab" type="button" selector="#block-discount-heading"/> + <element name="couponInput" type="input" selector="#coupon_code"/> + <element name="applyCodeBtn" type="button" selector="button[value='Apply Discount']" timeout="30"/> + <element name="cancelCoupon" type="button" selector="button[value='Cancel Coupon']"/> + <element name="discountInput" type="input" selector="#discount-code"/> + <element name="discountBlockActive" type="text" selector=".block.discount.active"/> + <element name="discountVerificationMsg" type="button" selector=".message-success div"/> + <element name="CancelCouponBtn" type="button" selector="#discount-form .action-cancel"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml new file mode 100644 index 0000000000000..327a126b8dc78 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCartRulesAppliedForProductInCartTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Check that cart rules applied for product in cart"/> + <description value="Check that cart rules applied for product in cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13629"/> + <useCaseId value="MAGETWO-94348"/> + <group value="salesRule"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category and product--> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">200</field> + <field key="quantity">500</field> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createPreReqCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="DeleteProductOnProductsGridPageByName" stepKey="deleteProductOnProductsGridPageByName"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{PriceRuleWithCondition.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters1"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Start creating a bundle product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!--Off dynamic price and set value--> + <scrollToTopOfPage stepKey="scrollToTopOfThePageToSeePriceTypeElement"/> + <click selector="{{AdminProductFormBundleSection.priceTypeSwitcher}}" stepKey="offDynamicPrice"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="0" stepKey="setProductPrice"/> + + <!-- Add option, a "Radio Buttons" type option, with one product and set fixed price 200--> + <actionGroup ref="CreateBundleProductForOneSimpleProductsWithRadioTypeOption" stepKey="createBundleProductWithRadioTypeOption"> + <argument name="bundleProduct" value="BundleProduct"/> + <argument name="simpleProductFirst" value="$$simpleProduct$$"/> + <argument name="simpleProductSecond"/> + </actionGroup> + <selectOption selector="{{AdminProductFormBundleSection.bundleSelectionPriceType}}" userInput="Fixed" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductFormBundleSection.bundleSelectionPriceValue}}" userInput="200" stepKey="fillPriceValue"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Create cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleWithProductSubselectionCondition" stepKey="createRule"> + <argument name="rule" value="PriceRuleWithCondition"/> + <argument name="condition" value="Category"/> + <argument name="operation" value="equals or greater than"/> + <argument name="totalQuantity" value="2"/> + <argument name="value" value="{{_defaultCategory.name}}"/> + </actionGroup> + + <!--Go to Storefront and add product to cart and checkout from cart--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="goToProduct"/> + <actionGroup ref="StorefrontAddProductToCartQuantityActionGroup" stepKey="addToCart"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="quantity" value="2"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check totals--> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="grabSubtotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="grabShippingTotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="grabTotal"/> + <assertEquals stepKey="assertSubtotal"> + <expectedResult type="string">$400.00</expectedResult> + <actualResult type="variable">$grabSubtotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertShippingTotal"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabShippingTotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertTotal"> + <expectedResult type="string">$410.00</expectedResult> + <actualResult type="variable">$grabTotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml new file mode 100644 index 0000000000000..573c77ff5f8a9 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CartPriceRuleForConfigurableProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Checking Cart Price Rule for configurable products"/> + <description value="Checking Cart Price Rule for configurable products"/> + <severity value="BLOCKER"/> + <testCaseId value="MAGETWO-95121"/> + <useCaseId value="MAGETWO-86098"/> + <group value="sales_rule"/> + </annotations> + + <before> + <!-- Create the configurable product --> + <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"/> + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create a simple product and give it the attribute with option --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!--Login to Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <actionGroup ref="deleteAllCartPriceRule" stepKey="deleteAllCartPriceRules"/> + + <!--Create cart price rule--> + <createData entity="SaleRule50PercentDiscountNoCoupon" stepKey="createCartPriceRule"> + <field key="discount_amount">70</field> + <field key="coupon_type">SPECIFIC_COUPON</field> + </createData> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + + <!--Set attribute sku property Use for Promo Rule Conditions = Yes and save attribute--> + <actionGroup ref="navigateToProductAttribute" stepKey="goToProductAttributeSkuPage"> + <argument name="attributeCode" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditions"/> + + <!-- Set condition for actions in cart price rule --> + <amOnPage url="{{AdminCartPriceRuleEditPage.url($$createCartPriceRule.rule_id$$)}}" stepKey="goToCartPriceRuleEditPage"/> + <actionGroup ref="SetConditionForActionsInCartPriceRuleActionGroup" stepKey="setConditionForActionsInCartPriceRule"> + <argument name="actionsAggregator" value="ANY"/> + <argument name="actionsValue" value="TRUE"/> + <argument name="childAttribute" value="SKU(Children Only)"/> + <argument name="actionOperator" value="is not one of"/> + <argument name="actionValue" value="$$createConfigChildProduct1.sku$$"/> + </actionGroup> + </before> + + <after> + <!--Remove configurable product and it's children--> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <!--Remove SalesRule--> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <!--Return default value to attribute sku--> + <actionGroup ref="navigateToProductAttribute" stepKey="goToProductAttributeSkuPage"> + <argument name="attributeCode" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditions"> + <argument name="useForPromoRule" value="No"/> + </actionGroup> + <!--Logout from Admin--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + + <!--Add the configurable product with first option to the cart --> + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectFirstOptionOfConfigProduct"> + <argument name="attributeLabel" value="$$createConfigProductAttribute.default_label$$"/> + <argument name="optionValue" value="$$getConfigAttributeOption1.value$$"/> + </actionGroup> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFirstOptionOfConfigProduct"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <!--Add the configurable product with second option to the cart --> + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectSecondOptionOfConfigProduct"> + <argument name="attributeLabel" value="$$createConfigProductAttribute.default_label$$"/> + <argument name="optionValue" value="$$getConfigAttributeOption2.value$$"/> + </actionGroup> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartSecondOptionOfConfigProduct"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <!--View and edit cart--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="storefrontOpenCartFromMinicart"/> + + <!-- Apply Cart Rule On Storefront --> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCartRule"> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountAmountVisible"/> + + <!--Verify values--> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.itemDiscount}}" stepKey="getDiscount"/> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.subtotal}}" stepKey="getSubtotal"/> + <assertEquals stepKey="checkDiscount"> + <expectedResult type="string">-$7.00</expectedResult> + <actualResult type="variable">$getDiscount</actualResult> + </assertEquals> + <assertEquals stepKey="checkSubtotal"> + <expectedResult type="string">$20.00</expectedResult> + <actualResult type="variable">$getSubtotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml new file mode 100644 index 0000000000000..7faaadb95b5ec --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="PriceRuleCategoryNestingTest"> + <annotations> + <description value="Category nesting level must be the same as were created in categories."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-88031"/> + <group value="sale_rules"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="subcategory1"/> + <createData entity="SubCategoryWithParent" stepKey="subcategory2"> + <requiredEntity createDataKey="subcategory1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="subcategory3"> + <requiredEntity createDataKey="subcategory2"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="subcategory4"> + <requiredEntity createDataKey="subcategory3"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="subcategory5"> + <requiredEntity createDataKey="subcategory4"/> + </createData> + </before> + <after> + <deleteData createDataKey="subcategory1" stepKey="deleteCategory1"/> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + <!-- Login as admin and open page for creation new Price Rule --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <amOnPage url="{{PriceRuleNewPage.url}}" stepKey="openCatalogPriceRulePage"/> + <waitForPageLoad stepKey="waitCatalogPriceRulePageLoad"/> + <!-- Open Conditions section and select Categories condition --> + <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="openConditionsSection"/> + <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="createNewRule"/> + <selectOption selector="{{PriceRuleConditionsSection.rulesDropdown}}" userInput="Magento\SalesRule\Model\Rule\Condition\Product\Found" stepKey="selectProductAttributes"/> + <waitForAjaxLoad stepKey="ajaxLoad1"/> + <waitForElement selector="{{PriceRuleConditionsSection.addProductAttributesButton}}" stepKey="wait1"/> + <scrollTo selector="{{PriceRuleConditionsSection.addProductAttributesButton}}" stepKey="scrollToAddProductAttributeButton"/> + <click selector="{{PriceRuleConditionsSection.addProductAttributesButton}}" stepKey="clickToshowAttributes"/> + <selectOption selector="{{PriceRuleConditionsSection.productAttributesDropdown}}" userInput="Magento\SalesRule\Model\Rule\Condition\Product|category_ids" stepKey="selectCategoryAttribute"/> + <waitForAjaxLoad stepKey="ajaxLoad2"/> + <!-- Select categories chooser --> + <waitForElement selector="{{PriceRuleConditionsSection.changeCategoriesButton}}" stepKey="wait2"/> + <click selector="{{PriceRuleConditionsSection.changeCategoriesButton}}" stepKey="changeCategories"/> + <click selector="{{PriceRuleConditionsSection.categoriesChooser}}" stepKey="showCategoriesChooser"/> + <waitForAjaxLoad stepKey="ajaxLoad3"/> + <!-- Click on categories to check that the deepest subcategory is clickable --> + <waitForElement selector="{{PriceRuleConditionsSection.treeRoot}}" stepKey="wait3"/> + <click selector="{{PriceRuleConditionsSection.lastTreeNode}}" stepKey="openLatestTreeNode1"/> + <click selector="{{PriceRuleConditionsSection.lastTreeNode}}" stepKey="openLatestTreeNode2"/> + <click selector="{{PriceRuleConditionsSection.lastTreeNode}}" stepKey="openLatestTreeNode3"/> + <waitForAjaxLoad stepKey="ajaxLoad4"/> + <waitForElement selector="{{PriceRuleConditionsSection.subcategory4level}}" stepKey="wait4"/> + <click selector="{{PriceRuleConditionsSection.subcategory4level}}" stepKey="openLatestTreeNode4"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml new file mode 100644 index 0000000000000..4a5ae1b03b91c --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAutoGeneratedCouponCodeTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="[Cart Price Rule] Auto generated coupon code considers 'Uses per Coupon' and 'Uses per Customer' options"/> + <description + value="[Cart Price Rule] Auto generated coupon code considers 'Uses per Coupon' and 'Uses per Customer' options"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-77724"/> + <group value="salesRule"/> + </annotations> + + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create simple product--> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"/> + <!-- Create a cart price rule --> + <createData entity="SalesRuleSpecificCoupon" stepKey="createSalesRule"/> + </before> + + <after> + <!-- Delete the cart price rule we made during the test --> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Search Cart Price Rule and go to edit Cart Price Rule --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForCartPriceRulesIndexPageIsOpened"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="$$createSalesRule.name$$" + stepKey="fillFieldFilterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="clickSearchButton"/> + <see selector="{{AdminCartPriceRulesSection.nameColumns}}" userInput="$$createSalesRule.name$$" + stepKey="seeRuleName"/> + <click selector="{{AdminCartPriceRulesSection.rowContainingText($$createSalesRule.name$$)}}" + stepKey="goToEditRule"/> + + <!-- Step 3-4. Navigate to Manage Coupon Codes section to generate 1 coupon code --> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" + dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" + stepKey="clickManageCouponCodes"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponQty}}" userInput="1" stepKey="fillFieldCouponQty"/> + <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerateCoupon"/> + <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="1 coupon(s) have been generated." + stepKey="seeSuccessMessage"/> + <grabTextFrom selector="{{AdminCartPriceRulesFormSection.generatedCouponByIndex('1')}}" + stepKey="couponCode"/> + + <!-- Step: 5. Login to storefront as previously created customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginAsCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Step: 6-7. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsOpened"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='You used coupon code "{$couponCode}"' stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{$couponCode}"' + stepKey="seeSuccessMessage1"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementDiscountVisible"/> + + <!-- Step 8. Go to Checkout and Click Place Order button --> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" + stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" + stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + + <!-- Step: 9-10. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsOpened1"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule1"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForErrorMessage"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages"/> + <waitForElementNotVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible"/> + + <!-- Step 11. Log out from storefront --> + <amOnPage url="{{StorefrontCustomerSignOutPage.url}}" stepKey="storefrontSignOut"/> + <waitForLoadingMaskToDisappear stepKey="waitSignOutPage"/> + + <!-- Step: 12-13. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage2"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsOpened2"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule2"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='You used coupon code "{$couponCode}"' stepKey="waitForSuccessMessage1"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{$couponCode}"' + stepKey="seeSuccessMessage2"/> + <waitForElementVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementDiscountVisible1"/> + + <!-- Step 14. Go to Checkout and Click Place Order button --> + <click selector="{{StorefrontCheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout1"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage1"/> + + <!-- Step: 15-16. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage3"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsOpened3"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule3"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForErrorMessage1"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages1"/> + <waitForElementNotVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible1"/> + + <!-- Step: 17. Reset Cookie --> + <resetCookie userInput="PHPSESSID" stepKey="resetCookie"/> + + <!-- Step: 18-19. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage4"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsOpened4"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule4"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForErrorMessage2"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages2"/> + <waitForElementNotVisible selector="{{StorefrontCheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible2"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml new file mode 100644 index 0000000000000..30f8379db5f84 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCategoryRulesShouldApplyToComplexProductsTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create cart price rule"/> + <title value="Category rules should apply to complex products"/> + <description value="Sales rules filtering on category should apply to all products, including complex products."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76029"/> + <group value="catalogRule"/> + </annotations> + <before> + <!-- Create two Categories: CAT1 and CAT2 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleSubCategory" stepKey="createCategory2"/> + + <!--Create config1 and config2--> + <actionGroup ref="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" stepKey="createConfigurableProduct1"> + <argument name="productName" value="config1"/> + </actionGroup> + <actionGroup ref="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" stepKey="createConfigurableProduct2"> + <argument name="productName" value="config2"/> + </actionGroup> + + <!-- Assign config1 and the associated child products to CAT1 --> + <createData entity="DefaultCategoryProductLink" stepKey="assignConfigurableProduct1ToCategory"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createConfigProductCreateConfigurableProduct1"/> + </createData> + + <createData entity="CustomCategoryProductLink" stepKey="assignConfig1ChildProduct1ToCategory"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createConfigChildProduct1CreateConfigurableProduct1"/> + </createData> + + <createData entity="CustomCategoryProductLink" stepKey="assignConfig1ChildProduct2ToCategory"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createConfigChildProduct2CreateConfigurableProduct1"/> + </createData> + + <!-- Assign config12 and the associated child products to CAT2 --> + <createData entity="DefaultCategoryProductLink" stepKey="assignConfigurableProduct2ToCategory2"> + <requiredEntity createDataKey="createCategory2"/> + <requiredEntity createDataKey="createConfigProductCreateConfigurableProduct2"/> + </createData> + + <createData entity="CustomCategoryProductLink" stepKey="assignConfig2ChildProduct1ToCategory2"> + <requiredEntity createDataKey="createCategory2"/> + <requiredEntity createDataKey="createConfigChildProduct1CreateConfigurableProduct2"/> + </createData> + + <createData entity="CustomCategoryProductLink" stepKey="assignConfig2ChildProduct2ToCategory2"> + <requiredEntity createDataKey="createCategory2"/> + <requiredEntity createDataKey="createConfigChildProduct2CreateConfigurableProduct2"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="deleteAllCartPriceRule" stepKey="deleteCartPriceRules"/> + </before> + <after> + <!--Delete configurable product 1--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct1" stepKey="deleteConfigProduct1"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory1"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct1" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct1" stepKey="deleteConfigProductAttribute1"/> + <!--Delete configurable product 2--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct2" stepKey="deleteConfigProduct2"/> + <deleteData createDataKey="createCategory2" stepKey="deleteCategory2"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct2" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct2" stepKey="deleteConfigChildProduct4"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct2" stepKey="deleteConfigProductAttribute2"/> + <!--Delete Cart Price Rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- 1: Create a cart price rule applying to CAT1 with discount --> + <createData entity="SalesRuleNoCouponWithFixedDiscount" stepKey="createCartPriceRule"/> + + <amOnPage url="{{AdminCartPriceRuleEditPage.url($$createCartPriceRule.rule_id$$)}}" stepKey="goToCartPriceRuleEditPage"/> + + <actionGroup ref="SetConditionForActionsInCartPriceRuleActionGroup" stepKey="setConditionForActionsInCartPriceRuleActionGroup"> + <argument name="actionValue" value="$$createCategory.id$$"/> + </actionGroup> + + <!-- 2: Go to frontend and add an item from both CAT1 and CAT2 to your cart --> + <!-- 2.1: Open configurable product 1 and add all his child products to cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateConfigurableProduct1Page"> + <argument name="productUrlKey" value="$$createConfigProductCreateConfigurableProduct1.custom_attributes[url_key]$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectConfigurableProduct1Option1"> + <argument name="attributeLabel" value="$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$"/> + <argument name="optionValue" value="$$createConfigProductAttributeOption1CreateConfigurableProduct1.option[store_labels][0][label]$$"/> + </actionGroup> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addConfigurableProduct1ToCart"> + <argument name="productName" value="$$createConfigProductCreateConfigurableProduct1.name$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectConfigurableProduct1Option2"> + <argument name="attributeLabel" value="$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$"/> + <argument name="optionValue" value="$$createConfigProductAttributeOption2CreateConfigurableProduct1.option[store_labels][0][label]$$"/> + </actionGroup> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addConfigurableProduct1ToCartAgain"> + <argument name="productName" value="$$createConfigProductCreateConfigurableProduct1.name$$"/> + </actionGroup> + + <!-- Discount amount is not applied --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="proceedToCartPage"/> + <waitForElementVisible time="30" selector="{{StorefrontCheckoutCartSummarySection.tableTotals}}" stepKey="waitForCartTotalsBlockLoaded"/> + <dontSeeElement selector="{{StorefrontCheckoutCartSummarySection.discountTotal}}" stepKey="assertDiscountIsNotApplied"/> + + <!-- 2.2: Open configurable product 2 and add all his child products to cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateConfigurableProduct2Page"> + <argument name="productUrlKey" value="$$createConfigProductCreateConfigurableProduct2.custom_attributes[url_key]$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectConfigurableProduct2Option1"> + <argument name="attributeLabel" value="$$createConfigProductAttributeCreateConfigurableProduct2.attribute[frontend_labels][0][label]$$"/> + <argument name="optionValue" value="$$createConfigProductAttributeOption1CreateConfigurableProduct2.option[store_labels][0][label]$$"/> + </actionGroup> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addConfigurableProduct2ToCart"> + <argument name="productName" value="$$createConfigProductCreateConfigurableProduct2.name$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectConfigurableAttributeOptionActionGroup" stepKey="selectConfigurableProduct2Option2"> + <argument name="attributeLabel" value="$$createConfigProductAttributeCreateConfigurableProduct2.attribute[frontend_labels][0][label]$$"/> + <argument name="optionValue" value="$$createConfigProductAttributeOption2CreateConfigurableProduct2.option[store_labels][0][label]$$"/> + </actionGroup> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addConfigurableProduct2ToCartAgain"> + <argument name="productName" value="$$createConfigProductCreateConfigurableProduct2.name$$"/> + </actionGroup> + + <!-- Discount amount is applied --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="proceedToCartPageAgain"/> + <waitForElementVisible time="30" selector="{{StorefrontCheckoutCartSummarySection.discountTotal}}" stepKey="waitForDiscountTotal"/> + <see selector="{{StorefrontCheckoutCartSummarySection.discountTotal}}" userInput="-$100.00" stepKey="assertDiscountAmount"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php index fb01476ed6b34..a19de4ff2ef90 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php @@ -62,7 +62,7 @@ public function testGetButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to delete this?' - ) . '\', \'' . $deleteUrl . '\')', + ) . '\', \'' . $deleteUrl . '\', {data: {}})', 'sort_order' => 20, ]; diff --git a/app/code/Magento/SalesRule/Test/Unit/Block/Rss/DiscountsTest.php b/app/code/Magento/SalesRule/Test/Unit/Block/Rss/DiscountsTest.php index cd00562bc4f7a..bd14773ea4923 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Block/Rss/DiscountsTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Block/Rss/DiscountsTest.php @@ -195,6 +195,9 @@ public function testIsAllowed($isAllowed) $this->assertEquals($isAllowed, $this->block->isAllowed()); } + /** + * @return array + */ public function isAllowedDataProvider() { return [ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToDataModelTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToDataModelTest.php index 82868b3723c75..1016d14066afc 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToDataModelTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToDataModelTest.php @@ -115,6 +115,9 @@ protected function setUp() ); } + /** + * @return array + */ private function getArrayData() { return [ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToModelTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToModelTest.php index 98e1d7cddee57..5dd67424418b7 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToModelTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Converter/ToModelTest.php @@ -273,6 +273,9 @@ public function testFormattingDate($data) $this->model->toModel($dataModel); } + /** + * @return array + */ public function expectedDatesProvider() { return [ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Coupon/MassgeneratorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Coupon/MassgeneratorTest.php index 8408636ae2669..1a6ebff3e61bc 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Coupon/MassgeneratorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Coupon/MassgeneratorTest.php @@ -109,7 +109,6 @@ public function testGeneratePool() 'setRuleId', 'setUsageLimit', 'setUsagePerCustomer', - 'setExpirationDate', 'setCreatedAt', 'setType', 'setCode', @@ -120,7 +119,6 @@ public function testGeneratePool() $couponMock->expects($this->any())->method('setRuleId')->will($this->returnSelf()); $couponMock->expects($this->any())->method('setUsageLimit')->will($this->returnSelf()); $couponMock->expects($this->any())->method('setUsagePerCustomer')->will($this->returnSelf()); - $couponMock->expects($this->any())->method('setExpirationDate')->will($this->returnSelf()); $couponMock->expects($this->any())->method('setCreatedAt')->will($this->returnSelf()); $couponMock->expects($this->any())->method('setType')->will($this->returnSelf()); $couponMock->expects($this->any())->method('setCode')->will($this->returnSelf()); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/CouponGeneratorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/CouponGeneratorTest.php new file mode 100644 index 0000000000000..24ea8f2ab5efb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/CouponGeneratorTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Test\Unit\Model; + +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterface; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory; +use Magento\SalesRule\Model\CouponGenerator; +use Magento\SalesRule\Model\Service\CouponManagementService; + +/** + * @covers \Magento\SalesRule\Model\CouponGenerator + */ +class CouponGeneratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var CouponGenerator + */ + private $couponGenerator; + + /** + * @var CouponManagementService|\PHPUnit_Framework_MockObject_MockObject + */ + private $couponManagementServiceMock; + + /** + * @var CouponGenerationSpecInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $generationSpecFactoryMock; + + /** + * @var CouponGenerationSpecInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $generationSpecMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->generationSpecFactoryMock = $this->getMockBuilder(CouponGenerationSpecInterfaceFactory::class) + ->disableOriginalConstructor()->setMethods(['create'])->getMock(); + $this->couponManagementServiceMock = $this->createMock(CouponManagementService::class); + $this->generationSpecMock = $this->createMock(CouponGenerationSpecInterface::class); + $this->couponGenerator = new CouponGenerator( + $this->couponManagementServiceMock, + $this->generationSpecFactoryMock + ); + } + + /** + * Test beforeSave method + * + * @return void + */ + public function testBeforeSave() + { + $expected = ['test']; + $this->generationSpecFactoryMock->expects($this->once())->method('create') + ->willReturn($this->generationSpecMock); + $this->couponManagementServiceMock->expects($this->once())->method('generate') + ->with($this->generationSpecMock)->willReturn($expected); + $actual = $this->couponGenerator->generateCodes([]); + self::assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php index ebdc10830f33f..e516f817a59d1 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/CouponRepositoryTest.php @@ -150,10 +150,14 @@ public function testSaveWithExceptions($exceptionObject, $exceptionName, $except $this->resource->expects($this->once())->method('save')->with($coupon) ->willThrowException($exceptionObject); } - $this->expectException($exceptionName, $exceptionMessage); + $this->expectException($exceptionName); + $this->expectExceptionMessage($exceptionMessage); $this->model->save($coupon); } + /** + * @return array + */ public function saveExceptionsDataProvider() { $msg = 'kiwis'; diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/DeltaPriceRoundTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/DeltaPriceRoundTest.php new file mode 100644 index 0000000000000..20e70e74b8fc6 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/DeltaPriceRoundTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SalesRule\Test\Unit\Model; + +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\SalesRule\Model\DeltaPriceRound; + +/** + * Tests for Magento\SalesRule\Model\DeltaPriceRound. + */ +class DeltaPriceRoundTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $priceCurrency; + + /** + * @var DeltaPriceRound + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->priceCurrency = $this->getMockForAbstractClass(PriceCurrencyInterface::class); + $this->priceCurrency->method('round') + ->willReturnCallback( + function ($amount) { + return round($amount, 2); + } + ); + + $this->model = new DeltaPriceRound($this->priceCurrency); + } + + /** + * Tests rounded price based on previous rounding operation delta. + * + * @param array $prices + * @param array $roundedPrices + * @return void + * @dataProvider roundDataProvider + */ + public function testRound(array $prices, array $roundedPrices) + { + foreach ($prices as $key => $price) { + $roundedPrice = $this->model->round($price, 'test'); + $this->assertEquals($roundedPrices[$key], $roundedPrice); + } + + $this->model->reset('test'); + } + + /** + * @return array + */ + public function roundDataProvider() + { + return [ + [ + 'prices' => [1.004, 1.004], + 'rounded prices' => [1.00, 1.01], + ], + [ + 'prices' => [1.005, 1.005], + 'rounded prices' => [1.01, 1.0], + ], + ]; + } + + /** + * @return void + */ + public function testReset() + { + $this->assertEquals(1.44, $this->model->round(1.444, 'test')); + $this->model->reset('test'); + $this->assertEquals(1.44, $this->model->round(1.444, 'test')); + } + + /** + * @return void + */ + public function testResetAll() + { + $this->assertEquals(1.44, $this->model->round(1.444, 'test1')); + $this->assertEquals(1.44, $this->model->round(1.444, 'test2')); + + $this->model->resetAll(); + + $this->assertEquals(1.44, $this->model->round(1.444, 'test1')); + $this->assertEquals(1.44, $this->model->round(1.444, 'test2')); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php new file mode 100644 index 0000000000000..5bf4def83fc81 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/ChildrenValidationLocatorTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Test\Unit\Model\Quote; + +use Magento\Catalog\Model\Product; +use Magento\Quote\Model\Quote\Item\AbstractItem as QuoteItem; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory; +use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; + +/** + * @covers \Magento\SalesRule\Model\Quote\ChildrenValidationLocator + */ +class ChildrenValidationLocatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ChildrenValidationLocator + */ + private $childrenValidationLocator; + + /** + * @var QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemMock; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var array + */ + private $productTypeChildrenValidationMap = [ + 'simple' => false, + 'bundle' => true, + ]; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->itemMock = $this->createMock(QuoteItem::class); + $this->productMock = $this->createMock(Product::class); + $this->childrenValidationLocator = new ChildrenValidationLocator($this->productTypeChildrenValidationMap); + } + + /** + * Test isChildrenValidationRequired method + * + * @dataProvider childrenValidationDataProvider + * + * @param string $typeId + * @param bool $isValidationRequired + * + * @return void + */ + public function testIsChildrenValidationRequired($typeId, $isValidationRequired) + { + $this->productMock->expects($this->once())->method('getTypeId')->willReturn($typeId); + $this->itemMock->expects($this->once())->method('getProduct')->willReturn($this->productMock); + $actual = $this->childrenValidationLocator->isChildrenValidationRequired($this->itemMock); + $expected = $isValidationRequired; + self::assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function childrenValidationDataProvider() + { + return [ + ['simple', $this->productTypeChildrenValidationMap['simple']], + ['bundle', $this->productTypeChildrenValidationMap['bundle']], + ['configurable', true], + ]; + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index 671f20a27a460..090dbd7fe5d6d 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -225,6 +225,9 @@ public function testCollectItemHasChildren($childItemData, $parentData, $expecte } } + /** + * @return array + */ public function collectItemHasChildrenDataProvider() { $data = [ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/CollectionTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/CollectionTest.php index 794b43c58e1ed..7db8dfc3b7291 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/CollectionTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/CollectionTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SalesRule\Test\Unit\Model\ResourceModel\Report; class CollectionTest extends \PHPUnit\Framework\TestCase @@ -65,9 +63,15 @@ protected function setUp() $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->reportResource = $this->createPartialMock(\Magento\Sales\Model\ResourceModel\Report::class, ['getConnection', 'getMainTable']); + $this->reportResource = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Report::class, + ['getConnection', 'getMainTable'] + ); - $this->connection = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['select', 'getDateFormatSql', 'quoteInto']); + $this->connection = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + ['select', 'getDateFormatSql', 'quoteInto'] + ); $this->selectMock = $this->createPartialMock(\Magento\Framework\DB\Select::class, ['from', 'where', 'group']); @@ -82,11 +86,18 @@ protected function setUp() ->method('getMainTable') ->will($this->returnValue('test_main_table')); - $this->ruleFactory = $this->createPartialMock(\Magento\SalesRule\Model\ResourceModel\Report\RuleFactory::class, ['create']); + $this->ruleFactory = $this->createPartialMock( + \Magento\SalesRule\Model\ResourceModel\Report\RuleFactory::class, + ['create'] + ); $this->object = new \Magento\SalesRule\Model\ResourceModel\Report\Collection( - $this->entityFactory, $this->loggerMock, $this->fetchStrategy, - $this->eventManager, $this->reportResource, $this->ruleFactory + $this->entityFactory, + $this->loggerMock, + $this->fetchStrategy, + $this->eventManager, + $this->reportResource, + $this->ruleFactory ); } @@ -176,6 +187,9 @@ public function testApplyRulesFilterWithRulesList() */ protected function getRuleMock() { - return $this->createPartialMock(\Magento\SalesRule\Model\ResourceModel\Report\Rule::class, ['getUniqRulesNamesList']); + return $this->createPartialMock( + \Magento\SalesRule\Model\ResourceModel\Report\Rule::class, + ['getUniqRulesNamesList'] + ); } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php index 2f1bee9fc686a..7a0d138dd9fd9 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php @@ -5,48 +5,56 @@ */ namespace Magento\SalesRule\Test\Unit\Model\Rule\Action\Discount; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Tests for Magento\SalesRule\Model\Rule\Action\Discount\CartFixed. + */ class CartFixedTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Rule|MockObject */ protected $rule; /** - * @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject */ protected $item; /** - * @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Validator|MockObject */ protected $validator; /** - * @var \Magento\SalesRule\Model\Rule\Action\Discount\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Rule\Action\Discount\Data|MockObject */ protected $data; /** - * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote|MockObject */ protected $quote; /** - * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Address|MockObject */ protected $address; /** - * @var CartFixed + * @var \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed */ protected $model; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|MockObject */ protected $priceCurrency; + /** + * @inheritdoc + */ protected function setUp() { $this->rule = $this->getMockBuilder(\Magento\Framework\DataObject::class) @@ -66,18 +74,26 @@ protected function setUp() $this->item->expects($this->any())->method('getAddress')->will($this->returnValue($this->address)); $this->validator = $this->createMock(\Magento\SalesRule\Model\Validator::class); + /** @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|MockObject $dataFactory */ $dataFactory = $this->createPartialMock( \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class, ['create'] ); $dataFactory->expects($this->any())->method('create')->will($this->returnValue($this->data)); + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + ->getMock(); + $deltaPriceRound = $this->getMockBuilder(\Magento\SalesRule\Model\DeltaPriceRound::class) + ->disableOriginalConstructor() + ->getMock(); + $this->priceCurrency = $this->getMockBuilder( \Magento\Framework\Pricing\PriceCurrencyInterface::class )->getMock(); $this->model = new \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed( $this->validator, $dataFactory, - $this->priceCurrency + $this->priceCurrency, + $deltaPriceRound ); } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index 0bce282747b16..929cdc1e1ec7e 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\SalesRule\Test\Unit\Model\Rule\Condition; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\ScopeResolverInterface; use \Magento\Framework\DB\Adapter\AdapterInterface; use \Magento\Framework\DB\Select; -use \Magento\Framework\Model\AbstractModel; +use Magento\Framework\Locale\Format; +use Magento\Framework\Locale\ResolverInterface; use Magento\Quote\Model\Quote\Item\AbstractItem; use \Magento\Rule\Model\Condition\Context; use \Magento\Backend\Helper\Data; @@ -130,8 +134,12 @@ protected function setUp() $this->collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $this->formatMock = $this->getMockBuilder(FormatInterface::class) - ->getMockForAbstractClass(); + $this->formatMock = new Format( + $this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock() + ); + $this->model = new SalesRuleProduct( $this->contextMock, $this->backendHelperMock, @@ -231,4 +239,81 @@ public function testValidateCategoriesIgnoresVisibility() $this->model->validate($item); } + + /** + * @param boolean $isValid + * @param string $conditionValue + * @param string $operator + * @param double $productPrice + * @dataProvider localisationProvider + */ + public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator = '>=', $productPrice = 2000.00) + { + $attr = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) + ->disableOriginalConstructor() + ->setMethods(['getAttribute']) + ->getMockForAbstractClass(); + + $attr->expects($this->any()) + ->method('getAttribute') + ->willReturn(''); + + /* @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods(['setQuoteItemPrice', 'getResource', 'hasData', 'getData',]) + ->getMock(); + + $product->expects($this->any()) + ->method('setQuoteItemPrice') + ->willReturnSelf(); + + $product->expects($this->any()) + ->method('getResource') + ->willReturn($attr); + + $product->expects($this->any()) + ->method('hasData') + ->willReturn(true); + + $product->expects($this->any()) + ->method('getData') + ->with('quote_item_price') + ->willReturn($productPrice); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['getPrice', 'getProduct',]) + ->getMockForAbstractClass(); + + $item->expects($this->any()) + ->method('getPrice') + ->willReturn($productPrice); + + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($product); + + $this->model->setAttribute('quote_item_price'); + $this->model->setData('operator', $operator); + + $this->assertEquals($isValid, $this->model->setValue($conditionValue)->validate($item)); + } + + /** + * DataProvider for testQuoteLocaleFormatPrice + * + * @return array + */ + public function localisationProvider(): array + { + return [ + 'number' => [true, 500.01], + 'locale' => [true, '1,500.03'], + 'operation' => [true, '1,500.03', '!='], + 'stringOperation' => [false, '1,500.03', '{}'], + 'smallPrice' => [false, '1,500.03', '>=', 1000], + ]; + } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RuleTest.php index 89f4e93901c1b..be9e25eb20302 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RuleTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RuleTest.php @@ -113,6 +113,9 @@ public function testBeforeSaveResetConditionToNull() $this->model->getActions(); } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function setupProdConditionMock() { $prodConditionMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Condition\Product\Combine::class) @@ -133,6 +136,9 @@ protected function setupProdConditionMock() return $prodConditionMock; } + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function setupConditionMock() { $conditionMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Condition\Combine::class) diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 814048c2ac1d0..ec4e775f1f6b3 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Test\Unit\Model; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RulesApplierTest extends \PHPUnit\Framework\TestCase { /** @@ -28,6 +31,11 @@ class RulesApplierTest extends \PHPUnit\Framework\TestCase */ protected $validatorUtility; + /** + * @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject + */ + protected $childrenValidationLocator; + protected function setUp() { $this->calculatorFactory = $this->createMock( @@ -38,11 +46,15 @@ protected function setUp() \Magento\SalesRule\Model\Utility::class, ['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty'] ); - + $this->childrenValidationLocator = $this->createPartialMock( + \Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class, + ['isChildrenValidationRequired'] + ); $this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier( $this->calculatorFactory, $this->eventManager, - $this->validatorUtility + $this->validatorUtility, + $this->childrenValidationLocator ); } @@ -84,6 +96,10 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $item->setDiscountCalculationPrice($positivePrice); $item->setData('calculation_price', $positivePrice); + $this->childrenValidationLocator->expects($this->any()) + ->method('isChildrenValidationRequired') + ->will($this->returnValue(true)); + $this->validatorUtility->expects($this->atLeastOnce()) ->method('canProcessRule') ->will($this->returnValue(true)); @@ -124,6 +140,9 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $this->assertEquals($appliedRuleIds, $result); } + /** + * @return array + */ public function dataProviderChildren() { return [ @@ -163,6 +182,10 @@ protected function getPreparedItem() return $item; } + /** + * @param $item + * @param $rule + */ protected function applyRule($item, $rule) { $qty = 2; diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/UtilityTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/UtilityTest.php index 5e48f3110a395..4ce0f2a0564f3 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/UtilityTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/UtilityTest.php @@ -362,6 +362,9 @@ public function testMergeIds($a1, $a2, $isSting, $expected) $this->assertEquals($expected, $this->utility->mergeIds($a1, $a2, $isSting)); } + /** + * @return array + */ public function mergeIdsDataProvider() { return [ diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 2e6a3c3c38af0..a4b45813a10bc 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -5,6 +5,13 @@ */ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\Store; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class ValidatorTest * @@SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,50 +24,55 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase protected $helper; /** - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $model; /** - * @var \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Item|MockObject */ protected $item; /** - * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Address|MockObject */ protected $addressMock; /** - * @var \Magento\SalesRule\Model\RulesApplier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\RulesApplier|MockObject */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Validator\Pool|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Validator\Pool|MockObject */ protected $validators; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Utility|MockObject */ protected $utility; /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|MockObject */ protected $ruleCollection; /** - * @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Data|MockObject */ protected $catalogData; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface|MockObject */ protected $messageManager; + /** + * @var PriceCurrencyInterface|MockObject + */ + private $priceCurrency; + protected function setUp() { $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -74,6 +86,7 @@ protected function setUp() ->setMethods( [ 'getShippingAmountForDiscount', + 'getBaseShippingAmountForDiscount', 'getQuote', 'getCustomAttributesCodes', 'setCartFixedRules' @@ -81,7 +94,7 @@ protected function setUp() ) ->getMock(); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject $item */ $this->item = $this->createPartialMock( \Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getAddress', 'getParentItemId'] @@ -100,10 +113,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection); + $this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMock(); - /** @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject $validator */ + /** @var Validator|MockObject $validator */ $this->model = $this->helper->getObject( - \Magento\SalesRule\Model\Validator::class, + Validator::class, [ 'context' => $context, 'registry' => $registry, @@ -112,7 +128,8 @@ protected function setUp() 'utility' => $this->utility, 'rulesApplier' => $this->rulesApplier, 'validators' => $this->validators, - 'messageManager' => $this->messageManager + 'messageManager' => $this->messageManager, + 'priceCurrency' => $this->priceCurrency ] ); $this->model->setWebsiteId(1); @@ -131,7 +148,7 @@ protected function setUp() } /** - * @return \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Quote\Model\Quote\Item|MockObject */ protected function getQuoteItemMock() { @@ -145,8 +162,8 @@ protected function getQuoteItemMock() $itemSimple = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['getAddress', '__wakeup']); $itemSimple->expects($this->any())->method('getAddress')->will($this->returnValue($this->addressMock)); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStoreId', '__wakeup']); + /** @var $quote Quote */ + $quote = $this->createPartialMock(Quote::class, ['getStoreId', '__wakeup']); $quote->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $itemData = include $fixturePath . 'quote_item_downloadable.php'; @@ -168,7 +185,7 @@ public function testCanApplyRules() $this->model->getCouponCode() ); $item = $this->getQuoteItemMock(); - $rule = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $rule = $this->createMock(Rule::class); $actionsCollection = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); $actionsCollection->expects($this->any()) ->method('validate') @@ -278,7 +295,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected() public function testInit() { $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), @@ -314,7 +331,7 @@ public function testCanApplyDiscount() public function testInitTotalsCanApplyDiscount() { $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getSimpleAction', 'getActions', 'getId'] ); $item1 = $this->getMockForAbstractClass( @@ -337,7 +354,7 @@ public function testInitTotalsCanApplyDiscount() $rule->expects($this->any()) ->method('getSimpleAction') - ->willReturn(\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION); + ->willReturn(Rule::CART_FIXED_ACTION); $iterator = new \ArrayIterator([$rule]); $this->ruleCollection->expects($this->once())->method('getIterator')->willReturn($iterator); $validator = $this->getMockBuilder(\Magento\Framework\Validator\AbstractValidator::class) @@ -392,7 +409,7 @@ public function testInitTotalsNoItems() /** * @param $ruleCollection - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function prepareRuleCollectionMock($ruleCollection) { @@ -427,14 +444,14 @@ public function testProcessShippingAmountNoRules() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } public function testProcessShippingAmountProcessDisabled() { - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); @@ -448,94 +465,104 @@ public function testProcessShippingAmountProcessDisabled() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } /** + * Tests shipping amounts according to rule simple action. + * * @param string $action + * @param int $ruleDiscount + * @param int $shippingDiscount * @dataProvider dataProviderActions */ - public function testProcessShippingAmountActions($action) + public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount) { - $discountAmount = 50; + $shippingAmount = 5; - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) ->getMock(); - $ruleMock->expects($this->any()) - ->method('getApplyToShipping') + $ruleMock->method('getApplyToShipping') ->willReturn(true); - $ruleMock->expects($this->any()) - ->method('getDiscountAmount') - ->willReturn($discountAmount); - $ruleMock->expects($this->any()) - ->method('getSimpleAction') + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') ->willReturn($action); $iterator = new \ArrayIterator([$ruleMock]); - $this->ruleCollection->expects($this->any()) - ->method('getIterator') + $this->ruleCollection->method('getIterator') ->willReturn($iterator); - $this->utility->expects($this->any()) - ->method('canProcessRule') + $this->utility->method('canProcessRule') ->willReturn(true); + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, - $this->model->processShippingAmount($this->setupAddressMock(5)) - ); + + $addressMock = $this->setupAddressMock($shippingAmount); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); } + /** + * @return array + */ public static function dataProviderActions() { return [ - [\Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::TO_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::BY_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION], + [Rule::TO_PERCENT_ACTION, 50, 2.5], + [Rule::BY_PERCENT_ACTION, 50, 2.5], + [Rule::TO_FIXED_ACTION, 5, 0], + [Rule::BY_FIXED_ACTION, 5, 5], + [Rule::CART_FIXED_ACTION, 5, 0], ]; } /** * @param null|int $shippingAmount - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function setupAddressMock($shippingAmount = null) { - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['setAppliedRuleIds', 'getStore']) ->getMock(); - $quoteMock->expects($this->any()) - ->method('getStore') + + $quoteMock->method('getStore') ->willReturn($storeMock); - $quoteMock->expects($this->any()) - ->method('setAppliedRuleIds') + + $quoteMock->method('setAppliedRuleIds') ->willReturnSelf(); - $this->addressMock->expects($this->any()) - ->method('getShippingAmountForDiscount') + $this->addressMock->method('getShippingAmountForDiscount') ->willReturn($shippingAmount); - $this->addressMock->expects($this->any()) - ->method('getQuote') + + $this->addressMock->method('getBaseShippingAmountForDiscount') + ->willReturn($shippingAmount); + + $this->addressMock->method('getQuote') ->willReturn($quoteMock); - $this->addressMock->expects($this->any()) - ->method('getCustomAttributesCodes') + + $this->addressMock->method('getCustomAttributesCodes') ->willReturn([]); + return $this->addressMock; } @@ -543,7 +570,7 @@ public function testReset() { $this->utility->expects($this->once()) ->method('resetRoundingDeltas'); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); $addressMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) @@ -557,6 +584,6 @@ public function testReset() $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf(\Magento\SalesRule\Model\Validator::class, $this->model->reset($addressMock)); + $this->assertInstanceOf(Validator::class, $this->model->reset($addressMock)); } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/SalesOrderAfterPlaceObserverTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/SalesOrderAfterPlaceObserverTest.php deleted file mode 100644 index b14c783019590..0000000000000 --- a/app/code/Magento/SalesRule/Test/Unit/Observer/SalesOrderAfterPlaceObserverTest.php +++ /dev/null @@ -1,180 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SalesRule\Test\Unit\Observer; - -class SalesOrderAfterPlaceObserverTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\SalesRule\Observer\SalesOrderAfterPlaceObserver|\PHPUnit_Framework_MockObject_MockObject - */ - protected $model; - - /** - * @var \Magento\SalesRule\Model\Coupon|\PHPUnit_Framework_MockObject_MockObject - */ - protected $couponMock; - - /** - * @var \Magento\SalesRule\Model\RuleFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $ruleFactory; - - /** - * @var - */ - protected $ruleCustomerFactory; - - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Usage|\PHPUnit_Framework_MockObject_MockObject - */ - protected $couponUsage; - - protected function setUp() - { - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->initMocks(); - - $this->model = $helper->getObject( - \Magento\SalesRule\Observer\SalesOrderAfterPlaceObserver::class, - [ - 'ruleFactory' => $this->ruleFactory, - 'ruleCustomerFactory' => $this->ruleCustomerFactory, - 'coupon' => $this->couponMock, - 'couponUsage' => $this->couponUsage, - ] - ); - } - - protected function initMocks() - { - $this->couponMock = $this->createPartialMock(\Magento\SalesRule\Model\Coupon::class, [ - '__wakeup', - 'save', - 'load', - 'getId', - 'setTimesUsed', - 'getTimesUsed', - 'getRuleId', - 'loadByCode', - 'updateCustomerCouponTimesUsed' - ]); - $this->ruleFactory = $this->createPartialMock(\Magento\SalesRule\Model\RuleFactory::class, ['create']); - $this->ruleCustomerFactory = $this->createPartialMock( - \Magento\SalesRule\Model\Rule\CustomerFactory::class, - ['create'] - ); - $this->couponUsage = $this->createMock(\Magento\SalesRule\Model\ResourceModel\Coupon\Usage::class); - } - - /** - * @param \\PHPUnit_Framework_MockObject_MockObject $observer - * @return \PHPUnit_Framework_MockObject_MockObject $order - */ - protected function initOrderFromEvent($observer) - { - $event = $this->createPartialMock(\Magento\Framework\Event::class, ['getOrder']); - $order = $this->createPartialMock( - \Magento\Sales\Model\Order::class, - ['getAppliedRuleIds', 'getCustomerId', 'getDiscountAmount', 'getCouponCode', '__wakeup'] - ); - - $observer->expects($this->any()) - ->method('getEvent') - ->will($this->returnValue($event)); - $event->expects($this->any()) - ->method('getOrder') - ->will($this->returnValue($order)); - - return $order; - } - - public function testSalesOrderAfterPlaceWithoutOrder() - { - $observer = $this->createMock(\Magento\Framework\Event\Observer::class); - $this->initOrderFromEvent($observer); - - $this->assertEquals($this->model, $this->model->execute($observer)); - } - - public function testSalesOrderAfterPlaceWithoutRuleId() - { - $observer = $this->createMock(\Magento\Framework\Event\Observer::class); - $order = $this->initOrderFromEvent($observer); - $ruleIds = null; - $order->expects($this->once()) - ->method('getAppliedRuleIds') - ->will($this->returnValue($ruleIds)); - - $this->ruleFactory->expects($this->never()) - ->method('create'); - $this->assertEquals($this->model, $this->model->execute($observer)); - } - - /** - * @param int|bool $ruleCustomerId - * @dataProvider salesOrderAfterPlaceDataProvider - */ - public function testSalesOrderAfterPlace($ruleCustomerId) - { - $observer = $this->createMock(\Magento\Framework\Event\Observer::class); - $rule = $this->createMock(\Magento\SalesRule\Model\Rule::class); - $ruleCustomer = $this->createPartialMock(\Magento\SalesRule\Model\Rule\Customer::class, [ - 'setCustomerId', - 'loadByCustomerRule', - 'getId', - 'setTimesUsed', - 'setRuleId', - 'save', - '__wakeup' - ]); - $order = $this->initOrderFromEvent($observer); - $ruleId = 1; - $couponId = 1; - $customerId = 1; - - $order->expects($this->exactly(2)) - ->method('getAppliedRuleIds') - ->will($this->returnValue($ruleId)); - $order->expects($this->once()) - ->method('getCustomerId') - ->will($this->returnValue($customerId)); - $this->ruleFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($rule)); - $rule->expects($this->once()) - ->method('getId') - ->will($this->returnValue($ruleId)); - $this->ruleCustomerFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($ruleCustomer)); - $ruleCustomer->expects($this->once()) - ->method('getId') - ->will($this->returnValue($ruleCustomerId)); - $ruleCustomer->expects($this->any()) - ->method('setCustomerId') - ->will($this->returnSelf()); - $ruleCustomer->expects($this->any()) - ->method('setRuleId') - ->will($this->returnSelf()); - $this->couponMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue($couponId)); - - $this->couponUsage->expects($this->once()) - ->method('updateCustomerCouponTimesUsed') - ->with($customerId, $couponId); - - $this->assertEquals($this->model, $this->model->execute($observer)); - } - - public function salesOrderAfterPlaceDataProvider() - { - return [ - 'With customer rule id' => [1], - 'Without customer rule id' => [null] - ]; - } -} diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index b84eb3b0682dc..8b0fac198df01 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-sales-rule", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/module-store": "100.2.*", "magento/module-rule": "100.2.*", @@ -25,7 +25,7 @@ "magento/module-sales-rule-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.1", + "version": "101.0.9", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index a8c350457a5a6..19cb0ff1b1eef 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -178,8 +178,11 @@ </argument> </arguments> </type> - <type name="\Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="coupon_label_plugin" type="Magento\SalesRule\Plugin\CartTotalRepository" /> </type> + <type name="\Magento\Sales\Model\Order"> + <plugin name="coupon_uses_increment_plugin" type="Magento\SalesRule\Plugin\CouponUsagesIncrement" /> + <plugin name="coupon_uses_decrement_plugin" type="Magento\SalesRule\Plugin\CouponUsagesDecrement" /> + </type> </config> diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index 43babc40a2ab5..eec0da74f619e 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="sales_order_place_after"> - <observer name="salesrule" instance="Magento\SalesRule\Observer\SalesOrderAfterPlaceObserver" /> - </event> <event name="sales_model_service_quote_submit_before"> <observer name="salesrule" instance="Magento\SalesRule\Observer\AddSalesRuleNameToOrderObserver" /> </event> @@ -27,4 +24,7 @@ <event name="magento_salesrule_api_data_ruleinterface_load_after"> <observer name="legacy_model_load" instance="Magento\Framework\EntityManager\Observer\AfterEntityLoad" /> </event> + <event name="sales_order_customer_assign_after"> + <observer name="sales_order_assign_customer_after" instance="Magento\SalesRule\Observer\AssignCouponDataAfterOrderCustomerAssignObserver" /> + </event> </config> diff --git a/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_edit.xml b/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_edit.xml index ad5bbbd262da7..3366af650f8e9 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_edit.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_edit.xml @@ -6,6 +6,7 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="styles" /> <body> <referenceContainer name="content"> <uiComponent name="sales_rule_form"/> diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 9b579f47759a6..570eb0bf151f0 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -452,7 +452,7 @@ <dataScope>discount_step</dataScope> </settings> </field> - <field name="apply_to_shipping" component="Magento_Ui/js/form/element/single-checkbox-toggle-notice" formElement="checkbox"> + <field name="apply_to_shipping" component="Magento_SalesRule/js/form/element/apply_to_shipping" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">sales_rule</item> diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js new file mode 100644 index 0000000000000..dfb3f909345b3 --- /dev/null +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -0,0 +1,37 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox-toggle-notice' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.simple_action:value' + } + }, + + /** + * Toggle element disabled state according to simple action value. + * + * @param {String} action + */ + toggleDisabled: function (action) { + switch (action) { + case 'cart_fixed': + this.disabled(true); + break; + default: + this.disabled(false); + } + + if (this.disabled()) { + this.checked(false); + } + } + }); +}); diff --git a/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml index 022403579b237..375324ed4cde6 100644 --- a/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml @@ -13,14 +13,10 @@ <item name="components" xsi:type="array"> <item name="block-totals" xsi:type="array"> <item name="children" xsi:type="array"> - <item name="before_grandtotal" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="discount" xsi:type="array"> - <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> - <item name="config" xsi:type="array"> - <item name="title" xsi:type="string" translate="true">Discount</item> - </item> - </item> + <item name="discount" xsi:type="array"> + <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> + <item name="config" xsi:type="array"> + <item name="title" xsi:type="string" translate="true">Discount</item> </item> </item> </item> diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/view/summary/discount.js b/app/code/Magento/SalesRule/view/frontend/web/js/view/summary/discount.js index 5b04700596272..f2924fe48e01b 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/view/summary/discount.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/view/summary/discount.js @@ -44,6 +44,25 @@ define([ return this.totals()['coupon_label']; }, + /** + * Get discount title + * + * @returns {null|String} + */ + getTitle: function () { + var discountSegments; + + if (!this.totals()) { + return null; + } + + discountSegments = this.totals()['total_segments'].filter(function (segment) { + return segment.code.indexOf('discount') !== -1; + }); + + return discountSegments.length ? discountSegments[0].title : null; + }, + /** * @return {Number} */ diff --git a/app/code/Magento/SalesRule/view/frontend/web/template/cart/totals/discount.html b/app/code/Magento/SalesRule/view/frontend/web/template/cart/totals/discount.html index 4b70b4b110c97..8fbb4a6ce74ae 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/template/cart/totals/discount.html +++ b/app/code/Magento/SalesRule/view/frontend/web/template/cart/totals/discount.html @@ -7,7 +7,7 @@ <!-- ko if: isDisplayed() --> <tr class="totals"> <th colspan="1" style="" class="mark" scope="row"> - <span class="title" data-bind="text: title"></span> + <span class="title" data-bind="text: getTitle()"></span> <span class="discount coupon" data-bind="text: getCouponLabel()"></span> </th> <td class="amount" data-bind="attr: {'data-th': title}"> diff --git a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html index 7246460382fa7..d622b5ea5762d 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html +++ b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html @@ -27,7 +27,7 @@ id="discount-code" name="discount_code" data-validate="{'required-entry':true}" - data-bind="value: couponCode, attr:{placeholder: $t('Enter discount code')} " /> + data-bind="value: couponCode, attr:{disabled:isApplied() , placeholder: $t('Enter discount code')} " /> </div> </div> </div> diff --git a/app/code/Magento/SalesRule/view/frontend/web/template/summary/discount.html b/app/code/Magento/SalesRule/view/frontend/web/template/summary/discount.html index 17a9559fa01f2..017e358c7e419 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/template/summary/discount.html +++ b/app/code/Magento/SalesRule/view/frontend/web/template/summary/discount.html @@ -7,7 +7,7 @@ <!-- ko if: isDisplayed() --> <tr class="totals discount"> <th class="mark" scope="row"> - <span class="title" data-bind="text: title"></span> + <span class="title" data-bind="text: getTitle()"></span> <span class="discount coupon" data-bind="text: getCouponCode()"></span> </th> <td class="amount"> diff --git a/app/code/Magento/SalesSequence/Test/Mftf/LICENSE.txt b/app/code/Magento/SalesSequence/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/SalesSequence/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 <insert your license name here>" 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/app/code/Magento/SalesSequence/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/SalesSequence/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/SalesSequence/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 <insert your license name here>" 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/app/code/Magento/SalesSequence/Test/Mftf/README.md b/app/code/Magento/SalesSequence/Test/Mftf/README.md new file mode 100644 index 0000000000000..969f7d0daf509 --- /dev/null +++ b/app/code/Magento/SalesSequence/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sales Sequence Functional Tests + +The Functional Test Module for **Magento Sales Sequence** module. diff --git a/app/code/Magento/SalesSequence/composer.json b/app/code/Magento/SalesSequence/composer.json index ce9e69a91a9c9..36df5806bacb7 100644 --- a/app/code/Magento/SalesSequence/composer.json +++ b/app/code/Magento/SalesSequence/composer.json @@ -2,11 +2,11 @@ "name": "magento/module-sales-sequence", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php index 158c588d11358..88df47283133a 100644 --- a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php +++ b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -19,6 +20,8 @@ */ class SampleDataDeployCommand extends Command { + const OPTION_NO_UPDATE = 'no-update'; + /** * @var \Magento\Framework\Filesystem */ @@ -66,6 +69,12 @@ protected function configure() { $this->setName('sampledata:deploy') ->setDescription('Deploy sample data modules'); + $this->addOption( + self::OPTION_NO_UPDATE, + null, + InputOption::VALUE_NONE, + 'Update composer.json without executing composer update' + ); parent::configure(); } @@ -80,6 +89,9 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!empty($sampleDataPackages)) { $baseDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); $commonArgs = ['--working-dir' => $baseDir, '--no-progress' => 1]; + if ($input->getOption(self::OPTION_NO_UPDATE)) { + $commonArgs['--no-update'] = 1; + } $packages = []; foreach ($sampleDataPackages as $name => $version) { $packages[] = "$name:$version"; diff --git a/app/code/Magento/SampleData/Console/Command/SampleDataRemoveCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataRemoveCommand.php index 36f5c591bedc3..5e10b6c6e5930 100644 --- a/app/code/Magento/SampleData/Console/Command/SampleDataRemoveCommand.php +++ b/app/code/Magento/SampleData/Console/Command/SampleDataRemoveCommand.php @@ -8,6 +8,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Magento\SampleData\Model\Dependency; use Symfony\Component\Console\Input\ArrayInput; @@ -22,6 +23,8 @@ */ class SampleDataRemoveCommand extends Command { + const OPTION_NO_UPDATE = 'no-update'; + /** * @var Filesystem */ @@ -69,6 +72,12 @@ protected function configure() { $this->setName('sampledata:remove') ->setDescription('Remove all sample data packages from composer.json'); + $this->addOption( + self::OPTION_NO_UPDATE, + null, + InputOption::VALUE_NONE, + 'Update composer.json without executing composer update' + ); parent::configure(); } @@ -81,6 +90,9 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!empty($sampleDataPackages)) { $baseDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath(); $commonArgs = ['--working-dir' => $baseDir, '--no-interaction' => 1, '--no-progress' => 1]; + if ($input->getOption(self::OPTION_NO_UPDATE)) { + $commonArgs['--no-update'] = 1; + } $packages = array_keys($sampleDataPackages); $arguments = array_merge(['command' => 'remove', 'packages' => $packages], $commonArgs); $commandInput = new ArrayInput($arguments); diff --git a/app/code/Magento/SampleData/Test/Mftf/README.md b/app/code/Magento/SampleData/Test/Mftf/README.md new file mode 100644 index 0000000000000..dcadf692f4959 --- /dev/null +++ b/app/code/Magento/SampleData/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sample Data Functional Tests + +The Functional Test Module for **Magento Sample Data** module. diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php new file mode 100644 index 0000000000000..090bb4256f807 --- /dev/null +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SampleData\Test\Unit\Console\Command; + +use Composer\Console\Application; +use Composer\Console\ApplicationFactory; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\SampleData\Model\Dependency; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\ArrayInputFactory; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +abstract class AbstractSampleDataCommandTest extends TestCase +{ + /** + * @var ReadInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $directoryReadMock; + + /** + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $directoryWriteMock; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + protected $filesystemMock; + + /** + * @var Dependency|\PHPUnit_Framework_MockObject_MockObject + */ + protected $sampleDataDependencyMock; + + /** + * @var ArrayInputFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $arrayInputFactoryMock; + + /** + * @var Application|\PHPUnit_Framework_MockObject_MockObject + */ + protected $applicationMock; + + /** + * @var ApplicationFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $applicationFactoryMock; + + /** + * @return void + */ + protected function setUp() + { + $this->directoryReadMock = $this->createMock(ReadInterface::class); + $this->directoryWriteMock = $this->createMock(WriteInterface::class); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->sampleDataDependencyMock = $this->createMock(Dependency::class); + $this->arrayInputFactoryMock = $this->createMock(ArrayInputFactory::class); + $this->applicationMock = $this->createMock(Application::class); + $this->applicationFactoryMock = $this->createPartialMock(ApplicationFactory::class, ['create']); + } + + /** + * @param array $sampleDataPackages Array in form [package_name => version_constraint] + * @param string $pathToComposerJson Fake path to composer.json + * @param int $appRunResult Composer exit code + * @param array $additionalComposerArgs Additional arguments that composer expects + */ + protected function setupMocks( + $sampleDataPackages, + $pathToComposerJson, + $appRunResult, + $additionalComposerArgs = [] + ) { + $this->directoryReadMock->expects($this->any())->method('getAbsolutePath')->willReturn($pathToComposerJson); + $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->with(DirectoryList::ROOT)->willReturn( + $this->directoryReadMock + ); + $this->sampleDataDependencyMock->expects($this->any())->method('getSampleDataPackages')->willReturn( + $sampleDataPackages + ); + $this->arrayInputFactoryMock->expects($this->never())->method('create'); + + $this->applicationMock->expects($this->any()) + ->method('run') + ->with( + new ArrayInput( + array_merge( + $this->expectedComposerArguments( + $sampleDataPackages, + $pathToComposerJson + ), + $additionalComposerArgs + ) + ), + $this->anything() + ) + ->willReturn($appRunResult); + + if (($appRunResult !== 0) && !empty($sampleDataPackages)) { + $this->applicationMock->expects($this->once())->method('resetComposer')->willReturnSelf(); + } + + $this->applicationFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->applicationMock); + } + + /** + * Expected arguments for composer based on sample data packages and composer.json path + * + * @param array $sampleDataPackages + * @param string $pathToComposerJson + * @return array + */ + abstract protected function expectedComposerArguments( + array $sampleDataPackages, + string $pathToComposerJson + ) : array; +} diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php index 464a6c9ccd832..450b2d8798f52 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php @@ -9,66 +9,26 @@ use Magento\SampleData\Console\Command\SampleDataDeployCommand; use Magento\Setup\Model\PackagesAuth; use Symfony\Component\Console\Tester\CommandTester; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\SampleData\Model\Dependency; -use Symfony\Component\Console\Input\ArrayInputFactory; -use Composer\Console\ApplicationFactory; -use Composer\Console\Application; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SampleDataDeployCommandTest extends \PHPUnit\Framework\TestCase +class SampleDataDeployCommandTest extends AbstractSampleDataCommandTest { /** - * @var ReadInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $directoryReadMock; - - /** - * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $directoryWriteMock; - - /** - * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject - */ - private $filesystemMock; - - /** - * @var Dependency|\PHPUnit_Framework_MockObject_MockObject - */ - private $sampleDataDependencyMock; - - /** - * @var ArrayInputFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $arrayInputFactoryMock; - - /** - * @var Application|\PHPUnit_Framework_MockObject_MockObject - */ - private $applicationMock; - - /** - * @var ApplicationFactory|\PHPUnit_Framework_MockObject_MockObject + * @param bool $authExist True to test with existing auth.json, false without */ - private $applicationFactoryMock; - - /** - * @return void - */ - protected function setUp() + protected function setupMocksForAuthFile($authExist) { - $this->directoryReadMock = $this->createMock(ReadInterface::class); - $this->directoryWriteMock = $this->createMock(WriteInterface::class); - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->sampleDataDependencyMock = $this->createMock(Dependency::class); - $this->arrayInputFactoryMock = $this->createMock(ArrayInputFactory::class); - $this->applicationMock = $this->createMock(Application::class); - $this->applicationFactoryMock = $this->createPartialMock(ApplicationFactory::class, ['create']); + $this->directoryWriteMock->expects($this->once()) + ->method('isExist') + ->with(PackagesAuth::PATH_TO_AUTH_FILE) + ->willReturn($authExist); + $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once())->method('writeFile')->with( + PackagesAuth::PATH_TO_AUTH_FILE, + '{}' + ); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::COMPOSER_HOME) + ->willReturn($this->directoryWriteMock); } /** @@ -82,68 +42,36 @@ protected function setUp() */ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) { - $pathToComposerJson = '/path/to/composer.json'; - - $this->directoryReadMock->expects($this->any()) - ->method('getAbsolutePath') - ->willReturn($pathToComposerJson); - $this->directoryWriteMock->expects($this->once()) - ->method('isExist') - ->with(PackagesAuth::PATH_TO_AUTH_FILE) - ->willReturn($authExist); - $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once()) - ->method('writeFile') - ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}'); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryRead') - ->with(DirectoryList::ROOT) - ->willReturn($this->directoryReadMock); - $this->filesystemMock->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::COMPOSER_HOME) - ->willReturn($this->directoryWriteMock); - $this->sampleDataDependencyMock->expects($this->any()) - ->method('getSampleDataPackages') - ->willReturn($sampleDataPackages); - $this->arrayInputFactoryMock->expects($this->never()) - ->method('create'); - - array_walk($sampleDataPackages, function (&$v, $k) { - $v = "$k:$v"; - }); - - $packages = array_values($sampleDataPackages); - - $requireArgs = [ - 'command' => 'require', - '--working-dir' => $pathToComposerJson, - '--no-progress' => 1, - 'packages' => $packages, - ]; - $commandInput = new \Symfony\Component\Console\Input\ArrayInput($requireArgs); - - $this->applicationMock->expects($this->any()) - ->method('run') - ->with($commandInput, $this->anything()) - ->willReturn($appRunResult); - - if (($appRunResult !== 0) && !empty($sampleDataPackages)) { - $this->applicationMock->expects($this->once())->method('resetComposer')->willReturnSelf(); - } + $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + $this->setupMocksForAuthFile($authExist); + $commandTester = $this->createCommandTester(); + $commandTester->execute([]); - $this->applicationFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->applicationMock); + $this->assertEquals($expectedMsg, $commandTester->getDisplay()); + } - $commandTester = new CommandTester( - new SampleDataDeployCommand( - $this->filesystemMock, - $this->sampleDataDependencyMock, - $this->arrayInputFactoryMock, - $this->applicationFactoryMock - ) + /** + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param string $expectedMsg + * @param bool $authExist + * @return void + * + * @dataProvider processDataProvider + */ + public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) + { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + ['--no-update' => 1] ); - $commandTester->execute([]); + $this->setupMocksForAuthFile($authExist); + $commandInput = ['--no-update' => 1]; + + $commandTester = $this->createCommandTester(); + $commandTester->execute($commandInput); $this->assertEquals($expectedMsg, $commandTester->getDisplay()); } @@ -154,13 +82,13 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM public function processDataProvider() { return [ - [ + 'No sample data found' => [ 'sampleDataPackages' => [], 'appRunResult' => 1, 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, 'authExist' => true, ], - [ + 'No auth.json found' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], @@ -169,7 +97,7 @@ public function processDataProvider() . PHP_EOL, 'authExist' => false, ], - [ + 'Successful sample data installation' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], @@ -204,6 +132,14 @@ public function testExecuteWithException() ->with(DirectoryList::COMPOSER_HOME) ->willReturn($this->directoryWriteMock); + $this->createCommandTester()->execute([]); + } + + /** + * @return CommandTester + */ + private function createCommandTester(): CommandTester + { $commandTester = new CommandTester( new SampleDataDeployCommand( $this->filesystemMock, @@ -212,6 +148,36 @@ public function testExecuteWithException() $this->applicationFactoryMock ) ); - $commandTester->execute([]); + return $commandTester; + } + + /** + * @param $sampleDataPackages + * @param $pathToComposerJson + * @return array + */ + protected function expectedComposerArguments( + array $sampleDataPackages, + string $pathToComposerJson + ) : array { + return [ + 'command' => 'require', + '--working-dir' => $pathToComposerJson, + '--no-progress' => 1, + 'packages' => $this->packageVersionStrings($sampleDataPackages), + ]; + } + + /** + * @param array $sampleDataPackages + * @return array + */ + private function packageVersionStrings(array $sampleDataPackages): array + { + array_walk($sampleDataPackages, function (&$v, $k) { + $v = "$k:$v"; + }); + + return array_values($sampleDataPackages); } } diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php new file mode 100644 index 0000000000000..7fce70fd9c376 --- /dev/null +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\SampleData\Test\Unit\Console\Command; + +use Magento\SampleData\Console\Command\SampleDataRemoveCommand; +use Symfony\Component\Console\Tester\CommandTester; + +class SampleDataRemoveCommandTest extends AbstractSampleDataCommandTest +{ + + /** + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param string $expectedMsg + * @return void + * + * @dataProvider processDataProvider + */ + public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg) + { + $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + $commandTester = $this->createCommandTester(); + $commandTester->execute([]); + + $this->assertEquals($expectedMsg, $commandTester->getDisplay()); + } + + /** + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param string $expectedMsg + * @return void + * + * @dataProvider processDataProvider + */ + public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg) + { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + ['--no-update' => 1] + ); + $commandInput = ['--no-update' => 1]; + + $commandTester = $this->createCommandTester(); + $commandTester->execute($commandInput); + + $this->assertEquals($expectedMsg, $commandTester->getDisplay()); + } + + /** + * @return array + */ + public function processDataProvider() + { + return [ + 'No sample data found' => [ + 'sampleDataPackages' => [], + 'appRunResult' => 1, + 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, + ], + 'Successful sample data installation' => [ + 'sampleDataPackages' => [ + 'magento/module-cms-sample-data' => '1.0.0-beta', + ], + 'appRunResult' => 0, + 'expectedMsg' => '', + ], + ]; + } + + /** + * @return CommandTester + */ + private function createCommandTester(): CommandTester + { + $commandTester = new CommandTester( + new SampleDataRemoveCommand( + $this->filesystemMock, + $this->sampleDataDependencyMock, + $this->arrayInputFactoryMock, + $this->applicationFactoryMock + ) + ); + return $commandTester; + } + + /** + * @param $sampleDataPackages + * @param $pathToComposerJson + * @return array + */ + protected function expectedComposerArguments( + array $sampleDataPackages, + string $pathToComposerJson + ) : array { + return [ + 'command' => 'remove', + '--working-dir' => $pathToComposerJson, + '--no-interaction' => 1, + '--no-progress' => 1, + 'packages' => array_keys($sampleDataPackages), + ]; + } +} diff --git a/app/code/Magento/SampleData/composer.json b/app/code/Magento/SampleData/composer.json index 9c2c84a1365e9..bc05f24b9b8bd 100644 --- a/app/code/Magento/SampleData/composer.json +++ b/app/code/Magento/SampleData/composer.json @@ -2,14 +2,14 @@ "name": "magento/module-sample-data", "description": "Sample Data fixtures", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*" }, "suggest": { "magento/sample-data-media": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Search/Api/SynonymGroupRepositoryInterface.php b/app/code/Magento/Search/Api/SynonymGroupRepositoryInterface.php index 3fa185abb4ea7..e30508637f9ae 100644 --- a/app/code/Magento/Search/Api/SynonymGroupRepositoryInterface.php +++ b/app/code/Magento/Search/Api/SynonymGroupRepositoryInterface.php @@ -31,7 +31,7 @@ public function save(\Magento\Search\Api\Data\SynonymGroupInterface $synonymGrou public function delete(\Magento\Search\Api\Data\SynonymGroupInterface $synonymGroup); /** - * Return a paritcular synonym group interface instance based on passed in synonym group id + * Return a particular synonym group interface instance based on passed in synonym group id * * @param int $synonymGroupId * @return \Magento\Search\Api\Data\SynonymGroupInterface diff --git a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php index a73edcce99760..6c54d80a61319 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php +++ b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __('Are you sure you want to delete this synonym group?') - . '\', \'' . $this->getDeleteUrl() . '\')', + . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Search/Block/Term.php b/app/code/Magento/Search/Block/Term.php index d92ba03bfcff8..ee62129051b97 100644 --- a/app/code/Magento/Search/Block/Term.php +++ b/app/code/Magento/Search/Block/Term.php @@ -95,8 +95,8 @@ protected function _loadTerms() continue; } $term->setRatio(($term->getPopularity() - $this->_minPopularity) / $range); - $temp[$term->getName()] = $term; - $termKeys[] = $term->getName(); + $temp[$term->getData('query_text')] = $term; + $termKeys[] = $term->getData('query_text'); } natcasesort($termKeys); @@ -128,7 +128,7 @@ public function getSearchUrl($obj) * url encoding will be done in Url.php http_build_query * so no need to explicitly called urlencode for the text */ - $url->setQueryParam('q', $obj->getName()); + $url->setQueryParam('q', $obj->getData('query_text')); return $url->getUrl('catalogsearch/result'); } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php index bb476423692d1..e531e947a5ab5 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php @@ -55,21 +55,23 @@ public function execute() $id = $this->getRequest()->getParam('group_id'); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); - if ($id) { + if ($this->getRequest()->isPost() && $id) { try { /** @var \Magento\Search\Model\SynonymGroup $synGroupModel */ $synGroupModel = $this->synGroupRepository->get($id); $this->synGroupRepository->delete($synGroupModel); - $this->messageManager->addSuccess(__('The synonym group has been deleted.')); + $this->messageManager->addSuccessMessage(__('The synonym group has been deleted.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->logger->error($e); } catch (\Exception $e) { - $this->messageManager->addError(__('An error was encountered while performing delete operation.')); + $this->messageManager->addErrorMessage( + __('An error was encountered while performing delete operation.') + ); $this->logger->error($e); } } else { - $this->messageManager->addError(__('We can\'t find a synonym group to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a synonym group to delete.')); } return $resultRedirect->setPath('*/*/'); diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Edit.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Edit.php index 506976247327f..3e6ac0abf2126 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Edit.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Edit.php @@ -66,7 +66,7 @@ public function execute() // 2. Initial checking if ($groupId && (!$synGroup->getGroupId())) { - $this->messageManager->addError(__('This synonyms group no longer exists.')); + $this->messageManager->addErrorMessage(__('This synonyms group no longer exists.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php index 4add418d95325..2f3d574e21485 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php @@ -6,8 +6,10 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\Exception\NotFoundException; + /** - * Mass-Delete Controller + * Mass-Delete Controller. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -56,13 +58,17 @@ public function __construct( } /** - * Execute action + * Execute action. * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); @@ -72,22 +78,23 @@ public function execute() $this->synGroupRepository->delete($synonymGroup); $deletedItems++; } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } if ($deletedItems != 0) { if ($collectionSize != $deletedItems) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('Failed to delete %1 synonym group(s).', $collectionSize - $deletedItems) ); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 synonym group(s) have been deleted.', $deletedItems) ); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + return $resultRedirect->setPath('*/*/'); } } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Save.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Save.php index ffa97ceb3e0e1..0ed73fd0cee32 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Save.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Save.php @@ -59,7 +59,7 @@ public function execute() $synGroup = $this->synGroupRepository->get($synGroupId); if (!$synGroup->getGroupId() && $synGroupId) { - $this->messageManager->addError(__('This synonym group no longer exists.')); + $this->messageManager->addErrorMessage(__('This synonym group no longer exists.')); return $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php index 3a1b80df2ea7e..52ff4a0388634 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php @@ -5,6 +5,7 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\Exception\NotFoundException; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; @@ -12,10 +13,15 @@ class Delete extends TermController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { - $id = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $id = (int)$this->getRequest()->getParam('id'); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if ($id) { @@ -23,16 +29,16 @@ public function execute() $model = $this->_objectManager->create(\Magento\Search\Model\Query::class); $model->setId($id); $model->delete(); - $this->messageManager->addSuccess(__('You deleted the search.')); + $this->messageManager->addSuccessMessage(__('You deleted the search.')); $resultRedirect->setPath('search/*/'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('search/*/edit', ['id' => $this->getRequest()->getParam('id')]); return $resultRedirect; } } - $this->messageManager->addError(__('We can\'t find a search term to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a search term to delete.')); $resultRedirect->setPath('search/*/'); return $resultRedirect; } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php index 85e14ae9fe0b0..3ee0ea240377f 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php @@ -43,7 +43,7 @@ public function execute() if ($id) { $model->load($id); if (!$model->getId()) { - $this->messageManager->addError(__('This search no longer exists.')); + $this->messageManager->addErrorMessage(__('This search no longer exists.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setPath('search/*'); diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php index b38d883b8faae..449450eeb8fd7 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php @@ -5,6 +5,7 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\Exception\NotFoundException; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; @@ -12,21 +13,26 @@ class MassDelete extends TermController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $searchIds = $this->getRequest()->getParam('search'); if (!is_array($searchIds)) { - $this->messageManager->addError(__('Please select searches.')); + $this->messageManager->addErrorMessage(__('Please select searches.')); } else { try { foreach ($searchIds as $searchId) { $model = $this->_objectManager->create(\Magento\Search\Model\Query::class)->load($searchId); $model->delete(); } - $this->messageManager->addSuccess(__('Total of %1 record(s) were deleted.', count($searchIds))); + $this->messageManager->addSuccessMessage(__('Total of %1 record(s) were deleted.', count($searchIds))); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php index 42e9373a20fe2..cd9b1347ed1ed 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php @@ -45,12 +45,15 @@ public function execute() $model->addData($data); $model->setIsProcessed(0); $model->save(); - $this->messageManager->addSuccess(__('You saved the search term.')); + $this->messageManager->addSuccessMessage(__('You saved the search term.')); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); return $this->proceedToEdit($data); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving the search query.')); + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while saving the search query.') + ); return $this->proceedToEdit($data); } } diff --git a/app/code/Magento/Search/Model/PopularSearchTerms.php b/app/code/Magento/Search/Model/PopularSearchTerms.php new file mode 100644 index 0000000000000..d5ddc0e1dac5f --- /dev/null +++ b/app/code/Magento/Search/Model/PopularSearchTerms.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Search\Model; + +/** + * Popular search terms + */ +class PopularSearchTerms +{ + const XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS = 'catalog/search/max_count_cacheable_search_terms'; + + /** + * Scope configuration + * + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Catalog search data + * + * @var \Magento\Search\Model\ResourceModel\Query\Collection + */ + private $queryCollection; + + /** + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Search\Model\ResourceModel\Query\Collection + */ + public function __construct( + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Search\Model\ResourceModel\Query\Collection $queryCollection + ) { + $this->scopeConfig = $scopeConfig; + $this->queryCollection = $queryCollection; + } + + /** + * Check if is cacheable search term + * + * @param string $term + * @param int $storeId + * @return bool + */ + public function isCacheable(string $term, int $storeId) + { + $terms = $this->queryCollection + ->setPopularQueryFilter($storeId) + ->setPageSize($this->getMaxCountCacheableSearchTerms($storeId)) + ->load() + ->getColumnValues('query_text'); + + return in_array($term, $terms); + } + + /** + * Retrieve maximum count cacheable search terms + * + * @param int $storeId + * @return int + */ + private function getMaxCountCacheableSearchTerms(int $storeId) + { + return $this->scopeConfig->getValue( + self::XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + } +} diff --git a/app/code/Magento/Search/Model/ResourceModel/Query.php b/app/code/Magento/Search/Model/ResourceModel/Query.php index 54849ffec5558..055c10eacf85b 100644 --- a/app/code/Magento/Search/Model/ResourceModel/Query.php +++ b/app/code/Magento/Search/Model/ResourceModel/Query.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Search\Model\ResourceModel; use Magento\Framework\DB\Select; diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 46e794a1954cf..f8d7e38a705dd 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,8 +87,8 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $phrase, - Fulltext::FULLTEXT_MODE_BOOLEAN + $this->escapePhrase($phrase), + Fulltext::FULLTEXT_MODE_NATURAL ); $query = $this->getConnection()->select()->from( $this->getMainTable() @@ -97,6 +97,18 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } + /** + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + * @param string $phrase + * @return string + */ + private function escapePhrase(string $phrase): string + { + return preg_replace('/@+|[@+-]+$|[<>]/', '', $phrase); + } + /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php b/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php deleted file mode 100644 index f2eee56042fc7..0000000000000 --- a/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Search\Model\SearchEngine; - -use Magento\Backend\Model\Menu; -use Magento\Backend\Model\Menu\Builder; -use Magento\Framework\Search\SearchEngine\ConfigInterface; -use Magento\Search\Model\EngineResolver; - -/** - * A plugin for Magento\Backend\Model\Menu\Builder class. Implements "after" for "getResult()". - * - * The purpose of this plugin is to go through the menu tree and remove "Search Terms" menu item if the - * selected search engine does not support "synonyms" feature. - */ -class MenuBuilder -{ - /** - * A constant to refer to "Search Synonyms" menu item id from etc/adminhtml/menu.xml - */ - const SEARCH_SYNONYMS_MENU_ITEM_ID = 'Magento_Search::search_synonyms'; - - /** - * @var ConfigInterface $searchFeatureConfig - */ - protected $searchFeatureConfig; - - /** - * @var EngineResolver $engineResolver - */ - protected $engineResolver; - - /** - * MenuBuilder constructor. - * - * @param ConfigInterface $searchFeatureConfig - * @param EngineResolver $engineResolver - */ - public function __construct( - ConfigInterface $searchFeatureConfig, - EngineResolver $engineResolver - ) { - $this->searchFeatureConfig = $searchFeatureConfig; - $this->engineResolver = $engineResolver; - } - - /** - * Removes 'Search Synonyms' from the menu if 'synonyms' is not supported - * - * @param Builder $subject - * @param Menu $menu - * @return Menu - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetResult(Builder $subject, Menu $menu) - { - $searchEngine = $this->engineResolver->getCurrentSearchEngine(); - if (!$this->searchFeatureConfig - ->isFeatureSupported(ConfigInterface::SEARCH_ENGINE_FEATURE_SYNONYMS, $searchEngine) - ) { - // "Search Synonyms" feature is not supported by the current configured search engine. - // Menu will be updated to remove it from the list - $menu->remove(self::SEARCH_SYNONYMS_MENU_ITEM_ID); - } - return $menu; - } -} diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index 1c11e35aaf72d..9c5082f4b020b 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -82,7 +82,7 @@ public function getSynonymsForPhrase($phrase) /** * Helper method to find the matching of $pattern to $synonymGroupsToExamine. * If matches, the particular array index is returned. - * Otherwise false will be returned. + * Otherwise null will be returned. * * @param string $pattern * @param array $synonymGroupsToExamine diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/StorefrontQuickSearchActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/StorefrontQuickSearchActionGroup.xml new file mode 100644 index 0000000000000..bf5ca6db4ce5c --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/StorefrontQuickSearchActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontQuickSearchActionGroup"> + <arguments> + <argument name="searchPhrase" type="string"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="{{searchPhrase}}" stepKey="fillSearchField"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/LICENSE.txt b/app/code/Magento/Search/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Search/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 <insert your license name here>" 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/app/code/Magento/Search/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Search/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Search/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 <insert your license name here>" 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/app/code/Magento/Search/Test/Mftf/README.md b/app/code/Magento/Search/Test/Mftf/README.md new file mode 100644 index 0000000000000..bd4bc14db41f7 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Search Functional Tests + +The Functional Test Module for **Magento Search** module. diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml new file mode 100644 index 0000000000000..35cb1f44531a4 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontQuickSearchSection"> + <element name="searchPhrase" type="input" selector="#search" timeout="10"/> + <element name="searchButton" type="button" selector="button.action.search" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php index a7f71941dc6b2..164f82da02f6f 100644 --- a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php @@ -55,8 +55,9 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManager\ObjectManager::class) ->disableOriginalConstructor() @@ -107,10 +108,10 @@ public function testDeleteAction() $this->repository->expects($this->once())->method('get')->with(10)->willReturn($this->synonymGroupMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('The synonym group has been deleted.')); - $this->messageManagerMock->expects($this->never())->method('addError'); + $this->messageManagerMock->expects($this->never())->method('addErrorMessage'); $this->resultRedirectMock->expects($this->once())->method('setPath')->with('*/*/')->willReturnSelf(); @@ -124,10 +125,10 @@ public function testDeleteActionNoId() ->willReturn(null); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t find a synonym group to delete.')); $this->messageManagerMock->expects($this->never()) - ->method('addSuccess'); + ->method('addSuccessMessage'); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php new file mode 100644 index 0000000000000..8b98959225528 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Controller\Adminhtml\Synonyms; + +/** + * Unit tests for Magento\Search\Controller\Adminhtml\Synonyms\MassDelete controller. + */ +class MassDeleteTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Search\Controller\Adminhtml\Synonyms\MassDelete + */ + private $controller; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterMock; + + /** + * @var \Magento\Search\Model\ResourceModel\SynonymGroup\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $collectionFactoryMock; + + /** + * @var \Magento\Search\Api\SynonymGroupRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $synGroupRepositoryMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->requestMock = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + true, + true, + ['isPost'] + ); + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); + $this->collectionFactoryMock = $this->createMock( + \Magento\Search\Model\ResourceModel\SynonymGroup\CollectionFactory::class + ); + $this->synGroupRepositoryMock = $this->createMock(\Magento\Search\Api\SynonymGroupRepositoryInterface::class); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $this->controller = $objectManagerHelper->getObject( + \Magento\Search\Controller\Adminhtml\Synonyms\MassDelete::class, + [ + 'context' => $this->contextMock, + 'filter' => $this->filterMock, + 'collectionFactory' => $this->collectionFactoryMock, + 'synGroupRepository' => $this->synGroupRepositoryMock, + ] + ); + } + + /** + * Check that error throws when request is not POST. + * + * @return void + * @expectedException \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithNotPostRequest() + { + $this->requestMock->expects($this->once())->method('isPost')->willReturn(false); + + $this->controller->execute(); + } +} diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php index efda8f52fcfe9..a9a705b74a8bb 100644 --- a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php @@ -46,7 +46,7 @@ protected function setUp() { $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['isPost']) ->getMockForAbstractClass(); $this->objectManager = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() @@ -54,7 +54,7 @@ protected function setUp() ->getMockForAbstractClass(); $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->disableOriginalConstructor() - ->setMethods(['addSuccess', 'addError']) + ->setMethods(['addSuccessMessage', 'addErrorMessage']) ->getMockForAbstractClass(); $this->pageFactory = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) ->setMethods([]) @@ -85,6 +85,7 @@ protected function setUp() $this->context->expects($this->any()) ->method('getResultFactory') ->willReturn($this->resultFactoryMock); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->controller = $this->objectManagerHelper->getObject( @@ -107,7 +108,7 @@ public function testExecute() $this->createQuery(0, 1); $this->createQuery(1, 2); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->will($this->returnSelf()); $this->resultRedirectMock->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/SaveTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/SaveTest.php index 09ae2c38fe525..28f4b65cd412f 100644 --- a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/SaveTest.php +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/SaveTest.php @@ -75,7 +75,7 @@ protected function setUp() $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->disableOriginalConstructor() - ->setMethods(['addSuccess', 'addError', 'addException']) + ->setMethods(['addSuccessMessage', 'addErrorMessage', 'addExceptionMessage']) ->getMockForAbstractClass(); $this->context->expects($this->any()) ->method('getMessageManager') @@ -143,7 +143,7 @@ public function testExecuteLoadQueryQueryId() $this->query->expects($this->once())->method('getId')->willReturn(false); $this->query->expects($this->once())->method('load')->with($queryId); - $this->messageManager->expects($this->once())->method('addSuccess'); + $this->messageManager->expects($this->once())->method('addSuccessMessage'); $this->redirect->expects($this->once())->method('setPath')->willReturnSelf(); $this->assertSame($this->redirect, $this->controller->execute()); @@ -161,7 +161,7 @@ public function testExecuteLoadQueryQueryIdQueryText() $this->query->expects($this->once())->method('loadByQueryText')->with($queryText); $this->query->expects($this->any())->method('getId')->willReturn($queryId); - $this->messageManager->expects($this->once())->method('addSuccess'); + $this->messageManager->expects($this->once())->method('addSuccessMessage'); $this->redirect->expects($this->once())->method('setPath')->willReturnSelf(); $this->assertSame($this->redirect, $this->controller->execute()); @@ -180,7 +180,7 @@ public function testExecuteLoadQueryQueryIdQueryText2() $this->query->expects($this->any())->method('getId')->willReturn(false); $this->query->expects($this->once())->method('load')->with($queryId); - $this->messageManager->expects($this->once())->method('addSuccess'); + $this->messageManager->expects($this->once())->method('addSuccessMessage'); $this->redirect->expects($this->once())->method('setPath')->willReturnSelf(); $this->assertSame($this->redirect, $this->controller->execute()); @@ -199,7 +199,7 @@ public function testExecuteLoadQueryQueryIdQueryTextException() $this->query->expects($this->once())->method('loadByQueryText')->with($queryText); $this->query->expects($this->any())->method('getId')->willReturn($anotherQueryId); - $this->messageManager->expects($this->once())->method('addError'); + $this->messageManager->expects($this->once())->method('addErrorMessage'); $this->session->expects($this->once())->method('setPageData'); $this->redirect->expects($this->once())->method('setPath')->willReturnSelf(); $this->assertSame($this->redirect, $this->controller->execute()); @@ -216,7 +216,7 @@ public function testExecuteException() $this->query->expects($this->once())->method('setStoreId'); $this->query->expects($this->once())->method('loadByQueryText')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once())->method('addException'); + $this->messageManager->expects($this->once())->method('addExceptionMessage'); $this->session->expects($this->once())->method('setPageData'); $this->redirect->expects($this->once())->method('setPath')->willReturnSelf(); $this->assertSame($this->redirect, $this->controller->execute()); diff --git a/app/code/Magento/Search/Test/Unit/Helper/DataTest.php b/app/code/Magento/Search/Test/Unit/Helper/DataTest.php index 291362734feff..1f9aad8d4316d 100644 --- a/app/code/Magento/Search/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Search/Test/Unit/Helper/DataTest.php @@ -127,6 +127,9 @@ public function testGetEscapedQueryText($queryText, $maxQueryLength, $expected) $this->assertEquals($expected, $this->model->getEscapedQueryText()); } + /** + * @return array + */ public function queryTextDataProvider() { return [ diff --git a/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php b/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php new file mode 100644 index 0000000000000..849a0c067d459 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Search\Model\PopularSearchTerms; +use Magento\Search\Model\ResourceModel\Query\Collection; +use Magento\Store\Model\ScopeInterface; + +/** + * @covers \Magento\Search\Model\PopularSearchTerms + */ +class PopularSearchTermsTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var PopularSearchTerms + */ + private $popularSearchTerms; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var Collection|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryCollectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->queryCollectionMock = $this->createMock(Collection::class); + $this->popularSearchTerms = new PopularSearchTerms($this->scopeConfigMock, $this->queryCollectionMock); + } + + /** + * Test isCacheableDataProvider method + * + * @dataProvider isCacheableDataProvider + * + * @param string $term + * @param array $terms + * @param $expected $terms + * + * @return void + */ + public function testIsCacheable($term, $terms, $expected) + { + $storeId = 7; + $pageSize = 25; + + $this->scopeConfigMock->expects($this->once())->method('getValue') + ->with( + PopularSearchTerms::XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS, + ScopeInterface::SCOPE_STORE, + $storeId + )->willReturn($pageSize); + $this->queryCollectionMock->expects($this->once())->method('setPopularQueryFilter')->with($storeId) + ->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('setPageSize')->with($pageSize) + ->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('load')->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('getColumnValues')->with('query_text') + ->willReturn($terms); + + $actual = $this->popularSearchTerms->isCacheable($term, $storeId); + self::assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function isCacheableDataProvider() + { + return [ + ['test01', [], false], + ['test02', ['test01', 'test02'], true], + ['test03', ['test01', 'test02'], false], + ['test04', ['test04'], true], + ]; + } +} diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php deleted file mode 100644 index 8e51631976bd8..0000000000000 --- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Search\Test\Unit\Model\SearchEngine; - -use Magento\Backend\Model\Menu; -use Magento\Backend\Model\Menu\Builder; -use Magento\Framework\Search\SearchEngine\ConfigInterface; -use Magento\Search\Model\EngineResolver; - -/** - * Class MenuBuilderTest. A unit test class to test functionality of - * Magento\Search\Model\SearchEngine\MenuBuilder class - */ -class MenuBuilderTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $searchFeatureConfig; - - /** - * @var EngineResolver|\PHPUnit_Framework_MockObject_MockObject - */ - protected $engineResolver; - - protected function setUp() - { - $this->searchFeatureConfig = $this->createMock(\Magento\Search\Model\SearchEngine\Config::class); - $this->engineResolver = $this->createMock(\Magento\Search\Model\EngineResolver::class); - } - - public function testAfterGetResult() - { - $this->engineResolver->expects($this->once())->method('getCurrentSearchEngine')->willReturn('mysql'); - $this->searchFeatureConfig - ->expects($this->once()) - ->method('isFeatureSupported') - ->with('synonyms', 'mysql') - ->willReturn(false); - /** @var \Magento\Backend\Model\Menu $menu */ - $menu = $this->createMock(\Magento\Backend\Model\Menu::class); - $menu->expects($this->once())->method('remove')->willReturn(true); - - /** @var \Magento\Backend\Model\Menu\Builder $menuBuilder */ - $menuBuilder = $this->createMock(\Magento\Backend\Model\Menu\Builder::class); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - /** @var \Magento\Search\Model\SearchEngine\MenuBuilder $searchMenuBuilder */ - $searchMenuBuilder = $objectManager->getObject( - \Magento\Search\Model\SearchEngine\MenuBuilder::class, - [ - 'searchFeatureConfig' => $this->searchFeatureConfig, - 'engineResolver' => $this->engineResolver - ] - ); - $this->assertInstanceOf( - \Magento\Backend\Model\Menu::class, - $searchMenuBuilder->afterGetResult($menuBuilder, $menu) - ); - } -} diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php new file mode 100644 index 0000000000000..45dcfbd2d0c32 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Search\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class SynonymAnalyzerTest + */ +class SynonymAnalyzerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Search\Model\SynonymAnalyzer + */ + private $synonymAnalyzer; + + /** + * @var \Magento\Search\Model\SynonymReader |\PHPUnit_Framework_MockObject_MockObject + */ + private $synReaderModel; + + /** + * Test set up + */ + protected function setUp() + { + $helper = new ObjectManager($this); + + $this->synReaderModel = $this->getMockBuilder(\Magento\Search\Model\SynonymReader::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->synonymAnalyzer = $helper->getObject( + \Magento\Search\Model\SynonymAnalyzer::class, + [ + 'synReader' => $this->synReaderModel, + ] + ); + } + + /** + * @test + */ + public function testGetSynonymsForPhrase() + { + $phrase = 'Elizabeth is the british queen'; + $expected = [ + 0 => [ 0 => "Elizabeth" ], + 1 => [ 0 => "is" ], + 2 => [ 0 => "the" ], + 3 => [ 0 => "british", 1 => "english" ], + 4 => [ 0 => "queen", 1 => "monarch" ], + ]; + $this->synReaderModel->expects($this->once()) + ->method('loadByPhrase') + ->with($phrase) + ->willReturnSelf() + ; + $this->synReaderModel->expects($this->once()) + ->method('getData') + ->willReturn([ + ['synonyms' => 'british,english'], + ['synonyms' => 'queen,monarch'], + ]) + ; + + $actual = $this->synonymAnalyzer->getSynonymsForPhrase($phrase); + $this->assertEquals($expected, $actual); + } + + /** + * @test + * + * Empty phrase scenario + */ + public function testGetSynonymsForPhraseEmptyPhrase() + { + $phrase = ''; + $expected = []; + $actual = $this->synonymAnalyzer->getSynonymsForPhrase($phrase); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php index f62c07b149c0e..4532479c482b5 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php @@ -53,7 +53,7 @@ public function testSaveCreate() /** * @expectedException \Magento\Search\Model\Synonym\MergeConflictException - * @expecteExceptionMessage (c,d,e) + * @expectedExceptionMessage Merge conflict with existing synonym group(s): (a,b,c) */ public function testSaveCreateMergeConflict() { @@ -138,7 +138,7 @@ public function testSaveUpdate() /** * @expectedException \Magento\Search\Model\Synonym\MergeConflictException - * @expecteExceptionMessage (d,h,i) + * @expectedExceptionMessage (d,h,i) */ public function testSaveUpdateMergeConflict() { diff --git a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php index 8cc9b809ff888..1dd7ddaf86bb7 100644 --- a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php +++ b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php @@ -63,11 +63,14 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete'), 'message' => __('Are you sure you want to delete synonym group with id: %1?', $item['group_id']) - ] + ], + 'post' => true, + '__disableTmpl' => true ]; $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl(self::SYNONYM_URL_PATH_EDIT, ['group_id' => $item['group_id']]), 'label' => __('View/Edit'), + '__disableTmpl' => true ]; } } diff --git a/app/code/Magento/Search/composer.json b/app/code/Magento/Search/composer.json index 5dc05010c525a..4c45d861b5510 100644 --- a/app/code/Magento/Search/composer.json +++ b/app/code/Magento/Search/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-search", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*", "magento/module-backend": "100.2.*", "magento/module-catalog-search": "100.2.*", @@ -11,7 +11,7 @@ "magento/module-ui": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Search/etc/di.xml b/app/code/Magento/Search/etc/di.xml index 5a10e3787e4fb..4409cebe39328 100755 --- a/app/code/Magento/Search/etc/di.xml +++ b/app/code/Magento/Search/etc/di.xml @@ -82,7 +82,4 @@ <argument name="dataStorage" xsi:type="object">Magento\Search\Model\SearchEngine\Config\Data</argument> </arguments> </type> - <type name="Magento\Backend\Model\Menu\Builder"> - <plugin name="SearchTermMenuBuilder" type="Magento\Search\Model\SearchEngine\MenuBuilder" /> - </type> </config> diff --git a/app/code/Magento/Search/view/frontend/requirejs-config.js b/app/code/Magento/Search/view/frontend/requirejs-config.js index c38cba4315eac..d945944daa1b0 100644 --- a/app/code/Magento/Search/view/frontend/requirejs-config.js +++ b/app/code/Magento/Search/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - quickSearch: 'Magento_Search/form-mini' + quickSearch: 'Magento_Search/js/form-mini', + 'Magento_Search/form-mini': 'Magento_Search/js/form-mini' } } }; diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 2ea87be13d5e3..888d5b678569f 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile +// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** @var $block \Magento\Framework\View\Element\Template */ @@ -12,30 +12,31 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); ?> <div class="block block-search"> - <div class="block block-title"><strong><?= /* @escapeNotVerified */ __('Search') ?></strong></div> + <div class="block block-title"><strong><?= $block->escapeHtml(__('Search')) ?></strong></div> <div class="block block-content"> - <form class="form minisearch" id="search_mini_form" action="<?= /* @escapeNotVerified */ $helper->getResultUrl() ?>" method="get"> + <form class="form minisearch" id="search_mini_form" action="<?= $block->escapeUrl($helper->getResultUrl()) ?>" method="get"> <div class="field search"> <label class="label" for="search" data-role="minisearch-label"> - <span><?= /* @escapeNotVerified */ __('Search') ?></span> + <span><?= $block->escapeHtml(__('Search')) ?></span> </label> <div class="control"> <input id="search" data-mage-init='{"quickSearch":{ "formSelector":"#search_mini_form", - "url":"<?= /* @escapeNotVerified */ $helper->getSuggestUrl()?>", + "url":"<?= $block->escapeUrl($helper->getSuggestUrl()) ?>", "destinationSelector":"#search_autocomplete"} }' type="text" - name="<?= /* @escapeNotVerified */ $helper->getQueryParamName() ?>" - value="<?= /* @escapeNotVerified */ $helper->getEscapedQueryText() ?>" - placeholder="<?= /* @escapeNotVerified */ __('Search entire store here...') ?>" + name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" + value="<?= $block->escapeHtmlAttr($helper->getEscapedQueryText()) ?>" + placeholder="<?= $block->escapeHtmlAttr(__('Search entire store here...')) ?>" class="input-text" - maxlength="<?= /* @escapeNotVerified */ $helper->getMaxQueryLength() ?>" + maxlength="<?= /* @noEscape */ (int)$helper->getMaxQueryLength() ?>" role="combobox" aria-haspopup="false" aria-autocomplete="both" - autocomplete="off"/> + autocomplete="off" + aria-expanded="false"/> <div id="search_autocomplete" class="search-autocomplete"></div> <?= $block->getChildHtml() ?> </div> @@ -44,7 +45,7 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); <button type="submit" title="<?= $block->escapeHtml(__('Search')) ?>" class="action search"> - <span><?= /* @escapeNotVerified */ __('Search') ?></span> + <span><?= $block->escapeHtml(__('Search')) ?></span> </button> </div> </form> diff --git a/app/code/Magento/Search/view/frontend/templates/term.phtml b/app/code/Magento/Search/view/frontend/templates/term.phtml index 4285b42fa0329..e152acacaa53d 100644 --- a/app/code/Magento/Search/view/frontend/templates/term.phtml +++ b/app/code/Magento/Search/view/frontend/templates/term.phtml @@ -3,23 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> -<?php if (sizeof($block->getTerms()) > 0): ?> +<?php if (count($block->getTerms()) > 0) : ?> <ul class="search-terms"> - <?php foreach ($block->getTerms() as $_term): ?> + <?php foreach ($block->getTerms() as $_term) : ?> <li class="item"> - <a href="<?= /* @escapeNotVerified */ $block->getSearchUrl($_term) ?>" - style="font-size:<?= /* @escapeNotVerified */ $_term->getRatio()*70+75 ?>%;"> - <?= $block->escapeHtml($_term->getName()) ?> + <a href="<?= $block->escapeUrl($block->getSearchUrl($_term)) ?>" + style="font-size:<?= /* @noEscape */ $_term->getRatio()*70+75 ?>%;"> + <?= $block->escapeHtml($_term->getData('query_text')) ?> </a> </li> <?php endforeach; ?> </ul> -<?php else: ?> +<?php else : ?> <div class="message notice"> - <div><?= /* @escapeNotVerified */ __('There are no search terms available.') ?></div> + <div><?= $block->escapeHtml(__('There are no search terms available.')) ?></div> </div> <?php endif; ?> diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js similarity index 88% rename from app/code/Magento/Search/view/frontend/web/form-mini.js rename to app/code/Magento/Search/view/frontend/web/js/form-mini.js index e8598f46eb5be..3f3e64738d46f 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -43,7 +43,8 @@ define([ '</li>', submitBtn: 'button[type="submit"]', searchLabel: '[data-role=minisearch-label]', - isExpandable: null + isExpandable: null, + suggestionDelay: 300 }, /** @inheritdoc */ @@ -55,7 +56,7 @@ define([ this.autoComplete = $(this.options.destinationSelector); this.searchForm = $(this.options.formSelector); this.submitBtn = this.searchForm.find(this.options.submitBtn)[0]; - this.searchLabel = $(this.options.searchLabel); + this.searchLabel = this.searchForm.find(this.options.searchLabel); this.isExpandable = this.options.isExpandable; _.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit'); @@ -70,8 +71,7 @@ define([ this.isExpandable = true; }.bind(this), exit: function () { - this.isExpandable = false; - this.element.removeAttr('aria-expanded'); + this.isExpandable = true; }.bind(this) }); @@ -98,14 +98,17 @@ define([ }, this), 250); }, this)); - this.element.trigger('blur'); + if (this.element.get(0) === document.activeElement) { + this.setActiveState(true); + } this.element.on('focus', this.setActiveState.bind(this, true)); this.element.on('keydown', this._onKeyDown); - this.element.on('input propertychange', this._onPropertyChange); + // Prevent spamming the server with requests by waiting till the user has stopped typing for period of time + this.element.on('input propertychange', _.debounce(this._onPropertyChange, this.options.suggestionDelay)); - this.searchForm.on('submit', $.proxy(function () { - this._onSubmit(); + this.searchForm.on('submit', $.proxy(function (e) { + this._onSubmit(e); this._updateAriaHasPopup(false); }, this)); }, @@ -125,11 +128,16 @@ define([ * @param {Boolean} isActive */ setActiveState: function (isActive) { + var searchValue; + this.searchForm.toggleClass('active', isActive); this.searchLabel.toggleClass('active', isActive); if (this.isExpandable) { this.element.attr('aria-expanded', isActive); + searchValue = this.element.val(); + this.element.val(''); + this.element.val(searchValue); } }, @@ -204,13 +212,17 @@ define([ switch (keyCode) { case $.ui.keyCode.HOME: - this._getFirstVisibleElement().addClass(this.options.selectClass); - this.responseList.selected = this._getFirstVisibleElement(); + if (this._getFirstVisibleElement()) { + this._getFirstVisibleElement().addClass(this.options.selectClass); + this.responseList.selected = this._getFirstVisibleElement(); + } break; case $.ui.keyCode.END: - this._getLastElement().addClass(this.options.selectClass); - this.responseList.selected = this._getLastElement(); + if (this._getLastElement()) { + this._getLastElement().addClass(this.options.selectClass); + this.responseList.selected = this._getLastElement(); + } break; case $.ui.keyCode.ESCAPE: @@ -220,6 +232,7 @@ define([ case $.ui.keyCode.ENTER: this.searchForm.trigger('submit'); + e.preventDefault(); break; case $.ui.keyCode.DOWN: @@ -297,12 +310,13 @@ define([ dropdown.append(html); }); + this._resetResponseList(true); + this.responseList.indexList = this.autoComplete.html(dropdown) .css(clonePosition) .show() .find(this.options.responseFieldElements + ':visible'); - this._resetResponseList(false); this.element.removeAttr('aria-activedescendant'); if (this.responseList.indexList.length) { @@ -329,6 +343,11 @@ define([ this._resetResponseList(false); } }.bind(this)); + } else { + this._resetResponseList(true); + this.autoComplete.hide(); + this._updateAriaHasPopup(false); + this.element.removeAttr('aria-activedescendant'); } }, this)); } else { diff --git a/app/code/Magento/Security/Controller/Adminhtml/Session/LogoutAll.php b/app/code/Magento/Security/Controller/Adminhtml/Session/LogoutAll.php index c533e740b2251..35d8f22d84d51 100644 --- a/app/code/Magento/Security/Controller/Adminhtml/Session/LogoutAll.php +++ b/app/code/Magento/Security/Controller/Adminhtml/Session/LogoutAll.php @@ -38,11 +38,11 @@ public function execute() { try { $this->sessionsManager->logoutOtherUserSessions(); - $this->messageManager->addSuccess(__('All other open sessions for this account were terminated.')); + $this->messageManager->addSuccessMessage(__('All other open sessions for this account were terminated.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __("We couldn't logout because of an error.")); + $this->messageManager->addExceptionMessage($e, __("We couldn't logout because of an error.")); } $this->_redirect('*/*/activity'); } diff --git a/app/code/Magento/Security/Model/AdminSessionInfo.php b/app/code/Magento/Security/Model/AdminSessionInfo.php index 1aeb1b671c5cf..77d864965baca 100644 --- a/app/code/Magento/Security/Model/AdminSessionInfo.php +++ b/app/code/Magento/Security/Model/AdminSessionInfo.php @@ -164,7 +164,7 @@ public function isOtherSessionsTerminated() * Setter for isOtherSessionsTerminated * * @param bool $isOtherSessionsTerminated - * @return this + * @return $this * @since 100.1.0 */ public function setIsOtherSessionsTerminated($isOtherSessionsTerminated) diff --git a/app/code/Magento/Security/Model/AdminSessionsManager.php b/app/code/Magento/Security/Model/AdminSessionsManager.php index 4ebdcc58240a1..af690f1899e7b 100644 --- a/app/code/Magento/Security/Model/AdminSessionsManager.php +++ b/app/code/Magento/Security/Model/AdminSessionsManager.php @@ -66,6 +66,14 @@ class AdminSessionsManager */ private $remoteAddress; + /** + * Max lifetime for session prolong to be valid (sec) + * + * Means that after session was prolonged + * all other prolongs will be ignored within this period + */ + private $maxIntervalBetweenConsecutiveProlongs = 60; + /** * @param ConfigInterface $securityConfig * @param \Magento\Backend\Model\Auth\Session $authSession @@ -124,11 +132,16 @@ public function processLogin() */ public function processProlong() { - $this->getCurrentSession()->setData( - 'updated_at', - $this->authSession->getUpdatedAt() - ); - $this->getCurrentSession()->save(); + if ($this->lastProlongIsOldEnough()) { + $this->getCurrentSession()->setData( + 'updated_at', + date( + \Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT, + $this->authSession->getUpdatedAt() + ) + ); + $this->getCurrentSession()->save(); + } return $this; } @@ -298,4 +311,45 @@ protected function createAdminSessionInfoCollection() { return $this->adminSessionInfoCollectionFactory->create(); } + + /** + * Calculates diff between now and last session updated_at + * and decides whether new prolong must be triggered or not + * + * This is done to limit amount of session prolongs and updates to database + * within some period of time - X + * X - is calculated in getIntervalBetweenConsecutiveProlongs() + * + * @see getIntervalBetweenConsecutiveProlongs() + * @return bool + */ + private function lastProlongIsOldEnough() + { + $lastProlongTimestamp = strtotime($this->getCurrentSession()->getUpdatedAt()); + $nowTimestamp = $this->authSession->getUpdatedAt(); + + $diff = $nowTimestamp - $lastProlongTimestamp; + + return (float) $diff > $this->getIntervalBetweenConsecutiveProlongs(); + } + + /** + * Calculates lifetime for session prolong to be valid + * + * Calculation is based on admin session lifetime + * Calculated result is in seconds and is in the interval + * between 1 (including) and MAX_INTERVAL_BETWEEN_CONSECUTIVE_PROLONGS (including) + * + * @return float + */ + private function getIntervalBetweenConsecutiveProlongs() + { + return (float) max( + 1, + min( + 4 * log((float)$this->securityConfig->getAdminSessionLifetime()), + $this->maxIntervalBetweenConsecutiveProlongs + ) + ); + } } diff --git a/app/code/Magento/Security/Model/Config.php b/app/code/Magento/Security/Model/Config.php index 100f4630a45a6..2135b81eb82b5 100644 --- a/app/code/Magento/Security/Model/Config.php +++ b/app/code/Magento/Security/Model/Config.php @@ -24,10 +24,17 @@ class Config implements ConfigInterface */ const XML_PATH_ADMIN_AREA = 'admin/security/'; + /** + * Configuration path to frontend area + */ + const XML_PATH_FRONTEND_AREA = 'customer/password/'; + /** * Configuration path to fronted area + * @deprecated + * @see \Magento\Security\Model\Config::XML_PATH_FRONTEND_AREA */ - const XML_PATH_FRONTED_AREA = 'customer/password/'; + const XML_PATH_FRONTED_AREA = self::XML_PATH_FRONTEND_AREA; /** * Configuration path to admin account sharing @@ -134,7 +141,7 @@ protected function getXmlPathPrefix() if ($this->scope->getCurrentScope() == \Magento\Framework\App\Area::AREA_ADMINHTML) { return self::XML_PATH_ADMIN_AREA; } - return self::XML_PATH_FRONTED_AREA; + return self::XML_PATH_FRONTEND_AREA; } /** diff --git a/app/code/Magento/Security/Model/Plugin/AccountManagement.php b/app/code/Magento/Security/Model/Plugin/AccountManagement.php index dea54b194880d..9476bf46df338 100644 --- a/app/code/Magento/Security/Model/Plugin/AccountManagement.php +++ b/app/code/Magento/Security/Model/Plugin/AccountManagement.php @@ -5,10 +5,12 @@ */ namespace Magento\Security\Model\Plugin; -use Magento\Security\Model\SecurityManager; use Magento\Customer\Model\AccountManagement as AccountManagementOriginal; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\ScopeInterface; use Magento\Framework\Exception\SecurityViolationException; use Magento\Security\Model\PasswordResetRequestEvent; +use Magento\Security\Model\SecurityManager; /** * Magento\Customer\Model\AccountManagement decorator @@ -30,21 +32,29 @@ class AccountManagement */ protected $passwordRequestEvent; + /** + * @var ScopeInterface + */ + private $scope; + /** * AccountManagement constructor. * * @param \Magento\Framework\App\RequestInterface $request * @param SecurityManager $securityManager * @param int $passwordRequestEvent + * @param ScopeInterface $scope */ public function __construct( \Magento\Framework\App\RequestInterface $request, \Magento\Security\Model\SecurityManager $securityManager, - $passwordRequestEvent = PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST + $passwordRequestEvent = PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST, + ScopeInterface $scope = null ) { $this->request = $request; $this->securityManager = $securityManager; $this->passwordRequestEvent = $passwordRequestEvent; + $this->scope = $scope ?: ObjectManager::getInstance()->get(ScopeInterface::class); } /** @@ -63,10 +73,14 @@ public function beforeInitiatePasswordReset( $template, $websiteId = null ) { - $this->securityManager->performSecurityCheck( - $this->passwordRequestEvent, - $email - ); + if ($this->scope->getCurrentScope() == \Magento\Framework\App\Area::AREA_FRONTEND + || $this->passwordRequestEvent == PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST) { + $this->securityManager->performSecurityCheck( + $this->passwordRequestEvent, + $email + ); + } + return [$email, $template, $websiteId]; } } diff --git a/app/code/Magento/Security/Model/Plugin/AuthSession.php b/app/code/Magento/Security/Model/Plugin/AuthSession.php index abec4dc7c29ef..08a81f12d9cfc 100644 --- a/app/code/Magento/Security/Model/Plugin/AuthSession.php +++ b/app/code/Magento/Security/Model/Plugin/AuthSession.php @@ -82,7 +82,7 @@ private function addUserLogoutNotification() $this->sessionsManager->getCurrentSession()->getStatus() ); } elseif ($message = $this->sessionsManager->getLogoutReasonMessage()) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } return $this; @@ -95,6 +95,6 @@ private function addUserLogoutNotification() */ private function isAjaxRequest() { - return (bool) $this->request->getParam('isAjax'); + return (bool)$this->request->getParam('isAjax'); } } diff --git a/app/code/Magento/Security/Model/Plugin/LoginController.php b/app/code/Magento/Security/Model/Plugin/LoginController.php index ab9c6e2857bce..ba1a18c4f0c06 100644 --- a/app/code/Magento/Security/Model/Plugin/LoginController.php +++ b/app/code/Magento/Security/Model/Plugin/LoginController.php @@ -53,7 +53,7 @@ public function beforeExecute(Login $login) { $logoutReasonCode = $this->securityCookie->getLogoutReasonCookie(); if ($this->isLoginForm($login) && $logoutReasonCode >= 0) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( $this->sessionsManager->getLogoutReasonMessageByStatus($logoutReasonCode) ); $this->securityCookie->deleteLogoutReasonCookie(); diff --git a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php index 7f1d85abeb74b..cbf41980c51d3 100644 --- a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php +++ b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php @@ -47,13 +47,13 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function check($securityEventType, $accountReference = null, $longIp = null) { $isEnabled = $this->securityConfig->getPasswordResetProtectionType() != ResetMethod::OPTION_NONE; $allowedAttemptsNumber = $this->securityConfig->getMaxNumberPasswordResetRequests(); - if ($isEnabled and $allowedAttemptsNumber) { + if ($isEnabled && $allowedAttemptsNumber) { $collection = $this->prepareCollection($securityEventType, $accountReference, $longIp); if ($collection->count() >= $allowedAttemptsNumber) { throw new SecurityViolationException( diff --git a/app/code/Magento/Security/Test/Mftf/LICENSE.txt b/app/code/Magento/Security/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Security/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 <insert your license name here>" 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/app/code/Magento/Security/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Security/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Security/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 <insert your license name here>" 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/app/code/Magento/Security/Test/Mftf/README.md b/app/code/Magento/Security/Test/Mftf/README.md new file mode 100644 index 0000000000000..65a0ff6c26e21 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Security Functional Tests + +The Functional Test Module for **Magento Security** module. diff --git a/app/code/Magento/Security/Test/Unit/Controller/Adminhtml/Session/LogoutAllTest.php b/app/code/Magento/Security/Test/Unit/Controller/Adminhtml/Session/LogoutAllTest.php index 8c2119bde667e..02335ef55aa93 100644 --- a/app/code/Magento/Security/Test/Unit/Controller/Adminhtml/Session/LogoutAllTest.php +++ b/app/code/Magento/Security/Test/Unit/Controller/Adminhtml/Session/LogoutAllTest.php @@ -74,7 +74,7 @@ public function setUp() $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->disableOriginalConstructor() - ->setMethods(['addSuccess', 'addError', 'addException']) + ->setMethods(['addSuccessMessage', 'addErrorMessage', 'addExceptionMessage']) ->getMockForAbstractClass(); $this->contextMock->expects($this->any()) ->method('getMessageManager') @@ -132,12 +132,12 @@ public function testExecute() $this->sessionsManager->expects($this->once()) ->method('logoutOtherUserSessions'); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with($successMessage); $this->messageManager->expects($this->never()) - ->method('addError'); + ->method('addErrorMessage'); $this->messageManager->expects($this->never()) - ->method('addException'); + ->method('addExceptionMessage'); $this->responseMock->expects($this->once()) ->method('setRedirect'); $this->actionFlagMock->expects($this->once()) @@ -158,7 +158,7 @@ public function testExecuteLocalizedException() ->method('logoutOtherUserSessions') ->willThrowException(new LocalizedException($phrase)); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($phrase); $this->controller->execute(); } @@ -173,7 +173,7 @@ public function testExecuteException() ->method('logoutOtherUserSessions') ->willThrowException(new \Exception()); $this->messageManager->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with(new \Exception(), $phrase); $this->controller->execute(); } diff --git a/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php b/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php index d81264f661762..ddfeaa59ac224 100644 --- a/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/AdminSessionsManagerTest.php @@ -99,7 +99,8 @@ public function setUp() 'setIsOtherSessionsTerminated', 'save', 'getUserId', - 'getSessionId' + 'getSessionId', + 'getUpdatedAt' ]); $this->securityConfigMock = $this->getMockBuilder(\Magento\Security\Model\ConfigInterface::class) @@ -216,7 +217,8 @@ public function testProcessLogin() public function testProcessProlong() { $sessionId = 50; - $updatedAt = '2015-12-31 23:59:59'; + $lastUpdatedAt = '2015-12-31 23:59:59'; + $newUpdatedAt = '2016-01-01 00:00:30'; $this->adminSessionInfoFactoryMock->expects($this->any()) ->method('create') @@ -230,13 +232,21 @@ public function testProcessProlong() ->method('load') ->willReturnSelf(); - $this->authSessionMock->expects($this->once()) + $this->currentSessionMock->expects($this->once()) + ->method('getUpdatedAt') + ->willReturn($lastUpdatedAt); + + $this->authSessionMock->expects($this->exactly(2)) ->method('getUpdatedAt') - ->willReturn($updatedAt); + ->willReturn(strtotime($newUpdatedAt)); + + $this->securityConfigMock->expects($this->once()) + ->method('getAdminSessionLifetime') + ->willReturn(100); $this->currentSessionMock->expects($this->once()) ->method('setData') - ->with('updated_at', $updatedAt) + ->with('updated_at', $newUpdatedAt) ->willReturnSelf(); $this->currentSessionMock->expects($this->once()) diff --git a/app/code/Magento/Security/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Security/Test/Unit/Model/ConfigTest.php index 7186502df73b5..3ef8655539b5a 100644 --- a/app/code/Magento/Security/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/ConfigTest.php @@ -167,7 +167,7 @@ protected function getXmlPathPrefix($scope) if ($scope == \Magento\Framework\App\Area::AREA_ADMINHTML) { return \Magento\Security\Model\Config::XML_PATH_ADMIN_AREA; } - return \Magento\Security\Model\Config::XML_PATH_FRONTED_AREA; + return \Magento\Security\Model\Config::XML_PATH_FRONTEND_AREA; } /** diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AccountManagementTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AccountManagementTest.php index 0935dc003d5b3..8f8128d395a0c 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AccountManagementTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AccountManagementTest.php @@ -6,7 +6,11 @@ namespace Magento\Security\Test\Unit\Model\Plugin; +use Magento\Customer\Model\AccountManagement; +use Magento\Framework\App\Area; +use Magento\Framework\Config\ScopeInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Security\Model\PasswordResetRequestEvent; /** * Test class for \Magento\Security\Model\Plugin\AccountManagement testing @@ -19,20 +23,25 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\App\RequestInterface + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $request; /** - * @var \Magento\Security\Model\SecurityManager + * @var \Magento\Security\Model\SecurityManager|\PHPUnit_Framework_MockObject_MockObject */ protected $securityManager; /** - * @var \Magento\Customer\Model\AccountManagement + * @var AccountManagement|\PHPUnit_Framework_MockObject_MockObject */ protected $accountManagement; + /** + * @var ScopeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scope; + /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ @@ -46,35 +55,45 @@ public function setUp() { $this->objectManager = new ObjectManager($this); - $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->securityManager = $this->createPartialMock( \Magento\Security\Model\SecurityManager::class, ['performSecurityCheck'] ); - $this->accountManagement = $this->createMock(\Magento\Customer\Model\AccountManagement::class); + $this->accountManagement = $this->createMock(AccountManagement::class); + $this->scope = $this->createMock(ScopeInterface::class); + } + + /** + * @param $area + * @param $passwordRequestEvent + * @param $expectedTimes + * @dataProvider beforeInitiatePasswordResetDataProvider + */ + public function testBeforeInitiatePasswordReset($area, $passwordRequestEvent, $expectedTimes) + { + $email = 'test@example.com'; + $template = AccountManagement::EMAIL_RESET; $this->model = $this->objectManager->getObject( \Magento\Security\Model\Plugin\AccountManagement::class, [ + 'passwordRequestEvent' => $passwordRequestEvent, 'request' => $this->request, - 'securityManager' => $this->securityManager + 'securityManager' => $this->securityManager, + 'scope' => $this->scope ] ); - } - /** - * @return void - */ - public function testBeforeInitiatePasswordReset() - { - $email = 'test@example.com'; - $template = \Magento\Customer\Model\AccountManagement::EMAIL_RESET; + $this->scope->expects($this->once()) + ->method('getCurrentScope') + ->willReturn($area); - $this->securityManager->expects($this->once()) + $this->securityManager->expects($this->exactly($expectedTimes)) ->method('performSecurityCheck') - ->with(\Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST, $email) + ->with($passwordRequestEvent, $email) ->willReturnSelf(); $this->model->beforeInitiatePasswordReset( @@ -83,4 +102,18 @@ public function testBeforeInitiatePasswordReset() $template ); } + + /** + * @return array + */ + public function beforeInitiatePasswordResetDataProvider() + { + return [ + [Area::AREA_ADMINHTML, PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST, 0], + [Area::AREA_ADMINHTML, PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST, 1], + [Area::AREA_FRONTEND, PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST, 1], + // This should never happen, but let's cover it with tests + [Area::AREA_FRONTEND, PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST, 1], + ]; + } } diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php index 5cb06d6143023..0f7f590b71de4 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthSessionTest.php @@ -112,7 +112,7 @@ public function testAroundProlongSessionIsNotActiveAndIsNotAjaxRequest() ->willReturn($errorMessage); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($errorMessage); $this->model->aroundProlong($this->authSessionMock, $proceed); diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/LoginControllerTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/LoginControllerTest.php index 2bb2bc3cafac7..aa066e23f67cb 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/LoginControllerTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/LoginControllerTest.php @@ -103,7 +103,7 @@ public function testBeforeExecute() ->willReturn($errorMessage); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($errorMessage); $this->securityCookieMock->expects($this->once()) diff --git a/app/code/Magento/Security/Test/Unit/Model/ResourceModel/PasswordResetRequestEvent/CollectionFactoryTest.php b/app/code/Magento/Security/Test/Unit/Model/ResourceModel/PasswordResetRequestEvent/CollectionFactoryTest.php index 525693631e86f..89bc5c2b85d50 100644 --- a/app/code/Magento/Security/Test/Unit/Model/ResourceModel/PasswordResetRequestEvent/CollectionFactoryTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/ResourceModel/PasswordResetRequestEvent/CollectionFactoryTest.php @@ -86,6 +86,9 @@ public function testCreate( $this->model->create($securityEventType, $accountReference, $longIp); } + /** + * @return array + */ public function createDataProvider() { return [ diff --git a/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php b/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php index b310bf63bc989..3a1855b3a220f 100644 --- a/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php @@ -87,7 +87,7 @@ public function testGetLogoutReasonCookie() ) ->willReturn($cookie); - $this->assertEquals(intval($cookie), $this->model->getLogoutReasonCookie()); + $this->assertEquals((int)$cookie, $this->model->getLogoutReasonCookie()); } /** @@ -114,7 +114,7 @@ public function testSetLogoutReasonCookie() ->method('setPublicCookie') ->with( SecurityCookie::LOGOUT_REASON_CODE_COOKIE_NAME, - intval($status), + (int)$status, $this->cookieMetadataMock ) ->willReturnSelf(); diff --git a/app/code/Magento/Security/composer.json b/app/code/Magento/Security/composer.json index 43af28b54dc03..b8cb41c075922 100644 --- a/app/code/Magento/Security/composer.json +++ b/app/code/Magento/Security/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-security", "description": "Security management module", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-backend": "100.2.*", "magento/module-store": "100.2.*", "magento/framework": "101.0.*" @@ -11,7 +11,7 @@ "magento/module-customer": "101.0.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Security/etc/adminhtml/di.xml b/app/code/Magento/Security/etc/adminhtml/di.xml index 6f07fb580490e..79477e9443097 100644 --- a/app/code/Magento/Security/etc/adminhtml/di.xml +++ b/app/code/Magento/Security/etc/adminhtml/di.xml @@ -17,14 +17,14 @@ </type> <type name="Magento\Security\Model\Plugin\AccountManagement"> <arguments> - <argument name="passwordRequestEvent" xsi:type="const">Magento\Security\Model\PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST</argument> + <argument name="passwordRequestEvent" xsi:type="const">Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST</argument> </arguments> </type> <type name="Magento\Security\Model\SecurityManager"> <arguments> <argument name="securityCheckers" xsi:type="array"> <item name="frequency" xsi:type="object">Magento\Security\Model\SecurityChecker\Frequency</item> - <item name="wuantity" xsi:type="object">Magento\Security\Model\SecurityChecker\Quantity</item> + <item name="quantity" xsi:type="object">Magento\Security\Model\SecurityChecker\Quantity</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Security/view/adminhtml/templates/session/activity.phtml b/app/code/Magento/Security/view/adminhtml/templates/session/activity.phtml index 88a3ff433a033..c61cce902ebba 100644 --- a/app/code/Magento/Security/view/adminhtml/templates/session/activity.phtml +++ b/app/code/Magento/Security/view/adminhtml/templates/session/activity.phtml @@ -7,7 +7,6 @@ /** * Account activity template * - * @codingStandardsIgnoreFile * @var $block \Magento\Security\Block\Adminhtml\Session\Activity */ @@ -39,21 +38,18 @@ $sessionInfoCollection = $block->getSessionInfoCollection(); </thead> <tbody> <?php - foreach ($sessionInfoCollection as $item): ?> + foreach ($sessionInfoCollection as $item) : ?> <tr> <td><?= $block->escapeHtml($item->getFormattedIp()) ?></td> <td><?= $block->escapeHtml($block->formatDateTime($item->getCreatedAt())) ?></td> </tr> - <?php - endforeach; - ?> + <?php endforeach; ?> </tbody> </table> <div class="button-container"> <button type="button" - <?php - if ($block->areMultipleSessionsActive()): ?> + <?php if ($block->areMultipleSessionsActive()) : ?> data-mage-init='{"confirmRedirect":{ "message": "<?= $block->escapeJs(__('Are you sure that you want to log out all other sessions?')) @@ -62,9 +58,7 @@ $sessionInfoCollection = $block->getSessionInfoCollection(); $block->escapeJs($block->escapeUrl($block->getUrl('security/session/logoutAll'))) ?>" }}' - <?php - else: ?>disabled<?php - endif ?> + <?php else : ?>disabled<?php endif ?> title="<?= $block->escapeHtmlAttr(__('Log out all other sessions')) ?>"> <?= $block->escapeHtml(__('Log out all other sessions')) ?> </button> diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 43e95ebe43d48..1c4b550361359 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -5,6 +5,7 @@ */ namespace Magento\SendFriend\Block; +use Magento\Captcha\Block\Captcha; use Magento\Customer\Model\Context; /** @@ -170,6 +171,7 @@ public function setFormData($data) /** * Retrieve Current Product Id * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getProductId() @@ -180,6 +182,7 @@ public function getProductId() /** * Retrieve current category id for product * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getCategoryId() @@ -222,4 +225,24 @@ public function canSend() { return !$this->sendfriend->isExceedLimit(); } + + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'product_sendtofriend_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + } } diff --git a/app/code/Magento/SendFriend/Controller/Product.php b/app/code/Magento/SendFriend/Controller/Product.php index 732bcef8b957a..388fcd23c2e0a 100644 --- a/app/code/Magento/SendFriend/Controller/Product.php +++ b/app/code/Magento/SendFriend/Controller/Product.php @@ -102,7 +102,7 @@ protected function _initProduct() } try { $product = $this->productRepository->getById($productId); - if (!$product->isVisibleInCatalog()) { + if (!$product->isVisibleInSiteVisibility() || !$product->isVisibleInCatalog()) { return false; } } catch (NoSuchEntityException $noEntityException) { diff --git a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php index 72dcf00d17c7d..36365ddfb44e5 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php +++ b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php @@ -4,13 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SendFriend\Controller\Product; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\ObjectManager; +use Magento\SendFriend\Model\CaptchaValidator; +/** + * Controller class Sendmail. Represents send-mail action request flow + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Sendmail extends \Magento\SendFriend\Controller\Product { /** @@ -23,6 +28,11 @@ class Sendmail extends \Magento\SendFriend\Controller\Product */ protected $catalogSession; + /** + * @var CaptchaValidator + */ + private $captchaValidator; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry @@ -31,6 +41,9 @@ class Sendmail extends \Magento\SendFriend\Controller\Product * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository * @param \Magento\Catalog\Model\Session $catalogSession + * @param CaptchaValidator|null $captchaValidator + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -39,11 +52,13 @@ public function __construct( \Magento\SendFriend\Model\SendFriend $sendFriend, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, - \Magento\Catalog\Model\Session $catalogSession + \Magento\Catalog\Model\Session $catalogSession, + CaptchaValidator $captchaValidator = null ) { parent::__construct($context, $coreRegistry, $formKeyValidator, $sendFriend, $productRepository); $this->categoryRepository = $categoryRepository; $this->catalogSession = $catalogSession; + $this->captchaValidator = $captchaValidator ?: ObjectManager::getInstance()->create(CaptchaValidator::class); } /** @@ -91,6 +106,7 @@ public function execute() try { $validate = $this->sendFriend->validate(); + $this->captchaValidator->validateSending($this->getRequest()); if ($validate === true) { $this->sendFriend->send(); $this->messageManager->addSuccess(__('The link to a friend was sent.')); diff --git a/app/code/Magento/SendFriend/Helper/Data.php b/app/code/Magento/SendFriend/Helper/Data.php index 5059a03964bce..7725ad605aea2 100644 --- a/app/code/Magento/SendFriend/Helper/Data.php +++ b/app/code/Magento/SendFriend/Helper/Data.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SendFriend\Helper; /** @@ -44,7 +42,11 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ public function isEnabled($store = null) { - return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLED, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** @@ -55,7 +57,11 @@ public function isEnabled($store = null) */ public function isAllowForGuest($store = null) { - return $this->scopeConfig->isSetFlag(self::XML_PATH_ALLOW_FOR_GUEST, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ALLOW_FOR_GUEST, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** @@ -66,7 +72,11 @@ public function isAllowForGuest($store = null) */ public function getMaxRecipients($store = null) { - return (int)$this->scopeConfig->getValue(self::XML_PATH_MAX_RECIPIENTS, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return (int)$this->scopeConfig->getValue( + self::XML_PATH_MAX_RECIPIENTS, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** @@ -77,7 +87,11 @@ public function getMaxRecipients($store = null) */ public function getMaxEmailPerPeriod($store = null) { - return (int)$this->scopeConfig->getValue(self::XML_PATH_MAX_PER_HOUR, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return (int)$this->scopeConfig->getValue( + self::XML_PATH_MAX_PER_HOUR, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** @@ -98,7 +112,11 @@ public function getPeriod() */ public function getLimitBy($store = null) { - return (int)$this->scopeConfig->getValue(self::XML_PATH_LIMIT_BY, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return (int)$this->scopeConfig->getValue( + self::XML_PATH_LIMIT_BY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** @@ -109,7 +127,11 @@ public function getLimitBy($store = null) */ public function getEmailTemplate($store = null) { - return $this->scopeConfig->getValue(self::XML_PATH_EMAIL_TEMPLATE, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + return $this->scopeConfig->getValue( + self::XML_PATH_EMAIL_TEMPLATE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); } /** diff --git a/app/code/Magento/SendFriend/Model/CaptchaValidator.php b/app/code/Magento/SendFriend/Model/CaptchaValidator.php new file mode 100644 index 0000000000000..20082e4880b4c --- /dev/null +++ b/app/code/Magento/SendFriend/Model/CaptchaValidator.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SendFriend\Model; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Class CaptchaValidator. Performs captcha validation + */ +class CaptchaValidator +{ + /** + * @var Data + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * CaptchaValidator constructor. + * + * @param Data $captchaHelper + * @param CaptchaStringResolver $captchaStringResolver + * @param UserContextInterface $currentUser + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Data $captchaHelper, + CaptchaStringResolver $captchaStringResolver, + UserContextInterface $currentUser, + CustomerRepositoryInterface $customerRepository + ) { + $this->captchaHelper = $captchaHelper; + $this->captchaStringResolver = $captchaStringResolver; + $this->currentUser = $currentUser; + $this->customerRepository = $customerRepository; + } + + /** + * Entry point for captcha validation + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function validateSending(RequestInterface $request) + { + $this->validateCaptcha($request); + } + + /** + * Validates captcha and triggers log attempt + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function validateCaptcha(RequestInterface $request) + { + $captchaTargetFormName = 'product_sendtofriend_form'; + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaTargetFormName); + + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $request, + $captchaTargetFormName + ); + + $isCorrectCaptcha = $captchaModel->isCorrect($word); + + if (!$isCorrectCaptcha) { + $this->logCaptchaAttempt($captchaModel); + throw new LocalizedException(__('Incorrect CAPTCHA')); + } + } + + $this->logCaptchaAttempt($captchaModel); + } + + /** + * Log captcha attempts + * + * @param DefaultModel $captchaModel + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function logCaptchaAttempt(DefaultModel $captchaModel) + { + $email = ''; + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $email = $this->customerRepository->getById($this->currentUser->getUserId())->getEmail(); + } + + $captchaModel->logAttempt($email); + } +} diff --git a/app/code/Magento/SendFriend/Model/SendFriend.php b/app/code/Magento/SendFriend/Model/SendFriend.php index c69d6342b4892..2502db9891241 100644 --- a/app/code/Magento/SendFriend/Model/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/SendFriend.php @@ -16,6 +16,7 @@ * @method \Magento\SendFriend\Model\SendFriend setTime(int $value) * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api @@ -162,6 +163,8 @@ protected function _construct() } /** + * Send email. + * * @return $this * @throws CoreException */ @@ -175,7 +178,7 @@ public function send() $this->inlineTranslation->suspend(); - $message = nl2br(htmlspecialchars($this->getSender()->getMessage())); + $message = nl2br($this->_escaper->escapeHtml($this->getSender()->getMessage())); $sender = [ 'name' => $this->_escaper->escapeHtml($this->getSender()->getName()), 'email' => $this->_escaper->escapeHtml($this->getSender()->getEmail()), @@ -236,7 +239,7 @@ public function validate() } $email = $this->getSender()->getEmail(); - if (empty($email) or !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { + if (empty($email) || !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { $errors[] = __('Invalid Sender Email'); } @@ -281,13 +284,13 @@ public function setRecipients($recipients) // validate array if (!is_array( $recipients - ) or !isset( + ) || !isset( $recipients['email'] - ) or !isset( + ) || !isset( $recipients['name'] - ) or !is_array( + ) || !is_array( $recipients['email'] - ) or !is_array( + ) || !is_array( $recipients['name'] ) ) { @@ -487,7 +490,7 @@ protected function _sentCountByCookies($increment = false) $oldTimes = explode(',', $oldTimes); foreach ($oldTimes as $oldTime) { $periodTime = $time - $this->_sendfriendData->getPeriod(); - if (is_numeric($oldTime) and $oldTime >= $periodTime) { + if (is_numeric($oldTime) && $oldTime >= $periodTime) { $newTimes[] = $oldTime; } } diff --git a/app/code/Magento/SendFriend/Test/Mftf/LICENSE.txt b/app/code/Magento/SendFriend/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/SendFriend/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 <insert your license name here>" 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/app/code/Magento/SendFriend/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/SendFriend/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/SendFriend/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 <insert your license name here>" 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/app/code/Magento/SendFriend/Test/Mftf/README.md b/app/code/Magento/SendFriend/Test/Mftf/README.md new file mode 100644 index 0000000000000..a6e18162013a3 --- /dev/null +++ b/app/code/Magento/SendFriend/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Send Friend Functional Tests + +The Functional Test Module for **Magento Send Friend** module. diff --git a/app/code/Magento/SendFriend/Test/Unit/Block/Plugin/Catalog/Product/ViewTest.php b/app/code/Magento/SendFriend/Test/Unit/Block/Plugin/Catalog/Product/ViewTest.php index 6dbab3a5573a8..2718e1fa44f6e 100644 --- a/app/code/Magento/SendFriend/Test/Unit/Block/Plugin/Catalog/Product/ViewTest.php +++ b/app/code/Magento/SendFriend/Test/Unit/Block/Plugin/Catalog/Product/ViewTest.php @@ -52,6 +52,9 @@ public function testAfterCanEmailToFriend($result, $callSendfriend) $this->assertTrue($this->view->afterCanEmailToFriend($this->productView, $result)); } + /** + * @return array + */ public function afterCanEmailToFriendDataSet() { return [ diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php index 9d48133c1d500..7657f1d3d61e3 100644 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php +++ b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php @@ -104,7 +104,7 @@ public function testExecute() /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) + ->setMethods(['isVisibleInCatalog', 'isVisibleInSiteVisibility']) ->getMockForAbstractClass(); $this->productRepositoryMock->expects($this->once()) @@ -116,6 +116,10 @@ public function testExecute() ->method('isVisibleInCatalog') ->willReturn(true); + $productMock->expects($this->once()) + ->method('isVisibleInSiteVisibility') + ->willReturn(true); + $this->registryMock->expects($this->once()) ->method('register') ->with('product', $productMock, false); @@ -193,7 +197,7 @@ public function testExecuteWithoutBlock() /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) + ->setMethods(['isVisibleInCatalog', 'isVisibleInSiteVisibility']) ->getMockForAbstractClass(); $this->productRepositoryMock->expects($this->once()) @@ -205,6 +209,10 @@ public function testExecuteWithoutBlock() ->method('isVisibleInCatalog') ->willReturn(true); + $productMock->expects($this->once()) + ->method('isVisibleInSiteVisibility') + ->willReturn(true); + $this->registryMock->expects($this->once()) ->method('register') ->with('product', $productMock, false); @@ -269,7 +277,7 @@ public function testExecuteWithNoticeAndNoData() /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) + ->setMethods(['isVisibleInCatalog', 'isVisibleInSiteVisibility']) ->getMockForAbstractClass(); $this->productRepositoryMock->expects($this->once()) @@ -281,6 +289,10 @@ public function testExecuteWithNoticeAndNoData() ->method('isVisibleInCatalog') ->willReturn(true); + $productMock->expects($this->once()) + ->method('isVisibleInSiteVisibility') + ->willReturn(true); + $this->registryMock->expects($this->once()) ->method('register') ->with('product', $productMock, false); @@ -391,7 +403,7 @@ public function testExecuteWithNonVisibleProduct() /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) + ->setMethods(['isVisibleInCatalog', 'isVisibleInSiteVisibility']) ->getMockForAbstractClass(); $this->productRepositoryMock->expects($this->once()) @@ -403,6 +415,10 @@ public function testExecuteWithNonVisibleProduct() ->method('isVisibleInCatalog') ->willReturn(false); + $productMock->expects($this->once()) + ->method('isVisibleInSiteVisibility') + ->willReturn(true); + /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php deleted file mode 100644 index c7881f366f520..0000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php +++ /dev/null @@ -1,906 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendmailTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Sendmail */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectMock; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlBuilderMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost', 'getPostValue', 'getParam']) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->categoryRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\CategoryRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class) - ->getMock(); - $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Sendmail::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'categoryRepository' => $this->categoryRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - 'redirect' => $this->redirectMock, - 'url' => $this->urlBuilderMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $productUrl = 'product_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - /** @var \Magento\Catalog\Api\Data\CategoryInterface|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ - $categoryMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\CategoryInterface::class) - ->getMockForAbstractClass(); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willReturn($categoryMock); - - $productMock->expects($this->once()) - ->method('setCategory') - ->with($categoryMock); - - $this->registryMock->expects($this->exactly(2)) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ['current_category', $categoryMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(true); - $this->sendFriendMock->expects($this->once()) - ->method('send') - ->willReturnSelf(); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('The link to a friend was sent.')) - ->willReturnSelf(); - - $productMock->expects($this->once()) - ->method('getProductUrl') - ->willReturn($productUrl); - - $this->redirectMock->expects($this->once()) - ->method('success') - ->with($productUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($productUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategory() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(['Some error']); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Some error')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategoryWithProblems() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn('Some error'); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('We found some problems with the data.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithLocalizedException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Localized Exception.'))); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Localized Exception.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $exception = new \Exception(__('Exception.')); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException($exception); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addException') - ->with($exception, __('Some emails were not sent.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutProduct() - { - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutData() - { - $productId = 11; - $formData = ''; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutFormKey() - { - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(false); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index 4b2f4c2aec7ee..2b32addf1bd90 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -2,14 +2,16 @@ "name": "magento/module-send-friend", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-customer": "101.0.*", - "magento/framework": "101.0.*" + "magento/framework": "101.0.*", + "magento/module-captcha": "100.2.*", + "magento/module-authorization": "100.2.*" }, "type": "magento2-module", - "version": "100.2.0", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SendFriend/etc/adminhtml/system.xml b/app/code/Magento/SendFriend/etc/adminhtml/system.xml index 785b7a8bb40c8..7889b58e125a9 100644 --- a/app/code/Magento/SendFriend/etc/adminhtml/system.xml +++ b/app/code/Magento/SendFriend/etc/adminhtml/system.xml @@ -13,8 +13,11 @@ <resource>Magento_Config::sendfriend</resource> <group id="email" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Email Templates</label> - <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enabled</label> + <comment> + <![CDATA[We strongly recommend to install a <a href="https://devdocs.magento.com/guides/v2.2/security/google-recaptcha.html" target="_blank">CAPTCHA solution</a> alongside enabling "Email to a Friend" to ensure abuse of this feature does not occur.]]> + </comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template" translate="label comment" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> diff --git a/app/code/Magento/SendFriend/etc/config.xml b/app/code/Magento/SendFriend/etc/config.xml index 9fa005dcd2fd4..6239a4da591e2 100644 --- a/app/code/Magento/SendFriend/etc/config.xml +++ b/app/code/Magento/SendFriend/etc/config.xml @@ -9,7 +9,7 @@ <default> <sendfriend> <email> - <enabled>1</enabled> + <enabled>0</enabled> <template>sendfriend_email_template</template> <allow_guest>0</allow_guest> <max_recipients>5</max_recipients> @@ -17,5 +17,21 @@ <check_by>0</check_by> </email> </sendfriend> + <captcha translate="label"> + <frontend> + <areas> + <product_sendtofriend_form> + <label>Send To Friend Form</label> + </product_sendtofriend_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <product_sendtofriend_form>1</product_sendtofriend_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/SendFriend/etc/module.xml b/app/code/Magento/SendFriend/etc/module.xml index fae2b90f710a3..c874c54cbc672 100644 --- a/app/code/Magento/SendFriend/etc/module.xml +++ b/app/code/Magento/SendFriend/etc/module.xml @@ -10,6 +10,7 @@ <module name="Magento_SendFriend" setup_version="2.0.0"> <sequence> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/SendFriend/i18n/en_US.csv b/app/code/Magento/SendFriend/i18n/en_US.csv index eee540c89a7b0..0f55c57fd7586 100644 --- a/app/code/Magento/SendFriend/i18n/en_US.csv +++ b/app/code/Magento/SendFriend/i18n/en_US.csv @@ -45,3 +45,4 @@ Enabled,Enabled "Max Recipients","Max Recipients" "Max Products Sent in 1 Hour","Max Products Sent in 1 Hour" "Limit Sending By","Limit Sending By" +"We strongly recommend to install a <a href=""https://devdocs.magento.com/guides/v2.2/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur.","We strongly recommend to install a <a href=""https://devdocs.magento.com/guides/v2.2/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur." diff --git a/app/code/Magento/SendFriend/view/frontend/email/product_share.html b/app/code/Magento/SendFriend/view/frontend/email/product_share.html index d2ed441494221..00a4d7b4d5ce5 100644 --- a/app/code/Magento/SendFriend/view/frontend/email/product_share.html +++ b/app/code/Magento/SendFriend/view/frontend/email/product_share.html @@ -10,10 +10,11 @@ "var email":"Recipient Email address", "var name":"Recipient name", "var message|raw":"Sender custom message", -"var sender_email":"Sender email", -"var sender_name":"Sender name", +"var sender_email":"Sender Email", +"var sender_name":"Sender Name", "var product_url":"URL for Product", -"var product_image":"URL for product small image (75 px)" +"var product_image":"URL for product small image (75 px)", +"var message":"Message" } @--> {{template config_path="design/email/header_template"}} diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 8065b7e236132..4d6f3d8c628b2 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" template="Magento_SendFriend::send.phtml"> + <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" cacheable="false" template="Magento_SendFriend::send.phtml"> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/SendFriend/view/frontend/requirejs-config.js b/app/code/Magento/SendFriend/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..f356cebe57946 --- /dev/null +++ b/app/code/Magento/SendFriend/view/frontend/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + 'Magento_SendFriend/back-event': 'Magento_SendFriend/js/back-event' + } + } +}; diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 7a972cd5f01c5..eb9318271c1d8 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Send to friend form */ @@ -17,13 +15,13 @@ <div class="secondary"> <button type="button" id="btn-remove<%- data._index_ %>" class="action remove" title="<?= $block->escapeHtmlAttr(__('Remove Recipent')) ?>"> - <span><?= $block->escapeJs($block->escapeHtml(__('Remove'))) ?></span> + <span><?= $block->escapeHtml(__('Remove')) ?></span> </button> </div> </div> <fieldset class="fieldset"> <div class="field name required"> - <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Name'))) ?></span></label> + <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> <input name="recipients[name][<%- data._index_ %>]" type="text" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" id="recipients-name<%- data._index_ %>" data-validate="{required:true}"/> @@ -31,10 +29,11 @@ </div> <div class="field email required"> - <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Email'))) ?></span></label> + <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" + data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"/> </div> </div> @@ -72,7 +71,8 @@ <label for="sender-email" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="sender[email]" value="<?= $block->escapeHtmlAttr($block->getEmail()) ?>" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="text" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="email" class="input-text" + data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"/> </div> </div> @@ -91,14 +91,14 @@ <legend class="legend"><span><?= $block->escapeHtml(__('Invitee')) ?></span></legend> <br /> <div id="recipients-options"></div> - <?php if ($block->getMaxRecipients()): ?> + <?php if ($block->getMaxRecipients()) : ?> <div id="max-recipient-message" style="display: none;" class="message notice limit" role="alert"> <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?></span> </div> <?php endif; ?> <div class="actions-toolbar"> <div class="secondary"> - <?php if (1 < $block->getMaxRecipients()): ?> + <?php if (1 < $block->getMaxRecipients()) : ?> <button type="button" id="add-recipient-button" class="action add"> <span><?= $block->escapeHtml(__('Add Invitee')) ?></span></button> <?php endif; ?> @@ -106,10 +106,11 @@ </div> <?= $block->getChildHtml('form_additional_info') ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" - class="action submit primary"<?php if (!$block->canSend()): ?> disabled="disabled"<?php endif ?>> + class="action submit primary"<?php if (!$block->canSend()) : ?> disabled="disabled"<?php endif ?>> <span><?= $block->escapeHtml(__('Send Email')) ?></span></button> </div> <div class="secondary"> @@ -121,7 +122,7 @@ <script type="text/x-magento-init"> { "a[role='back']": { - "Magento_SendFriend/back-event": {} + "Magento_SendFriend/js/back-event": {} } } </script> diff --git a/app/code/Magento/SendFriend/view/frontend/web/back-event.js b/app/code/Magento/SendFriend/view/frontend/web/js/back-event.js similarity index 100% rename from app/code/Magento/SendFriend/view/frontend/web/back-event.js rename to app/code/Magento/SendFriend/view/frontend/web/js/back-event.js diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php index b4ff445c63f4e..e5e419328eea4 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php @@ -74,6 +74,7 @@ public function getShipment() * Configuration for popup window for packaging * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getConfigDataJson() { @@ -86,7 +87,7 @@ public function getConfigDataJson() $itemsName = []; $itemsWeight = []; $itemsProductId = []; - + $itemsOrderItemId = []; if ($shipmentId) { $urlParams['shipment_id'] = $shipmentId; $createLabelUrl = $this->getUrl('adminhtml/order_shipment/createLabel', $urlParams); diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php index 9e340cc31ff17..1d3f6ad1ee5a3 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php @@ -10,7 +10,7 @@ class Grid extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'order/packaging/grid.phtml'; + protected $_template = 'Magento_Shipping::order/packaging/grid.phtml'; /** * Core registry diff --git a/app/code/Magento/Shipping/Block/Adminhtml/View.php b/app/code/Magento/Shipping/Block/Adminhtml/View.php index 04df7f6e35e24..711dd2f46c1c6 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/View.php @@ -58,7 +58,7 @@ protected function _construct() 'onclick', "deleteConfirm('" . __( 'Are you sure you want to send a Shipment email to customer?' - ) . "', '" . $this->getEmailUrl() . "')" + ) . "', '" . $this->getEmailUrl() . "', {data: {}})" ); } diff --git a/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php new file mode 100644 index 0000000000000..661068d42c35d --- /dev/null +++ b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Shipping\Block\DataProviders\Tracking; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Shipping\Model\Tracking\Result\Status; + +/** + * Extension point to provide ability to change tracking details titles + */ +class DeliveryDateTitle implements ArgumentInterface +{ + /** + * Return title if carrier is defined + * + * @param Status $trackingStatus + * @return \Magento\Framework\Phrase|string + */ + public function getTitle(Status $trackingStatus) + { + return $trackingStatus->getCarrier() ? __('Delivered on:') : ''; + } +} diff --git a/app/code/Magento/Shipping/Block/Order/Shipment.php b/app/code/Magento/Shipping/Block/Order/Shipment.php index 653fb357f0b1d..21e960985d6b6 100644 --- a/app/code/Magento/Shipping/Block/Order/Shipment.php +++ b/app/code/Magento/Shipping/Block/Order/Shipment.php @@ -18,7 +18,7 @@ class Shipment extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/shipment.phtml'; + protected $_template = 'Magento_Shipping::order/shipment.phtml'; /** * Core registry diff --git a/app/code/Magento/Shipping/Block/Tracking/Popup.php b/app/code/Magento/Shipping/Block/Tracking/Popup.php index 3e1be0ce37ef3..1eb679bd8cc76 100644 --- a/app/code/Magento/Shipping/Block/Tracking/Popup.php +++ b/app/code/Magento/Shipping/Block/Tracking/Popup.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Block\Tracking; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; /** + * Tracking popup + * * @api * @since 100.0.2 */ @@ -107,13 +107,15 @@ public function formatDeliveryTime($time, $date = null) */ public function getContactUsEnabled() { - return (bool)$this->_scopeConfig->getValue( - 'contacts/contacts/enabled', + return $this->_scopeConfig->isSetFlag( + 'contact/contact/enabled', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } /** + * Get support email + * * @return string */ public function getStoreSupportEmail() @@ -125,6 +127,8 @@ public function getStoreSupportEmail() } /** + * Get contact us url + * * @return string */ public function getContactUs() diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Email.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Email.php index 1f5d6e2cb7a89..f08204fdff0d7 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Email.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Email.php @@ -57,7 +57,9 @@ public function execute() $this->_objectManager->create(\Magento\Shipping\Model\ShipmentNotifier::class) ->notify($shipment); $shipment->save(); - $this->messageManager->addSuccess(__('You sent the shipment.')); + $this->messageManager->addSuccess( + __('An email confirming the order is underway has been sent to the customer.') + ); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($e->getMessage()); diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index 5eca94bce562b..5a81bdb3cf32f 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -6,7 +6,6 @@ */ namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** @@ -97,7 +96,7 @@ public function execute() $formKeyIsValid = $this->_formKeyValidator->validate($this->getRequest()); $isPost = $this->getRequest()->isPost(); if (!$formKeyIsValid || !$isPost) { - $this->messageManager->addError(__('We can\'t save the shipment right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the shipment right now.')); return $resultRedirect->setPath('sales/order/index'); } @@ -108,7 +107,6 @@ public function execute() } $isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label']; - $responseAjax = new \Magento\Framework\DataObject(); try { @@ -136,7 +134,7 @@ public function execute() ->validate($shipment, [QuantityValidator::class]); if ($validationResult->hasMessages()) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); @@ -160,7 +158,7 @@ public function execute() $shipmentCreatedMessage = __('The shipment has been created.'); $labelCreatedMessage = __('You created the shipping label.'); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( $isNeedCreateLabel ? $shipmentCreatedMessage . ' ' . $labelCreatedMessage : $shipmentCreatedMessage ); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); @@ -169,7 +167,7 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage($e->getMessage()); } else { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } catch (\Exception $e) { @@ -178,7 +176,7 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { - $this->messageManager->addError(__('Cannot save shipment.')); + $this->messageManager->addErrorMessage(__('Cannot save shipment.')); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } diff --git a/app/code/Magento/Shipping/Helper/Data.php b/app/code/Magento/Shipping/Helper/Data.php index 78e23cb4aeac2..549e19576c492 100644 --- a/app/code/Magento/Shipping/Helper/Data.php +++ b/app/code/Magento/Shipping/Helper/Data.php @@ -4,13 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** * Shipping data helper */ namespace Magento\Shipping\Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\UrlInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Store\Model\StoreManagerInterface; + class Data extends \Magento\Framework\App\Helper\AbstractHelper { /** @@ -21,19 +26,28 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper protected $_allowedHashKeys = ['ship_id', 'order_id', 'track_id']; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; + /** + * @var UrlInterface|null + */ + private $url; + /** * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param StoreManagerInterface $storeManager + * @param UrlInterface|null $url */ public function __construct( \Magento\Framework\App\Helper\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + UrlInterface $url = null ) { $this->_storeManager = $storeManager; + $this->url = $url ?: ObjectManager::getInstance()->get(UrlInterface::class); + parent::__construct($context); } @@ -56,7 +70,7 @@ public function decodeTrackingHash($hash) * Retrieve tracking url with params * * @param string $key - * @param \Magento\Sales\Model\Order|\Magento\Sales\Model\Order\Shipment|\Magento\Sales\Model\Order\Shipment\Track $model + * @param Order|Shipment|Track $model * @param string $method Optional - method of a model to get id * @return string */ @@ -64,12 +78,13 @@ protected function _getTrackingUrl($key, $model, $method = 'getId') { $urlPart = "{$key}:{$model->{$method}()}:{$model->getProtectCode()}"; $params = [ + '_scope' => $model->getStoreId(), + '_nosid' => true, '_direct' => 'shipping/tracking/popup', '_query' => ['hash' => $this->urlEncoder->encode($urlPart)] ]; - $storeModel = $this->_storeManager->getStore($model->getStoreId()); - return $storeModel->getUrl('', $params); + return $this->url->getUrl('', $params); } /** @@ -80,11 +95,11 @@ protected function _getTrackingUrl($key, $model, $method = 'getId') */ public function getTrackingPopupUrlBySalesModel($model) { - if ($model instanceof \Magento\Sales\Model\Order) { + if ($model instanceof Order) { return $this->_getTrackingUrl('order_id', $model); - } elseif ($model instanceof \Magento\Sales\Model\Order\Shipment) { + } elseif ($model instanceof Shipment) { return $this->_getTrackingUrl('ship_id', $model); - } elseif ($model instanceof \Magento\Sales\Model\Order\Shipment\Track) { + } elseif ($model instanceof Track) { return $this->_getTrackingUrl('track_id', $model, 'getEntityId'); } return ''; diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 7d8be395259aa..e6740593f5331 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Model\Carrier; use Magento\Quote\Model\Quote\Address\RateResult\Error; @@ -314,7 +312,8 @@ public function checkAvailableShipCountries(\Magento\Framework\DataObject $reque return $error; } else { /* - * The admin set not to show the shipping module if the delivery country is not within specific countries + * The admin set not to show the shipping module + * if the delivery country is not within specific countries */ return false; } @@ -330,11 +329,24 @@ public function checkAvailableShipCountries(\Magento\Framework\DataObject $reque * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + public function processAdditionalValidation(\Magento\Framework\DataObject $request) { return $this; } + /** + * Processing additional validation to check is carrier applicable. + * + * @param \Magento\Framework\DataObject $request + * @return $this|bool|\Magento\Framework\DataObject + * @deprecated + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + { + return $this->processAdditionalValidation($request); + } + /** * Determine whether current carrier enabled for activity * @@ -395,6 +407,9 @@ public function getSortOrder() */ protected function _updateFreeMethodQuote($request) { + if (!$request->getFreeShipping()) { + return; + } if ($request->getFreeMethodWeight() == $request->getPackageWeight() || !$request->hasFreeMethodWeight()) { return; } @@ -436,12 +451,18 @@ protected function _updateFreeMethodQuote($request) } } } + } else { + /** + * if we can apply free shipping for all order we should force price + * to $0.00 for shipping with out sending second request to carrier + */ + $price = 0; } /** * if we did not get our free shipping method in response we must use its old price */ - if (!is_null($price)) { + if ($price !== null) { $this->_result->getRateById($freeRateId)->setPrice($price); } } @@ -454,7 +475,7 @@ protected function _updateFreeMethodQuote($request) */ public function getFinalPriceWithHandlingFee($cost) { - $handlingFee = $this->getConfigData('handling_fee'); + $handlingFee = (float)$this->getConfigData('handling_fee'); $handlingType = $this->getConfigData('handling_type'); if (!$handlingType) { $handlingType = self::HANDLING_TYPE_FIXED; diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index d42da53263ca4..019baeef062c5 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -28,6 +28,14 @@ abstract class AbstractCarrierOnline extends AbstractCarrier const GUAM_REGION_CODE = 'GU'; + const SPAIN_COUNTRY_ID = 'ES'; + + const CANARY_ISLANDS_COUNTRY_ID = 'IC'; + + const SANTA_CRUZ_DE_TENERIFE_REGION_ID = 'Santa Cruz de Tenerife'; + + const LAS_PALMAS_REGION_ID = 'Las Palmas'; + /** * Array of quotes * @@ -301,10 +309,24 @@ public function getAllItems(RateRequest $request) * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject + * @deprecated * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + { + return $this->processAdditionalValidation($request); + } + + /** + * Processing additional validation to check if carrier applicable. + * + * @param \Magento\Framework\DataObject $request + * @return $this|bool|\Magento\Framework\DataObject + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processAdditionalValidation(\Magento\Framework\DataObject $request) { //Skip by item validation if there is no items in request if (!count($this->getAllItems($request))) { diff --git a/app/code/Magento/Shipping/Model/Config.php b/app/code/Magento/Shipping/Model/Config.php index 590a71f01a495..f2754fedc79f4 100644 --- a/app/code/Magento/Shipping/Model/Config.php +++ b/app/code/Magento/Shipping/Model/Config.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Model; /** @@ -66,7 +64,11 @@ public function getActiveCarriers($store = null) $carriers = []; $config = $this->_scopeConfig->getValue('carriers', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); foreach (array_keys($config) as $carrierCode) { - if ($this->_scopeConfig->isSetFlag('carriers/' . $carrierCode . '/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store)) { + if ($this->_scopeConfig->isSetFlag( + 'carriers/' . $carrierCode . '/active', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + )) { $carrierModel = $this->_carrierFactory->create($carrierCode, $store); if ($carrierModel) { $carriers[$carrierCode] = $carrierModel; diff --git a/app/code/Magento/Shipping/Model/Info.php b/app/code/Magento/Shipping/Model/Info.php index ec03d06c5b2d1..ed4c1c3f6d127 100644 --- a/app/code/Magento/Shipping/Model/Info.php +++ b/app/code/Magento/Shipping/Model/Info.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Model; use Magento\Sales\Model\Order\Shipment; @@ -116,7 +114,7 @@ protected function _initOrder() /** @var \Magento\Sales\Model\Order $order */ $order = $this->_orderFactory->create()->load($this->getOrderId()); - if (!$order->getId() || $this->getProtectCode() != $order->getProtectCode()) { + if (!$order->getId() || $this->getProtectCode() !== $order->getProtectCode()) { return false; } @@ -132,7 +130,7 @@ protected function _initShipment() { /* @var $model Shipment */ $ship = $this->shipmentRepository->get($this->getShipId()); - if (!$ship->getEntityId() || $this->getProtectCode() != $ship->getProtectCode()) { + if (!$ship->getEntityId() || $this->getProtectCode() !== $ship->getProtectCode()) { return false; } @@ -197,7 +195,7 @@ public function getTrackingInfoByTrackId() { /** @var \Magento\Shipping\Model\Order\Track $track */ $track = $this->_trackFactory->create()->load($this->getTrackId()); - if ($track->getId() && $this->getProtectCode() == $track->getProtectCode()) { + if ($track->getId() && $this->getProtectCode() === $track->getProtectCode()) { $this->_trackingInfo = [[$track->getNumberDetail()]]; } return $this->_trackingInfo; diff --git a/app/code/Magento/Shipping/Model/Shipping.php b/app/code/Magento/Shipping/Model/Shipping.php index 2223cb8ae3bf2..329fca61a8b28 100644 --- a/app/code/Magento/Shipping/Model/Shipping.php +++ b/app/code/Magento/Shipping/Model/Shipping.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Model; use Magento\Framework\App\ObjectManager; @@ -241,7 +242,7 @@ public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $req } /** - * Collect rates of given carrier + * Prepare carrier to find rates. * * @param string $carrierCode * @param \Magento\Quote\Model\Quote\Address\RateRequest $request @@ -251,15 +252,16 @@ public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $req */ public function collectCarrierRates($carrierCode, $request) { - /* @var $carrier \Magento\Shipping\Model\Carrier\AbstractCarrier */ - $carrier = $this->_carrierFactory->createIfActive($carrierCode, $request->getStoreId()); + $carrier = $this->isShippingCarrierAvailable($carrierCode) + ? $this->_carrierFactory->create($carrierCode, $request->getStoreId()) + : null; if (!$carrier) { return $this; } $carrier->setActiveFlag($this->_availabilityConfigField); $result = $carrier->checkAvailableShipCountries($request); if (false !== $result && !$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Error) { - $result = $carrier->proccessAdditionalValidation($request); + $result = $carrier->processAdditionalValidation($request); } /* * Result will be false if the admin set not to show the shipping module @@ -322,6 +324,7 @@ public function collectCarrierRates($carrierCode, $request) /** * Compose Packages For Carrier. + * * Divides order into items and items into parts if it's necessary * * @param \Magento\Shipping\Model\Carrier\AbstractCarrier $carrier @@ -334,6 +337,7 @@ public function composePackagesForCarrier($carrier, $request) { $allItems = $request->getAllItems(); $fullItems = []; + $weightItems = []; $maxWeight = (double)$carrier->getConfigData('max_package_weight'); @@ -404,22 +408,21 @@ public function composePackagesForCarrier($carrier, $request) if (!empty($decimalItems)) { foreach ($decimalItems as $decimalItem) { - $fullItems = array_merge( - $fullItems, - array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']) - ); + $weightItems[] = array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight']); } } else { - $fullItems = array_merge($fullItems, array_fill(0, $qty, $itemWeight)); + $weightItems[] = array_fill(0, $qty, $itemWeight); } } + $fullItems = array_merge($fullItems, ...$weightItems); sort($fullItems); return $this->_makePieces($fullItems, $maxWeight); } /** - * Make pieces + * Make pieces. + * * Compose packages list based on given items, so that each package is as heavy as possible * * @param array $items @@ -510,4 +513,18 @@ public function setCarrierAvailabilityConfigField($code = 'active') $this->_availabilityConfigField = $code; return $this; } + + /** + * Checks availability of carrier. + * + * @param string $carrierCode + * @return bool + */ + private function isShippingCarrierAvailable(string $carrierCode): bool + { + return $this->_scopeConfig->isSetFlag( + 'carriers/' . $carrierCode . '/' . $this->_availabilityConfigField, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Shipping/Model/Shipping/Labels.php b/app/code/Magento/Shipping/Model/Shipping/Labels.php index b1709b0a16998..0c3b19888304c 100644 --- a/app/code/Magento/Shipping/Model/Shipping/Labels.php +++ b/app/code/Magento/Shipping/Model/Shipping/Labels.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Model\Shipping; use Magento\Framework\DataObject; @@ -117,8 +115,8 @@ public function requestToShipment(Shipment $orderShipment) ) ); - if (!$admin->getFirstname() - || !$admin->getLastname() + if (!$admin->getFirstName() + || !$admin->getLastName() || !$storeInfo->getName() || !$storeInfo->getPhone() || !$originStreet1 @@ -140,7 +138,8 @@ public function requestToShipment(Shipment $orderShipment) ) { throw new LocalizedException( __( - 'We don\'t have enough information to create shipping labels. Please make sure your store information and settings are complete.' + 'We don\'t have enough information to create shipping labels. " + . "Please make sure your store information and settings are complete.' ) ); } @@ -187,8 +186,8 @@ protected function setShipperDetails( ); $request->setShipperContactPersonName($storeAdmin->getName()); - $request->setShipperContactPersonFirstName($storeAdmin->getFirstname()); - $request->setShipperContactPersonLastName($storeAdmin->getLastname()); + $request->setShipperContactPersonFirstName($storeAdmin->getFirstName()); + $request->setShipperContactPersonLastName($storeAdmin->getLastName()); $request->setShipperContactCompanyName($store->getName()); $request->setShipperContactPhoneNumber($store->getPhone()); $request->setShipperEmail($storeAdmin->getEmail()); diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeFlatRateShippingMethodStatusActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeFlatRateShippingMethodStatusActionGroup.xml new file mode 100644 index 0000000000000..977ee065a8fb1 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeFlatRateShippingMethodStatusActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeFlatRateShippingMethodStatusActionGroup"> + <arguments> + <argument name="status" type="string" defaultValue="1"/> + </arguments> + <conditionalClick selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTab}}" dependentSelector="{{AdminShippingMethodFlatRateSection.carriersFlatRateActive}}" visible="false" stepKey="expandTab"/> + <selectOption selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateActive}}" userInput="{{status}}" stepKey="changeFlatRateMethodStatus"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfigs"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml new file mode 100644 index 0000000000000..e506ca3a7662f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Enable/Disable Table Rates shipping method --> + <actionGroup name="AdminChangeTableRatesShippingMethodStatusActionGroup"> + <arguments> + <argument name="status" type="string" defaultValue="1"/> + </arguments> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTab"/> + <uncheckOption selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" userInput="{{status}}" stepKey="changeTableRatesMethodStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminOpenShippingMethodsConfigPageActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminOpenShippingMethodsConfigPageActionGroup.xml new file mode 100644 index 0000000000000..a1fefcf13afa4 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminOpenShippingMethodsConfigPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenShippingMethodsConfigPageActionGroup"> + <amOnPage url="{{AdminShippingMethodsConfigPage.url}}" stepKey="navigateToAdminShippingMethodsPage"/> + <waitForPageLoad stepKey="waitForAdminShippingMethodsPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml new file mode 100644 index 0000000000000..1d90867b110dd --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyBasicShipmentInformation"> + <arguments> + <argument name="customer"/> + <argument name="shippingAddress"/> + <argument name="billingAddress"/> + <argument name="customerGroup" defaultValue="GeneralCustomerGroup"/> + </arguments> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> + <see selector="{{AdminShipmentOrderAndAccountInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.street[0]}}" stepKey="seeBillingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.city}}" stepKey="seeBillingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.country}}" stepKey="seeBillingAddressCountry"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{billingAddress.postcode}}" stepKey="seeBillingAddressPostcode"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.street[0]}}" stepKey="seeShippingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.city}}" stepKey="seeShippingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.country}}" stepKey="seeShippingAddressCountry"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.postcode}}" stepKey="seeShippingAddressPostcode"/> + </actionGroup> + + <actionGroup name="SeeProductInShipmentItems"> + <arguments> + <argument name="product"/> + </arguments> + <seeElement selector="{{AdminShipmentItemsSection.productColumn(product.name)}}" stepKey="seeProductInShipmentItemsGrid"/> + </actionGroup> + + <actionGroup name="StartCreateShipmentFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeNewShipmentUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Shipment" stepKey="seeNewShipmentPageTitle"/> + </actionGroup> + + <actionGroup name="SubmitShipment"> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageShipping"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontNoQuotesMessageActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontNoQuotesMessageActionGroup.xml new file mode 100644 index 0000000000000..060e8a4f2e21e --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontNoQuotesMessageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreFrontNoQuotesMessageActionGroup"> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.noQuotesMsg}}" stepKey="waitForNoQuotesMsgVisible"/> + <see selector="{{CheckoutShippingMethodsSection.noQuotesMsg}}" userInput="Sorry, no quotes are available for this order at this time" stepKey="assertNoQuotesMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodAvailableActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodAvailableActionGroup.xml new file mode 100644 index 0000000000000..9892bc7cbc9ea --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodAvailableActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreFrontShippingMethodAvailableActionGroup"> + <arguments> + <argument name="shippingMethodName" type="string" defaultValue="Flat Rate"/> + </arguments> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName(shippingMethodName)}}" stepKey="waitForShippingMethodLoad"/> + <seeElement selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName(shippingMethodName)}}" stepKey="seeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodUnavailableActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodUnavailableActionGroup.xml new file mode 100644 index 0000000000000..d425a6d93a1df --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AssertStoreFrontShippingMethodUnavailableActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreFrontShippingMethodUnavailableActionGroup"> + <arguments> + <argument name="shippingMethodName" type="string" defaultValue="Free Shipping"/> + </arguments> + <waitForElementNotVisible selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName(shippingMethodName)}}" stepKey="waitForShippingMethodNotVisible"/> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName(shippingMethodName)}}" stepKey="dontSeeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml new file mode 100644 index 0000000000000..c0bd1bf8d0451 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + + <!-- Enable Flat Rate Shipping method config --> + <entity name="FlatRateShippingMethodConfig" type="flat_rate_shipping_method"> + <requiredEntity type="active">flatRateActiveEnable</requiredEntity> + </entity> + + <entity name="flatRateActiveEnable" type="active"> + <data key="value">1</data> + </entity> + <!-- Disable Flat Rate Shipping method config --> + <entity name="DisableFlatRateShippingMethodConfig" type="flat_rate_shipping_method"> + <requiredEntity type="active">flatRateActiveDisable</requiredEntity> + </entity> + <entity name="flatRateActiveDisable" type="active"> + <data key="value">0</data> + </entity> + <!-- Flat Rate Shipping method default setup --> + <entity name="FlatRateShippingMethodDefault" type="flat_rate_shipping_method"> + <requiredEntity type="active">flatRateActiveDefault</requiredEntity> + <requiredEntity type="title">flatRateTitleDefault</requiredEntity> + <requiredEntity type="name">flatRateNameDefault</requiredEntity> + <requiredEntity type="type">flatRateTypeDefault</requiredEntity> + <requiredEntity type="price">flatRatePriceDefault</requiredEntity> + <requiredEntity type="handling_type">flatRateHandlingTypeDefault</requiredEntity> + <requiredEntity type="handling_fee">flatRateHandlingFeeDefault</requiredEntity> + <requiredEntity type="specificerrmsg">flatRateSpecificerrmsgDefault</requiredEntity> + <requiredEntity type="sallowspecific">flatRateSallowspecificDefault</requiredEntity> + <requiredEntity type="specificcountry">flatRateSpecificcountryDefault</requiredEntity> + <requiredEntity type="showmethod">flatRateShowmethodDefault</requiredEntity> + <requiredEntity type="sort_order">flatRateSortOrderDefault</requiredEntity> + </entity> + + <entity name="flatRateActiveDefault" type="active"> + <data key="value">1</data> + </entity> + <entity name="flatRateTitleDefault" type="title"> + <data key="value">Flat Rate</data> + </entity> + <entity name="flatRateNameDefault" type="name"> + <data key="value">Fixed</data> + </entity> + <entity name="flatRateTypeDefault" type="type"> + <data key="value">I</data> + </entity> + <entity name="flatRatePriceDefault" type="price"> + <data key="value">5.00</data> + </entity> + <entity name="flatRateHandlingFeeDefault" type="handling_fee"> + <data key="value">F</data> + </entity> + <entity name="flatRateSpecificerrmsgDefault" type="specificerrmsg"> + <data key="value">This shipping method is not available. To use this shipping method, please contact us.</data> + </entity> + <entity name="flatRateSallowspecificDefault" type="sallowspecific"> + <data key="value">0</data> + </entity> + <entity name="flatRateSpecificcountryDefault" type="specificcountry"> + <data key="value" /> + </entity> + <entity name="flatRateShowmethodDefault" type="showmethod"> + <data key="value">0</data> + </entity> + <entity name="flatRateSortOrderDefault" type="sort_order"> + <data key="value" /> + </entity> +</entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml new file mode 100644 index 0000000000000..a6f01cd500322 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + + <!-- Enable Free Shipping method --> + <entity name="FreeShippinMethodConfig" type="free_shipping_method"> + <requiredEntity type="active">freeActiveEnable</requiredEntity> + </entity> + + <entity name="freeActiveEnable" type="active"> + <data key="value">1</data> + </entity> + + <!-- Free Shipping method default setup --> + <entity name="FreeShippinMethodDefault" type="free_shipping_method"> + <requiredEntity type="active">freeActiveDefault</requiredEntity> + <requiredEntity type="title">freeTitleDefault</requiredEntity> + <requiredEntity type="name">freeNameDefault</requiredEntity> + <requiredEntity type="free_shipping_subtotal">freeShippingSubtotalDefault</requiredEntity> + <requiredEntity type="specificerrmsg">freeSpecificerrmsgDefault</requiredEntity> + <requiredEntity type="sallowspecific">freeSallowspecificDefault</requiredEntity> + <requiredEntity type="specificcountry">freeSpecificcountryDefault</requiredEntity> + <requiredEntity type="showmethod">freeShowmethodDefault</requiredEntity> + <requiredEntity type="sort_order">freeSortOrderDefault</requiredEntity> + </entity> + + <entity name="freeActiveDefault" type="active"> + <data key="value">0</data> + </entity> + <entity name="freeTitleDefault" type="title"> + <data key="value">Free Shipping</data> + </entity> + <entity name="freeNameDefault" type="name"> + <data key="value">Free</data> + </entity> + <entity name="freeShippingSubtotalDefault" type="free_shipping_subtotal"> + <data key="value" /> + </entity> + <entity name="freeSpecificerrmsgDefault" type="specificerrmsg"> + <data key="value">This shipping method is not available. To use this shipping method, please contact us.</data> + </entity> + <entity name="freeSallowspecificDefault" type="sallowspecific"> + <data key="value">0</data> + </entity> + <entity name="freeSpecificcountryDefault" type="specificcountry"> + <data key="value" /> + </entity> + <entity name="freeShowmethodDefault" type="showmethod"> + <data key="value">0</data> + </entity> + <entity name="freeSortOrderDefault" type="sort_order"> + <data key="value" /> + </entity> + + <!--Set Free Shipping Subtotal to 101--> + <entity name="setFreeShippingSubtotal" type="free_shipping_method"> + <requiredEntity type="free_shipping_subtotal">freeShippingSubtotal</requiredEntity> + </entity> + <entity name="freeShippingSubtotal" type="free_shipping_subtotal"> + <data key="value">101</data> + </entity> + <!--Set to default Free Shipping Subtotal--> + <entity name="setFreeShippingSubtotalToDefault" type="free_shipping_method"> + <requiredEntity type="free_shipping_subtotal">freeShippingSubtotalDefault</requiredEntity> + </entity> + <entity name="freeShippingSubtotalDefault" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml new file mode 100644 index 0000000000000..47ef68cc9d765 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- Prices from file "table_rate_30895.csv" --> + <entity name="TableRatesWeightVSDestination" type="shipping_method"> + <data key="condition">Weight vs. Destination</data> + <data key="priceCA">5.00</data> + <data key="price">10.00</data> + <data key="title">Best Way</data> + <data key="methodName">Table Rate</data> + </entity> +</entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/LICENSE.txt b/app/code/Magento/Shipping/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Shipping/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 <insert your license name here>" 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/app/code/Magento/Shipping/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Shipping/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Shipping/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 <insert your license name here>" 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/app/code/Magento/Shipping/Test/Mftf/Metadata/shipping_methods-meta.xml b/app/code/Magento/Shipping/Test/Mftf/Metadata/shipping_methods-meta.xml new file mode 100644 index 0000000000000..647b7a6519c13 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Metadata/shipping_methods-meta.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="FlatRateShippingMethodSetup" dataType="flat_rate_shipping_method" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="flat_rate_shipping_method"> + <object key="flatrate" dataType="flat_rate_shipping_method"> + <object key="fields" dataType="flat_rate_shipping_method"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + <object key="title" dataType="title"> + <field key="value">string</field> + </object> + <object key="name" dataType="name"> + <field key="value">string</field> + </object> + <object key="type" dataType="type"> + <field key="value">string</field> + </object> + <object key="price" dataType="price"> + <field key="value">string</field> + </object> + <object key="handling_type" dataType="handling_type"> + <field key="value">string</field> + </object> + <object key="handling_fee" dataType="handling_fee"> + <field key="value">string</field> + </object> + <object key="specificerrmsg" dataType="specificerrmsg"> + <field key="value">string</field> + </object> + <object key="sallowspecific" dataType="sallowspecific"> + <field key="value">string</field> + </object> + <object key="specificcountry" dataType="specificcountry"> + <field key="value">string</field> + </object> + <object key="showmethod" dataType="showmethod"> + <field key="value">string</field> + </object> + <object key="sort_order" dataType="sort_order"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + <operation name="FreeShippingMethodSetup" dataType="free_shipping_method" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="free_shipping_method"> + <object key="freeshipping" dataType="free_shipping_method"> + <object key="fields" dataType="free_shipping_method"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + <object key="title" dataType="title"> + <field key="value">string</field> + </object> + <object key="name" dataType="name"> + <field key="value">string</field> + </object> + <object key="free_shipping_subtotal" dataType="free_shipping_subtotal"> + <field key="value">string</field> + </object> + <object key="specificerrmsg" dataType="specificerrmsg"> + <field key="value">string</field> + </object> + <object key="sallowspecific" dataType="sallowspecific"> + <field key="value">string</field> + </object> + <object key="specificcountry" dataType="specificcountry"> + <field key="value">string</field> + </object> + <object key="showmethod" dataType="showmethod"> + <field key="value">string</field> + </object> + <object key="sort_order" dataType="sort_order"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml new file mode 100644 index 0000000000000..bd311d3390043 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Page/AdminShipmentNewPage.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminShipmentNewPage" url="order_shipment/new/order_id/" area="admin" module="Magento_Shipping"> + <section name="AdminShipmentMainActionsSection"/> + <section name="AdminShipmentOrderAndAccountInformationSection"/> + <section name="AdminShipmentAddressInformationSection"/> + <section name="AdminShipmentItemsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Shipping/Test/Mftf/README.md b/app/code/Magento/Shipping/Test/Mftf/README.md new file mode 100644 index 0000000000000..6acc747f50d71 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Shipping Functional Tests + +The Functional Test Module for **Magento Shipping** module. diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml new file mode 100644 index 0000000000000..39035868c4b65 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentAddressInformationSection"> + <element name="billingAddress" type="text" selector=".order-billing-address address"/> + <element name="billingAddressEdit" type="button" selector=".order-billing-address .actions a"/> + <element name="shippingAddress" type="text" selector=".order-shipping-address address"/> + <element name="shippingAddressEdit" type="button" selector=".order-shipping-address .actions a"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml new file mode 100644 index 0000000000000..55d26d729090c --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentItemsSection"> + <element name="itemQty" type="text" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-ordered-qty .qty-table" parameterized="true"/> + <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-qty input.qty-item" parameterized="true"/> + <element name="productColumn" type="text" selector="//*[contains(@class,'order-shipment-table')]//td[@class = 'col-product']//div[contains(text(),'{{productName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml new file mode 100644 index 0000000000000..a21c3229e3549 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentMainActionsSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentMainActionsSection"> + <element name="submitShipment" type="button" selector="button.action-default.save.submit-button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml new file mode 100644 index 0000000000000..2a83995cf3bbc --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentOrderAndAccountInformationSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentOrderAndAccountInformationSection"> + <element name="customerName" type="text" selector=".order-account-information table tr:first-of-type > td span"/> + <element name="customerEmail" type="text" selector=".order-account-information table tr:nth-of-type(2) > td a"/> + <element name="customerGroup" type="text" selector=".order-account-information table tr:nth-of-type(3) > td"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml new file mode 100644 index 0000000000000..a7ed0ab498bea --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodFlatRateSection"> + <element name="carriersFlatRateTab" type="button" selector="#carriers_flatrate-head"/> + <element name="carriersFlatRateActive" type="select" selector="#carriers_flatrate_active"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml new file mode 100644 index 0000000000000..3c570201c9970 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodTableRatesSection"> + <element name="carriersTableRateTab" type="button" selector="#carriers_tablerate-head"/> + <element name="enabledUseSystemValue" type="checkbox" selector="#carriers_tablerate_active_inherit"/> + <element name="carriersTableRateActive" type="select" selector="#carriers_tablerate_active"/> + <element name="condition" type="select" selector="#carriers_tablerate_condition_name"/> + <element name="importFile" type="input" selector="#carriers_tablerate_import"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml new file mode 100644 index 0000000000000..22b10a16da3cc --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="TableRatesShippingMethodForDifferentStatesTest"> + <annotations> + <features value="Shipping"/> + <stories value="Table Rates"/> + <title value="Table rates shipping method for different states test"/> + <description value="Checkout with Table Rates for different states of the USA"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-7438"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Create product --> + <createData entity="SimpleProduct3" stepKey="createProduct"/> + + <!-- Create customer --> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Rollback config --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodSystemConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteByNameActionGroup" stepKey="AdminSwitchStoreViewToMainWebsite"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="disableTableRatesShippingMethod"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveSystemConfig"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteByNameActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + + <!-- Make sure you have Condition Weight vs. Destination --> + <see selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRatesWeightVSDestination.condition}}" stepKey="seeDefaultCondition"/> + + <!-- Import file and save config --> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="table_rate_30895.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add product to the shopping cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Open the shopping cart page --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> + + <!-- Expand Estimate Shipping and Tax section in Summary --> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTax"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!-- See available Table Rate option --> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodLabel"> + <argument name="shippingMethod" value="{{TableRatesWeightVSDestination.title}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertShippingMethodOptionPresentInCartActionGroup" stepKey="assertShippingMethodOption"> + <argument name="methodName" value="{{TableRatesWeightVSDestination.methodName}}"/> + <argument name="price" value="{{TableRatesWeightVSDestination.priceCA}}"/> + </actionGroup> + + <!-- Change State to New York --> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_NY.state}}" stepKey="selectAnotherState"/> + <waitForPageLoad stepKey="waitForShippingMethodLoad"/> + + <!-- See available Table Rate option for another state --> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodLabelForAnotherState"> + <argument name="shippingMethod" value="{{TableRatesWeightVSDestination.title}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertShippingMethodOptionPresentInCartActionGroup" stepKey="assertShippingMethodOptionForAnotherState"> + <argument name="methodName" value="{{TableRatesWeightVSDestination.methodName}}"/> + <argument name="price" value="{{TableRatesWeightVSDestination.price}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php index 2bffe74baa9b0..43cdd833bfd6d 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + /** * Class AddTrackTest * @@ -72,7 +71,8 @@ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->shipmentLoader = $this->getMockBuilder( - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class) + \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class + ) ->disableOriginalConstructor() ->setMethods(['setShipmentId', 'setOrderId', 'setShipment', 'setTracking', 'load']) ->getMock(); @@ -84,10 +84,16 @@ protected function setUp() 'getTitle', 'getView' ]); - $this->response = $this->createPartialMock(\Magento\Framework\App\ResponseInterface::class, ['setRedirect', 'sendResponse', 'setBody']); + $this->response = $this->createPartialMock( + \Magento\Framework\App\ResponseInterface::class, + ['setRedirect', 'sendResponse', 'setBody'] + ); $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['create', 'get']); + $this->objectManager = $this->createPartialMock( + \Magento\Framework\ObjectManager\ObjectManager::class, + ['create', 'get'] + ); $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->disableOriginalConstructor() @@ -134,7 +140,10 @@ public function testExecute() $orderId = 10003; $tracking = []; $shipmentData = ['items' => [], 'send_email' => '']; - $shipment = $this->createPartialMock(\Magento\Sales\Model\Order\Shipment::class, ['addTrack', '__wakeup', 'save']); + $shipment = $this->createPartialMock( + \Magento\Sales\Model\Order\Shipment::class, + ['addTrack', '__wakeup', 'save'] + ); $this->request->expects($this->any()) ->method('getParam') ->will( diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/EmailTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/EmailTest.php index 7e4e27efe5ba7..3507b7fe2e274 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/EmailTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/EmailTest.php @@ -207,7 +207,7 @@ public function testEmail() ->will($this->returnValue(true)); $this->messageManager->expects($this->once()) ->method('addSuccess') - ->with('You sent the shipment.'); + ->with('An email confirming the order is underway has been sent to the customer.'); $path = '*/*/view'; $arguments = ['shipment_id' => $shipmentId]; $this->prepareRedirect($path, $arguments, 0); diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index 491a1e01f1720..de0e95d591880 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; @@ -110,7 +108,8 @@ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->shipmentLoader = $this->getMockBuilder( - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class) + \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class + ) ->disableOriginalConstructor() ->setMethods(['setShipmentId', 'setOrderId', 'setShipment', 'setTracking', 'load']) ->getMock(); @@ -131,26 +130,47 @@ protected function setUp() 'getObjectManager', 'getSession', 'getActionFlag', 'getHelper', 'getResultRedirectFactory', 'getFormKeyValidator' ]); - $this->response = $this->createPartialMock(\Magento\Framework\App\ResponseInterface::class, ['setRedirect', 'sendResponse']); + $this->response = $this->createPartialMock( + \Magento\Framework\App\ResponseInterface::class, + ['setRedirect', 'sendResponse'] + ); $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['create', 'get']); - $this->messageManager = $this->createPartialMock(\Magento\Framework\Message\Manager::class, ['addSuccess', 'addError']); - $this->session = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice', 'getCommentText']); + $this->objectManager = $this->createPartialMock( + \Magento\Framework\ObjectManager\ObjectManager::class, + ['create', 'get'] + ); + $this->messageManager = $this->createPartialMock( + \Magento\Framework\Message\Manager::class, + ['addSuccessMessage', 'addErrorMessage'] + ); + $this->session = $this->createPartialMock( + \Magento\Backend\Model\Session::class, + ['setIsUrlNotice', 'getCommentText'] + ); $this->actionFlag = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get']); $this->helper = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirect = $this->createPartialMock(\Magento\Framework\Controller\Result\Redirect::class, ['setPath']); + $this->resultRedirect = $this->createPartialMock( + \Magento\Framework\Controller\Result\Redirect::class, + ['setPath'] + ); $this->resultRedirect->expects($this->any()) ->method('setPath') ->willReturn($this->resultRedirect); - $resultRedirectFactory = $this->createPartialMock(\Magento\Framework\Controller\Result\RedirectFactory::class, ['create']); + $resultRedirectFactory = $this->createPartialMock( + \Magento\Framework\Controller\Result\RedirectFactory::class, + ['create'] + ); $resultRedirectFactory->expects($this->once()) ->method('create') ->willReturn($this->resultRedirect); - $this->formKeyValidator = $this->createPartialMock(\Magento\Framework\Data\Form\FormKey\Validator::class, ['validate']); + $this->formKeyValidator = $this->createPartialMock( + \Magento\Framework\Data\Form\FormKey\Validator::class, + ['validate'] + ); $this->context->expects($this->once()) ->method('getMessageManager') @@ -216,7 +236,7 @@ public function testExecute($formKeyIsValid, $isPost) if (!$formKeyIsValid || !$isPost) { $this->messageManager->expects($this->once()) - ->method('addError'); + ->method('addErrorMessage'); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -231,7 +251,10 @@ public function testExecute($formKeyIsValid, $isPost) $orderId = 10003; $tracking = []; $shipmentData = ['items' => [], 'send_email' => '']; - $shipment = $this->createPartialMock(\Magento\Sales\Model\Order\Shipment::class, ['load', 'save', 'register', 'getOrder', 'getOrderId', '__wakeup']); + $shipment = $this->createPartialMock( + \Magento\Sales\Model\Order\Shipment::class, + ['load', 'save', 'register', 'getOrder', 'getOrderId', '__wakeup'] + ); $order = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['setCustomerNoteNotify', '__wakeup']); $this->request->expects($this->any()) diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php index 65460d1a13eea..2db8eabffae61 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php @@ -204,6 +204,14 @@ public function testExecuteNoShipment() $this->assertEquals($this->resultForwardMock, $this->controller->execute()); } + /** + * @param $orderId + * @param $shipmentId + * @param $shipment + * @param $tracking + * @param $comeFrom + * @param $returnShipment + */ protected function loadShipment($orderId, $shipmentId, $shipment, $tracking, $comeFrom, $returnShipment) { $valueMap = [ diff --git a/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php b/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php index 37439082b9111..eb41d9498fc94 100644 --- a/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php @@ -99,7 +99,7 @@ public function testComposePackages() $this->stockItemData->expects($this->atLeastOnce())->method('getIsDecimalDivided') ->will($this->returnValue(true)); - $this->carrier->proccessAdditionalValidation($request); + $this->carrier->processAdditionalValidation($request); } public function testParseXml() diff --git a/app/code/Magento/Shipping/Test/Unit/Model/InfoTest.php b/app/code/Magento/Shipping/Test/Unit/Model/InfoTest.php new file mode 100644 index 0000000000000..31c2e3a5175a5 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Unit/Model/InfoTest.php @@ -0,0 +1,300 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Shipping\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Shipping\Model\Info; +use Magento\Shipping\Model\ResourceModel\Order\Track\CollectionFactory; + +/** + * Test for \Magento\Shipping\Model\Info. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class InfoTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Info + */ + private $info; + + /** + * @var \Magento\Shipping\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helper; + + /** + * @var \Magento\Sales\Model\OrderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderFactory; + + /** + * @var \Magento\Sales\Api\ShipmentRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shipmentRepository; + + /** + * @var \Magento\Shipping\Model\Order\TrackFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $trackFactory; + + /** + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $trackCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->helper = $this->getMockBuilder(\Magento\Shipping\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderFactory = $this->getMockBuilder(\Magento\Sales\Model\OrderFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->shipmentRepository = $this->getMockBuilder(\Magento\Sales\Api\ShipmentRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->trackFactory = $this->getMockBuilder(\Magento\Shipping\Model\Order\TrackFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->trackCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $objectManagerHelper = new ObjectManager($this); + $this->info = $objectManagerHelper->getObject( + Info::class, + [ + 'shippingData' => $this->helper, + 'orderFactory' => $this->orderFactory, + 'shipmentRepository' => $this->shipmentRepository, + 'trackFactory' => $this->trackFactory, + 'trackCollectionFactory' => $this->trackCollectionFactory, + ] + ); + } + + public function testLoadByHashWithOrderId() + { + $hash = strtr(base64_encode('order_id:1:protected_code'), '+/=', '-_,'); + $decodedHash = [ + 'key' => 'order_id', + 'id' => 1, + 'hash' => 'protected_code', + ]; + $shipmentId = 1; + $shipmentIncrementId = 3; + $trackDetails = 'track_details'; + + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $shipmentCollection = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getIterator']) + ->getMock(); + $order = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getProtectCode', 'getShipmentsCollection']) + ->getMock(); + $order->expects($this->atLeastOnce())->method('load')->with($decodedHash['id'])->willReturnSelf(); + $order->expects($this->atLeastOnce())->method('getId')->willReturn($decodedHash['id']); + $order->expects($this->atLeastOnce())->method('getProtectCode')->willReturn($decodedHash['hash']); + $order->expects($this->atLeastOnce())->method('getShipmentsCollection')->willReturn($shipmentCollection); + + $shipment = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + ->disableOriginalConstructor() + ->setMethods(['getIncrementId', 'getId']) + ->getMock(); + $shipment->expects($this->atLeastOnce())->method('getIncrementId')->willReturn($shipmentIncrementId); + $shipment->expects($this->atLeastOnce())->method('getId')->willReturn($shipmentId); + + $shipmentCollection->expects($this->any())->method('getIterator')->willReturn(new \ArrayIterator([$shipment])); + $this->orderFactory->expects($this->atLeastOnce())->method('create')->willReturn($order); + $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + ->disableOriginalConstructor() + ->setMethods(['setShipment', 'getNumberDetail']) + ->getMock(); + $track->expects($this->atLeastOnce())->method('setShipment')->with($shipment)->willReturnSelf(); + $track->expects($this->atLeastOnce())->method('getNumberDetail')->willReturn($trackDetails); + $trackCollection = $this->getMockBuilder(\Magento\Shipping\Model\ResourceModel\Order\Track\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getIterator', 'setShipmentFilter']) + ->getMock(); + $trackCollection->expects($this->atLeastOnce()) + ->method('setShipmentFilter') + ->with($shipmentId) + ->willReturnSelf(); + $trackCollection->expects($this->atLeastOnce()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$track])); + $this->trackCollectionFactory->expects($this->atLeastOnce())->method('create')->willReturn($trackCollection); + + $this->info->loadByHash($hash); + $this->assertEquals([$shipmentIncrementId => [$trackDetails]], $this->info->getTrackingInfo()); + } + + public function testLoadByHashWithOrderIdWrongCode() + { + $hash = strtr(base64_encode('order_id:1:0'), '+/=', '-_,'); + $decodedHash = [ + 'key' => 'order_id', + 'id' => 1, + 'hash' => '0', + ]; + + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $order = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getProtectCode', 'getShipmentsCollection']) + ->getMock(); + $order->expects($this->atLeastOnce())->method('load')->with($decodedHash['id'])->willReturnSelf(); + $order->expects($this->atLeastOnce())->method('getId')->willReturn($decodedHash['id']); + $order->expects($this->atLeastOnce())->method('getProtectCode')->willReturn('0e123123123'); + $this->orderFactory->expects($this->atLeastOnce())->method('create')->willReturn($order); + $this->info->loadByHash($hash); + $this->assertEmpty($this->info->getTrackingInfo()); + } + + public function testLoadByHashWithShipmentId() + { + $hash = strtr(base64_encode('ship_id:1:protected_code'), '+/=', '-_,'); + $decodedHash = [ + 'key' => 'ship_id', + 'id' => 1, + 'hash' => 'protected_code', + ]; + $shipmentIncrementId = 3; + $trackDetails = 'track_details'; + + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $shipment = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId', 'getProtectCode', 'getIncrementId', 'getId']) + ->getMock(); + $shipment->expects($this->atLeastOnce())->method('getIncrementId')->willReturn($shipmentIncrementId); + $shipment->expects($this->atLeastOnce())->method('getId')->willReturn($decodedHash['id']); + $shipment->expects($this->atLeastOnce())->method('getEntityId')->willReturn(3); + $shipment->expects($this->atLeastOnce())->method('getProtectCode')->willReturn($decodedHash['hash']); + $this->shipmentRepository->expects($this->atLeastOnce()) + ->method('get') + ->with($decodedHash['id']) + ->willReturn($shipment); + $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + ->disableOriginalConstructor() + ->setMethods(['setShipment', 'getNumberDetail']) + ->getMock(); + $track->expects($this->atLeastOnce())->method('setShipment')->with($shipment)->willReturnSelf(); + $track->expects($this->atLeastOnce())->method('getNumberDetail')->willReturn($trackDetails); + $trackCollection = $this->getMockBuilder(\Magento\Shipping\Model\ResourceModel\Order\Track\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getIterator', 'setShipmentFilter']) + ->getMock(); + $trackCollection->expects($this->atLeastOnce()) + ->method('setShipmentFilter') + ->with($decodedHash['id']) + ->willReturnSelf(); + $trackCollection->expects($this->atLeastOnce()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$track])); + $this->trackCollectionFactory->expects($this->atLeastOnce())->method('create')->willReturn($trackCollection); + + $this->info->loadByHash($hash); + $this->assertEquals([$shipmentIncrementId => [$trackDetails]], $this->info->getTrackingInfo()); + } + + public function testLoadByHashWithShipmentIdWrongCode() + { + $hash = strtr(base64_encode('ship_id:1:0'), '+/=', '-_,'); + $decodedHash = [ + 'key' => 'ship_id', + 'id' => 1, + 'hash' => '0', + ]; + + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $shipment = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId', 'getProtectCode', 'getIncrementId', 'getId']) + ->getMock(); + $shipment->expects($this->atLeastOnce())->method('getEntityId')->willReturn(3); + $shipment->expects($this->atLeastOnce())->method('getProtectCode')->willReturn('0e123123123'); + $this->shipmentRepository->expects($this->atLeastOnce()) + ->method('get') + ->with($decodedHash['id']) + ->willReturn($shipment); + + $this->info->loadByHash($hash); + $this->assertEmpty($this->info->getTrackingInfo()); + } + + public function testLoadByHashWithTrackId() + { + $hash = base64_encode('hash'); + $decodedHash = [ + 'key' => 'track_id', + 'id' => 1, + 'hash' => 'protected_code', + ]; + $trackDetails = 'track_details'; + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getProtectCode', 'getNumberDetail']) + ->getMock(); + $track->expects($this->atLeastOnce())->method('load')->with($decodedHash['id'])->willReturnSelf(); + $track->expects($this->atLeastOnce())->method('getId')->willReturn($decodedHash['id']); + $track->expects($this->atLeastOnce())->method('getProtectCode')->willReturn($decodedHash['hash']); + $track->expects($this->atLeastOnce())->method('getNumberDetail')->willReturn($trackDetails); + $this->trackFactory->expects($this->atLeastOnce())->method('create')->willReturn($track); + + $this->info->loadByHash($hash); + $this->assertEquals([[$trackDetails]], $this->info->getTrackingInfo()); + } + + public function testLoadByHashWithWrongCode() + { + $hash = base64_encode('hash'); + $decodedHash = [ + 'key' => 'track_id', + 'id' => 1, + 'hash' => 'protected_code', + ]; + $this->helper->expects($this->atLeastOnce()) + ->method('decodeTrackingHash') + ->with($hash) + ->willReturn($decodedHash); + $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getId', 'getProtectCode', 'getNumberDetail']) + ->getMock(); + $track->expects($this->atLeastOnce())->method('load')->with($decodedHash['id'])->willReturnSelf(); + $track->expects($this->atLeastOnce())->method('getId')->willReturn($decodedHash['id']); + $track->expects($this->atLeastOnce())->method('getProtectCode')->willReturn('0e123123123'); + $this->trackFactory->expects($this->atLeastOnce())->method('create')->willReturn($track); + + $this->info->loadByHash($hash); + $this->assertEmpty($this->info->getTrackingInfo()); + } +} diff --git a/app/code/Magento/Shipping/Test/Unit/Model/ShipmentTest.php b/app/code/Magento/Shipping/Test/Unit/Model/ShipmentTest.php index ef83917909bcd..9eb5d0ab521e6 100644 --- a/app/code/Magento/Shipping/Test/Unit/Model/ShipmentTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Model/ShipmentTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Shipping\Test\Unit\Model; use \Magento\Sales\Api\OrderRepositoryInterface; @@ -35,13 +33,20 @@ protected function setUp() 'context' => $this->createMock(\Magento\Framework\Model\Context::class), 'registry' => $this->createMock(\Magento\Framework\Registry::class), 'localeDate' => $this->createMock( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class), + \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class + ), 'dateTime' => $this->createMock(\Magento\Framework\Stdlib\DateTime::class), 'orderRepository' => $this->orderRepository, - 'shipmentItemCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Shipment\Item\CollectionFactory::class), - 'trackCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Shipment\Track\CollectionFactory::class), + 'shipmentItemCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Shipment\Item\CollectionFactory::class + ), + 'trackCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Shipment\Track\CollectionFactory::class + ), 'commentFactory' => $this->createMock(\Magento\Sales\Model\Order\Shipment\CommentFactory::class), - 'commentCollectionFactory' => $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\CollectionFactory::class), + 'commentCollectionFactory' => $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\CollectionFactory::class + ), ]; $this->shipment = $objectManagerHelper->getObject( \Magento\Sales\Model\Order\Shipment::class, @@ -54,7 +59,10 @@ public function testGetOrder() $orderId = 100000041; $this->shipment->setOrderId($orderId); $entityName = 'shipment'; - $order = $this->createPartialMock(\Magento\Sales\Model\Order::class, ['load', 'setHistoryEntityName', '__wakeUp']); + $order = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + ['load', 'setHistoryEntityName', '__wakeUp'] + ); $this->shipment->setOrderId($orderId); $order->expects($this->atLeastOnce()) ->method('setHistoryEntityName') diff --git a/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php b/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php index 727675407b8a0..050db21ce27a7 100644 --- a/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Model/ShippingTest.php @@ -3,17 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Shipping\Test\Unit\Model; -use \Magento\Shipping\Model\Shipping; +namespace Magento\Shipping\Test\Unit\Model; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\CatalogInventory\Model\Stock\Item as StockItem; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Shipping\Model\Carrier\AbstractCarrier; +use Magento\Shipping\Model\Carrier\AbstractCarrierInterface; +use Magento\Shipping\Model\Carrier\CarrierInterface; +use Magento\Shipping\Model\CarrierFactory; +use Magento\Shipping\Model\Shipping; use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\Store; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Unit tests for \Magento\Shipping\Model\Shipping class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ShippingTest extends \PHPUnit\Framework\TestCase { /** - * Test identification number of product + * Test identification number of product. * * @var int */ @@ -25,71 +42,81 @@ class ShippingTest extends \PHPUnit\Framework\TestCase protected $shipping; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Shipping\Model\Carrier\AbstractCarrier + * @var MockObject|StockRegistry */ - protected $carrier; + protected $stockRegistry; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject|StockItem */ - protected $stockRegistry; + protected $stockItemData; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject|AbstractCarrierInterface */ - protected $stockItemData; + private $carrier; + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @inheritdoc + */ protected function setUp() { - $this->carrier = $this->createMock(\Magento\Shipping\Model\Carrier\AbstractCarrier::class); - $this->carrier->expects($this->any())->method('getConfigData')->will($this->returnCallback(function ($key) { - $configData = [ - 'max_package_weight' => 10, - ]; - return isset($configData[$key]) ? $configData[$key] : 0; - })); - $this->stockRegistry = $this->createMock(\Magento\CatalogInventory\Model\StockRegistry::class); - $this->stockItemData = $this->createMock(\Magento\CatalogInventory\Model\Stock\Item::class); - - $objectManagerHelper = new ObjectManagerHelper($this); - $this->shipping = $objectManagerHelper->getObject( - \Magento\Shipping\Model\Shipping::class, - ['stockRegistry' => $this->stockRegistry] + $this->stockRegistry = $this->createMock(StockRegistry::class); + $this->stockItemData = $this->createMock(StockItem::class); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + + $this->shipping = (new ObjectManagerHelper($this))->getObject( + Shipping::class, + [ + 'stockRegistry' => $this->stockRegistry, + 'carrierFactory' => $this->getCarrierFactory(), + 'scopeConfig' => $this->scopeConfig, + ] ); } /** - * @covers \Magento\Shipping\Model\Shipping::composePackagesForCarrier + * Compose Packages For Carrier. + * + * @return void */ public function testComposePackages() { $request = new RateRequest(); - /** \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface */ - $item = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item::class) + $item = $this->getMockBuilder(QuoteItem::class) ->disableOriginalConstructor() - ->setMethods([ - 'getQty', 'getIsQtyDecimal', 'getProductType', 'getProduct', 'getWeight', '__wakeup', 'getStore', - ]) - ->getMock(); - $product = $this->createMock(\Magento\Catalog\Model\Product::class); - - $item->expects($this->any())->method('getQty')->will($this->returnValue(1)); - $item->expects($this->any())->method('getWeight')->will($this->returnValue(10)); - $item->expects($this->any())->method('getIsQtyDecimal')->will($this->returnValue(true)); - $item->expects($this->any())->method('getProductType') - ->will($this->returnValue(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)); - $item->expects($this->any())->method('getProduct')->will($this->returnValue($product)); - - $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getWebsiteId']); - $store->expects($this->any()) - ->method('getWebsiteId') - ->will($this->returnValue(10)); - $item->expects($this->any())->method('getStore')->will($this->returnValue($store)); - - $product->expects($this->any())->method('getId')->will($this->returnValue($this->productId)); + ->setMethods( + [ + 'getQty', + 'getIsQtyDecimal', + 'getProductType', + 'getProduct', + 'getWeight', + '__wakeup', + 'getStore', + ] + )->getMock(); + $product = $this->createMock(Product::class); + + $item->method('getQty')->will($this->returnValue(1)); + $item->method('getWeight')->will($this->returnValue(10)); + $item->method('getIsQtyDecimal')->will($this->returnValue(true)); + $item->method('getProductType')->will($this->returnValue(ProductType::TYPE_SIMPLE)); + $item->method('getProduct')->will($this->returnValue($product)); + + $store = $this->createPartialMock(Store::class, ['getWebsiteId']); + $store->method('getWebsiteId')->will($this->returnValue(10)); + $item->method('getStore')->will($this->returnValue($store)); + + $product->method('getId')->will($this->returnValue($this->productId)); $request->setData('all_items', [$item]); - $this->stockItemData->expects($this->any())->method('getIsDecimalDivided')->will($this->returnValue(true)); + $this->stockItemData->method('getIsDecimalDivided')->will($this->returnValue(true)); /** Testable service calls to CatalogInventory module */ $this->stockRegistry->expects($this->atLeastOnce())->method('getStockItem') @@ -101,7 +128,62 @@ public function testComposePackages() ->will($this->returnValue(true)); $this->stockItemData->expects($this->atLeastOnce())->method('getQtyIncrements') ->will($this->returnValue(0.5)); + $this->carrier->method('getConfigData') + ->willReturnCallback(function ($key) { + $configData = [ + 'max_package_weight' => 10, + ]; + return isset($configData[$key]) ? $configData[$key] : 0; + }); $this->shipping->composePackagesForCarrier($this->carrier, $request); } + + /** + * Active flag should be set before collecting carrier rates. + * + * @return void + */ + public function testCollectCarrierRatesSetActiveFlag() + { + $carrierCode = 'carrier'; + $scopeStore = 'store'; + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with( + 'carriers/' . $carrierCode . '/active', + $scopeStore + ) + ->willReturn(true); + $this->carrier->expects($this->atLeastOnce()) + ->method('setActiveFlag') + ->with('active'); + + $this->shipping->collectCarrierRates($carrierCode, new RateRequest()); + } + + /** + * @return CarrierFactory|MockObject + */ + private function getCarrierFactory() + { + $carrierFactory = $this->getMockBuilder(CarrierFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->carrier = $this->getMockBuilder(AbstractCarrierInterface::class) + ->setMethods( + [ + 'setActiveFlag', + 'checkAvailableShipCountries', + 'processAdditionalValidation', + 'getConfigData', + 'collectRates', + ] + ) + ->getMockForAbstractClass(); + $carrierFactory->method('create')->willReturn($this->carrier); + + return $carrierFactory; + } } diff --git a/app/code/Magento/Shipping/composer.json b/app/code/Magento/Shipping/composer.json index 25766e8a45e31..f5f997d21a22a 100644 --- a/app/code/Magento/Shipping/composer.json +++ b/app/code/Magento/Shipping/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-shipping", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-sales": "101.0.*", @@ -25,7 +25,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.11", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Shipping/etc/adminhtml/di.xml b/app/code/Magento/Shipping/etc/adminhtml/di.xml index 54d5d9664e66f..36bd1ae9d3505 100644 --- a/app/code/Magento/Shipping/etc/adminhtml/di.xml +++ b/app/code/Magento/Shipping/etc/adminhtml/di.xml @@ -7,4 +7,11 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Shipping\Model\Shipping" type="Magento\Shipping\Model\Shipping\Labels" /> + + <type name="Magento\Shipping\Helper\Data"> + <arguments> + <!-- Use frontend URL model--> + <argument name="url" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Shipping/etc/di.xml b/app/code/Magento/Shipping/etc/di.xml index 5834678157058..51c77c2cecd2d 100644 --- a/app/code/Magento/Shipping/etc/di.xml +++ b/app/code/Magento/Shipping/etc/di.xml @@ -9,4 +9,22 @@ <preference for="Magento\Quote\Model\Quote\Address\RateCollectorInterface" type="Magento\Shipping\Model\Shipping" /> <preference for="Magento\Shipping\Model\CarrierFactoryInterface" type="Magento\Shipping\Model\CarrierFactory" /> <preference for="Magento\Shipping\Model\Carrier\Source\GenericInterface" type="Magento\Shipping\Model\Carrier\Source\GenericDefault" /> + + <virtualType name="Magento\Shipping\Model\Carrier\VirtualDebug" type="Magento\Framework\Logger\Handler\Base"> + <arguments> + <argument name="fileName" xsi:type="string">/var/log/shipping.log</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Shipping\Model\Method\VirtualLogger" type="Magento\Framework\Logger\Monolog"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="debug" xsi:type="object">Magento\Shipping\Model\Carrier\VirtualDebug</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Shipping\Model\Carrier\AbstractCarrier"> + <arguments> + <argument name="logger" xsi:type="object">Magento\Shipping\Model\Method\VirtualLogger</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Shipping/i18n/en_US.csv b/app/code/Magento/Shipping/i18n/en_US.csv index f777e64ef98c9..0bda9d2a71477 100644 --- a/app/code/Magento/Shipping/i18n/en_US.csv +++ b/app/code/Magento/Shipping/i18n/en_US.csv @@ -28,7 +28,7 @@ Shipments,Shipments "Cannot add tracking number.","Cannot add tracking number." "You created the shipping label.","You created the shipping label." "An error occurred while creating shipping label.","An error occurred while creating shipping label." -"You sent the shipment.","You sent the shipment." +"An email confirming the order is underway has been sent to the customer.","An email confirming the order is underway has been sent to the customer." "Cannot send shipment information.","Cannot send shipment information." "There are no shipping labels related to selected orders.","There are no shipping labels related to selected orders." "New Shipment","New Shipment" diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml index 0b40bf40e97a3..83ee75463ffb6 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml @@ -3,48 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +//phpcs:disable Magento2.Files.LineLength.MaxExceeded ?> -<form id="edit_form" method="post" action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>"> +<form id="edit_form" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> <?php $_order = $block->getShipment()->getOrder() ?> <?= $block->getChildHtml('order_info') ?> <div class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-payment-method"> <?php /* Billing Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?=$block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?></div> + <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> </div> </div> <div class="admin__page-section-item order-shipping-address"> <?php /* Shipping Address */ ?> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping Information')) ?></span> </div> <div class="admin__page-section-item-content shipping-description-wrapper"> - <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> + <div class="shipping-description-title"> + <?= $block->escapeHtml($_order->getShippingDescription()) ?> + </div> <div class="shipping-description-content"> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> + <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> + <?php else : ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() + && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml index 35e36aa5584c9..c19aa66b5b7f0 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml @@ -3,32 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace +//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items to Ship') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items to Ship')) ?></span> </div> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-shipment-table"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-ordered-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> - <th class="col-qty<?php if ($block->isShipmentRegular()): ?> last<?php endif; ?>"> - <span><?= /* @escapeNotVerified */ __('Qty to Ship') ?></span> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-ordered-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> + <th class="col-qty<?php if ($block->isShipmentRegular()) : ?> last<?php endif; ?>"> + <span><?= $block->escapeHtml(__('Qty to Ship')) ?></span> </th> - <?php if (!$block->canShipPartiallyItem()): ?> - <th class="col-ship last"><span><?= /* @escapeNotVerified */ __('Ship') ?></span></th> + <?php if (!$block->canShipPartiallyItem()) : ?> + <th class="col-ship last"><span><?= $block->escapeHtml(__('Ship')) ?></span></th> <?php endif; ?> </tr> </thead> <?php $_items = $block->getShipment()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item): if ($_item->getOrderItem()->getParentItem()): continue; endif; $_i++ ?> - <tbody class="<?= /* @escapeNotVerified */ $_i%2 ? 'odd' : 'even' ?>"> + <?php $_i = 0; foreach ($_items as $_item) : + if ($_item->getOrderItem()->getParentItem()) : + continue; + endif; + $_i++ ?> + <tbody class="<?= /* @noEscape */ $_i%2 ? 'odd' : 'even' ?>"> <?= $block->getItemHtml($_item) ?> <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> </tbody> @@ -39,24 +42,24 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipment Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipment Total')) ?></span> </div> <div class="admin__page-section-content order-comments-history"> <div class="admin__page-section-item"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipment Comments') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipment Comments')) ?></span> </div> <div class="admin__page-section-item-content"> <div id="order-history_form" class="admin__field"> <label class="admin__field-label" for="shipment_comment_text"> - <span><?= /* @escapeNotVerified */ __('Comment Text') ?></span></label> + <span><?= $block->escapeHtml(__('Comment Text')) ?></span></label> <div class="admin__field-control"> <textarea id="shipment_comment_text" class="admin__control-textarea" name="shipment[comment_text]" rows="3" - cols="5"><?= /* @escapeNotVerified */ $block->getShipment()->getCommentText() ?></textarea> + cols="5"><?= $block->escapeHtml($block->getShipment()->getCommentText()) ?></textarea> </div> </div> </div> @@ -64,10 +67,10 @@ </div> <div class="admin__page-section-item order-totals order-totals-actions"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipment Options') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipment Options')) ?></span> </div> <div class="admin__page-section-item-content"> - <?php if ($block->canCreateShippingLabel()): ?> + <?php if ($block->canCreateShippingLabel()) : ?> <div class="field choice admin__field admin__field-option field-create"> <input id="create_shipping_label" class="admin__control-checkbox" @@ -77,7 +80,7 @@ onclick="toggleCreateLabelCheckbox();"/> <label class="admin__field-label" for="create_shipping_label"> - <span><?= /* @escapeNotVerified */ __('Create Shipping Label') ?></span></label> + <span><?= $block->escapeHtml(__('Create Shipping Label')) ?></span></label> </div> <?php endif ?> @@ -89,10 +92,10 @@ type="checkbox"/> <label class="admin__field-label" for="notify_customer"> - <span><?= /* @escapeNotVerified */ __('Append Comments') ?></span></label> + <span><?=$block->escapeHtml(__('Append Comments')) ?></span></label> </div> - <?php if ($block->canSendShipmentEmail()): ?> + <?php if ($block->canSendShipmentEmail()) : ?> <div class="field choice admin__field admin__field-option field-email"> <input id="send_email" class="admin__control-checkbox" @@ -101,7 +104,7 @@ type="checkbox"/> <label class="admin__field-label" for="send_email"> - <span><?= /* @escapeNotVerified */ __('Email Copy of Shipment') ?></span></label> + <span><?= $block->escapeHtml(__('Email Copy of Shipment')) ?></span></label> </div> <?php endif; ?> <?= $block->getChildHtml('submit_before') ?> @@ -147,7 +150,7 @@ window.toggleCreateLabelCheckbox = function() { window.submitShipment = function(btn) { if (!validQtyItems()) { alert({ - content: '<?= /* @escapeNotVerified */ __('Invalid value(s) for Qty to Ship') ?>' + content: '<?= $block->escapeJs($block->escapeHtml(__('Invalid value(s) for Qty to Ship'))) ?>' }); return; } diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/items/renderer/default.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/items/renderer/default.phtml index b5d2aafe18ea6..caea091eacb95 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/items/renderer/default.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/items/renderer/default.phtml @@ -3,28 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace +//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore ?> <?php $_item = $block->getItem() ?> <tr> <td class="col-product"><?= $block->getColumnHtml($_item, 'name') ?></td> <td class="col-ordered-qty"><?= $block->getColumnHtml($_item, 'qty') ?></td> - <td class="col-qty <?php if ($block->isShipmentRegular()): ?>last<?php endif; ?>"> - <?php if ($block->canShipPartiallyItem()): ?> + <td class="col-qty <?php if ($block->isShipmentRegular()) : ?>last<?php endif; ?>"> + <?php if ($block->canShipPartiallyItem()) : ?> <input type="text" class="input-text admin__control-text qty-item" - name="shipment[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" - value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" /> - <?php else: ?> - <?= /* @escapeNotVerified */ $_item->getQty()*1 ?> + name="shipment[items][<?= (int) $_item->getOrderItemId() ?>]" + value="<?= /* @noEscape */ $_item->getQty()*1 ?>" /> + <?php else : ?> + <?= /* @noEscape */ $_item->getQty()*1 ?> <?php endif; ?> </td> - <?php if (!$block->canShipPartiallyItem()): ?> + <?php if (!$block->canShipPartiallyItem()) : ?> <td class="col-ship last"> - <input type="hidden" name="shipment[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" value="0" /> - <input type="checkbox" name="shipment[items][<?= /* @escapeNotVerified */ $_item->getOrderItemId() ?>]" value="<?= /* @escapeNotVerified */ $_item->getQty()*1 ?>" checked /> + <input type="hidden" name="shipment[items][<?= (int) $_item->getOrderItemId() ?>]" value="0" /> + <input type="checkbox" + name="shipment[items][<?= (int) $_item->getOrderItemId() ?>]" + value="<?= /* @noEscape */ $_item->getQty()*1 ?>" checked /> </td> <?php endif; ?> </tr> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml index 1a9d44c19db40..22d546f4fb474 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace +//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore +//phpcs:disable Squiz.Operators.IncrementDecrementUsage.NotAllowed +//phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable ?> <div class="grid"> <?php $randomId = rand(); ?> @@ -19,29 +20,31 @@ id="select-items-<?= /* @noEscape */ $randomId ?>" onchange="packaging.checkAllItems(this);" class="checkbox admin__control-checkbox" - title="<?= /* @escapeNotVerified */ __('Select All') ?>"> + title="<?= $block->escapeHtmlAttr(__('Select All')) ?>"> <label for="select-items-<?= /* @noEscape */ $randomId ?>"></label> </label> </th> - <th class="data-grid-th"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="data-grid-th"><?= /* @escapeNotVerified */ __('Weight') ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Weight')) ?></th> <th class="data-grid-th" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> - <?= /* @escapeNotVerified */ __('Customs Value') ?> + <?= $block->escapeHtml(__('Customs Value')) ?> </th> - <th class="data-grid-th"><?= /* @escapeNotVerified */ __('Qty Ordered') ?></th> - <th class="data-grid-th"><?= /* @escapeNotVerified */ __('Qty') ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty Ordered')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty')) ?></th> </tr> </thead> <tbody> <?php $i=0; ?> - <?php foreach ($block->getCollection() as $item): ?> + <?php foreach ($block->getCollection() as $item) : ?> <?php $_order = $block->getShipment()->getOrder(); $_orderItem = $_order->getItemById($item->getOrderItemId()); ?> <?php if ($item->getIsVirtual() - || ($_orderItem->isShipSeparately() && !($_orderItem->getParentItemId() || $_orderItem->getParentItem())) - || (!$_orderItem->isShipSeparately() && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))): ?> + || ($_orderItem->isShipSeparately() + && !($_orderItem->getParentItemId() || $_orderItem->getParentItem())) + || (!$_orderItem->isShipSeparately() + && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))) : ?> <?php continue; ?> <?php endif; ?> <tr class="data-grid-controls-row data-row <?= ($i++ % 2 != 0) ? '_odd-row' : '' ?>"> @@ -51,16 +54,16 @@ <input type="checkbox" name="" id="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>" - value="<?= /* @escapeNotVerified */ $id ?>" + value="<?= (int) $id ?>" class="checkbox admin__control-checkbox"> <label for="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>"></label> </label> </td> <td> - <?= /* @escapeNotVerified */ $item->getName() ?> + <?= $block->escapeHtml($item->getName()) ?> </td> <td data-role="item-weight"> - <?= /* @escapeNotVerified */ $item->getWeight() ?> + <?= $block->escapeHtml($item->getWeight()) ?> </td> <?php if ($block->displayCustomsValue()) { @@ -72,25 +75,32 @@ } ?> - <td <?= /* @escapeNotVerified */ $customsValueDisplay ?>> + <td <?= /* @noEscape */ $customsValueDisplay ?>> <input type="text" name="customs_value" - class="input-text admin__control-text <?= /* @escapeNotVerified */ $customsValueValidation ?>" - value="<?= /* @escapeNotVerified */ $block->formatPrice($item->getPrice()) ?>" + class="input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" + value="<?= $block->escapeHtmlAttr($block->formatPrice($item->getPrice())) ?>" size="10" onblur="packaging.recalcContainerWeightAndCustomsValue(this);"> </td> <td> - <?= /* @escapeNotVerified */ $item->getOrderItem()->getQtyOrdered()*1 ?> + <?= /* @noEscape */ $item->getOrderItem()->getQtyOrdered()*1 ?> </td> <td> - <input type="hidden" name="price" value="<?= /* @escapeNotVerified */ $item->getPrice() ?>"> + <input type="hidden" name="price" value="<?= $block->escapeHtml($item->getPrice()) ?>"> <input type="text" name="qty" - value="<?= /* @escapeNotVerified */ $item->getQty()*1 ?>" - class="input-text admin__control-text qty<?php if ($item->getOrderItem()->getIsQtyDecimal()): ?> qty-decimal<?php endif ?>">  - <button type="button" class="action-delete" data-action="package-delete-item" onclick="packaging.deleteItem(this);" style="display:none;"> - <span><?= /* @escapeNotVerified */ __('Delete') ?></span> + value="<?= /* @noEscape */ $item->getQty()*1 ?>" + class="input-text admin__control-text qty + <?php if ($item->getOrderItem()->getIsQtyDecimal()) : ?> + qty-decimal + <?php endif ?>">  + <button type="button" + class="action-delete" + data-action="package-delete-item" + onclick="packaging.deleteItem(this);" + style="display:none;"> + <span><?= $block->escapeHtml(__('Delete')) ?></span> </button> </td> </tr> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml index 1d9cf5688be2d..8d47f533449a7 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Magento2.Files.LineLength.MaxExceeded +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <div id="packed_window"> -<?php foreach ($block->getPackages() as $packageId => $package): ?> +<?php foreach ($block->getPackages() as $packageId => $package) : ?> <?php $package = new \Magento\Framework\DataObject($package) ?> <?php $params = new \Magento\Framework\DataObject($package->getParams()) ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Package') . ' ' . $packageId ?></span> + <span class="title"><?= $block->escapeHtml(__('Package') . ' ' . $packageId) ?></span> </div> <div class="admin__page-section-content"> <div class="row row-gutter"> @@ -22,22 +21,24 @@ <table class="admin__table-secondary"> <tbody> <tr> - <th><?= /* @escapeNotVerified */ __('Type') ?></th> - <td><?= /* @escapeNotVerified */ $block->getContainerTypeByCode($params->getContainer()) ?></td> + <th><?= $block->escapeHtml(__('Type')) ?></th> + <td> + <?= $block->escapeHtml($block->getContainerTypeByCode($params->getContainer())) ?> + </td> </tr> <tr> - <?php if ($block->displayCustomsValue()): ?> - <th><?= /* @escapeNotVerified */ __('Customs Value') ?></th> - <td><?= /* @escapeNotVerified */ $block->displayCustomsPrice($params->getCustomsValue()) ?></td> - <?php else: ?> - <th><?= /* @escapeNotVerified */ __('Total Weight') ?></th> - <td><?= /* @escapeNotVerified */ $params->getWeight() . ' ' . $this->helper('Magento\Shipping\Helper\Carrier')->getMeasureWeightName($params->getWeightUnits()) ?></td> + <?php if ($block->displayCustomsValue()) : ?> + <th><?= $block->escapeHtml(__('Customs Value')) ?></th> + <td><?= $block->escapeHtml($block->displayCustomsPrice($params->getCustomsValue())) ?></td> + <?php else : ?> + <th><?= $block->escapeHtml(__('Total Weight')) ?></th> + <td><?= $block->escapeHtml($params->getWeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureWeightName($params->getWeightUnits())) ?></td> <?php endif; ?> </tr> - <?php if ($params->getSize()): ?> + <?php if ($params->getSize()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Size') ?></th> - <td><?= /* @escapeNotVerified */ ucfirst(strtolower($params->getSize())) ?></td> + <th><?= $block->escapeHtml(__('Size')) ?></th> + <td><?= $block->escapeHtml(ucfirst(strtolower($params->getSize()))) ?></td> </tr> <?php endif; ?> </tbody> @@ -47,31 +48,31 @@ <table class="admin__table-secondary"> <tbody> <tr> - <th><?= /* @escapeNotVerified */ __('Length') ?></th> + <th><?= $block->escapeHtml(__('Length')) ?></th> <td> - <?php if ($params->getLength() != null): ?> - <?= /* @escapeNotVerified */ $params->getLength() . ' ' . $this->helper('Magento\Shipping\Helper\Carrier')->getMeasureDimensionName($params->getDimensionUnits()) ?> - <?php else: ?> + <?php if ($params->getLength() != null) : ?> + <?= $block->escapeHtml($params->getLength() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else : ?> -- <?php endif; ?> </td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Width') ?></th> + <th><?= $block->escapeHtml(__('Width')) ?></th> <td> - <?php if ($params->getWidth() != null): ?> - <?= /* @escapeNotVerified */ $params->getWidth() . ' ' . $this->helper('Magento\Shipping\Helper\Carrier')->getMeasureDimensionName($params->getDimensionUnits()) ?> - <?php else: ?> + <?php if ($params->getWidth() != null) : ?> + <?= $block->escapeHtml($params->getWidth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else : ?> -- <?php endif; ?> </td> </tr> <tr> - <th><?= /* @escapeNotVerified */ __('Height') ?></th> + <th><?= $block->escapeHtml(__('Height')) ?></th> <td> - <?php if ($params->getHeight() != null): ?> - <?= /* @escapeNotVerified */ $params->getHeight() . ' ' . $this->helper('Magento\Shipping\Helper\Carrier')->getMeasureDimensionName($params->getDimensionUnits()) ?> - <?php else: ?> + <?php if ($params->getHeight() != null) : ?> + <?= $block->escapeHtml($params->getHeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else : ?> -- <?php endif; ?> </td> @@ -82,26 +83,26 @@ <div class="col-m-4"> <table class="admin__table-secondary"> <tbody> - <?php if ($params->getDeliveryConfirmation() != null): ?> + <?php if ($params->getDeliveryConfirmation() != null) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Signature Confirmation') ?></th> - <td><?= /* @escapeNotVerified */ $block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation()) ?></td> + <th><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> + <td><?= $block->escapeHtml($block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation())) ?></td> </tr> <?php endif; ?> - <?php if ($params->getContentType() != null): ?> + <?php if ($params->getContentType() != null) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Contents') ?></th> - <?php if ($params->getContentType() == 'OTHER'): ?> + <th><?= $block->escapeHtml(__('Contents')) ?></th> + <?php if ($params->getContentType() == 'OTHER') : ?> <td><?= $block->escapeHtml($params->getContentTypeOther()) ?></td> - <?php else: ?> - <td><?= /* @escapeNotVerified */ $block->getContentTypeByCode($params->getContentType()) ?></td> + <?php else : ?> + <td><?= $block->escapeHtml($block->getContentTypeByCode($params->getContentType())) ?></td> <?php endif; ?> </tr> <?php endif; ?> - <?php if ($params->getGirth()): ?> + <?php if ($params->getGirth()) : ?> <tr> - <th><?= /* @escapeNotVerified */ __('Girth') ?></th> - <td><?= /* @escapeNotVerified */ $params->getGirth() . ' ' . $this->helper('Magento\Shipping\Helper\Carrier')->getMeasureDimensionName($params->getGirthDimensionUnits()) ?></td> + <th><?= $block->escapeHtml(__('Girth')) ?></th> + <td><?= $block->escapeHtml($params->getGirth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getGirthDimensionUnits())) ?></td> </tr> <?php endif; ?> </tbody> @@ -110,19 +111,19 @@ </div> </div> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items in the Package') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items in the Package')) ?></span> </div> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-weight"><span><?= /* @escapeNotVerified */ __('Weight') ?></span></th> - <?php if ($block->displayCustomsValue()): ?> - <th class="col-custom"><span><?= /* @escapeNotVerified */ __('Customs Value') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-weight"><span><?= $block->escapeHtml(__('Weight')) ?></span></th> + <?php if ($block->displayCustomsValue()) : ?> + <th class="col-custom"><span><?= $block->escapeHtml(__('Customs Value')) ?></span></th> <?php endif; ?> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty Ordered') ?></span></th> - <th class="col-qty"><span><?= /* @escapeNotVerified */ __('Qty') ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty Ordered')) ?></span></th> + <th class="col-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> </tr> </thead> <tbody id=""> @@ -130,19 +131,21 @@ <?php $item = new \Magento\Framework\DataObject($item) ?> <tr title="#" id=""> <td class="col-product"> - <?= /* @escapeNotVerified */ $item->getName() ?> + <?= $block->escapeHtml($item->getName()) ?> </td> <td class="col-weight"> - <?= /* @escapeNotVerified */ $item->getWeight() ?> + <?= $block->escapeHtml($item->getWeight()) ?> </td> - <?php if ($block->displayCustomsValue()): ?> - <td class="col-custom"><?= /* @escapeNotVerified */ $block->displayCustomsPrice($item->getCustomsValue()) ?></td> + <?php if ($block->displayCustomsValue()) : ?> + <td class="col-custom"> + <?= $block->escapeHtml($block->displayCustomsPrice($item->getCustomsValue())) ?> + </td> <?php endif; ?> <td class="col-qty"> - <?= /* @escapeNotVerified */ $block->getQtyOrderedItem($item->getOrderItemId()) ?> + <?= $block->escapeHtml($block->getQtyOrderedItem($item->getOrderItemId())) ?> </td> <td class="col-qty"> - <?= /* @escapeNotVerified */ $item->getQty()*1 ?> + <?= /* @noEscape */ $item->getQty()*1 ?> </td> </tr> <?php endforeach; ?> @@ -163,8 +166,8 @@ "#packed_window": { "Magento_Shipping/js/packages":{ "type":"slide", - "title":"<?= /* @escapeNotVerified */ __('Packages') ?>", - "url": "<?= /* @escapeNotVerified */ $block->getPrintButton() ?>" + "title":"<?= $block->escapeHtml(__('Packages')) ?>", + "url": "<?= $block->escapeUrl($block->getPrintButton()) ?>" } } } diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index a1f2d2741839b..cd25cb919adb5 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -3,9 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket +//phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile ?> <?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <?php @@ -21,21 +20,21 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : "Magento_Ui/js/modal/modal" ], function(jQuery){ - window.packaging = new Packaging(<?= /* @escapeNotVerified */ $block->getConfigDataJson() ?>); + window.packaging = new Packaging(<?= /* @noEscape */ $block->getConfigDataJson() ?>); packaging.changeContainerType($$('select[name=package_container]')[0]); packaging.checkSizeAndGirthParameter( $$('select[name=package_container]')[0], - <?= /* @escapeNotVerified */ $girthEnabled ?> + <?= /* @noEscape */ $girthEnabled ?> ); packaging.setConfirmPackagingCallback(function(){ packaging.setParamsCreateLabelRequest($('edit_form').serialize(true)); packaging.sendCreateLabelRequest(); }); packaging.setLabelCreatedCallback(function(response){ - setLocation("<?php /* @escapeNotVerified */ echo $block->getUrl( + setLocation("<?php $block->escapeJs($block->escapeUrl($block->getUrl( 'sales/order/view', ['order_id' => $block->getShipment()->getOrderId()] - ); ?>"); + ))); ?>"); }); packaging.setCancelCallback(function() { if ($('create_shipping_label')) { @@ -52,23 +51,23 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : }); jQuery('#packaging_window').modal({ type: 'slide', - title: '<?= /* @escapeNotVerified */ __('Create Packages') ?>', + title: '<?= $block->escapeJs($block->escapeHtml(__('Create Packages'))) ?>', buttons: [{ - text: '<?= /* @escapeNotVerified */ __('Cancel') ?>', + text: '<?= $block->escapeJs($block->escapeHtml(__('Cancel'))) ?>', 'class': 'action-secondary', click: function () { packaging.cancelPackaging(); this.closeModal(); } }, { - text: '<?= /* @escapeNotVerified */ __('Save') ?>', + text: '<?= $block->escapeJs($block->escapeHtml(__('Save'))) ?>', 'attr': {'disabled':'disabled', 'data-action':'save-packages'}, 'class': 'action-primary _disabled', click: function () { packaging.confirmPackaging(); } }, { - text: '<?= /* @escapeNotVerified */ __('Add Package') ?>', + text: '<?= $block->escapeJs($block->escapeHtml(__('Add Package'))) ?>', 'attr': {'data-action':'add-packages'}, 'class': 'action-secondary', click: function () { diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index c32b63bddab56..f91741f439d46 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -3,9 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Magento2.Files.LineLength.MaxExceeded ?> <?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <div id="packaging_window"> @@ -13,14 +11,20 @@ <section class="admin__page-section" id="package_template" style="display:none;"> <div class="admin__page-section-title"> <span class="title"> - <?= /* @escapeNotVerified */ __('Package') ?> <span data-role="package-number"></span> + <?= $block->escapeHtml(__('Package')) ?> <span data-role="package-number"></span> </span> <div class="actions _primary"> - <button type="button" class="action-secondary" data-action="package-save-items" onclick="packaging.packItems(this);"> - <span><?= /* @escapeNotVerified */ __('Add Selected Product(s) to Package') ?></span> + <button type="button" + class="action-secondary" + data-action="package-save-items" + onclick="packaging.packItems(this);"> + <span><?= $block->escapeHtml(__('Add Selected Product(s) to Package')) ?></span> </button> - <button type="button" class="action-secondary" data-action="package-add-items" onclick="packaging.getItemsForPack(this);"> - <span><?= /* @escapeNotVerified */ __('Add Products to Package') ?></span> + <button type="button" + class="action-secondary" + data-action="package-add-items" + onclick="packaging.getItemsForPack(this);"> + <span><?= $block->escapeHtml(__('Add Products to Package')) ?></span> </button> </div> </div> @@ -28,23 +32,23 @@ <table class="data-table admin__control-table"> <thead> <tr> - <th class="col-type"><?= /* @escapeNotVerified */ __('Type') ?></th> - <?php if ($girthEnabled == 1): ?> - <th class="col-size"><?= /* @escapeNotVerified */ __('Size') ?></th> - <th class="col-girth"><?= /* @escapeNotVerified */ __('Girth') ?></th> + <th class="col-type"><?= $block->escapeHtml(__('Type')) ?></th> + <?php if ($girthEnabled == 1) : ?> + <th class="col-size"><?= $block->escapeHtml(__('Size')) ?></th> + <th class="col-girth"><?= $block->escapeHtml(__('Girth')) ?></th> <th> </th> <?php endif; ?> <th class="col-custom" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> - <?= /* @escapeNotVerified */ __('Customs Value') ?> + <?= $block->escapeHtml(__('Customs Value')) ?> </th> - <th class="col-total-weight"><?= /* @escapeNotVerified */ __('Total Weight') ?></th> - <th class="col-length"><?= /* @escapeNotVerified */ __('Length') ?></th> - <th class="col-width"><?= /* @escapeNotVerified */ __('Width') ?></th> - <th class="col-height"><?= /* @escapeNotVerified */ __('Height') ?></th> + <th class="col-total-weight"><?= $block->escapeHtml(__('Total Weight')) ?></th> + <th class="col-length"><?= $block->escapeHtml(__('Length')) ?></th> + <th class="col-width"><?= $block->escapeHtml(__('Width')) ?></th> + <th class="col-height"><?= $block->escapeHtml(__('Height')) ?></th> <th> </th> - <?php if ($block->getDeliveryConfirmationTypes()): ?> - <th class="col-signature"><?= /* @escapeNotVerified */ __('Signature Confirmation') ?></th> - <?php endif; ?> + <?php if ($block->getDeliveryConfirmationTypes()) : ?> + <th class="col-signature"><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> + <?php endif; ?> <th class="col-actions"> </th> </tr> </thead> @@ -53,29 +57,29 @@ <td class="col-type"> <?php $containers = $block->getContainers(); ?> <select name="package_container" - onchange="packaging.changeContainerType(this);packaging.checkSizeAndGirthParameter(this, <?= /* @escapeNotVerified */ $girthEnabled ?>);" - <?php if (empty($containers)):?> - title="<?= /* @escapeNotVerified */ __('USPS domestic shipments don\'t use package types.') ?>" + onchange="packaging.changeContainerType(this);packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);" + <?php if (empty($containers)) : ?> + title="<?= $block->escapeHtmlAttr(__('USPS domestic shipments don\'t use package types.')) ?>" disabled="" class="admin__control-select disabled" - <?php else: ?> + <?php else : ?> class="admin__control-select" <?php endif; ?>> - <?php foreach ($block->getContainers() as $key => $value): ?> - <option value="<?= /* @escapeNotVerified */ $key ?>" > - <?= /* @escapeNotVerified */ $value ?> + <?php foreach ($containers as $key => $value) : ?> + <option value="<?= $block->escapeHtmlAttr($key) ?>" > + <?= $block->escapeHtml($value) ?> </option> <?php endforeach; ?> </select> </td> - <?php if ($girthEnabled == 1 && !empty($sizeSource)): ?> + <?php if ($girthEnabled == 1 && !empty($sizeSource)) : ?> <td> <select name="package_size" class="admin__control-select" - onchange="packaging.checkSizeAndGirthParameter(this, <?= /* @escapeNotVerified */ $girthEnabled ?>);"> - <?php foreach ($sizeSource as $key => $value): ?> - <option value="<?= /* @escapeNotVerified */ $sizeSource[$key]['value'] ?>"> - <?= /* @escapeNotVerified */ $sizeSource[$key]['label'] ?> + onchange="packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);"> + <?php foreach ($sizeSource as $key => $value) : ?> + <option value="<?= $block->escapeHtmlAttr($sizeSource[$key]['value']) ?>"> + <?= $block->escapeHtml($sizeSource[$key]['label']) ?> </option> <?php endforeach; ?> </select> @@ -89,11 +93,11 @@ <select name="container_girth_dimension_units" class="options-units-dimensions measures admin__control-select" onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= /* @escapeNotVerified */ __('in') ?></option> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Length::CENTIMETER ?>" ><?= /* @escapeNotVerified */ __('cm') ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> </select> </td> - <?php endif; ?> + <?php endif; ?> <?php if ($block->displayCustomsValue()) { $customsValueDisplay = ''; @@ -103,13 +107,15 @@ $customsValueValidation = ''; } ?> - <td class="col-custom" <?= /* @escapeNotVerified */ $customsValueDisplay ?>> + <td class="col-custom" <?= /* @noEscape */ $customsValueDisplay ?>> <div class="admin__control-addon"> <input type="text" - class="customs-value input-text admin__control-text <?= /* @escapeNotVerified */ $customsValueValidation ?>" + class="customs-value input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" name="package_customs_value" /> <span class="admin__addon-suffix"> - <span class="customs-value-currency"><?= /* @escapeNotVerified */ $block->getCustomValueCurrencyCode() ?></span> + <span class="customs-value-currency"> + <?= $block->escapeHtml($block->getCustomValueCurrencyCode()) ?> + </span> </span> </div> </td> @@ -121,8 +127,8 @@ <select name="container_weight_units" class="options-units-weight measures admin__control-select" onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Weight::POUND ?>" selected="selected" ><?= /* @escapeNotVerified */ __('lb') ?></option> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Weight::KILOGRAM ?>" ><?= /* @escapeNotVerified */ __('kg') ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::POUND ?>" selected="selected" ><?= $block->escapeHtml(__('lb')) ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::KILOGRAM ?>" ><?= $block->escapeHtml(__('kg')) ?></option> </select> <span class="admin__addon-prefix"></span> </div> @@ -146,35 +152,37 @@ <select name="container_dimension_units" class="options-units-dimensions measures admin__control-select" onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= /* @escapeNotVerified */ __('in') ?></option> - <option value="<?= /* @escapeNotVerified */ Zend_Measure_Length::CENTIMETER ?>" ><?= /* @escapeNotVerified */ __('cm') ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> </select> </td> - <?php if ($block->getDeliveryConfirmationTypes()): ?> + <?php if ($block->getDeliveryConfirmationTypes()) : ?> <td> <select name="delivery_confirmation_types" class="admin__control-select"> - <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value): ?> - <option value="<?= /* @escapeNotVerified */ $key ?>" > - <?= /* @escapeNotVerified */ $value ?> + <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value) : ?> + <option value="<?= $block->escapeHtmlAttr($key) ?>" > + <?= $block->escapeHtml($value) ?> </option> <?php endforeach; ?> </select> </td> - <?php endif; ?> + <?php endif; ?> <td class="col-actions"> - <button type="button" class="action-delete DeletePackageBtn" onclick="packaging.deletePackage(this);"> - <span><?= /* @escapeNotVerified */ __('Delete Package') ?></span> + <button type="button" + class="action-delete DeletePackageBtn" + onclick="packaging.deletePackage(this);"> + <span><?= $block->escapeHtml(__('Delete Package')) ?></span> </button> </td> </tr> </tbody> </table> - <?php if ($block->getContentTypes()): ?> + <?php if ($block->getContentTypes()) : ?> <table class="data-table admin__control-table" cellspacing="0"> <thead> <tr> - <th><?= /* @escapeNotVerified */ __('Contents') ?></th> - <th><?= /* @escapeNotVerified */ __('Explanation') ?></th> + <th><?= $block->escapeHtml(__('Contents')) ?></th> + <th><?= $block->escapeHtml(__('Explanation')) ?></th> </tr> </thead> <tbody> @@ -183,9 +191,9 @@ <select name="content_type" class="admin__control-select" onchange="packaging.changeContentTypes(this);"> - <?php foreach ($block->getContentTypes() as $key => $value): ?> - <option value="<?= /* @escapeNotVerified */ $key ?>" > - <?= /* @escapeNotVerified */ $value ?> + <?php foreach ($block->getContentTypes() as $key => $value) : ?> + <option value="<?= $block->escapeHtmlAttr($key) ?>" > + <?= $block->escapeHtml($value) ?> </option> <?php endforeach; ?> </select> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml index 02cdca120fdbc..d65fa819eaeed 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking */?> <script> @@ -68,8 +65,8 @@ require(['prototype'], function(){ id="trackingC<%- data.index %>" class="select admin__control-select carrier" disabled="disabled"> - <?php foreach ($block->getCarriers() as $_code => $_name): ?> - <option value="<?= /* @escapeNotVerified */ $_code ?>"><?= $block->escapeHtml($_name) ?></option> + <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> </td> @@ -94,7 +91,7 @@ require(['prototype'], function(){ type="button" class="action-default action-delete" onclick="trackingControl.deleteRow(event);return false"> - <span><?= /* @escapeNotVerified */ __('Delete') ?></span> + <span><?= $block->escapeHtml(__('Delete')) ?></span> </button> </td> </tr> @@ -104,10 +101,10 @@ require(['prototype'], function(){ <table class="data-table admin__control-table" id="tracking_numbers_table"> <thead> <tr class="headings"> - <th class="col-carrier"><?= /* @escapeNotVerified */ __('Carrier') ?></th> - <th class="col-title"><?= /* @escapeNotVerified */ __('Title') ?></th> - <th class="col-number"><?= /* @escapeNotVerified */ __('Number') ?></th> - <th class="col-delete"><?= /* @escapeNotVerified */ __('Action') ?></th> + <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> + <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> + <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> + <th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> <tfoot> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml index 0587d5eabe681..67587f19774c4 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml @@ -3,19 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +//phpcs:disable Magento2.Files.LineLength.MaxExceeded ?> <?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View */ ?> <div class="admin__control-table-wrapper"> <table class="data-table admin__control-table" id="shipment_tracking_info"> <thead> <tr class="headings"> - <th class="col-carrier"><?= /* @escapeNotVerified */ __('Carrier') ?></th> - <th class="col-title"><?= /* @escapeNotVerified */ __('Title') ?></th> - <th class="col-number"><?= /* @escapeNotVerified */ __('Number') ?></th> - <th class="col-delete last"><?= /* @escapeNotVerified */ __('Action') ?></th> + <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> + <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> + <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> + <th class="col-delete last"><?= $block->escapeHtml(__('Action')) ?></th> </tr> </thead> <tfoot> @@ -24,8 +24,8 @@ <select name="carrier" class="select admin__control-select" onchange="selectCarrier(this)"> - <?php foreach ($block->getCarriers() as $_code => $_name): ?> - <option value="<?= /* @escapeNotVerified */ $_code ?>"><?= $block->escapeHtml($_name) ?></option> + <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> </td> @@ -46,21 +46,23 @@ <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> </tr> </tfoot> - <?php if ($_tracks = $block->getShipment()->getAllTracks()): ?> + <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> <tbody> - <?php $i = 0; foreach ($_tracks as $_track):$i++ ?> - <tr class="<?= /* @escapeNotVerified */ ($i%2 == 0) ? 'even' : 'odd' ?>"> - <td class="col-carrier"><?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?></td> + <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> + <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> + <td class="col-carrier"> + <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> + </td> <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> <td class="col-number"> - <?php if ($_track->isCustom()): ?> - <?= $block->escapeHtml($_track->getNumber()) ?> - <?php else: ?> - <a href="#" onclick="popWin('<?= /* @escapeNotVerified */ $this->helper('Magento\Shipping\Helper\Data')->getTrackingPopupUrlBySalesModel($_track) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> - <div id="shipment_tracking_info_response_<?= /* @escapeNotVerified */ $_track->getId() ?>"></div> + <?php if ($_track->isCustom()) : ?> + <?= $block->escapeHtml($_track->getNumber()) ?> + <?php else : ?> + <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> + <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> <?php endif; ?> </td> - <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= /* @escapeNotVerified */ $block->getRemoveUrl($_track) ?>'); return false;"><span><?= /* @escapeNotVerified */ __('Delete') ?></span></button></td> + <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> </tr> <?php endforeach; ?> </tbody> @@ -78,7 +80,7 @@ function selectCarrier(elem) { } function deleteTrackingNumber(url) { - if (confirm('<?= /* @escapeNotVerified */ __('Are you sure?') ?>')) { + if (confirm('<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>')) { submitAndReloadArea($('shipment_tracking_info').parentNode, url) } } diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml index b7281f16cd353..551a86532252e 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml @@ -3,39 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Shipping\Block\Adminhtml\View */ ?> <?php $order = $block->getOrder() ?> -<?php if ($order->getIsVirtual()) : return '';endif; ?> +<?php if ($order->getIsVirtual()) : + return ''; +endif; ?> <?php /* Shipping Method */ ?> <div class="admin__page-section-item order-shipping-method"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping & Handling Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping & Handling Information')) ?></span> </div> <div class="admin__page-section-item-content"> <?php if ($order->getTracksCollection()->count()) : ?> - <p><a href="#" id="linkId" onclick="popWin('<?= /* @escapeNotVerified */ $this->helper('Magento\Shipping\Helper\Data')->getTrackingPopupUrlBySalesModel($order) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')" title="<?= /* @escapeNotVerified */ __('Track Order') ?>"><?= /* @escapeNotVerified */ __('Track Order') ?></a></p> + <p><a href="#" id="linkId" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($order))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')" title="<?= $block->escapeHtmlAttr(__('Track Order')) ?>"><?= $block->escapeHtml(__('Track Order')) ?></a></p> <?php endif; ?> - <?php if ($order->getShippingDescription()): ?> + <?php if ($order->getShippingDescription()) : ?> <strong><?= $block->escapeHtml($order->getShippingDescription()) ?></strong> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> + <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> <?php $_excl = $block->displayShippingPriceInclTax($order); ?> - <?php else: ?> + <?php else : ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $_excl ?> + <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() + && $_incl != $_excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> - <?php else: ?> - <?= /* @escapeNotVerified */ __('No shipping information available') ?> + <?php else : ?> + <?= $block->escapeHtml(__('No shipping information available')) ?> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml index 55c782eb0fc82..dcf7759be9983 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml @@ -3,73 +3,78 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +/** + * @var \Magento\Shipping\Block\Adminhtml\View\Form $block + */ +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +//phpcs:disable Magento2.Files.LineLength.MaxExceeded +$order = $block->getShipment()->getOrder(); ?> -<?php $_order = $block->getShipment()->getOrder() ?> -<?= $block->getChildHtml('order_info') ?> +<?= $block->getChildHtml('order_info'); ?> <section class="admin__page-section order-shipment-billing-shipping"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment & Shipping Method') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')); ?></span> </div> <div class="admin__page-section-content"> - - <?php /* Billing Address */ ?> <div class="admin__page-section-item order-payment-method"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Payment Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Payment Information')); ?></span> </div> <div class="admin__page-section-item-content"> <div><?= $block->getChildHtml('order_payment') ?></div> - <div class="order-payment-currency"><?= /* @escapeNotVerified */ __('The order was placed using %1.', $_order->getOrderCurrencyCode()) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $order->getOrderCurrencyCode())); ?> + </div> </div> </div> - <?php /* Shipping Address */ ?> <div class="admin__page-section-item order-shipping-address"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipping and Tracking Information') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipping and Tracking Information')); ?></span> </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <?php if ($block->getShipment()->getTracksCollection()->count()): ?> + <?php if ($block->getShipment()->getTracksCollection()->count()) : ?> <p> - <a href="#" id="linkId" onclick="popWin('<?= /* @escapeNotVerified */ $this->helper('Magento\Shipping\Helper\Data')->getTrackingPopupUrlBySalesModel($block->getShipment()) ?>','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')" title="<?= /* @escapeNotVerified */ __('Track this shipment') ?>"><?= /* @escapeNotVerified */ __('Track this shipment') ?></a> + <a href="#" id="linkId" onclick="popWin('<?= $block->escapeUrl($this->helper(\Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($block->getShipment())); ?>','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')" + title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> + <?= $block->escapeHtml(__('Track this shipment')); ?> + </a> </p> <?php endif; ?> <div class="shipping-description-title"> - <?= $block->escapeHtml($_order->getShippingDescription()) ?> + <?= $block->escapeHtml($order->getShippingDescription()); ?> </div> - <?= /* @escapeNotVerified */ __('Total Shipping Charges') ?>: + <?= $block->escapeHtml(__('Total Shipping Charges')); ?>: - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingPriceIncludingTax()): ?> - <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else: ?> - <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php $excl = $block->displayShippingPriceInclTax($order); ?> + <?php else : ?> + <?php $excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> - <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> + <?php $incl = $block->displayShippingPriceInclTax($order); ?> - <?= /* @escapeNotVerified */ $_excl ?> - <?php if ($this->helper('Magento\Tax\Helper\Data')->displayShippingBothPrices() && $_incl != $_excl): ?> - (<?= /* @escapeNotVerified */ __('Incl. Tax') ?> <?= /* @escapeNotVerified */ $_incl ?>) + <?= /* @noEscape */ $excl; ?> + <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $incl != $excl) : ?> + (<?= $block->escapeHtml(__('Incl. Tax')); ?> <?= /* @noEscape */ $incl; ?>) <?php endif; ?> </div> - <?php if ($block->canCreateShippingLabel()): ?> + <p> - <?= /* @escapeNotVerified */ $block->getCreateLabelButton() ?> - <?php if ($block->getShipment()->getShippingLabel()): ?> - <?= /* @escapeNotVerified */ $block->getPrintLabelButton() ?> + <?php if ($block->canCreateShippingLabel()) : ?> + <?= /* @noEscape */ $block->getCreateLabelButton(); ?> + <?php endif ?> + <?php if ($block->getShipment()->getShippingLabel()) : ?> + <?= /* @noEscape */ $block->getPrintLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getPackages()): ?> - <?= /* @escapeNotVerified */ $block->getShowPackagesButton() ?> + <?php if ($block->getShipment()->getPackages()) : ?> + <?= /* @noEscape */ $block->getShowPackagesButton(); ?> <?php endif ?> </p> - <?php endif ?> - <?= $block->getChildHtml('shipment_tracking') ?> + <?= $block->getChildHtml('shipment_tracking'); ?> - <?= $block->getChildHtml('shipment_packaging') ?> + <?= $block->getChildHtml('shipment_packaging'); ?> <script> require([ 'jquery', @@ -80,10 +85,7 @@ window.packaging.sendCreateLabelRequest(); }); window.packaging.setLabelCreatedCallback(function () { - setLocation("<?php /* @escapeNotVerified */ echo $block->getUrl( - 'adminhtml/order_shipment/view', - ['shipment_id' => $block->getShipment()->getId()] - ); ?>"); + setLocation("<?php $block->escapeUrl($block->getUrl('adminhtml/order_shipment/view', ['shipment_id' => $block->getShipment()->getId()])); ?>"); }); }; @@ -101,23 +103,23 @@ <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Items Shipped') ?></span> + <span class="title"><?= $block->escapeHtml(__('Items Shipped')); ?></span> </div> - <?= $block->getChildHtml('shipment_items') ?> + <?= $block->getChildHtml('shipment_items'); ?> </section> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Order Total') ?></span> + <span class="title"><?= $block->escapeHtml(__('Order Total')); ?></span> </div> <div class="admin__page-section-content"> - <?= $block->getChildHtml('shipment_packed') ?> + <?= $block->getChildHtml('shipment_packed'); ?> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipment History') ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipment History')); ?></span> </div> - <div class="admin__page-section-item-content"><?= $block->getChildHtml('order_comments') ?></div> + <div class="admin__page-section-item-content"><?= $block->getChildHtml('order_comments'); ?></div> </div> </div> </section> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml index 8dddfaedda4e5..fa9fe7a84221b 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/items.phtml @@ -3,24 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-shipment-table"> <thead> <tr class="headings"> - <th class="col-product"><span><?= /* @escapeNotVerified */ __('Product') ?></span></th> - <th class="col-qty last"><span><?= /* @escapeNotVerified */ __('Qty Shipped') ?></span></th> + <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> + <th class="col-qty last"><span><?= $block->escapeHtml(__('Qty Shipped')) ?></span></th> </tr> </thead> <?php $_items = $block->getShipment()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item): if ($_item->getOrderItem()->getParentItem()): continue; endif; $_i++ ?> - <tbody class="<?= /* @escapeNotVerified */ $_i%2 ? 'odd' : 'even' ?>"> - <?= $block->getItemHtml($_item) ?> - <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> - </tbody> + <?php $_i = 0; foreach ($_items as $_item) : + if (!empty($_item->getOrderItem())) : + if ($_item->getOrderItem()->getParentItem()) : + continue; + endif; + $_i++ ?> + <tbody class="<?= /* @noEscape */ $_i%2 ? 'odd' : 'even' ?>"> + <?= $block->getItemHtml($_item) ?> + <?= $block->getItemExtraInfoHtml($_item->getOrderItem()) ?> + </tbody> + <?php endif; ?> <?php endforeach; ?> </table> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/items/renderer/default.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/items/renderer/default.phtml index c035295f77d45..3a41eabd20c08 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/items/renderer/default.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/items/renderer/default.phtml @@ -3,12 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php $_item = $block->getItem() ?> <tr class="border"> <td class="col-product"><?= $block->getColumnHtml($_item, 'name') ?></td> - <td class="col-qty last"><?= /* @escapeNotVerified */ $_item->getQty()*1 ?></td> + <td class="col-qty last"><?= /* @noEscape */ $_item->getQty()*1 ?></td> </tr> diff --git a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml index 1f5b0ae4630ad..67d03da2599bf 100644 --- a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml +++ b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml @@ -8,7 +8,11 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="empty" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false" /> + <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false"> + <arguments> + <argument name="delivery_date_title" xsi:type="object">Magento\Shipping\Block\DataProviders\Tracking\DeliveryDateTitle</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Shipping/view/frontend/templates/items.phtml b/app/code/Magento/Shipping/view/frontend/templates/items.phtml index 1f4958d1775fb..f0f1423ed47a2 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/items.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/items.phtml @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - +//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace +//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore +//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +//phpcs:disable Magento2.Files.LineLength.MaxExceeded ?> <?php /** @var $block \Magento\Shipping\Block\Items */ ?> <?php $_order = $block->getOrder() ?> @@ -13,69 +14,71 @@ <?php if ($_order->getTracksCollection()->count()) : ?> <?= $block->getChildHtml('track-all-link') ?> <?php endif; ?> - <a href="<?= /* @escapeNotVerified */ $block->getPrintAllShipmentsUrl($_order) ?>" + <a href="<?= $block->escapeUrl($block->getPrintAllShipmentsUrl($_order)) ?>" onclick="this.target='_blank'" class="action print"> - <span><?= /* @escapeNotVerified */ __('Print All Shipments') ?></span> + <span><?= $block->escapeHtml(__('Print All Shipments')) ?></span> </a> </div> -<?php foreach ($_order->getShipmentsCollection() as $_shipment): ?> -<div class="order-title"> - <strong><?= /* @escapeNotVerified */ __('Shipment #') ?><?= /* @escapeNotVerified */ $_shipment->getIncrementId() ?></strong> - <a href="<?= /* @escapeNotVerified */ $block->getPrintShipmentUrl($_shipment) ?>" - onclick="this.target='_blank'" - class="action print"> - <span><?= /* @escapeNotVerified */ __('Print Shipment') ?></span> - </a> - <a href="#" - data-mage-init='{"popupWindow": {"windowURL":"<?= /* @escapeNotVerified */ $this->helper('Magento\Shipping\Helper\Data')->getTrackingPopupUrlBySalesModel($_shipment) ?>","windowName":"trackshipment","width":800,"height":600,"top":0,"left":0,"resizable":1,"scrollbars":1}}' - title="<?= /* @escapeNotVerified */ __('Track this shipment') ?>" - class="action track"> - <span><?= /* @escapeNotVerified */ __('Track this shipment') ?></span> - </a> -</div> -<?php $tracks = $_shipment->getTracksCollection(); ?> -<?php if ($tracks->count()): ?> - <dl class="order-tracking" id="my-tracking-table-<?= /* @escapeNotVerified */ $_shipment->getId() ?>"> - <dt class="tracking-title"> - <?= /* @escapeNotVerified */ __('Tracking Number(s):') ?> - </dt> - <dd class="tracking-content"> - <?php - $i = 1; - $_size = $tracks->count(); - foreach ($tracks as $track): ?> - <?php if ($track->isCustom()): ?><?= $block->escapeHtml($track->getNumber()) ?><?php else: ?><a - href="#" - data-mage-init='{"popupWindow": {"windowURL":"<?= /* @escapeNotVerified */ $this->helper('Magento\Shipping\Helper\Data')->getTrackingPopupUrlBySalesModel($track) ?>","windowName":"trackorder","width":800,"height":600,"left":0,"top":0,"resizable":1,"scrollbars":1}}' - class="action track"><span><?= $block->escapeHtml($track->getNumber()) ?></span> - </a><?php endif; ?><?php if ($i != $_size): ?>, <?php endif; ?> - <?php $i++; - endforeach; ?> - </dd> - </dl> -<?php endif; ?> -<div class="table-wrapper order-items-shipment"> - <table class="data table table-order-items shipment" id="my-shipment-table-<?= /* @escapeNotVerified */ $_shipment->getId() ?>"> - <caption class="table-caption"><?= /* @escapeNotVerified */ __('Items Shipped') ?></caption> - <thead> - <tr> - <th class="col name"><?= /* @escapeNotVerified */ __('Product Name') ?></th> - <th class="col sku"><?= /* @escapeNotVerified */ __('SKU') ?></th> - <th class="col qty"><?= /* @escapeNotVerified */ __('Qty Shipped') ?></th> - </tr> - </thead> - <?php $_items = $_shipment->getAllItems(); ?> - <?php $_count = count($_items) ?> - <?php foreach ($_items as $_item): ?> - <?php if ($_item->getOrderItem()->getParentItem()) { - continue; -} ?> - <tbody> - <?= $block->getItemHtml($_item) ?> - </tbody> - <?php endforeach; ?> - </table> -</div> -<?= $block->getCommentsHtml($_shipment) ?> +<?php foreach ($_order->getShipmentsCollection() as $_shipment) : ?> + <div class="order-title"> + <strong><?= $block->escapeHtml(__('Shipment #')) ?><?= $block->escapeHtml($_shipment->getIncrementId()) ?></strong> + <a href="<?= $block->escapeUrl($block->getPrintShipmentUrl($_shipment)) ?>" + onclick="this.target='_blank'" + class="action print"> + <span><?= $block->escapeHtml(__('Print Shipment')) ?></span> + </a> + <a href="#" + data-mage-init='{"popupWindow": {"windowURL":"<?= $block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_shipment)) ?>","windowName":"trackshipment","width":800,"height":600,"top":0,"left":0,"resizable":1,"scrollbars":1}}' + title="<?= $block->escapeHtml(__('Track this shipment')) ?>" + class="action track"> + <span><?= $block->escapeHtml(__('Track this shipment')) ?></span> + </a> + </div> + <?php $tracks = $_shipment->getTracksCollection(); ?> + <?php if ($tracks->count()) : ?> + <dl class="order-tracking" id="my-tracking-table-<?= (int) $_shipment->getId() ?>"> + <dt class="tracking-title"> + <?= $block->escapeHtml(__('Tracking Number(s):')) ?> + </dt> + <dd class="tracking-content"> + <?php + $i = 1; + $_size = $tracks->count(); + foreach ($tracks as $track) : ?> + <?php if ($track->isCustom()) : ?> + <?= $block->escapeHtml($track->getNumber()) ?> + <?php else : ?> + <a href="#" + data-mage-init='{"popupWindow": {"windowURL":"<?= $block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($track)) ?>","windowName":"trackorder","width":800,"height":600,"left":0,"top":0,"resizable":1,"scrollbars":1}}' + class="action track"><span><?= $block->escapeHtml($track->getNumber()) ?></span> + </a> + <?php endif; ?> + <?php if ($i != $_size) : ?>, <?php endif; ?> + <?php $i++; + endforeach; ?> + </dd> + </dl> + <?php endif; ?> + <div class="table-wrapper order-items-shipment"> + <table class="data table table-order-items shipment" id="my-shipment-table-<?= (int) $_shipment->getId() ?>"> + <caption class="table-caption"><?= $block->escapeHtml(__('Items Shipped')) ?></caption> + <thead> + <tr> + <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> + <th class="col qty"><?= $block->escapeHtml(__('Qty Shipped')) ?></th> + </tr> + </thead> + <?php $_items = $_shipment->getAllItems(); ?> + <?php foreach ($_items as $_item) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <tbody> + <?= $block->getItemHtml($_item) ?> + </tbody> + <?php endif; ?> + <?php endforeach; ?> + </table> + </div> + <?= $block->getCommentsHtml($_shipment) ?> <?php endforeach; ?> diff --git a/app/code/Magento/Shipping/view/frontend/templates/order/shipment.phtml b/app/code/Magento/Shipping/view/frontend/templates/order/shipment.phtml index 512c06626d758..da251da3d35d4 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/order/shipment.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/order/shipment.phtml @@ -8,8 +8,8 @@ <?= $block->getChildHtml('shipment_items') ?> <div class="actions-toolbar"> <div class="secondary"> - <a href="<?= /* @escapeNotVerified */ $block->getBackUrl() ?>" class="action back"> - <span><?= /* @escapeNotVerified */ $block->getBackTitle() ?></span> + <a href="<?= $block->escapeUrl($block->getBackUrl()) ?>" class="action back"> + <span><?= $block->escapeHtml($block->getBackTitle()) ?></span> </a> </div> </div> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml index 344ad2a87e4f4..7dca84565e674 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml @@ -4,9 +4,8 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block \Magento\Framework\View\Element\Template */ +//phpcs:disable Magento2.Files.LineLength.MaxExceeded $parentBlock = $block->getParentBlock(); $track = $block->getData('track'); @@ -21,21 +20,21 @@ $fields = [ ]; $number = is_object($track) ? $track->getTracking() : $track['number']; ?> -<table class="data table order tracking" id="tracking-table-popup-<?= /* @noEscape */ $number ?>"> +<table class="data table order tracking" id="tracking-table-popup-<?= $block->escapeHtml($number) ?>"> <caption class="table-caption"><?= $block->escapeHtml(__('Order tracking')) ?></caption> <tbody> - <?php if (is_object($track)): ?> + <?php if (is_object($track)) : ?> <tr> <th class="col label" scope="row"><?= $block->escapeHtml(__('Tracking Number:')) ?></th> <td class="col value"><?= $block->escapeHtml($number) ?></td> </tr> - <?php if ($track->getCarrierTitle()): ?> + <?php if ($track->getCarrierTitle()) : ?> <tr> <th class="col label" scope="row"><?= $block->escapeHtml(__('Carrier:')) ?></th> <td class="col value"><?= $block->escapeHtml($track->getCarrierTitle()) ?></td> </tr> <?php endif; ?> - <?php if ($track->getErrorMessage()): ?> + <?php if ($track->getErrorMessage()) : ?> <tr> <th class="col label" scope="row"><?= $block->escapeHtml(__('Error:')) ?></th> <td class="col error"> @@ -51,12 +50,12 @@ $number = is_object($track) ? $track->getTracking() : $track['number']; <a href="mailto:<?= /* @noEscape */ $email ?>"><?= /* @noEscape */ $email ?></a> </td> </tr> - <?php elseif ($track->getTrackSummary()): ?> + <?php elseif ($track->getTrackSummary()) : ?> <tr> <th class="col label" scope="row"><?= $block->escapeHtml(__('Info:')) ?></th> <td class="col value"><?= $block->escapeHtml($track->getTrackSummary()) ?></td> </tr> - <?php elseif ($track->getUrl()): ?> + <?php elseif ($track->getUrl()) : ?> <tr> <th class="col label" scope="row"><?= $block->escapeHtml(__('Track:')) ?></th> <td class="col value"> @@ -65,9 +64,9 @@ $number = is_object($track) ? $track->getTracking() : $track['number']; </a> </td> </tr> - <?php else: ?> - <?php foreach ($fields as $title => $property): ?> - <?php if (!empty($track->$property())): ?> + <?php else : ?> + <?php foreach ($fields as $title => $property) : ?> + <?php if (!empty($track->$property())) : ?> <tr> <th class="col label" scope="row"><?= /* @noEscape */ $block->escapeHtml(__($title . ':')) ?></th> <td class="col value"><?= $block->escapeHtml($track->$property()) ?></td> @@ -75,16 +74,16 @@ $number = is_object($track) ? $track->getTracking() : $track['number']; <?php endif;?> <?php endforeach; ?> - <?php if ($track->getDeliverydate()): ?> + <?php if ($track->getDeliverydate()) : ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__('Delivered on:')) ?></th> + <th class="col label" scope="row"><?= $block->escapeHtml($parentBlock->getDeliveryDateTitle()->getTitle($track)) ?></th> <td class="col value"> <?= /* @noEscape */ $parentBlock->formatDeliveryDateTime($track->getDeliverydate(), $track->getDeliverytime()) ?> </td> </tr> <?php endif; ?> <?php endif; ?> - <?php elseif (isset($track['title']) && isset($track['number']) && $track['number']): ?> + <?php elseif (isset($track['title']) && isset($track['number']) && $track['number']) : ?> <?php /* if the tracking is custom value */ ?> <tr> <th class="col label" scope="row"> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/link.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/link.phtml index 188e24ee11515..24658649d0a0c 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/link.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/link.phtml @@ -3,13 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - ?> <?php /** @var $block \Magento\Shipping\Block\Tracking\Link */ ?> <?php $order = $block->getOrder() ?> -<a href="#" class="action track" title="<?= /* @escapeNotVerified */ $block->getLabel() ?>" - data-mage-init='{"popupWindow": {"windowURL":"<?= /* @escapeNotVerified */ $block->getWindowUrl($order) ?>","windowName":"trackorder","width":800,"height":600,"left":0,"top":0,"resizable":1,"scrollbars":1}}'> - <span><?= /* @escapeNotVerified */ $block->getLabel() ?></span> +<a href="#" class="action track" title="<?= $block->escapeHtmlAttr($block->getLabel()) ?>" + data-mage-init='{"popupWindow": { + "windowURL":"<?= $block->escapeUrl($block->getWindowUrl($order)) ?>", + "windowName":"trackorder", + "width":800, + "height":600, + "left":0, + "top":0, + "resizable":1, + "scrollbars":1 + }}'> + <span><?= $block->escapeHtml($block->getLabel()) ?></span> </a> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml index eb888595c7f97..733c7864b522f 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml @@ -6,20 +6,19 @@ use Magento\Framework\View\Element\Template; -// @codingStandardsIgnoreFile - /** @var $block \Magento\Shipping\Block\Tracking\Popup */ +//phpcs:disable Magento2.Files.LineLength.MaxExceeded $results = $block->getTrackingInfo(); ?> <div class="page tracking"> - <?php if (!empty($results)): ?> - <?php foreach ($results as $shipId => $result): ?> - <?php if ($shipId): ?> + <?php if (!empty($results)) : ?> + <?php foreach ($results as $shipId => $result) : ?> + <?php if ($shipId) : ?> <div class="order subtitle caption"><?= /* @noEscape */ $block->escapeHtml(__('Shipment #')) . $shipId ?></div> <?php endif; ?> - <?php if (!empty($result)): ?> - <?php foreach ($result as $counter => $track): ?> + <?php if (!empty($result)) : ?> + <?php foreach ($result as $counter => $track) : ?> <div class="table-wrapper"> <?php $shipmentBlockIdentifier = $shipId . '.' . $counter; @@ -27,12 +26,11 @@ $results = $block->getTrackingInfo(); 'track' => $track, 'template' => 'Magento_Shipping::tracking/details.phtml', 'storeSupportEmail' => $block->getStoreSupportEmail() - ] - ); + ]); ?> <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.details.' . $shipmentBlockIdentifier) ?> </div> - <?php if (is_object($track) && !empty($track->getProgressdetail())): ?> + <?php if (is_object($track) && !empty($track->getProgressdetail())) : ?> <?php $block->addChild('shipping.tracking.progress.' . $shipmentBlockIdentifier, Template::class, [ 'track' => $track, @@ -42,13 +40,13 @@ $results = $block->getTrackingInfo(); <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.progress.' . $shipmentBlockIdentifier) ?> <?php endif; ?> <?php endforeach; ?> - <?php else: ?> + <?php else : ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available for this shipment.')) ?></div> </div> <?php endif; ?> <?php endforeach; ?> - <?php else: ?> + <?php else : ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available.')) ?></div> </div> @@ -62,3 +60,13 @@ $results = $block->getTrackingInfo(); </button> </div> </div> +<script> + require([ + 'jquery' + ], function (jQuery) { + /* hide the close button when the content doesn't open in a modal window */ + if (window.opener === null || typeof window.opener === "undefined") { + jQuery('.actions button.close').hide(); + } + }); +</script> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml index 8a9d5fc0fe371..e15c39367529f 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml @@ -1,17 +1,15 @@ <?php /** -* Copyright © Magento, Inc. All rights reserved. -* See COPYING.txt for license details. -*/ - -// @codingStandardsIgnoreFile + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ /** @var $block \Magento\Framework\View\Element\Template */ $parentBlock = $block->getParentBlock(); $track = $block->getData('track'); ?> <div class="table-wrapper"> - <table class="data table order tracking" id="track-history-table-<?= /* @noEscape */ $track->getTracking() ?>"> + <table class="data table order tracking" id="track-history-table-<?= $block->escapeHtml($track->getTracking()) ?>"> <caption class="table-caption"><?= $block->escapeHtml(__('Track history')) ?></caption> <thead> <tr> @@ -22,9 +20,13 @@ $track = $block->getData('track'); </tr> </thead> <tbody> - <?php foreach ($track->getProgressdetail() as $detail): ?> - <?php $detailDate = (!empty($detail['deliverydate']) ? $parentBlock->formatDeliveryDate($detail['deliverydate']) : ''); ?> - <?php $detailTime = (!empty($detail['deliverytime']) ? $parentBlock->formatDeliveryTime($detail['deliverytime'], $detail['deliverydate']) : ''); ?> + <?php foreach ($track->getProgressdetail() as $detail) : ?> + <?php $detailDate = (!empty($detail['deliverydate']) ? + $parentBlock->formatDeliveryDate($detail['deliverydate'] . ' ' . $detail['deliverytime']) : + ''); ?> + <?php $detailTime = (!empty($detail['deliverytime']) ? + $parentBlock->formatDeliveryTime($detail['deliverytime'], $detail['deliverydate']) : + ''); ?> <tr> <td data-th="<?= $block->escapeHtml(__('Location')) ?>" class="col location"> <?= (!empty($detail['deliverylocation']) ? $block->escapeHtml($detail['deliverylocation']) : '') ?> diff --git a/app/code/Magento/Signifyd/Block/Fingerprint.php b/app/code/Magento/Signifyd/Block/Fingerprint.php index 7afa092b3d0da..f43bffce1fc1a 100644 --- a/app/code/Magento/Signifyd/Block/Fingerprint.php +++ b/app/code/Magento/Signifyd/Block/Fingerprint.php @@ -42,7 +42,7 @@ class Fingerprint extends Template * @var string * @since 100.2.0 */ - protected $_template = 'fingerprint.phtml'; + protected $_template = 'Magento_Signifyd::fingerprint.phtml'; /** * @param Context $context @@ -85,6 +85,8 @@ public function getSignifydOrderSessionId() */ public function isModuleActive() { - return $this->config->isActive(); + $storeId = $this->quoteSession->getQuote()->getStoreId(); + + return $this->config->isActive($storeId); } } diff --git a/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php index 870705db941cc..168ab67f8cf50 100644 --- a/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php +++ b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php @@ -5,8 +5,8 @@ */ namespace Magento\Signifyd\Model\CaseServices; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NotFoundException; use Magento\Signifyd\Api\CaseRepositoryInterface; use Magento\Signifyd\Api\Data\CaseInterface; use Magento\Signifyd\Model\CommentsHistoryUpdater; @@ -73,7 +73,6 @@ public function __construct( * @param CaseInterface $case * @param array $data * @return void - * @throws NotFoundException * @throws LocalizedException */ public function update(CaseInterface $case, array $data) @@ -111,7 +110,7 @@ private function setCaseData(CaseInterface $case, array $data) 'orderId' ]; foreach ($data as $key => $value) { - $methodName = 'set' . ucfirst($key); + $methodName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key); if (!in_array($key, $notResolvedKeys) && method_exists($case, $methodName)) { call_user_func([$case, $methodName], $value); } diff --git a/app/code/Magento/Signifyd/Model/Config.php b/app/code/Magento/Signifyd/Model/Config.php index b68380ee15bf3..9beb281de0550 100644 --- a/app/code/Magento/Signifyd/Model/Config.php +++ b/app/code/Magento/Signifyd/Model/Config.php @@ -34,13 +34,15 @@ public function __construct(ScopeConfigInterface $scopeConfig) * If this config option set to false no Signifyd integration should be available * (only possibility to configure Signifyd setting in admin) * + * @param int|null $storeId * @return bool */ - public function isActive() + public function isActive($storeId = null) { $enabled = $this->scopeConfig->isSetFlag( 'fraud_protection/signifyd/active', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $enabled; } @@ -51,13 +53,15 @@ public function isActive() * @see https://www.signifyd.com/docs/api/#/introduction/authentication * @see https://app.signifyd.com/settings * + * @param int|null $storeId * @return string */ - public function getApiKey() + public function getApiKey($storeId = null) { $apiKey = $this->scopeConfig->getValue( 'fraud_protection/signifyd/api_key', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $apiKey; } @@ -66,13 +70,15 @@ public function getApiKey() * Base URL to Signifyd REST API. * Usually equals to https://api.signifyd.com/v2 and should not be changed * + * @param int|null $storeId * @return string */ - public function getApiUrl() + public function getApiUrl($storeId = null) { $apiUrl = $this->scopeConfig->getValue( 'fraud_protection/signifyd/api_url', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $apiUrl; } @@ -80,13 +86,15 @@ public function getApiUrl() /** * If is "true" extra information about interaction with Signifyd API are written to debug.log file * + * @param int|null $storeId * @return bool */ - public function isDebugModeEnabled() + public function isDebugModeEnabled($storeId = null) { $debugModeEnabled = $this->scopeConfig->isSetFlag( 'fraud_protection/signifyd/debug', - ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE, + $storeId ); return $debugModeEnabled; } diff --git a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php index a26beda520944..5be5ccbc5e55a 100644 --- a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php +++ b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php @@ -60,7 +60,7 @@ public function __construct( * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentCvv($paymentCode) { @@ -73,7 +73,7 @@ public function createPaymentCvv($paymentCode) * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentAvs($paymentCode) { diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php index 0950ca1e22cfa..b69137903f42e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/ApiClient.php @@ -36,12 +36,15 @@ public function __construct( * * @param string $url * @param string $method - * @param array $params + * @param array $params + * @param int|null $storeId * @return array + * @throws ApiCallException + * @throws \Zend_Http_Client_Exception */ - public function makeApiCall($url, $method, array $params = []) + public function makeApiCall($url, $method, array $params = [], $storeId = null) { - $result = $this->requestBuilder->doRequest($url, $method, $params); + $result = $this->requestBuilder->doRequest($url, $method, $params, $storeId); return $result; } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php index 41006bd7d1e0e..6390b85c52a5a 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/HttpClientFactory.php @@ -73,12 +73,14 @@ public function __construct( * @param string $url * @param string $method * @param array $params + * @param int|null $storeId * @return ZendClient + * @throws \Zend_Http_Client_Exception */ - public function create($url, $method, array $params = []) + public function create($url, $method, array $params = [], $storeId = null) { - $apiKey = $this->getApiKey(); - $apiUrl = $this->buildFullApiUrl($url); + $apiKey = $this->getApiKey($storeId); + $apiUrl = $this->buildFullApiUrl($url, $storeId); $client = $this->createNewClient(); $client->setHeaders( @@ -107,22 +109,24 @@ private function createNewClient() * Signifyd API key for merchant account. * * @see https://www.signifyd.com/docs/api/#/introduction/authentication + * @param int|null $storeId * @return string */ - private function getApiKey() + private function getApiKey($storeId) { - return $this->config->getApiKey(); + return $this->config->getApiKey($storeId); } /** * Full URL for Singifyd API based on relative URL. * * @param string $url + * @param int|null $storeId * @return string */ - private function buildFullApiUrl($url) + private function buildFullApiUrl($url, $storeId) { - $baseApiUrl = $this->getBaseApiUrl(); + $baseApiUrl = $this->getBaseApiUrl($storeId); $fullUrl = $baseApiUrl . self::$urlSeparator . ltrim($url, self::$urlSeparator); return $fullUrl; @@ -131,11 +135,12 @@ private function buildFullApiUrl($url) /** * Base Sigifyd API URL without trailing slash. * + * @param int|null $storeId * @return string */ - private function getBaseApiUrl() + private function getBaseApiUrl($storeId) { - $baseApiUrl = $this->config->getApiUrl(); + $baseApiUrl = $this->config->getApiUrl($storeId); return rtrim($baseApiUrl, self::$urlSeparator); } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php index 2ab4395e1990d..aab5ea3b6428c 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestBuilder.php @@ -5,7 +5,7 @@ */ namespace Magento\Signifyd\Model\SignifydGateway\Client; -use Magento\Framework\HTTP\ZendClient; +use Magento\Signifyd\Model\SignifydGateway\ApiCallException; /** * Class RequestBuilder @@ -50,13 +50,16 @@ public function __construct( * * @param string $url * @param string $method - * @param array $params + * @param array $params + * @param int|null $storeId * @return array + * @throws ApiCallException + * @throws \Zend_Http_Client_Exception */ - public function doRequest($url, $method, array $params = []) + public function doRequest($url, $method, array $params = [], $storeId = null) { - $client = $this->clientCreator->create($url, $method, $params); - $response = $this->requestSender->send($client); + $client = $this->clientCreator->create($url, $method, $params, $storeId); + $response = $this->requestSender->send($client, $storeId); $result = $this->responseHandler->handle($response); return $result; diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php index 38128a799fd59..b9f02fc83b931 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Client/RequestSender.php @@ -39,15 +39,16 @@ public function __construct( * debug information is recorded to debug.log. * * @param ZendClient $client + * @param int|null $storeId * @return \Zend_Http_Response * @throws ApiCallException */ - public function send(ZendClient $client) + public function send(ZendClient $client, $storeId = null) { try { $response = $client->request(); - $this->debuggerFactory->create()->success( + $this->debuggerFactory->create($storeId)->success( $client->getUri(true), $client->getLastRequest(), $response->getStatus() . ' ' . $response->getMessage(), @@ -56,7 +57,7 @@ public function send(ZendClient $client) return $response; } catch (\Exception $e) { - $this->debuggerFactory->create()->failure( + $this->debuggerFactory->create($storeId)->failure( $client->getUri(true), $client->getLastRequest(), $e diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php index 02031e6f5b9b5..3d7eee1b92f20 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php @@ -30,7 +30,7 @@ class DebuggerFactory /** * DebuggerFactory constructor. * - * @param bjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $config */ public function __construct( @@ -44,11 +44,12 @@ public function __construct( /** * Create debugger instance * + * @param int|null $storeId * @return DebuggerInterface */ - public function create() + public function create($storeId = null) { - if (!$this->config->isDebugModeEnabled()) { + if (!$this->config->isDebugModeEnabled($storeId)) { return $this->objectManager->get(BlackHole::class); } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php index ddcaa6cd696f2..f8840b1f6fe71 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Gateway.php @@ -5,8 +5,9 @@ */ namespace Magento\Signifyd\Model\SignifydGateway; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Signifyd\Api\CaseRepositoryInterface; use Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilderInterface; -use Magento\Signifyd\Model\SignifydGateway\ApiClient; /** * Signifyd Gateway. @@ -53,18 +54,34 @@ class Gateway */ private $apiClient; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var CaseRepositoryInterface + */ + private $caseRepository; + /** * Gateway constructor. * * @param CreateCaseBuilderInterface $createCaseBuilder * @param ApiClient $apiClient + * @param OrderRepositoryInterface $orderRepository + * @param CaseRepositoryInterface $caseRepository */ public function __construct( CreateCaseBuilderInterface $createCaseBuilder, - ApiClient $apiClient + ApiClient $apiClient, + OrderRepositoryInterface $orderRepository, + CaseRepositoryInterface $caseRepository ) { $this->createCaseBuilder = $createCaseBuilder; $this->apiClient = $apiClient; + $this->orderRepository = $orderRepository; + $this->caseRepository = $caseRepository; } /** @@ -74,15 +91,18 @@ public function __construct( * @param int $orderId * @return int Signifyd case (investigation) identifier * @throws GatewayException + * @throws \Zend_Http_Client_Exception */ public function createCase($orderId) { $caseParams = $this->createCaseBuilder->build($orderId); + $storeId = $this->getStoreIdFromOrder($orderId); $caseCreationResult = $this->apiClient->makeApiCall( '/cases', 'POST', - $caseParams + $caseParams, + $storeId ); if (!isset($caseCreationResult['investigationId'])) { @@ -99,15 +119,18 @@ public function createCase($orderId) * @param int $signifydCaseId * @return string * @throws GatewayException + * @throws \Zend_Http_Client_Exception */ public function submitCaseForGuarantee($signifydCaseId) { + $storeId = $this->getStoreIdFromCase($signifydCaseId); $guaranteeCreationResult = $this->apiClient->makeApiCall( '/guarantees', 'POST', [ 'caseId' => $signifydCaseId, - ] + ], + $storeId ); $disposition = $this->processDispositionResult($guaranteeCreationResult); @@ -121,15 +144,18 @@ public function submitCaseForGuarantee($signifydCaseId) * @param int $caseId * @return string * @throws GatewayException + * @throws \Zend_Http_Client_Exception */ public function cancelGuarantee($caseId) { + $storeId = $this->getStoreIdFromCase($caseId); $result = $this->apiClient->makeApiCall( '/cases/' . $caseId . '/guarantee', 'PUT', [ 'guaranteeDisposition' => self::GUARANTEE_CANCELED - ] + ], + $storeId ); $disposition = $this->processDispositionResult($result); @@ -172,4 +198,31 @@ private function processDispositionResult(array $result) return $disposition; } + + /** + * Returns store id by case. + * + * @param int $caseId + * @return int|null + */ + private function getStoreIdFromCase(int $caseId) + { + $case = $this->caseRepository->getByCaseId($caseId); + $orderId = $case->getOrderId(); + + return $this->getStoreIdFromOrder($orderId); + } + + /** + * Returns store id from order. + * + * @param int $orderId + * @return int|null + */ + private function getStoreIdFromOrder(int $orderId) + { + $order = $this->orderRepository->get($orderId); + + return $order->getStoreId(); + } } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php index 858ce0f0f3287..5e544e4b4048e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php @@ -7,12 +7,13 @@ use Magento\Framework\App\Area; use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Intl\DateTimeFactory; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Model\Order; +use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; use Magento\Signifyd\Model\PaymentVerificationFactory; use Magento\Signifyd\Model\SignifydOrderSessionId; -use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; /** * Prepare data related to purchase event represented in case creation request. @@ -72,6 +73,7 @@ public function __construct( * * @param Order $order * @return array + * @throws ConfigurationMismatchException */ public function build(Order $order) { @@ -202,6 +204,7 @@ private function getOrderChannel() * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getAvsCode(OrderPaymentInterface $orderPayment) { @@ -214,6 +217,7 @@ private function getAvsCode(OrderPaymentInterface $orderPayment) * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getCvvCode(OrderPaymentInterface $orderPayment) { diff --git a/app/code/Magento/Signifyd/Observer/PlaceOrder.php b/app/code/Magento/Signifyd/Observer/PlaceOrder.php index 3798522dbe506..90f601bede8a6 100644 --- a/app/code/Magento/Signifyd/Observer/PlaceOrder.php +++ b/app/code/Magento/Signifyd/Observer/PlaceOrder.php @@ -9,6 +9,7 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\NotFoundException; use Magento\Sales\Api\Data\OrderInterface; use Magento\Signifyd\Api\CaseCreationServiceInterface; use Magento\Signifyd\Model\Config; @@ -52,13 +53,10 @@ public function __construct( /** * {@inheritdoc} + * @throws NotFoundException */ public function execute(Observer $observer) { - if (!$this->signifydIntegrationConfig->isActive()) { - return; - } - $orders = $this->extractOrders( $observer->getEvent() ); @@ -68,7 +66,10 @@ public function execute(Observer $observer) } foreach ($orders as $order) { - $this->createCaseForOrder($order); + $storeId = $order->getStoreId(); + if ($this->signifydIntegrationConfig->isActive($storeId)) { + $this->createCaseForOrder($order); + } } } @@ -77,6 +78,7 @@ public function execute(Observer $observer) * * @param OrderInterface $order * @return void + * @throws NotFoundException */ private function createCaseForOrder($order) { diff --git a/app/code/Magento/Signifyd/Test/Mftf/LICENSE.txt b/app/code/Magento/Signifyd/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Signifyd/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 <insert your license name here>" 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/app/code/Magento/Signifyd/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Signifyd/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Signifyd/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 <insert your license name here>" 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/app/code/Magento/Signifyd/Test/Mftf/README.md b/app/code/Magento/Signifyd/Test/Mftf/README.md new file mode 100644 index 0000000000000..9391d7b314ea5 --- /dev/null +++ b/app/code/Magento/Signifyd/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Signifyd Functional Tests + +The Functional Test Module for **Magento Signifyd** module. diff --git a/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php b/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php index 1a8cfdc703247..8b98be338b973 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Controller/Webhooks/HandlerTest.php @@ -140,7 +140,7 @@ protected function setUp() } /** - * Successfull case + * Successful case */ public function testExecuteSuccessfully() { diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php index 7ba3ab3eef4f6..6b7a6112a932e 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreateGuaranteeAbilityTest.php @@ -198,6 +198,9 @@ public function testIsAvailableWithCanceledOrder($state) $this->assertFalse($this->createGuaranteeAbility->isAvailable($orderId)); } + /** + * @return array + */ public function isAvailableWithCanceledOrderDataProvider() { return [ diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php index a22bfe12222a6..f80a9a83e7117 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/Guarantee/CreationServiceTest.php @@ -203,6 +203,12 @@ public function testCreateForOrderWithCaseUpdate() ); } + /** + * @param $orderId + * @param array $caseData + * + * @return MockObject + */ private function withCaseEntityExistsForOrderId($orderId, array $caseData = []) { $this->createGuaranteeAbility->expects(self::once()) @@ -226,6 +232,9 @@ private function withCaseEntityExistsForOrderId($orderId, array $caseData = []) return $dummyCaseEntity; } + /** + * @param $failureMessage + */ private function withGatewayFailure($failureMessage) { $this->gateway @@ -233,6 +242,9 @@ private function withGatewayFailure($failureMessage) ->willThrowException(new GatewayException($failureMessage)); } + /** + * @param $gatewayResult + */ private function withGatewaySuccess($gatewayResult) { $this->gateway diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php index 776e8a75b9646..4aefd63355773 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/HttpClientFactoryTest.php @@ -101,14 +101,17 @@ public function testCreateHttpClient() public function testCreateWithParams() { $param = ['id' => 1]; + $storeId = 1; $json = '{"id":1}'; $this->config->expects($this->once()) ->method('getApiKey') + ->with($storeId) ->willReturn('testKey'); $this->config->expects($this->once()) ->method('getApiUrl') + ->with($storeId) ->willReturn(self::$dummy); $this->dataEncoder->expects($this->once()) @@ -121,7 +124,7 @@ public function testCreateWithParams() ->with($this->equalTo($json), 'application/json') ->willReturnSelf(); - $client = $this->httpClient->create('url', 'method', $param); + $client = $this->httpClient->create('url', 'method', $param, $storeId); $this->assertInstanceOf(ZendClient::class, $client); } diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php index bf0c6ee238d5f..1ee55d7ad150c 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/Client/ResponseHandlerTest.php @@ -92,6 +92,9 @@ public function testHandleFailureMessage($code, $message) } } + /** + * @return array + */ public function errorsProvider() { return [ diff --git a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php index f7aa65f842b91..ce97a7baaeace 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Model/SignifydGateway/GatewayTest.php @@ -5,7 +5,10 @@ */ namespace Magento\Signifyd\Test\Unit\Model\SignifydGateway; -use \PHPUnit\Framework\TestCase as TestCase; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Signifyd\Api\CaseRepositoryInterface; +use Magento\Signifyd\Api\Data\CaseInterface; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Magento\Signifyd\Model\SignifydGateway\Gateway; use Magento\Signifyd\Model\SignifydGateway\GatewayException; @@ -30,6 +33,16 @@ class GatewayTest extends \PHPUnit\Framework\TestCase */ private $gateway; + /** + * @var OrderRepositoryInterface|MockObject + */ + private $orderRepository; + + /** + * @var CaseRepositoryInterface|MockObject + */ + private $caseRepository; + public function setUp() { $this->createCaseBuilder = $this->getMockBuilder(CreateCaseBuilderInterface::class) @@ -39,16 +52,27 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); + $this->orderRepository = $this->getMockBuilder(OrderRepositoryInterface::class) + ->getMockForAbstractClass(); + + $this->caseRepository= $this->getMockBuilder(CaseRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->gateway = new Gateway( $this->createCaseBuilder, - $this->apiClient + $this->apiClient, + $this->orderRepository, + $this->caseRepository ); } public function testCreateCaseForSpecifiedOrder() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -68,7 +92,10 @@ public function testCreateCaseForSpecifiedOrder() public function testCreateCaseCallsValidApiMethod() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -79,7 +106,8 @@ public function testCreateCaseCallsValidApiMethod() ->with( $this->equalTo('/cases'), $this->equalTo('POST'), - $this->isType('array') + $this->isType('array'), + $this->equalTo($dummyStoreId) ) ->willReturn([ 'investigationId' => $dummySignifydInvestigationId @@ -92,7 +120,10 @@ public function testCreateCaseCallsValidApiMethod() public function testCreateCaseNormalFlow() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -113,7 +144,10 @@ public function testCreateCaseNormalFlow() public function testCreateCaseWithFailedApiCall() { $dummyOrderId = 1; + $dummyStoreId = 2; $apiCallFailureMessage = 'Api call failed'; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -131,6 +165,9 @@ public function testCreateCaseWithFailedApiCall() public function testCreateCaseWithMissedResponseRequiredData() { $dummyOrderId = 1; + $dummyStoreId = 2; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -147,7 +184,10 @@ public function testCreateCaseWithMissedResponseRequiredData() public function testCreateCaseWithAdditionalResponseData() { $dummyOrderId = 1; + $dummyStoreId = 2; $dummySignifydInvestigationId = 42; + + $this->withOrderEntity($dummyOrderId, $dummyStoreId); $this->createCaseBuilder ->method('build') ->willReturn([]); @@ -169,8 +209,10 @@ public function testCreateCaseWithAdditionalResponseData() public function testSubmitCaseForGuaranteeCallsValidApiMethod() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyDisposition = 'APPROVED'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->expects($this->atLeastOnce()) ->method('makeApiCall') @@ -179,7 +221,8 @@ public function testSubmitCaseForGuaranteeCallsValidApiMethod() $this->equalTo('POST'), $this->equalTo([ 'caseId' => $dummySygnifydCaseId - ]) + ]), + $this->equalTo($dummyStoreId) )->willReturn([ 'disposition' => $dummyDisposition ]); @@ -191,8 +234,10 @@ public function testSubmitCaseForGuaranteeCallsValidApiMethod() public function testSubmitCaseForGuaranteeWithFailedApiCall() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $apiCallFailureMessage = 'Api call failed'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willThrowException(new ApiCallException($apiCallFailureMessage)); @@ -208,10 +253,12 @@ public function testSubmitCaseForGuaranteeWithFailedApiCall() public function testSubmitCaseForGuaranteeReturnsDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyDisposition = 'APPROVED'; $dummyGuaranteeId = 123; $dummyRereviewCount = 0; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -231,9 +278,11 @@ public function testSubmitCaseForGuaranteeReturnsDisposition() public function testSubmitCaseForGuaranteeWithMissedDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyGuaranteeId = 123; $dummyRereviewCount = 0; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -248,8 +297,10 @@ public function testSubmitCaseForGuaranteeWithMissedDisposition() public function testSubmitCaseForGuaranteeWithUnexpectedDisposition() { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; $dummyUnexpectedDisposition = 'UNEXPECTED'; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -267,7 +318,9 @@ public function testSubmitCaseForGuaranteeWithUnexpectedDisposition() public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpectedDisposition) { $dummySygnifydCaseId = 42; + $dummyStoreId = 1; + $this->withCaseEntity($dummySygnifydCaseId, $dummyStoreId); $this->apiClient ->method('makeApiCall') ->willReturn([ @@ -294,11 +347,20 @@ public function testSubmitCaseForGuaranteeWithExpectedDisposition($dummyExpected public function testCancelGuarantee() { $caseId = 123; + $dummyStoreId = 1; + $this->withCaseEntity($caseId, $dummyStoreId); $this->apiClient->expects(self::once()) ->method('makeApiCall') - ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED]) - ->willReturn(['disposition' => Gateway::GUARANTEE_CANCELED]); + ->with( + '/cases/' . $caseId . '/guarantee', + 'PUT', + ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED], + $dummyStoreId + ) + ->willReturn( + ['disposition' => Gateway::GUARANTEE_CANCELED] + ); $result = $this->gateway->cancelGuarantee($caseId); self::assertEquals(Gateway::GUARANTEE_CANCELED, $result); @@ -314,16 +376,26 @@ public function testCancelGuarantee() public function testCancelGuaranteeWithUnexpectedDisposition() { $caseId = 123; + $dummyStoreId = 1; + $this->withCaseEntity($caseId, $dummyStoreId); $this->apiClient->expects(self::once()) ->method('makeApiCall') - ->with('/cases/' . $caseId . '/guarantee', 'PUT', ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED]) + ->with( + '/cases/' . $caseId . '/guarantee', + 'PUT', + ['guaranteeDisposition' => Gateway::GUARANTEE_CANCELED], + $dummyStoreId + ) ->willReturn(['disposition' => Gateway::GUARANTEE_DECLINED]); $result = $this->gateway->cancelGuarantee($caseId); $this->assertEquals(Gateway::GUARANTEE_CANCELED, $result); } + /** + * @return array + */ public function supportedGuaranteeDispositionsProvider() { return [ @@ -335,4 +407,46 @@ public function supportedGuaranteeDispositionsProvider() 'UNREQUESTED' => ['UNREQUESTED'], ]; } + + /** + * Specifies order entity mock execution. + * + * @param int $orderId + * @param int $storeId + * @return void + */ + private function withOrderEntity($orderId, $storeId) + { + $orderEntity = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $orderEntity->method('getStoreId') + ->willReturn($storeId); + $this->orderRepository->method('get') + ->with($orderId) + ->willReturn($orderEntity); + } + + /** + * Specifies case entity mock execution. + * + * @param int $caseId + * @param int $storeId + * @return void + */ + private function withCaseEntity($caseId, $storeId) + { + $orderId = 1; + + $caseEntity = $this->getMockBuilder(CaseInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $caseEntity->method('getOrderId') + ->willReturn($orderId); + $this->caseRepository->method('getByCaseId') + ->with($caseId) + ->willReturn($caseEntity); + + $this->withOrderEntity($orderId, $storeId); + } } diff --git a/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php index e2870953ec280..5617f5552a374 100644 --- a/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php +++ b/app/code/Magento/Signifyd/Test/Unit/Observer/PlaceOrderTest.php @@ -97,7 +97,10 @@ protected function setUp() */ public function testExecuteWithDisabledModule() { - $this->withActiveSignifydIntegration(false); + $orderId = 1; + $storeId = 2; + $this->withActiveSignifydIntegration(false, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->creationService->expects(self::never()) ->method('createForOrder'); @@ -113,7 +116,7 @@ public function testExecuteWithDisabledModule() public function testExecuteWithoutOrder() { $this->withActiveSignifydIntegration(true); - $this->withOrderEntity(null); + $this->withOrderEntity(null, null); $this->creationService->expects(self::never()) ->method('createForOrder'); @@ -129,8 +132,9 @@ public function testExecuteWithoutOrder() public function testExecuteWithOfflinePayment() { $orderId = 1; - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $storeId = 2; + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(false); $this->creationService->expects(self::never()) @@ -147,10 +151,11 @@ public function testExecuteWithOfflinePayment() public function testExecuteWithFailedCaseCreation() { $orderId = 1; + $storeId = 2; $exceptionMessage = __('Case with the same order id already exists.'); - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(true); $this->creationService->method('createForOrder') @@ -172,9 +177,10 @@ public function testExecuteWithFailedCaseCreation() public function testExecute() { $orderId = 1; + $storeId = 2; - $this->withActiveSignifydIntegration(true); - $this->withOrderEntity($orderId); + $this->withActiveSignifydIntegration(true, $storeId); + $this->withOrderEntity($orderId, $storeId); $this->withAvailablePaymentMethod(true); $this->creationService @@ -191,9 +197,10 @@ public function testExecute() * Specifies order entity mock execution. * * @param int $orderId + * @param int $storeId * @return void */ - private function withOrderEntity($orderId) + private function withOrderEntity($orderId, $storeId) { $this->orderEntity = $this->getMockBuilder(OrderInterface::class) ->disableOriginalConstructor() @@ -201,6 +208,8 @@ private function withOrderEntity($orderId) $this->orderEntity->method('getEntityId') ->willReturn($orderId); + $this->orderEntity->method('getStoreId') + ->willReturn($storeId); $this->observer->method('getEvent') ->willReturn($this->event); @@ -214,11 +223,13 @@ private function withOrderEntity($orderId) * Specifies config mock execution. * * @param bool $isActive + * @param int|null $storeId * @return void */ - private function withActiveSignifydIntegration($isActive) + private function withActiveSignifydIntegration($isActive, $storeId = null) { $this->config->method('isActive') + ->with($storeId) ->willReturn($isActive); } diff --git a/app/code/Magento/Signifyd/composer.json b/app/code/Magento/Signifyd/composer.json index a162cef1614ca..a7e2c3af0395d 100644 --- a/app/code/Magento/Signifyd/composer.json +++ b/app/code/Magento/Signifyd/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-signifyd", "description": "Submitting Case Entry to Signifyd on Order Creation", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-config": "101.0.*", "magento/framework": "101.0.*", "magento/module-sales": "101.0.*", @@ -17,7 +17,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.7", "license": [ "proprietary" ], diff --git a/app/code/Magento/Signifyd/etc/adminhtml/system.xml b/app/code/Magento/Signifyd/etc/adminhtml/system.xml index d9ba2f7ffdff2..2dd75d2d91e5b 100644 --- a/app/code/Magento/Signifyd/etc/adminhtml/system.xml +++ b/app/code/Magento/Signifyd/etc/adminhtml/system.xml @@ -11,9 +11,9 @@ <label>Fraud Protection</label> <tab>sales</tab> <resource>Magento_Sales::fraud_protection</resource> - <group id="signifyd" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="signifyd" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0"> <fieldset_css>signifyd-logo-header</fieldset_css> - <group id="about" translate="label" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="about" translate="label comment" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> <frontend_model>Magento\Signifyd\Block\Adminhtml\System\Config\Fieldset\Info</frontend_model> <fieldset_css>signifyd-about-header</fieldset_css> <label><![CDATA[Protect your store from fraud with Guaranteed Fraud Protection by Signifyd.]]></label> @@ -26,12 +26,12 @@ </comment> <more_url>https://www.signifyd.com/magento-guaranteed-fraud-protection</more_url> </group> - <group id="config" translate="label" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <group id="config" translate="label comment" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> <fieldset_css>signifyd-about-header</fieldset_css> <label>Configuration</label> <comment><![CDATA[<a href="https://www.signifyd.com/resources/manual/magento-2/signifyd-on-magento-integration-guide/" target="_blank">View our setup guide</a> for step-by-step instructions on how to integrate Signifyd with Magento.<br />For support contact <a href="mailto:support@signifyd.com">support@signifyd.com</a>.]]> </comment> - <field id="active" translate="label comment" type="select" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="active" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Enable this Solution</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>fraud_protection/signifyd/active</config_path> @@ -52,7 +52,7 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <config_path>fraud_protection/signifyd/debug</config_path> </field> - <field id="webhook_url" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="webhook_url" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Webhook URL</label> <comment><![CDATA[Your webhook URL will be used to <a href="https://app.signifyd.com/settings/notifications" target="_blank">configure</a> a guarantee completed webhook in Signifyd. Webhooks are used to sync Signifyd`s guarantee decisions back to Magento.]]></comment> <attribute type="handler_url">signifyd/webhooks/handler</attribute> diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml index 92ad8a0bfd87a..c586019ca3d12 100644 --- a/app/code/Magento/Signifyd/etc/di.xml +++ b/app/code/Magento/Signifyd/etc/di.xml @@ -15,11 +15,7 @@ <preference for="Magento\Signifyd\Api\GuaranteeCancelingServiceInterface" type="Magento\Signifyd\Model\Guarantee\CancelingService" /> <preference for="Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilderInterface" type="Magento\Signifyd\Model\SignifydGateway\Request\CreateCaseBuilder" /> - <virtualType name="SignifydAvsDefaultMapper" type="Magento\Signifyd\Model\PredefinedVerificationCode"> - <arguments> - <argument name="code" xsi:type="string">U</argument> - </arguments> - </virtualType> + <virtualType name="SignifydAvsDefaultMapper" type="Magento\Signifyd\Model\PredefinedVerificationCode" /> <virtualType name="SignifydCvvDefaultMapper" type="Magento\Signifyd\Model\PredefinedVerificationCode" /> <type name="Magento\Signifyd\Model\PaymentVerificationFactory"> @@ -72,6 +68,9 @@ <argument name="converter" xsi:type="object">Magento\Signifyd\Model\PaymentMethodMapper\XmlToArrayConfigConverter</argument> <argument name="schemaLocator" xsi:type="object">PaymentMapperSchemaLocator</argument> <argument name="fileName" xsi:type="string">signifyd_payment_mapping.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/config/payment_method_list/payment_method" xsi:type="string">name</item> + </argument> </arguments> </virtualType> <virtualType name="PaymentMethodConfigData" type="Magento\Framework\Config\Data"> diff --git a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml index 096a968167173..9ff952d04925d 100644 --- a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml +++ b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xml @@ -6,7 +6,7 @@ */ /** * Custom payment method might adds a block in payment_method_list e.g. - * <payment_method> + * <payment_method name="custom_payment_method"> * <magento_code>custom_payment_method</magento_code> * <signifyd_code>PAYMENT_CARD</signifyd_code> * </payment_method> @@ -18,63 +18,63 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Signifyd:etc/signifyd_payment_mapping.xsd"> <payment_method_list> - <payment_method> + <payment_method name="braintree"> <magento_code>braintree</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="braintree_paypal"> <magento_code>braintree_paypal</magento_code> <signifyd_code>PAYPAL_ACCOUNT</signifyd_code> </payment_method> - <payment_method> + <payment_method name="paypal_express"> <magento_code>paypal_express</magento_code> <signifyd_code>PAYPAL_ACCOUNT</signifyd_code> </payment_method> - <payment_method> + <payment_method name="paypal_express_bml"> <magento_code>paypal_express_bml</magento_code> <signifyd_code>PAYPAL_ACCOUNT</signifyd_code> </payment_method> - <payment_method> + <payment_method name="payflow_express"> <magento_code>payflow_express</magento_code> <signifyd_code>PAYPAL_ACCOUNT</signifyd_code> </payment_method> - <payment_method> + <payment_method name="payflow_express_bml"> <magento_code>payflow_express_bml</magento_code> <signifyd_code>PAYPAL_ACCOUNT</signifyd_code> </payment_method> - <payment_method> + <payment_method name="payflowpro"> <magento_code>payflowpro</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="payflow_link"> <magento_code>payflow_link</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="payflow_advanced"> <magento_code>payflow_advanced</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="hosted_pro"> <magento_code>hosted_pro</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="authorizenet_directpost"> <magento_code>authorizenet_directpost</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="worldpay"> <magento_code>worldpay</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="eway"> <magento_code>eway</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="cybersource"> <magento_code>cybersource</magento_code> <signifyd_code>PAYMENT_CARD</signifyd_code> </payment_method> - <payment_method> + <payment_method name="free"> <magento_code>free</magento_code> <signifyd_code>FREE</signifyd_code> </payment_method> diff --git a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd index f48805c328e09..bb3b3036d0c9c 100644 --- a/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd +++ b/app/code/Magento/Signifyd/etc/signifyd_payment_mapping.xsd @@ -23,5 +23,12 @@ <xs:element minOccurs="1" name="magento_code"/> <xs:element minOccurs="1" name="signifyd_code"/> </xs:sequence> + <xs:attribute name="name" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + Element's unique identifier. + </xs:documentation> + </xs:annotation> + </xs:attribute> </xs:complexType> </xs:schema> diff --git a/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml b/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml index a42b7f93b5b92..07f1b2c2e4ae6 100644 --- a/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml +++ b/app/code/Magento/Signifyd/view/adminhtml/templates/case_info.phtml @@ -3,15 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile ?> <?php /** @var $block Magento\Signifyd\Block\Adminhtml\CaseInfo */ ?> <?php - - if ($block->isEmptyCase()) { - return ''; - } +if ($block->isEmptyCase()) { + return ''; +} ?> <section class="admin__page-section order-case-info"> <div class="admin__page-section-title"> diff --git a/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml b/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml index 356bab9c62ded..657043b895f60 100644 --- a/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml +++ b/app/code/Magento/Signifyd/view/frontend/templates/fingerprint.phtml @@ -4,11 +4,9 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var $block Magento\Signifyd\Block\Fingerprint */ ?> -<?php if ($block->isModuleActive()): ?> +<?php if ($block->isModuleActive()) : ?> <script async id="sig-api" diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php b/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php index d1b5fd1df45c4..ff3334dc38531 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php @@ -5,6 +5,9 @@ */ namespace Magento\Sitemap\Block\Adminhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Helper\PostHelper; + /** * Sitemap edit form container * @@ -19,18 +22,26 @@ class Edit extends \Magento\Backend\Block\Widget\Form\Container */ protected $_coreRegistry = null; + /** + * @var PostHelper + */ + private $postHelper; + /** * @param \Magento\Backend\Block\Widget\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param PostHelper|null $postHelper */ public function __construct( \Magento\Backend\Block\Widget\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + PostHelper $postHelper = null ) { $this->_coreRegistry = $registry; parent::__construct($context, $data); + $this->postHelper = $postHelper ?: ObjectManager::getInstance()->create(PostHelper::class); } /** @@ -64,6 +75,30 @@ protected function _construct() ); } + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + $this->buttonList->update( + 'delete', + '', + [ + 'label' => __('Delete'), + 'class' => 'delete', + 'onclick' => 'deleteConfirm(\'Are you sure you want to do this?\', \'' . + $this->getDeleteUrl() . '\','.$this->postHelper->getPostData($this->getDeleteUrl()).')', + 'id' => 'delete', + 'button_key' => 'delete_button', + 'region' => 'toolbar', + 'level' => 0, + 'sort_order' => 10 + ] + ); + + parent::_prepareLayout(); + } + /** * Get edit form container header text * diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php index 5e90cf6e12f6e..45290c8ce0a9d 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php @@ -48,6 +48,8 @@ protected function _construct() } /** + * Configure form for sitemap. + * * @return $this */ protected function _prepareForm() @@ -73,7 +75,8 @@ protected function _prepareForm() 'name' => 'sitemap_filename', 'required' => true, 'note' => __('example: sitemap.xml'), - 'value' => $model->getSitemapFilename() + 'value' => $model->getSitemapFilename(), + 'class' => 'validate-length maximum-length-32', ] ); diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php b/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php index ffc2bf5f6d1cf..ac845ce8cb858 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php @@ -62,6 +62,7 @@ public function render(\Magento\Framework\DataObject $row) { /** @var $sitemap \Magento\Sitemap\Model\Sitemap */ $sitemap = $this->_sitemapFactory->create(); + $sitemap->setStoreId($row->getStoreId()); $url = $this->escapeHtml($sitemap->getSitemapUrl($row->getSitemapPath(), $row->getSitemapFilename())); $fileName = preg_replace('/^\//', '', $row->getSitemapPath() . $row->getSitemapFilename()); diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index f0be0fe7ab682..fe2b9d5a846a6 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NotFoundException; class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap { @@ -39,9 +40,15 @@ public function __construct( * Delete action * * @return void + * @throws NotFoundException + * @throws \Magento\Framework\Exception\FileSystemException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $directory = $this->getFilesystem()->getDirectoryWrite(DirectoryList::ROOT); // check if we know what should be deleted $id = $this->getRequest()->getParam('sitemap_id'); @@ -53,6 +60,9 @@ public function execute() $sitemap->load($id); // delete file $sitemapPath = $sitemap->getSitemapPath(); + if ($sitemapPath && $sitemapPath[0] === DIRECTORY_SEPARATOR) { + $sitemapPath = mb_substr($sitemapPath, 1); + } $sitemapFilename = $sitemap->getSitemapFilename(); $path = $directory->getRelativePath($sitemapPath . $sitemapFilename); @@ -61,20 +71,20 @@ public function execute() } $sitemap->delete(); // display success message - $this->messageManager->addSuccess(__('You deleted the sitemap.')); + $this->messageManager->addSuccessMessage(__('You deleted the sitemap.')); // go to grid $this->_redirect('adminhtml/*/'); return; } catch (\Exception $e) { // display error message - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); // go back to edit form $this->_redirect('adminhtml/*/edit', ['sitemap_id' => $id]); return; } } // display error message - $this->messageManager->addError(__('We can\'t find a sitemap to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a sitemap to delete.')); // go to grid $this->_redirect('adminhtml/*/'); } diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php index 04ab4f8725e0e..111353550b9cd 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php @@ -41,7 +41,7 @@ public function execute() if ($id) { $model->load($id); if (!$model->getId()) { - $this->messageManager->addError(__('This sitemap no longer exists.')); + $this->messageManager->addErrorMessage(__('This sitemap no longer exists.')); $this->_redirect('adminhtml/*/'); return; } diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php index 67d2ce4f4f148..9592ab6f57c55 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php @@ -6,8 +6,29 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Backend\App\Action; +use Magento\Store\Model\App\Emulation; +use Magento\Framework\App\ObjectManager; + class Generate extends \Magento\Sitemap\Controller\Adminhtml\Sitemap { + /** @var \Magento\Store\Model\App\Emulation $appEmulation */ + private $appEmulation; + + /** + * Generate constructor. + * @param Action\Context $context + * @param \Magento\Store\Model\App\Emulation|null $appEmulation + */ + public function __construct( + Action\Context $context, + Emulation $appEmulation = null + ) { + parent::__construct($context); + $this->appEmulation = $appEmulation ?: ObjectManager::getInstance() + ->get(\Magento\Store\Model\App\Emulation::class); + } + /** * Generate sitemap * @@ -23,18 +44,26 @@ public function execute() // if sitemap record exists if ($sitemap->getId()) { try { + //We need to emulate to get the correct frontend URL for the product images + $this->appEmulation->startEnvironmentEmulation( + $sitemap->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $sitemap->generateXml(); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename()) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t generate the sitemap right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t generate the sitemap right now.')); + } finally { + $this->appEmulation->stopEnvironmentEmulation(); } } else { - $this->messageManager->addError(__('We can\'t find a sitemap to generate.')); + $this->messageManager->addErrorMessage(__('We can\'t find a sitemap to generate.')); } // go to grid diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 5c38cc68e6ef7..c2e62235cc6d9 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php @@ -5,39 +5,112 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; -use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Controller; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Validator\StringLength; +use Magento\MediaStorage\Model\File\Validator\AvailablePath; +use Magento\Sitemap\Model\SitemapFactory; +/** + * Save sitemap controller. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Save extends \Magento\Sitemap\Controller\Adminhtml\Sitemap { + /** + * Maximum length of sitemap filename + */ + const MAX_FILENAME_LENGTH = 32; + + /** + * @var StringLength + */ + private $stringValidator; + + /** + * @var AvailablePath + */ + private $pathValidator; + + /** + * @var \Magento\Sitemap\Helper\Data + */ + private $sitemapHelper; + + /** + * @var \Magento\Framework\Filesystem + */ + private $filesystem; + + /** + * @var SitemapFactory + */ + private $sitemapFactory; + + /** + * @param Context $context + * @param StringLength|null $stringValidator + * @param AvailablePath|null $pathValidator + * @param \Magento\Sitemap\Helper\Data|null $sitemapHelper + * @param \Magento\Framework\Filesystem|null $filesystem + * @param SitemapFactory|null $sitemapFactory + */ + public function __construct( + Context $context, + StringLength $stringValidator = null, + AvailablePath $pathValidator = null, + \Magento\Sitemap\Helper\Data $sitemapHelper = null, + \Magento\Framework\Filesystem $filesystem = null, + SitemapFactory $sitemapFactory = null + ) { + parent::__construct($context); + $this->stringValidator = $stringValidator ?: $this->_objectManager->get(StringLength::class); + $this->pathValidator = $pathValidator ?: $this->_objectManager->get(AvailablePath::class); + $this->sitemapHelper = $sitemapHelper ?: $this->_objectManager->get(\Magento\Sitemap\Helper\Data::class); + $this->filesystem = $filesystem ?: $this->_objectManager->get(\Magento\Framework\Filesystem::class); + $this->sitemapFactory = $sitemapFactory ?: $this->_objectManager->get(SitemapFactory::class); + } + /** * Validate path for generation * * @param array $data * @return bool - * @throws \Exception */ protected function validatePath(array $data) { if (!empty($data['sitemap_filename']) && !empty($data['sitemap_path'])) { $data['sitemap_path'] = '/' . ltrim($data['sitemap_path'], '/'); $path = rtrim($data['sitemap_path'], '\\/') . '/' . $data['sitemap_filename']; - /** @var $validator \Magento\MediaStorage\Model\File\Validator\AvailablePath */ - $validator = $this->_objectManager->create(\Magento\MediaStorage\Model\File\Validator\AvailablePath::class); - /** @var $helper \Magento\Sitemap\Helper\Data */ - $helper = $this->_objectManager->get(\Magento\Sitemap\Helper\Data::class); - $validator->setPaths($helper->getValidPaths()); - if (!$validator->isValid($path)) { - foreach ($validator->getMessages() as $message) { - $this->messageManager->addError($message); + $this->pathValidator->setPaths($this->sitemapHelper->getValidPaths()); + if (!$this->pathValidator->isValid($path)) { + foreach ($this->pathValidator->getMessages() as $message) { + $this->messageManager->addErrorMessage($message); } // save data in session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData($data); + $this->_session->setFormData($data); + + // redirect to edit form + return false; + } + + $filename = rtrim($data['sitemap_filename']); + $this->stringValidator->setMax(self::MAX_FILENAME_LENGTH); + if (!$this->stringValidator->isValid($filename)) { + foreach ($this->stringValidator->getMessages() as $message) { + $this->messageManager->addErrorMessage($message); + } + // save data in session + $this->_session->setFormData($data); + // redirect to edit form return false; } } + return true; } @@ -49,9 +122,8 @@ protected function validatePath(array $data) */ protected function clearSiteMap(\Magento\Sitemap\Model\Sitemap $model) { - /** @var \Magento\Framework\Filesystem\Directory\Write $directory */ - $directory = $this->_objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::ROOT); + /** @var \Magento\Framework\Filesystem $directory */ + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); if ($this->getRequest()->getParam('sitemap_id')) { $model->load($this->getRequest()->getParam('sitemap_id')); @@ -74,7 +146,7 @@ protected function saveData($data) { // init model and set data /** @var \Magento\Sitemap\Model\Sitemap $model */ - $model = $this->_objectManager->create(\Magento\Sitemap\Model\Sitemap::class); + $model = $this->sitemapFactory->create(); $this->clearSiteMap($model); $model->setData($data); @@ -83,16 +155,18 @@ protected function saveData($data) // save the data $model->save(); // display success message - $this->messageManager->addSuccess(__('You saved the sitemap.')); + $this->messageManager->addSuccessMessage(__('You saved the sitemap.')); // clear previously saved data from session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData(false); + $this->_session->setFormData(false); + return $model->getId(); } catch (\Exception $e) { // display error message - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); // save data in session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData($data); + $this->_session->setFormData($data); } + return false; } @@ -110,21 +184,25 @@ protected function getResult($id) // check if 'Save and Continue' if ($this->getRequest()->getParam('back')) { $resultRedirect->setPath('adminhtml/*/edit', ['sitemap_id' => $id]); + return $resultRedirect; } // go to grid or forward to generate action if ($this->getRequest()->getParam('generate')) { $this->getRequest()->setParam('sitemap_id', $id); + return $this->resultFactory->create(Controller\ResultFactory::TYPE_FORWARD) ->forward('generate'); } $resultRedirect->setPath('adminhtml/*/'); + return $resultRedirect; } $resultRedirect->setPath( 'adminhtml/*/edit', ['sitemap_id' => $this->getRequest()->getParam('sitemap_id')] ); + return $resultRedirect; } @@ -132,9 +210,14 @@ protected function getResult($id) * Save action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + // check if data sent $data = $this->getRequest()->getPostValue(); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ @@ -145,11 +228,14 @@ public function execute() 'adminhtml/*/edit', ['sitemap_id' => $this->getRequest()->getParam('sitemap_id')] ); + return $resultRedirect; } + return $this->getResult($this->saveData($data)); } $resultRedirect->setPath('adminhtml/*/'); + return $resultRedirect; } } diff --git a/app/code/Magento/Sitemap/Model/Observer.php b/app/code/Magento/Sitemap/Model/Observer.php index 3ae3061310a0b..a536ec998b827 100644 --- a/app/code/Magento/Sitemap/Model/Observer.php +++ b/app/code/Magento/Sitemap/Model/Observer.php @@ -19,6 +19,8 @@ class Observer /** * Cronjob expression configuration + * + * @deprecated Use \Magento\Cron\Model\Config\Backend\Sitemap::CRON_STRING_PATH instead. */ const XML_PATH_CRON_EXPR = 'crontab/default/jobs/generate_sitemaps/schedule/cron_expr'; @@ -113,7 +115,6 @@ public function scheduledGenerateSitemaps() $sitemap->generateXml(); } catch (\Exception $e) { $errors[] = $e->getMessage(); - throw $e; } } @@ -122,8 +123,7 @@ public function scheduledGenerateSitemaps() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) ) { - $translate = $this->_translateModel->getTranslateInline(); - $this->_translateModel->setTranslateInline(false); + $this->inlineTranslation->suspend(); $this->_transportBuilder->setTemplateIdentifier( $this->_scopeConfig->getValue( diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php index 8d7caca12b96f..8e37ab5fe8afe 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php @@ -8,6 +8,8 @@ use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Store\Model\Store; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\ScopeInterface; +use Magento\Catalog\Helper\Product as HelperProduct; /** * Sitemap resource product collection model @@ -88,6 +90,13 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @var \Magento\Catalog\Helper\Image */ private $catalogImageHelper; + + /** + * Scope Config + * + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context @@ -102,6 +111,7 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param string $connectionName * @param \Magento\Catalog\Model\Product $productModel * @param \Magento\Catalog\Helper\Image $catalogImageHelper + * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -116,7 +126,8 @@ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, $connectionName = null, \Magento\Catalog\Model\Product $productModel = null, - \Magento\Catalog\Helper\Image $catalogImageHelper = null + \Magento\Catalog\Helper\Image $catalogImageHelper = null, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null ) { $this->_productResource = $productResource; $this->_storeManager = $storeManager; @@ -129,6 +140,8 @@ public function __construct( $this->productModel = $productModel ?: ObjectManager::getInstance()->get(\Magento\Catalog\Model\Product::class); $this->catalogImageHelper = $catalogImageHelper ?: ObjectManager::getInstance() ->get(\Magento\Catalog\Helper\Image::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance() + ->get(\Magento\Framework\App\Config\ScopeConfigInterface::class); parent::__construct($context, $connectionName); } @@ -272,6 +285,10 @@ public function getCollection($storeId) } $connection = $this->getConnection(); + $urlsConfigCondition = ''; + if ($this->isCategoryProductURLsConfig($storeId)) { + $urlsConfigCondition = 'NOT '; + } $this->_select = $connection->select()->from( ['e' => $this->getMainTable()], @@ -282,7 +299,8 @@ public function getCollection($storeId) [] )->joinLeft( ['url_rewrite' => $this->getTable('url_rewrite')], - 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 AND url_rewrite.metadata IS NULL' + 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 AND url_rewrite.metadata IS ' + . $urlsConfigCondition . 'NULL' . $connection->quoteInto(' AND url_rewrite.store_id = ?', $store->getId()) . $connection->quoteInto(' AND url_rewrite.entity_type = ?', ProductUrlRewriteGenerator::ENTITY_TYPE), ['url' => 'request_path'] @@ -450,4 +468,20 @@ private function getProductImageUrl($image) return $imgUrl; } + + /** + * Return Use Categories Path for Product URLs config value + * + * @param $storeId + * + * @return bool + */ + private function isCategoryProductURLsConfig($storeId) + { + return (bool)$this->scopeConfig->getValue( + HelperProduct::XML_PATH_PRODUCT_URL_USE_CATEGORY, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } } diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php index d050ea84ecccb..01addd0c19666 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php @@ -6,12 +6,15 @@ namespace Magento\Sitemap\Model\ResourceModel\Cms; use Magento\Cms\Api\Data\PageInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\Model\ResourceModel\Db\Context; -use Magento\Framework\Model\AbstractModel; +use Magento\Cms\Api\GetUtilityPageIdentifiersInterface; use Magento\Cms\Model\Page as CmsPage; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; /** * Sitemap cms page collection model @@ -19,7 +22,7 @@ * @api * @since 100.0.2 */ -class Page extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Page extends AbstractDb { /** * @var MetadataPool @@ -34,19 +37,29 @@ class Page extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $entityManager; /** - * @param Context $context - * @param MetadataPool $metadataPool - * @param EntityManager $entityManager - * @param string $connectionName + * @var GetUtilityPageIdentifiersInterface + * @since 100.2.0 + */ + private $getUtilityPageIdentifiers; + + /** + * @param Context $context + * @param MetadataPool $metadataPool + * @param EntityManager $entityManager + * @param string $connectionName + * @param GetUtilityPageIdentifiersInterface $getUtilityPageIdentifiers */ public function __construct( Context $context, MetadataPool $metadataPool, EntityManager $entityManager, - $connectionName = null + $connectionName = null, + GetUtilityPageIdentifiersInterface $getUtilityPageIdentifiers = null ) { - $this->metadataPool = $metadataPool; - $this->entityManager = $entityManager; + $this->metadataPool = $metadataPool; + $this->entityManager = $entityManager; + $this->getUtilityPageIdentifiers = $getUtilityPageIdentifiers ?: + ObjectManager::getInstance()->get(GetUtilityPageIdentifiersInterface::class); parent::__construct($context, $connectionName); } @@ -90,8 +103,8 @@ public function getCollection($storeId) )->where( 'main_table.is_active = 1' )->where( - 'main_table.identifier != ?', - \Magento\Cms\Model\Page::NOROUTE_PAGE_ID + 'main_table.identifier NOT IN (?)', + $this->getUtilityPageIdentifiers->execute() )->where( 'store_table.store_id IN(?)', [0, $storeId] diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index f6a5f029eafca..0b5ba59cde373 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Sitemap\Model; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; @@ -155,12 +153,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Last mode min timestamp value @@ -273,30 +270,37 @@ public function collectSitemapItems() /** @var $helper \Magento\Sitemap\Helper\Data */ $helper = $this->_sitemapData; $storeId = $this->getStoreId(); - - $this->addSitemapItem(new DataObject( - [ - 'changefreq' => $helper->getCategoryChangefreq($storeId), - 'priority' => $helper->getCategoryPriority($storeId), - 'collection' => $this->_categoryFactory->create()->getCollection($storeId), - ] - )); - - $this->addSitemapItem(new DataObject( - [ - 'changefreq' => $helper->getProductChangefreq($storeId), - 'priority' => $helper->getProductPriority($storeId), - 'collection' => $this->_productFactory->create()->getCollection($storeId), - ] - )); - - $this->addSitemapItem(new DataObject( - [ - 'changefreq' => $helper->getPageChangefreq($storeId), - 'priority' => $helper->getPagePriority($storeId), - 'collection' => $this->_cmsFactory->create()->getCollection($storeId), - ] - )); + $this->_storeManager->setCurrentStore($storeId); + + $this->addSitemapItem( + new DataObject( + [ + 'changefreq' => $helper->getCategoryChangefreq($storeId), + 'priority' => $helper->getCategoryPriority($storeId), + 'collection' => $this->_categoryFactory->create()->getCollection($storeId), + ] + ) + ); + + $this->addSitemapItem( + new DataObject( + [ + 'changefreq' => $helper->getProductChangefreq($storeId), + 'priority' => $helper->getProductPriority($storeId), + 'collection' => $this->_productFactory->create()->getCollection($storeId), + ] + ) + ); + + $this->addSitemapItem( + new DataObject( + [ + 'changefreq' => $helper->getPageChangefreq($storeId), + 'priority' => $helper->getPagePriority($storeId), + 'collection' => $this->_cmsFactory->create()->getCollection($storeId), + ] + ) + ); } /** @@ -367,7 +371,8 @@ public function beforeSave() if (!preg_match('#^[a-zA-Z0-9_\.]+$#', $this->getSitemapFilename())) { throw new \Magento\Framework\Exception\LocalizedException( __( - 'Please use only letters (a-z or A-Z), numbers (0-9) or underscores (_) in the filename. No spaces or other characters are allowed.' + 'Please use only letters (a-z or A-Z), numbers (0-9) or underscores (_) in the filename.' + . ' No spaces or other characters are allowed.' ) ); } @@ -504,31 +509,31 @@ protected function _isSplitRequired($row) protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $priority = null, $images = null) { $url = $this->_getUrl($url); - $row = '<loc>' . htmlspecialchars($url) . '</loc>'; + $row = '<loc>' . $this->_escaper->escapeUrl($url) . '</loc>'; if ($lastmod) { $row .= '<lastmod>' . $this->_getFormattedLastmodDate($lastmod) . '</lastmod>'; } if ($changefreq) { - $row .= '<changefreq>' . $changefreq . '</changefreq>'; + $row .= '<changefreq>' . $this->_escaper->escapeHtml($changefreq) . '</changefreq>'; } if ($priority) { - $row .= sprintf('<priority>%.1f</priority>', $priority); + $row .= sprintf('<priority>%.1f</priority>', $this->_escaper->escapeHtml($priority)); } if ($images) { // Add Images to sitemap foreach ($images->getCollection() as $image) { $row .= '<image:image>'; - $row .= '<image:loc>' . htmlspecialchars($image->getUrl()) . '</image:loc>'; - $row .= '<image:title>' . htmlspecialchars($images->getTitle()) . '</image:title>'; + $row .= '<image:loc>' . $this->_escaper->escapeUrl($image->getUrl()) . '</image:loc>'; + $row .= '<image:title>' . $this->_escaper->escapeHtml($images->getTitle()) . '</image:title>'; if ($image->getCaption()) { - $row .= '<image:caption>' . htmlspecialchars($image->getCaption()) . '</image:caption>'; + $row .= '<image:caption>' . $this->_escaper->escapeHtml($image->getCaption()) . '</image:caption>'; } $row .= '</image:image>'; } // Add PageMap image for Google web search $row .= '<PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"><DataObject type="thumbnail">'; - $row .= '<Attribute name="name" value="' . htmlspecialchars($images->getTitle()) . '"/>'; - $row .= '<Attribute name="src" value="' . htmlspecialchars($images->getThumbnail()) . '"/>'; + $row .= '<Attribute name="name" value="' . $this->_escaper->escapeHtml($images->getTitle()) . '"/>'; + $row .= '<Attribute name="src" value="' . $this->_escaper->escapeUrl($images->getThumbnail()) . '"/>'; $row .= '</DataObject></PageMap>'; } @@ -545,7 +550,7 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr protected function _getSitemapIndexRow($sitemapFilename, $lastmod = null) { $url = $this->getSitemapUrl($this->getSitemapPath(), $sitemapFilename); - $row = '<loc>' . htmlspecialchars($url) . '</loc>'; + $row = '<loc>' . $this->_escaper->escapeUrl($url) . '</loc>'; if ($lastmod) { $row .= '<lastmod>' . $this->_getFormattedLastmodDate($lastmod) . '</lastmod>'; } @@ -613,7 +618,7 @@ protected function _finalizeSitemap($type = self::TYPE_URL) */ protected function _getCurrentSitemapFilename($index) { - return self::INDEX_FILE_PREFIX . '-' . $this->getStoreId() . '-' . $index . '.xml'; + return str_replace('.xml', '', $this->getSitemapFilename()) . '-' . $this->getStoreId() . '-' . $index . '.xml'; } /** @@ -687,6 +692,7 @@ protected function _getFormattedLastmodDate($date) */ protected function _getDocumentRoot() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction return realpath($this->_request->getServer('DOCUMENT_ROOT')); } @@ -697,6 +703,7 @@ protected function _getDocumentRoot() */ protected function _getStoreBaseDomain() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $storeParsedUrl = parse_url($this->_getStoreBaseUrl()); $url = $storeParsedUrl['scheme'] . '://' . $storeParsedUrl['host']; diff --git a/app/code/Magento/Sitemap/Test/Mftf/LICENSE.txt b/app/code/Magento/Sitemap/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Sitemap/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 <insert your license name here>" 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/app/code/Magento/Sitemap/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Sitemap/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Sitemap/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 <insert your license name here>" 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/app/code/Magento/Sitemap/Test/Mftf/README.md b/app/code/Magento/Sitemap/Test/Mftf/README.md new file mode 100644 index 0000000000000..8d506744827f6 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Sitemap Functional Tests + +The Functional Test Module for **Magento Sitemap** module. diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php index ed004fe88b318..6ac9ba2d91af9 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php @@ -49,7 +49,7 @@ protected function setUp() ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['isPost']) ->getMockForAbstractClass(); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) ->disableOriginalConstructor() @@ -81,6 +81,7 @@ public function testExecuteWithoutSitemapId() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn(0); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock->expects($this->once())->method('setRedirect'); $this->messageManagerMock->expects($this->any()) ->method('addError') @@ -97,6 +98,7 @@ public function testExecuteCannotFindSitemap() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) ->disableOriginalConstructor() @@ -123,6 +125,7 @@ public function testExecute() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) ->disableOriginalConstructor() @@ -135,7 +138,7 @@ public function testExecute() $this->sitemapFactoryMock->expects($this->once())->method('create')->willReturn($sitemapMock); $writeDirectoryMock->expects($this->any()) ->method('getRelativePath') - ->with($sitemapPath . $sitemapFilename) + ->with($sitemapFilename) ->willReturn($relativePath); $writeDirectoryMock->expects($this->once())->method('isFile')->with($relativePath)->willReturn(true); $writeDirectoryMock->expects($this->once())->method('delete')->with($relativePath)->willReturn(true); diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php index 36e3aa0312627..a5763f55c340d 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php @@ -7,54 +7,89 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\Controller\ResultFactory; +use Magento\Sitemap\Controller\Adminhtml\Sitemap\Save; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Sitemap\Controller\Adminhtml\Sitemap\Save */ - protected $saveController; + private $saveController; /** * @var \Magento\Backend\App\Action\Context */ - protected $context; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - */ - protected $objectManagerHelper; + private $contextMock; /** * @var \Magento\Framework\HTTP\PhpEnvironment\Request|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + private $requestMock; /** * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; + private $resultFactoryMock; /** * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectMock; + private $resultRedirectMock; /** * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; + + /** + * @var \Magento\Framework\Validator\StringLength|\PHPUnit_Framework_MockObject_MockObject + */ + private $lengthValidator; + + /** + * @var \Magento\MediaStorage\Model\File\Validator\AvailablePath|\PHPUnit_Framework_MockObject_MockObject + */ + private $pathValidator; + + /** + * @var \Magento\Sitemap\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helper; + /** + * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileSystem; + + /** + * @var \Magento\Sitemap\Model\SitemapFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $siteMapFactory; + + /** + * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $session; + + /** + * @inheritdoc + */ protected function setUp() { + $this->contextMock = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) + ->disableOriginalConstructor() + ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods(['getPostValue']) + ->setMethods(['getPostValue', 'isPost']) ->getMockForAbstractClass(); $this->resultRedirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) ->disableOriginalConstructor() @@ -66,35 +101,65 @@ protected function setUp() ->getMock(); $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->getMock(); + $this->helper = $this->getMockBuilder(\Magento\Sitemap\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->session = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->setMethods(['setFormData']) + ->getMock(); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(ResultFactory::TYPE_REDIRECT) - ->willReturn($this->resultRedirectMock); + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManagerMock); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->once()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); + $this->contextMock->expects($this->once()) + ->method('getSession') + ->willReturn($this->session); + + $this->lengthValidator = $this->getMockBuilder(\Magento\Framework\Validator\StringLength::class) + ->disableOriginalConstructor() + ->getMock(); + $this->pathValidator = + $this->getMockBuilder(\Magento\MediaStorage\Model\File\Validator\AvailablePath::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fileSystem = $this->createMock(\Magento\Framework\Filesystem::class); + $this->siteMapFactory = $this->createMock(\Magento\Sitemap\Model\SitemapFactory::class); $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->context = $this->objectManagerHelper->getObject( - \Magento\Backend\App\Action\Context::class, - [ - 'resultFactory' => $this->resultFactoryMock, - 'request' => $this->requestMock, - 'messageManager' => $this->messageManagerMock, - 'objectManager' => $this->objectManagerMock - ] - ); $this->saveController = $this->objectManagerHelper->getObject( - \Magento\Sitemap\Controller\Adminhtml\Sitemap\Save::class, + Save::class, [ - 'context' => $this->context + 'context' => $this->contextMock, + 'stringValidator' => $this->lengthValidator, + 'pathValidator' => $this->pathValidator, + 'sitemapHelper' => $this->helper, + 'filesystem' => $this->fileSystem, + 'sitemapFactory' => $this->siteMapFactory, ] ); } + /** + * @return void + */ public function testSaveEmptyDataShouldRedirectToDefault() { $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn([]); + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(true); + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); $this->resultRedirectMock->expects($this->once()) ->method('setPath') ->with('adminhtml/*/') @@ -103,58 +168,119 @@ public function testSaveEmptyDataShouldRedirectToDefault() $this->assertSame($this->resultRedirectMock, $this->saveController->execute()); } + /** + * @return void + */ public function testTryToSaveInvalidDataShouldFailWithErrors() { - $validatorClass = \Magento\MediaStorage\Model\File\Validator\AvailablePath::class; - $helperClass = \Magento\Sitemap\Helper\Data::class; $validPaths = []; $messages = ['message1', 'message2']; - $sessionClass = \Magento\Backend\Model\Session::class; $data = ['sitemap_filename' => 'sitemap_filename', 'sitemap_path' => '/sitemap_path']; $siteMapId = 1; $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn($data); + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(true); $this->requestMock->expects($this->once()) ->method('getParam') ->with('sitemap_id') ->willReturn($siteMapId); + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); - $validator = $this->createMock($validatorClass); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('setPaths') ->with($validPaths) ->willReturnSelf(); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('isValid') ->with('/sitemap_path/sitemap_filename') ->willReturn(false); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('getMessages') ->willReturn($messages); - $helper = $this->createMock($helperClass); - $helper->expects($this->once()) + $this->helper->expects($this->once()) ->method('getValidPaths') ->willReturn($validPaths); - $session = $this->createPartialMock($sessionClass, ['setFormData']); - $session->expects($this->once()) + $this->session->expects($this->once()) ->method('setFormData') ->with($data) ->willReturnSelf(); - $this->objectManagerMock->expects($this->once()) + $this->messageManagerMock->expects($this->at(0)) + ->method('addErrorMessage') + ->withConsecutive( + [$messages[0]], + [$messages[1]] + ) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('adminhtml/*/edit', ['sitemap_id' => $siteMapId]) + ->willReturnSelf(); + + $this->assertSame($this->resultRedirectMock, $this->saveController->execute()); + } + + /** + * @return void + */ + public function testTryToSaveInvalidFileNameShouldFailWithErrors() + { + $validPaths = []; + $messages = ['message1', 'message2']; + $data = ['sitemap_filename' => 'sitemap_filename', 'sitemap_path' => '/sitemap_path']; + $siteMapId = 1; + + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('sitemap_id') + ->willReturn($siteMapId); + $this->resultFactoryMock->expects($this->once()) ->method('create') - ->with($validatorClass) - ->willReturn($validator); - $this->objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnMap([[$helperClass, $helper], [$sessionClass, $session]]); + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); + + $this->lengthValidator->expects($this->once()) + ->method('isValid') + ->with('sitemap_filename') + ->willReturn(false); + $this->lengthValidator->expects($this->once()) + ->method('getMessages') + ->willReturn($messages); + + $this->pathValidator->expects($this->once()) + ->method('setPaths') + ->with($validPaths) + ->willReturnSelf(); + $this->pathValidator->expects($this->once()) + ->method('isValid') + ->with('/sitemap_path/sitemap_filename') + ->willReturn(true); + + $this->helper->expects($this->once()) + ->method('getValidPaths') + ->willReturn($validPaths); + + $this->session->expects($this->once()) + ->method('setFormData') + ->with($data) + ->willReturnSelf(); $this->messageManagerMock->expects($this->at(0)) - ->method('addError') + ->method('addErrorMessage') ->withConsecutive( [$messages[0]], [$messages[1]] @@ -168,4 +294,17 @@ public function testTryToSaveInvalidDataShouldFailWithErrors() $this->assertSame($this->resultRedirectMock, $this->saveController->execute()); } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\NotFoundException + * @expectedExceptionMessage Page not found + */ + public function testExecuteNotPost() + { + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(false); + + $this->saveController->execute(); + } } diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php index 92e6f4e2e2293..ac88f23ff9d69 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php @@ -7,6 +7,10 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Class ObserverTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -96,11 +100,11 @@ protected function setUp() ); } - /** - * @expectedException \Exception - */ - public function testScheduledGenerateSitemapsThrowsException() + public function testScheduledGenerateSitemapsSendsExceptionEmail() { + $exception = 'Sitemap Exception'; + $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $this->scopeConfigMock->expects($this->once())->method('isSetFlag')->willReturn(true); $this->collectionFactoryMock->expects($this->once()) @@ -111,7 +115,55 @@ public function testScheduledGenerateSitemapsThrowsException() ->method('getIterator') ->willReturn(new \ArrayIterator([$this->sitemapMock])); - $this->sitemapMock->expects($this->once())->method('generateXml')->willThrowException(new \Exception()); + $this->sitemapMock->expects($this->once()) + ->method('generateXml') + ->willThrowException(new \Exception($exception)); + + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with( + \Magento\Sitemap\Model\Observer::XML_PATH_ERROR_RECIPIENT, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ->willReturn('error-recipient@example.com'); + + $this->inlineTranslationMock->expects($this->once()) + ->method('suspend'); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateIdentifier') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateOptions') + ->with([ + 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + ]) + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateVars') + ->with(['warnings' => $exception]) + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setFrom') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('addTo') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('getTransport') + ->willReturn($transport); + + $transport->expects($this->once()) + ->method('sendMessage'); + + $this->inlineTranslationMock->expects($this->once()) + ->method('resume'); $this->observer->scheduledGenerateSitemaps(); } diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index 83210c5789776..2fe6f66ea4be0 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -160,7 +160,7 @@ public function testNotAllowedPath() * Check not exists sitemap path validation * * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage Please create the specified folder "" before saving the sitemap. + * @expectedExceptionMessage Please create the specified folder "/" before saving the sitemap. */ public function testPathNotExists() { @@ -253,6 +253,8 @@ public function testGenerateXml($maxLines, $maxFileSize, $expectedFile, $expecte $expectedWrites, null ); + $this->storeManagerMock->expects($this->once())->method('setCurrentStore')->with(1); + $model->generateXml(); $this->assertCount(count($expectedFile), $actualData, 'Number of generated files is incorrect'); @@ -360,6 +362,8 @@ public function testAddSitemapToRobotsTxt($maxLines, $maxFileSize, $expectedFile $expectedWrites, $robotsInfo ); + $this->storeManagerMock->expects($this->once())->method('setCurrentStore')->with(1); + $model->generateXml(); } @@ -531,7 +535,7 @@ protected function _getModelMock($mockBeforeSave = false) ) ); - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/'; $this->_sitemapProductMock->expects( $this->any() )->method( @@ -632,16 +636,27 @@ protected function _getModelConstructorArgs() ->setMethods(['getStore']) ->getMockForAbstractClass(); + $escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) + ->setMethods(['escapeHtml']) + ->disableOriginalConstructor() + ->getMock(); + $escaper->expects($this->any())->method('escapeHtml')->willReturnArgument(0); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $escaper = $objectManager->getObject( + \Magento\Framework\Escaper::class + ); $constructArguments = $objectManager->getConstructArguments( \Magento\Sitemap\Model\Sitemap::class, [ + 'escaper' => $escaper, 'categoryFactory' => $categoryFactory, 'productFactory' => $productFactory, 'cmsFactory' => $cmsFactory, 'storeManager' => $this->storeManagerMock, 'sitemapData' => $this->_helperMockSitemap, - 'filesystem' => $this->_filesystemMock + 'filesystem' => $this->_filesystemMock, + 'escaper' => $escaper ] ); $constructArguments['resource'] = null; diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index 7111154efbf85..738e439ba5131 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -14,18 +14,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>caption & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index cc2ff96dd28f2..0e9d78cb08619 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -32,18 +32,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>caption & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/10f519365b01716ddb90abc57de5a837/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/composer.json b/app/code/Magento/Sitemap/composer.json index 678e6f5fe198e..d6240839bcd65 100644 --- a/app/code/Magento/Sitemap/composer.json +++ b/app/code/Magento/Sitemap/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-sitemap", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-eav": "101.0.*", @@ -18,7 +18,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Sitemap/etc/config.xml b/app/code/Magento/Sitemap/etc/config.xml index 73468baadcb90..6f14ff728ac4f 100644 --- a/app/code/Magento/Sitemap/etc/config.xml +++ b/app/code/Magento/Sitemap/etc/config.xml @@ -42,5 +42,16 @@ </valid_paths> </file> </sitemap> + <crontab> + <default> + <jobs> + <sitemap_generate> + <schedule> + <cron_expr>0 0 * * *</cron_expr> + </schedule> + </sitemap_generate> + </jobs> + </default> + </crontab> </default> </config> diff --git a/app/code/Magento/Sitemap/etc/module.xml b/app/code/Magento/Sitemap/etc/module.xml index 0edfcf84f644f..0cfe3d551d162 100644 --- a/app/code/Magento/Sitemap/etc/module.xml +++ b/app/code/Magento/Sitemap/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_Sitemap" setup_version="2.0.0"> <sequence> <module name="Magento_Robots"/> + <module name="Magento_Cms"/> <module name="Magento_Catalog"/> </sequence> </module> diff --git a/app/code/Magento/Store/Api/Data/WebsiteInterface.php b/app/code/Magento/Store/Api/Data/WebsiteInterface.php index 176e82f5905b5..fae9fa368d3d1 100644 --- a/app/code/Magento/Store/Api/Data/WebsiteInterface.php +++ b/app/code/Magento/Store/Api/Data/WebsiteInterface.php @@ -13,6 +13,11 @@ */ interface WebsiteInterface extends \Magento\Framework\Api\ExtensibleDataInterface { + /** + * Contains code of admin website + */ + const ADMIN_CODE = 'admin'; + /** * @return int */ diff --git a/app/code/Magento/Store/App/Action/Plugin/Context.php b/app/code/Magento/Store/App/Action/Plugin/Context.php index 66fec992b38d7..8f771ced591fa 100644 --- a/app/code/Magento/Store/App/Action/Plugin/Context.php +++ b/app/code/Magento/Store/App/Action/Plugin/Context.php @@ -7,7 +7,9 @@ namespace Magento\Store\App\Action\Plugin; use Magento\Framework\App\Http\Context as HttpContext; -use Magento\Framework\Phrase; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Api\StoreCookieManagerInterface; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\StoreManagerInterface; @@ -65,35 +67,96 @@ public function __construct( * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeDispatch(AbstractAction $subject, RequestInterface $request) - { - /** @var \Magento\Store\Model\Store $defaultStore */ - $defaultStore = $this->storeManager->getWebsite()->getDefaultStore(); + public function beforeDispatch( + AbstractAction $subject, + RequestInterface $request + ) { + if ($this->isAlreadySet()) { + //If required store related value were already set for + //HTTP processors then just continuing as we were. + return; + } + /** @var string|array|null $storeCode */ $storeCode = $request->getParam( StoreResolverInterface::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie() ); - if (is_array($storeCode)) { if (!isset($storeCode['_data']['code'])) { - throw new \InvalidArgumentException(new Phrase('Invalid store parameter.')); + $this->processInvalidStoreRequested(); } $storeCode = $storeCode['_data']['code']; } - /** @var \Magento\Store\Model\Store $currentStore */ - $currentStore = $storeCode ? $this->storeManager->getStore($storeCode) : $defaultStore; + if ($storeCode === '') { + //Empty code - is an invalid code and it was given explicitly + //(the value would be null if the code wasn't found). + $this->processInvalidStoreRequested(); + } + try { + $currentStore = $this->storeManager->getStore($storeCode); + } catch (NoSuchEntityException $exception) { + $this->processInvalidStoreRequested($exception); + } + + $this->updateContext($currentStore); + } + + /** + * Take action in case of invalid store requested. + * + * @param \Throwable|null $previousException + * + * @throws NotFoundException + */ + private function processInvalidStoreRequested( + \Throwable $previousException = null + ) { + $store = $this->storeManager->getStore(); + $this->updateContext($store); + + throw new NotFoundException( + $previousException + ? __($previousException->getMessage()) + : __('Invalid store requested.'), + $previousException + ); + } + + /** + * Update context accordingly to the store found. + * + * @param StoreInterface $store + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function updateContext(StoreInterface $store) + { + /** @var StoreInterface $defaultStore */ + $defaultStore = $this->storeManager->getWebsite()->getDefaultStore(); $this->httpContext->setValue( StoreManagerInterface::CONTEXT_STORE, - $currentStore->getCode(), - $this->storeManager->getDefaultStoreView()->getCode() + $store->getCode(), + $store->isUseStoreInUrl() ? $store->getCode() : $defaultStore->getCode() ); $this->httpContext->setValue( HttpContext::CONTEXT_CURRENCY, - $this->session->getCurrencyCode() ?: $currentStore->getDefaultCurrencyCode(), + $this->session->getCurrencyCode() + ?: $store->getDefaultCurrencyCode(), $defaultStore->getDefaultCurrencyCode() ); } + + /** + * Check if there a need to find the current store. + * + * @return bool + */ + private function isAlreadySet(): bool + { + $storeKey = StoreManagerInterface::CONTEXT_STORE; + + return $this->httpContext->getValue($storeKey) !== null; + } } diff --git a/app/code/Magento/Store/App/Config/Type/Scopes.php b/app/code/Magento/Store/App/Config/Type/Scopes.php index 9fbecc4db303e..e6b9d0000e4a6 100644 --- a/app/code/Magento/Store/App/Config/Type/Scopes.php +++ b/app/code/Magento/Store/App/Config/Type/Scopes.php @@ -114,5 +114,6 @@ private function convertIdPathToCodePath(array $patchChunks) public function clean() { $this->data = null; + $this->idCodeMap = []; } } diff --git a/app/code/Magento/Store/App/Request/PathInfoProcessor.php b/app/code/Magento/Store/App/Request/PathInfoProcessor.php index a38ea6d1272e8..3fa78dc94aa35 100644 --- a/app/code/Magento/Store/App/Request/PathInfoProcessor.php +++ b/app/code/Magento/Store/App/Request/PathInfoProcessor.php @@ -44,7 +44,7 @@ public function process(\Magento\Framework\App\RequestInterface $request, $pathI if ($store->isUseStoreInUrl()) { if (!$request->isDirectAccessFrontendName($storeCode) && $storeCode != Store::ADMIN_CODE) { - $this->storeManager->setCurrentStore($storeCode); + $this->storeManager->setCurrentStore($store->getCode()); $pathInfo = '/' . (isset($pathParts[1]) ? $pathParts[1] : ''); return $pathInfo; } elseif (!empty($storeCode)) { diff --git a/app/code/Magento/Store/App/Response/Redirect.php b/app/code/Magento/Store/App/Response/Redirect.php index d826ad3425f54..9c0da1a7a7adf 100644 --- a/app/code/Magento/Store/App/Response/Redirect.php +++ b/app/code/Magento/Store/App/Response/Redirect.php @@ -81,17 +81,16 @@ public function __construct( protected function _getUrl() { $refererUrl = $this->_request->getServer('HTTP_REFERER'); - $url = (string)$this->_request->getParam(self::PARAM_NAME_REFERER_URL); - if ($url) { - $refererUrl = $url; - } - $url = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_BASE64_URL); - if ($url) { - $refererUrl = $this->_urlCoder->decode($url); - } - $url = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED); - if ($url) { - $refererUrl = $this->_urlCoder->decode($url); + $encodedUrl = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED) + ?: $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_BASE64_URL); + + if ($encodedUrl) { + $refererUrl = $this->_urlCoder->decode($encodedUrl); + } else { + $url = (string)$this->_request->getParam(self::PARAM_NAME_REFERER_URL); + if ($url) { + $refererUrl = $url; + } } if (!$this->_isUrlInternal($refererUrl)) { @@ -171,16 +170,6 @@ public function success($defaultUrl) */ public function updatePathParams(array $arguments) { - if ($this->_session->getCookieShouldBeReceived() - && $this->_sidResolver->getUseSessionInUrl() - && $this->_canUseSessionIdInParam - ) { - $arguments += [ - '_query' => [ - $this->_sidResolver->getSessionIdQueryParam($this->_session) => $this->_session->getSessionId(), - ] - ]; - } return $arguments; } diff --git a/app/code/Magento/Store/Block/Switcher.php b/app/code/Magento/Store/Block/Switcher.php index 3d8d46983b5aa..2c437fb3577df 100644 --- a/app/code/Magento/Store/Block/Switcher.php +++ b/app/code/Magento/Store/Block/Switcher.php @@ -10,7 +10,10 @@ namespace Magento\Store\Block; use Magento\Directory\Helper\Data; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\Group; +use Magento\Framework\Url\Helper\Data as UrlHelper; /** * @api @@ -28,20 +31,28 @@ class Switcher extends \Magento\Framework\View\Element\Template */ protected $_postDataHelper; + /** + * @var UrlHelper + */ + private $urlHelper; + /** * Constructs * * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper * @param array $data + * @param UrlHelper $urlHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Data\Helper\PostHelper $postDataHelper, - array $data = [] + array $data = [], + UrlHelper $urlHelper = null ) { $this->_postDataHelper = $postDataHelper; parent::__construct($context, $data); + $this->urlHelper = $urlHelper ?: ObjectManager::getInstance()->get(UrlHelper::class); } /** @@ -220,15 +231,20 @@ public function getStoreName() * @param \Magento\Store\Model\Store $store * @param array $data * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getTargetStorePostData(\Magento\Store\Model\Store $store, $data = []) { - $data[\Magento\Store\Api\StoreResolverInterface::PARAM_NAME] - = $store->getCode(); - //We need to fromStore as true because it will enable proper URL - //rewriting during store switching. + $data[\Magento\Store\Api\StoreResolverInterface::PARAM_NAME] = $store->getCode(); + $data['___from_store'] = $this->_storeManager->getStore()->getCode(); + + $urlOnTargetStore = $store->getCurrentUrl(false); + $data[ActionInterface::PARAM_NAME_URL_ENCODED] = $this->urlHelper->getEncodedUrl($urlOnTargetStore); + + $url = $this->getUrl('stores/store/redirect'); + return $this->_postDataHelper->getPostData( - $store->getCurrentUrl(true), + $url, $data ); } diff --git a/app/code/Magento/Store/Console/Command/StoreListCommand.php b/app/code/Magento/Store/Console/Command/StoreListCommand.php index aaaa8afb76fd2..467f0c4784a51 100644 --- a/app/code/Magento/Store/Console/Command/StoreListCommand.php +++ b/app/code/Magento/Store/Console/Command/StoreListCommand.php @@ -6,9 +6,12 @@ */ namespace Magento\Store\Console\Command; +use Magento\Framework\App\ObjectManager; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table as TableHelper; +use Symfony\Component\Console\Helper\TableFactory as TableHelperFactory; /** * Class StoreListCommand @@ -23,11 +26,19 @@ class StoreListCommand extends Command private $storeManager; /** + * @var TableHelperFactory + */ + private $tableHelperFactory; + + /** + * @inheritDoc */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager + \Magento\Store\Model\StoreManagerInterface $storeManager, + TableHelperFactory $tableHelperFactory = null ) { $this->storeManager = $storeManager; + $this->tableHelperFactory = $tableHelperFactory ?? ObjectManager::getInstance()->get(TableHelperFactory::class); parent::__construct(); } @@ -48,7 +59,8 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { try { - $table = $this->getHelperSet()->get('table'); + /** @var TableHelper $table */ + $table = $this->tableHelperFactory->create(['output' => $output]); $table->setHeaders(['ID', 'Website ID', 'Group ID', 'Name', 'Code', 'Sort Order', 'Is Active']); foreach ($this->storeManager->getStores(true, true) as $store) { @@ -63,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ]); } - $table->render($output); + $table->render(); return \Magento\Framework\Console\Cli::RETURN_SUCCESS; } catch (\Exception $e) { diff --git a/app/code/Magento/Store/Console/Command/WebsiteListCommand.php b/app/code/Magento/Store/Console/Command/WebsiteListCommand.php index ce0359d1bb799..f5ce537593cab 100644 --- a/app/code/Magento/Store/Console/Command/WebsiteListCommand.php +++ b/app/code/Magento/Store/Console/Command/WebsiteListCommand.php @@ -6,9 +6,12 @@ */ namespace Magento\Store\Console\Command; +use Magento\Framework\App\ObjectManager; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table as TableHelper; +use Symfony\Component\Console\Helper\TableFactory as TableHelperFactory; /** * Class WebsiteListCommand @@ -23,11 +26,19 @@ class WebsiteListCommand extends Command private $manager; /** + * @var TableHelperFactory + */ + private $tableHelperFactory; + + /** + * @inheritDoc */ public function __construct( - \Magento\Store\Api\WebsiteRepositoryInterface $websiteManagement + \Magento\Store\Api\WebsiteRepositoryInterface $websiteManagement, + TableHelperFactory $tableHelperFactory = null ) { $this->manager = $websiteManagement; + $this->tableHelperFactory = $tableHelperFactory ?? ObjectManager::getInstance()->get(TableHelperFactory::class); parent::__construct(); } @@ -48,7 +59,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { try { - $table = $this->getHelperSet()->get('table'); + $table = $this->tableHelperFactory->create(['output' => $output]); $table->setHeaders(['ID', 'Default Group Id', 'Name', 'Code', 'Sort Order', 'Is Default']); foreach ($this->manager->getList() as $website) { diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php new file mode 100644 index 0000000000000..04714b17a0cc0 --- /dev/null +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -0,0 +1,113 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Controller\Store; + +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreResolver; +use Magento\Framework\Session\SidResolverInterface; +use Magento\Framework\Session\Generic as Session; + +/** + * Builds correct url to target store and performs redirect. + */ +class Redirect extends \Magento\Framework\App\Action\Action +{ + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var StoreResolverInterface + */ + private $storeResolver; + + /** + * @var SidResolverInterface + */ + private $sidResolver; + + /** + * @var Session + */ + private $session; + + /** + * @param Context $context + * @param StoreRepositoryInterface $storeRepository + * @param StoreResolverInterface $storeResolver + * @param Session $session + * @param SidResolverInterface $sidResolver + */ + public function __construct( + Context $context, + StoreRepositoryInterface $storeRepository, + StoreResolverInterface $storeResolver, + Session $session, + SidResolverInterface $sidResolver + ) { + parent::__construct($context); + $this->storeRepository = $storeRepository; + $this->storeResolver = $storeResolver; + $this->session = $session; + $this->sidResolver = $sidResolver; + } + + /** + * @return ResponseInterface|\Magento\Framework\Controller\ResultInterface + * @throws NoSuchEntityException + */ + public function execute() + { + /** @var \Magento\Store\Model\Store $currentStore */ + $currentStore = $this->storeRepository->getById($this->storeResolver->getCurrentStoreId()); + $targetStoreCode = $this->_request->getParam(StoreResolver::PARAM_NAME); + $fromStoreCode = $this->_request->getParam('___from_store'); + $error = null; + + if ($targetStoreCode === null) { + return $this->_redirect($currentStore->getBaseUrl()); + } + + try { + /** @var \Magento\Store\Model\Store $targetStore */ + $fromStore = $this->storeRepository->get($fromStoreCode); + } catch (NoSuchEntityException $e) { + $error = __('Requested store is not found'); + } + + if ($error !== null) { + $this->messageManager->addErrorMessage($error); + $this->_redirect->redirect($this->_response, $currentStore->getBaseUrl()); + } else { + $encodedUrl = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED); + + $query = [ + '___from_store' => $fromStore->getCode(), + StoreResolverInterface::PARAM_NAME => $targetStoreCode, + \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => $encodedUrl, + ]; + + if ($this->sidResolver->getUseSessionInUrl()) { + // allow customers to stay logged in during store switching + $sidName = $this->sidResolver->getSessionIdQueryParam($this->session); + $query[$sidName] = $this->session->getSessionId(); + } + + $arguments = [ + '_nosid' => true, + '_query' => $query + ]; + $this->_redirect->redirect($this->_response, 'stores/store/switch', $arguments); + } + } +} diff --git a/app/code/Magento/Store/Controller/Store/SwitchAction.php b/app/code/Magento/Store/Controller/Store/SwitchAction.php index aa023e400460f..646ee4fe4d132 100644 --- a/app/code/Magento/Store/Controller/Store/SwitchAction.php +++ b/app/code/Magento/Store/Controller/Store/SwitchAction.php @@ -4,21 +4,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Store\Controller\Store; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context as ActionContext; use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Api\StoreCookieManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; -use Magento\Store\Model\Store; use Magento\Store\Model\StoreIsInactiveException; use Magento\Store\Model\StoreResolver; use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\StoreSwitcher; +use Magento\Store\Model\StoreSwitcherInterface; /** - * Switch current store view. + * Handles store switching url and makes redirect. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SwitchAction extends Action { @@ -29,6 +33,7 @@ class SwitchAction extends Action /** * @var HttpContext + * @deprecated */ protected $httpContext; @@ -39,9 +44,15 @@ class SwitchAction extends Action /** * @var StoreManagerInterface + * @deprecated */ protected $storeManager; + /** + * @var StoreSwitcherInterface + */ + private $storeSwitcher; + /** * Initialize dependencies. * @@ -50,69 +61,55 @@ class SwitchAction extends Action * @param HttpContext $httpContext * @param StoreRepositoryInterface $storeRepository * @param StoreManagerInterface $storeManager + * @param StoreSwitcherInterface $storeSwitcher */ public function __construct( ActionContext $context, StoreCookieManagerInterface $storeCookieManager, HttpContext $httpContext, StoreRepositoryInterface $storeRepository, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + StoreSwitcherInterface $storeSwitcher = null ) { parent::__construct($context); $this->storeCookieManager = $storeCookieManager; $this->httpContext = $httpContext; $this->storeRepository = $storeRepository; $this->storeManager = $storeManager; + $this->messageManager = $context->getMessageManager(); + $this->storeSwitcher = $storeSwitcher ?: ObjectManager::getInstance()->get(StoreSwitcher::class); } /** * @return void + * @throws StoreSwitcher\CannotSwitchStoreException */ public function execute() { - $currentActiveStore = $this->storeManager->getStore(); - $storeCode = $this->_request->getParam( + $targetStoreCode = $this->_request->getParam( StoreResolver::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie() ); + $fromStoreCode = $this->_request->getParam('___from_store'); + + $requestedUrlToRedirect = $this->_redirect->getRedirectUrl(); + $redirectUrl = $requestedUrlToRedirect; + $error = null; try { - $store = $this->storeRepository->getActiveStoreByCode($storeCode); + $fromStore = $this->storeRepository->get($fromStoreCode); + $targetStore = $this->storeRepository->getActiveStoreByCode($targetStoreCode); } catch (StoreIsInactiveException $e) { $error = __('Requested store is inactive'); } catch (NoSuchEntityException $e) { $error = __('Requested store is not found'); } - - if (isset($error)) { - $this->messageManager->addError($error); - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); - return; - } - - $defaultStoreView = $this->storeManager->getDefaultStoreView(); - if ($defaultStoreView->getId() == $store->getId()) { - $this->storeCookieManager->deleteStoreCookie($store); + if ($error !== null) { + $this->messageManager->addErrorMessage($error); } else { - $this->httpContext->setValue(Store::ENTITY, $store->getCode(), $defaultStoreView->getCode()); - $this->storeCookieManager->setStoreCookie($store); + $redirectUrl = $this->storeSwitcher->switch($fromStore, $targetStore, $requestedUrlToRedirect); } - if ($store->isUseStoreInUrl()) { - // Change store code in redirect url - if (strpos($this->_redirect->getRedirectUrl(), $currentActiveStore->getBaseUrl()) !== false) { - $this->getResponse()->setRedirect( - str_replace( - $currentActiveStore->getBaseUrl(), - $store->getBaseUrl(), - $this->_redirect->getRedirectUrl() - ) - ); - } else { - $this->getResponse()->setRedirect($store->getBaseUrl()); - } - } else { - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); - } + $this->getResponse()->setRedirect($redirectUrl); } } diff --git a/app/code/Magento/Store/Controller/Store/SwitchRequest.php b/app/code/Magento/Store/Controller/Store/SwitchRequest.php new file mode 100644 index 0000000000000..77df8fb8b1ca4 --- /dev/null +++ b/app/code/Magento/Store/Controller/Store/SwitchRequest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Controller\Store; + +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Customer\Api\CustomerRepositoryInterface; +use \Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Url\DecoderInterface; +use \Magento\Framework\App\ActionInterface; +use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; + +/** + * Builds correct url to target store and performs redirect. + */ +class SwitchRequest extends Action +{ + + /** + * @var customerSession + */ + private $customerSession; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var HashGenerator + */ + private $hashGenerator; + + /** + * @var DecoderInterface + */ + private $urlDecoder; + + /** + * @param Context $context + * @param CustomerSession $session + * @param CustomerRepositoryInterface $customerRepository + * @param HashGenerator $hashGenerator + * @param DecoderInterface $urlDecoder + */ + public function __construct( + Context $context, + CustomerSession $session, + CustomerRepositoryInterface $customerRepository, + HashGenerator $hashGenerator, + DecoderInterface $urlDecoder + ) { + parent::__construct($context); + $this->customerSession = $session; + $this->customerRepository = $customerRepository; + $this->hashGenerator = $hashGenerator; + $this->urlDecoder = $urlDecoder; + } + + /** + * Execute action + * + * @return void + */ + public function execute() + { + $fromStoreCode = (string)$this->_request->getParam('___from_store'); + $customerId = (int)$this->_request->getParam('customer_id'); + $timeStamp = (string)$this->_request->getParam('time_stamp'); + $signature = (string)$this->_request->getParam('signature'); + $error = null; + $encodedUrl = (string)$this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); + $targetUrl = $this->urlDecoder->decode($encodedUrl); + + $data = new HashData( + [ + "customer_id" => $customerId, + "time_stamp" => $timeStamp, + "___from_store" => $fromStoreCode + ] + ); + + if ($targetUrl && $this->hashGenerator->validateHash($signature, $data)) { + try { + $customer = $this->customerRepository->getById($customerId); + if (!$this->customerSession->isLoggedIn()) { + $this->customerSession->setCustomerDataAsLoggedIn($customer); + } + $this->getResponse()->setRedirect($targetUrl); + } catch (NoSuchEntityException $e) { + $error = __('The requested customer does not exist.'); + } catch (LocalizedException $e) { + $error = __('There was an error retrieving the customer record.'); + } + } else { + $error = __('The requested store cannot be found. Please check the request and try again.'); + } + + if ($error !== null) { + $this->messageManager->addErrorMessage($error); + } + } +} diff --git a/app/code/Magento/Store/Model/BaseUrlChecker.php b/app/code/Magento/Store/Model/BaseUrlChecker.php index c0d27fd0d7a09..b22b18c5002ad 100644 --- a/app/code/Magento/Store/Model/BaseUrlChecker.php +++ b/app/code/Magento/Store/Model/BaseUrlChecker.php @@ -47,7 +47,7 @@ public function execute($uri, $request) */ public function isEnabled() { - return (bool) $this->scopeConfig->getValue( + return (bool)$this->scopeConfig->getValue( 'web/url/redirect_to_base', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php index 768482aa44636..c18ea47a21eb0 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php @@ -16,8 +16,6 @@ /** * The processor for creating of new entities. - * - * {@inheritdoc} */ class Create implements ProcessorInterface { @@ -52,6 +50,10 @@ class Create implements ProcessorInterface /** * The event manager. * + * @deprecated logic moved inside of "afterSave" method + * \Magento\Store\Model\Website::afterSave + * \Magento\Store\Model\Group::afterSave + * \Magento\Store\Model\Store::afterSave * @var ManagerInterface */ private $eventManager; @@ -80,7 +82,9 @@ public function __construct( /** * Creates entities in application according to the data set. * - * {@inheritdoc} + * @param array $data The data to be processed + * @return void + * @throws RuntimeException If processor was unable to finish execution */ public function run(array $data) { @@ -172,8 +176,11 @@ private function createGroups(array $items, array $data) ); $group = $this->groupFactory->create(); + if (!isset($groupData['root_category_id'])) { + $groupData['root_category_id'] = 0; + } + $group->setData($groupData); - $group->setRootCategoryId(0); $group->getResource()->save($group); $group->getResource()->addCommitCallback(function () use ($data, $group, $website) { @@ -181,8 +188,6 @@ private function createGroups(array $items, array $data) $group->setDefaultStoreId($store->getStoreId()); $group->setWebsite($website); $group->getResource()->save($group); - - $this->eventManager->dispatch('store_group_save', ['group' => $group]); }); } } @@ -224,8 +229,7 @@ private function createStores(array $items, array $data) } /** - * Searches through given websites and compares with current websites. - * Returns found website. + * Searches through given websites and compares with current websites and returns found website. * * @param array $data The data to be searched in * @param string $websiteId The website id @@ -247,8 +251,7 @@ private function detectWebsiteById(array $data, $websiteId) } /** - * Searches through given groups and compares with current websites. - * Returns found group. + * Searches through given groups and compares with current websites and returns found group. * * @param array $data The data to be searched in * @param string $groupId The group id @@ -270,8 +273,7 @@ private function detectGroupById(array $data, $groupId) } /** - * Searches through given stores and compares with current stores. - * Returns found store. + * Searches through given stores and compares with current stores and returns found store. * * @param array $data The data to be searched in * @param string $storeId The store id diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Delete.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Delete.php index 8660a0ba7152d..475c15122773e 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Delete.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Delete.php @@ -168,10 +168,7 @@ private function deleteStores(array $items) foreach ($items as $storeCode) { $store = $this->storeRepository->get($storeCode); - $store->getResource()->delete($store); - $store->getResource()->addCommitCallback(function () use ($store) { - $this->eventManager->dispatch('store_delete', ['store' => $store]); - }); + $store->delete(); } } diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Update.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Update.php index 35f3957b168d7..155506291e59d 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Update.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Update.php @@ -176,10 +176,7 @@ private function updateStores(array $items, array $data) $store->setGroup($group); } - $store->getResource()->save($store); - $store->getResource()->addCommitCallback(function () use ($store) { - $this->eventManager->dispatch('store_edit', ['store' => $store]); - }); + $store->save(); } } @@ -214,11 +211,7 @@ private function updateGroups(array $items, array $data) if ($website && $website->getId() != $group->getWebsiteId()) { $group->setWebsite($website); } - - $group->getResource()->save($group); - $group->getResource()->addCommitCallback(function () use ($group) { - $this->eventManager->dispatch('store_group_save', ['group' => $group]); - }); + $group->save(); } } diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index 01f56fb9e0566..ccc3c65491422 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -95,6 +95,11 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ protected $_storeManager; + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -106,6 +111,7 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\Event\ManagerInterface|null $eventManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -118,11 +124,14 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Framework\Event\ManagerInterface $eventManager = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; $this->_storeManager = $storeManager; + $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Event\ManagerInterface::class); parent::__construct( $context, $registry, @@ -405,6 +414,11 @@ public function beforeDelete() */ public function afterDelete() { + $group = $this; + $this->getResource()->addCommitCallback(function () use ($group) { + $this->_storeManager->reinitStores(); + $this->eventManager->dispatch($this->_eventPrefix . '_delete', ['group' => $group]); + }); $result = parent::afterDelete(); if ($this->getId() === $this->getWebsite()->getDefaultGroupId()) { @@ -421,6 +435,19 @@ public function afterDelete() return $result; } + /** + * @inheritdoc + */ + public function afterSave() + { + $group = $this; + $this->getResource()->addCommitCallback(function () use ($group) { + $this->_storeManager->reinitStores(); + $this->eventManager->dispatch($this->_eventPrefix . '_save', ['group' => $group]); + }); + return parent::afterSave(); + } + /** * Get/Set isReadOnly flag * diff --git a/app/code/Magento/Store/Model/Indexer/WebsiteDimensionProvider.php b/app/code/Magento/Store/Model/Indexer/WebsiteDimensionProvider.php new file mode 100644 index 0000000000000..302c2f828367a --- /dev/null +++ b/app/code/Magento/Store/Model/Indexer/WebsiteDimensionProvider.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model\Indexer; + +use Magento\Framework\Indexer\Dimension; +use Magento\Store\Model\ResourceModel\Website\CollectionFactory as WebsiteCollectionFactory; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Store\Model\Store; + +class WebsiteDimensionProvider implements DimensionProviderInterface +{ + /** + * Name for website dimension for multidimensional indexer + * 'ws' - stands for 'website_store' + */ + const DIMENSION_NAME = 'ws'; + + /** + * @var WebsiteCollectionFactory + */ + private $collectionFactory; + + /** + * @var \SplFixedArray + */ + private $websitesDataIterator; + + /** + * @var DimensionFactory + */ + private $dimensionFactory; + + /** + * @param WebsiteCollectionFactory $collectionFactory + * @param DimensionFactory $dimensionFactory + */ + public function __construct(WebsiteCollectionFactory $collectionFactory, DimensionFactory $dimensionFactory) + { + $this->dimensionFactory = $dimensionFactory; + $this->collectionFactory = $collectionFactory; + } + + /** + * @return Dimension[]|\Traversable + */ + public function getIterator(): \Traversable + { + foreach ($this->getWebsites() as $website) { + yield $this->dimensionFactory->create(self::DIMENSION_NAME, (string)$website); + } + } + + /** + * @return array + */ + private function getWebsites(): array + { + if ($this->websitesDataIterator === null) { + $websites = $this->collectionFactory->create() + ->addFieldToFilter('code', ['neq' => Store::ADMIN_CODE]) + ->getAllIds(); + $this->websitesDataIterator = is_array($websites) ? $websites : []; + } + + return $this->websitesDataIterator; + } +} diff --git a/app/code/Magento/Store/Model/PathConfig.php b/app/code/Magento/Store/Model/PathConfig.php index dfe4eee31f9a2..6eeb93d7475fa 100644 --- a/app/code/Magento/Store/Model/PathConfig.php +++ b/app/code/Magento/Store/Model/PathConfig.php @@ -83,6 +83,8 @@ public function shouldBeSecure($path) */ public function getDefaultPath() { - return $this->scopeConfig->getValue('web/default/front', ScopeInterface::SCOPE_STORE); + $store = $this->storeManager->getStore(); + $value = $this->scopeConfig->getValue('web/default/front', ScopeInterface::SCOPE_STORE, $store); + return $value; } } diff --git a/app/code/Magento/Store/Model/Plugin/StoreCookie.php b/app/code/Magento/Store/Model/Plugin/StoreCookie.php index 612d35e136d7e..9fca86ab71762 100644 --- a/app/code/Magento/Store/Model/Plugin/StoreCookie.php +++ b/app/code/Magento/Store/Model/Plugin/StoreCookie.php @@ -82,12 +82,5 @@ public function beforeDispatch( $this->storeCookieManager->deleteStoreCookie($this->storeManager->getDefaultStoreView()); } } - if ($this->storeCookieManager->getStoreCodeFromCookie() === null - || $request->getParam(StoreResolverInterface::PARAM_NAME) !== null - ) { - $storeId = $this->storeResolver->getCurrentStoreId(); - $store = $this->storeRepository->getActiveStoreById($storeId); - $this->storeCookieManager->setStoreCookie($store); - } } } diff --git a/app/code/Magento/Store/Model/ResourceModel/Website.php b/app/code/Magento/Store/Model/ResourceModel/Website.php index 0b77dfb09e80b..d6fefd60ae54a 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Store\Model\ResourceModel; /** diff --git a/app/code/Magento/Store/Model/ResourceModel/Website/Collection.php b/app/code/Magento/Store/Model/ResourceModel/Website/Collection.php index 5b3a74d5d8d39..64b459fc96153 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website/Collection.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website/Collection.php @@ -138,11 +138,11 @@ public function joinGroupAndStore() $this->getSelect()->joinLeft( ['group_table' => $this->getTable('store_group')], 'main_table.website_id = group_table.website_id', - ['group_id' => 'group_id', 'group_title' => 'name'] + ['group_id' => 'group_id', 'group_title' => 'name', 'group_code' => 'code'] )->joinLeft( ['store_table' => $this->getTable('store')], 'group_table.group_id = store_table.group_id', - ['store_id' => 'store_id', 'store_title' => 'name'] + ['store_id' => 'store_id', 'store_title' => 'name', 'store_code' => 'code'] ); $this->addOrder('group_table.name', \Magento\Framework\DB\Select::SQL_ASC) // store name ->addOrder( @@ -150,8 +150,7 @@ public function joinGroupAndStore() \Magento\Framework\DB\Select::SQL_ASC ) // view is admin ->addOrder('store_table.sort_order', \Magento\Framework\DB\Select::SQL_ASC) // view sort order - ->addOrder('store_table.name', \Magento\Framework\DB\Select::SQL_ASC) // view name - ; + ->addOrder('store_table.name', \Magento\Framework\DB\Select::SQL_ASC); // view name } return $this; } diff --git a/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php b/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php index e42b226653b44..259d41381170d 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php @@ -10,6 +10,18 @@ */ class Collection extends \Magento\Store\Model\ResourceModel\Website\Collection { + /** + * @inheritdoc + */ + protected function _construct() + { + parent::_construct(); + + $this->_map['fields']['store_title'] = 'store_table.name'; + $this->_map['fields']['group_title'] = 'group_table.name'; + $this->_map['fields']['name'] = 'main_table.name'; + } + /** * Join website and store names * @@ -21,4 +33,37 @@ protected function _initSelect() $this->joinGroupAndStore(); return $this; } + + /** + * @inheritdoc + */ + public function load($printQuery = false, $logQuery = false) + { + if ($this->isLoaded()) { + return $this; + } + + return $this->loadWithFilter($printQuery, $logQuery); + } + + /** + * @inheritdoc + */ + public function joinGroupAndStore() + { + if (!$this->getFlag('groups_and_stores_joined')) { + $this->_idFieldName = 'website_group_store'; + $this->getSelect()->joinLeft( + ['group_table' => $this->getTable('store_group')], + 'main_table.website_id = group_table.website_id', + ['group_id' => 'group_id', 'group_title' => 'name', 'group_code' => 'code'] + )->joinLeft( + ['store_table' => $this->getTable('store')], + 'group_table.group_id = store_table.group_id', + ['store_id' => 'store_id', 'store_title' => 'name', 'store_code' => 'code'] + ); + } + + return $this; + } } diff --git a/app/code/Magento/Store/Model/ScopeTreeProvider.php b/app/code/Magento/Store/Model/ScopeTreeProvider.php index cb7d0d3b3ffee..f490e9a7aba7c 100644 --- a/app/code/Magento/Store/Model/ScopeTreeProvider.php +++ b/app/code/Magento/Store/Model/ScopeTreeProvider.php @@ -7,28 +7,48 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ScopeTreeProviderInterface; -use Magento\Store\Model\Group; -use Magento\Store\Model\Store; -use Magento\Store\Model\Website; +use Magento\Store\Api\GroupRepositoryInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +/** + * Class for building scopes tree. + */ class ScopeTreeProvider implements ScopeTreeProviderInterface { /** - * @var StoreManagerInterface + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var GroupRepositoryInterface */ - protected $storeManager; + private $groupRepository; /** - * @param StoreManagerInterface $storeManager + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @param WebsiteRepositoryInterface $websiteRepository + * @param GroupRepositoryInterface $groupRepository + * @param StoreRepositoryInterface $storeRepository */ public function __construct( - StoreManagerInterface $storeManager + WebsiteRepositoryInterface $websiteRepository, + GroupRepositoryInterface $groupRepository, + StoreRepositoryInterface $storeRepository ) { - $this->storeManager = $storeManager; + $this->websiteRepository = $websiteRepository; + $this->groupRepository = $groupRepository; + $this->storeRepository = $storeRepository; } /** * @inheritdoc + * @return array */ public function get() { @@ -38,35 +58,54 @@ public function get() 'scopes' => [], ]; + $groups = []; + foreach ($this->groupRepository->getList() as $group) { + $groups[$group->getWebsiteId()][] = $group; + } + $stores = []; + foreach ($this->storeRepository->getList() as $store) { + $stores[$store->getStoreGroupId()][] = $store; + } + /** @var Website $website */ - foreach ($this->storeManager->getWebsites() as $website) { + foreach ($this->websiteRepository->getList() as $website) { + if (!$website->getId()) { + continue; + } + $websiteScope = [ 'scope' => ScopeInterface::SCOPE_WEBSITES, 'scope_id' => $website->getId(), 'scopes' => [], ]; - /** @var Group $group */ - foreach ($website->getGroups() as $group) { - $groupScope = [ - 'scope' => ScopeInterface::SCOPE_GROUP, - 'scope_id' => $group->getId(), - 'scopes' => [], - ]; - - /** @var Store $store */ - foreach ($group->getStores() as $store) { - $storeScope = [ - 'scope' => ScopeInterface::SCOPE_STORES, - 'scope_id' => $store->getId(), + if (!empty($groups[$website->getId()])) { + /** @var Group $group */ + foreach ($groups[$website->getId()] as $group) { + $groupScope = [ + 'scope' => ScopeInterface::SCOPE_GROUP, + 'scope_id' => $group->getId(), 'scopes' => [], ]; - $groupScope['scopes'][] = $storeScope; + + if (!empty($stores[$group->getId()])) { + /** @var Store $store */ + foreach ($stores[$group->getId()] as $store) { + $storeScope = [ + 'scope' => ScopeInterface::SCOPE_STORES, + 'scope_id' => $store->getId(), + 'scopes' => [], + ]; + $groupScope['scopes'][] = $storeScope; + } + } + $websiteScope['scopes'][] = $groupScope; } - $websiteScope['scopes'][] = $groupScope; } + $defaultScope['scopes'][] = $websiteScope; } + return $defaultScope; } } diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 56751f2188411..6807ca8311822 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -319,6 +319,11 @@ class Store extends AbstractExtensibleModel implements */ private $urlModifier; + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -344,6 +349,7 @@ class Store extends AbstractExtensibleModel implements * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param bool $isCustomEntryPoint * @param array $data optional generic object data + * @param \Magento\Framework\Event\ManagerInterface|null $eventManager * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -371,7 +377,8 @@ public function __construct( \Magento\Store\Api\WebsiteRepositoryInterface $websiteRepository, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, $isCustomEntryPoint = false, - array $data = [] + array $data = [], + \Magento\Framework\Event\ManagerInterface $eventManager = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -390,6 +397,8 @@ public function __construct( $this->_currencyInstalled = $currencyInstalled; $this->groupRepository = $groupRepository; $this->websiteRepository = $websiteRepository; + $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Event\ManagerInterface::class); parent::__construct( $context, $registry, @@ -535,8 +544,8 @@ public function setName($name) public function getConfig($path) { $data = $this->_config->getValue($path, ScopeInterface::SCOPE_STORE, $this->getCode()); - if (!$data) { - $data = $this->_config->getValue($path, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + if ($data === null) { + $data = $this->_config->getValue($path); } return $data === false ? null : $data; } @@ -879,7 +888,10 @@ public function setCurrentCurrencyCode($code) if (in_array($code, $this->getAvailableCurrencyCodes())) { $this->_getSession()->setCurrencyCode($code); - $defaultCode = $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $defaultCode = ($this->_storeManager->getStore() !== null) + ? $this->_storeManager->getStore()->getDefaultCurrency()->getCode() + : $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $this->_httpContext->setValue(Context::CONTEXT_CURRENCY, $code, $defaultCode); } return $this; @@ -892,23 +904,17 @@ public function setCurrentCurrencyCode($code) */ public function getCurrentCurrencyCode() { + $availableCurrencyCodes = \array_values($this->getAvailableCurrencyCodes(true)); // try to get currently set code among allowed - $code = $this->_httpContext->getValue(Context::CONTEXT_CURRENCY); - $code = $code === null ? $this->_getSession()->getCurrencyCode() : $code; - if (empty($code)) { + $code = $this->_httpContext->getValue(Context::CONTEXT_CURRENCY) ?? $this->_getSession()->getCurrencyCode(); + if (empty($code) || !\in_array($code, $availableCurrencyCodes)) { $code = $this->getDefaultCurrencyCode(); - } - if (in_array($code, $this->getAvailableCurrencyCodes(true))) { - return $code; + if (!\in_array($code, $availableCurrencyCodes) && !empty($availableCurrencyCodes)) { + $code = $availableCurrencyCodes[0]; + } } - // take first one of allowed codes - $codes = array_values($this->getAvailableCurrencyCodes(true)); - if (empty($codes)) { - // return default code, if no codes specified at all - return $this->getDefaultCurrencyCode(); - } - return array_shift($codes); + return $code; } /** @@ -1048,6 +1054,15 @@ public function getWebsiteId() public function afterSave() { $this->_storeManager->reinitStores(); + if ($this->isObjectNew()) { + $event = $this->_eventPrefix . '_add'; + } else { + $event = $this->_eventPrefix . '_edit'; + } + $store = $this; + $this->getResource()->addCommitCallback(function () use ($event, $store) { + $this->eventManager->dispatch($event, ['store' => $store]); + }); return parent::afterSave(); } @@ -1128,7 +1143,7 @@ public function isDefault() /** * Retrieve current url for store * - * @param bool|string $fromStore + * @param bool $fromStore * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1195,7 +1210,7 @@ public function getCurrentUrl($fromStore = true) . (isset($storeParsedUrl['port']) ? ':' . $storeParsedUrl['port'] : '') . $storeParsedUrl['path'] . $requestStringPath - . ($currentUrlQueryParams ? '?' . http_build_query($currentUrlQueryParams, '', '&') : ''); + . ($currentUrlQueryParams ? '?' . http_build_query($currentUrlQueryParams) : ''); return $currentUrl; } @@ -1228,6 +1243,11 @@ public function beforeDelete() */ public function afterDelete() { + $store = $this; + $this->getResource()->addCommitCallback(function () use ($store) { + $this->_storeManager->reinitStores(); + $this->eventManager->dispatch($this->_eventPrefix . '_delete', ['store' => $store]); + }); parent::afterDelete(); $this->_configCacheType->clean(); @@ -1242,6 +1262,7 @@ public function afterDelete() $this->getGroup()->setDefaultStoreId($defaultId); $this->getGroup()->save(); } + return $this; } @@ -1310,12 +1331,14 @@ public function getIdentities() } /** + * Return Store Path + * * @return string */ public function getStorePath() { $parsedUrl = parse_url($this->getBaseUrl()); - return isset($parsedUrl['path']) ? $parsedUrl['path'] : '/'; + return $parsedUrl['path'] ?? '/'; } /** diff --git a/app/code/Magento/Store/Model/StoreManager.php b/app/code/Magento/Store/Model/StoreManager.php index a3423b96c3fe4..c47750c0fd8d0 100644 --- a/app/code/Magento/Store/Model/StoreManager.php +++ b/app/code/Magento/Store/Model/StoreManager.php @@ -234,11 +234,11 @@ public function getWebsites($withDefault = false, $codeKey = false) public function reinitStores() { $this->currentStoreId = null; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [StoreResolver::CACHE_TAG, Store::CACHE_TAG]); - $this->scopeConfig->clean(); $this->storeRepository->clean(); $this->websiteRepository->clean(); $this->groupRepository->clean(); + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [StoreResolver::CACHE_TAG, Store::CACHE_TAG]); + $this->scopeConfig->clean(); } /** diff --git a/app/code/Magento/Store/Model/StoreManagerInterface.php b/app/code/Magento/Store/Model/StoreManagerInterface.php index 84e498851ec32..220155c47f6df 100644 --- a/app/code/Magento/Store/Model/StoreManagerInterface.php +++ b/app/code/Magento/Store/Model/StoreManagerInterface.php @@ -6,6 +6,8 @@ namespace Magento\Store\Model; +use Magento\Framework\Exception\NoSuchEntityException; + /** * Store manager interface * @@ -46,6 +48,7 @@ public function isSingleStoreMode(); * * @param null|string|bool|int|\Magento\Store\Api\Data\StoreInterface $storeId * @return \Magento\Store\Api\Data\StoreInterface + * @throws NoSuchEntityException If given store doesn't exist. */ public function getStore($storeId = null); diff --git a/app/code/Magento/Store/Model/StoreResolver/Website.php b/app/code/Magento/Store/Model/StoreResolver/Website.php index 29f85716fea29..d4bb990307f1e 100644 --- a/app/code/Magento/Store/Model/StoreResolver/Website.php +++ b/app/code/Magento/Store/Model/StoreResolver/Website.php @@ -45,8 +45,10 @@ public function getAllowedStoreIds($scopeCode) $stores = []; $website = $scopeCode ? $this->websiteRepository->get($scopeCode) : $this->websiteRepository->getDefault(); foreach ($this->storeRepository->getList() as $store) { - if ($store->isActive() && $store->getWebsiteId() == $website->getId()) { - $stores[] = $store->getId(); + if ($store->isActive()) { + if (!$scopeCode || ($store->getWebsiteId() === $website->getId())) { + $stores[] = $store->getId(); + } } } return $stores; diff --git a/app/code/Magento/Store/Model/StoreSwitcher.php b/app/code/Magento/Store/Model/StoreSwitcher.php new file mode 100644 index 0000000000000..476196ad84f8a --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\CannotSwitchStoreException; + +/** + * Handles store switching procedure and detects url for final redirect after store switching. + */ +class StoreSwitcher implements StoreSwitcherInterface +{ + /** + * @var StoreSwitcherInterface[] + */ + private $storeSwitchers; + + /** + * @param StoreSwitcherInterface[] $storeSwitchers + * @throws \Exception + */ + public function __construct(array $storeSwitchers) + { + foreach ($storeSwitchers as $switcherName => $switcherInstance) { + if (!$switcherInstance instanceof StoreSwitcherInterface) { + throw new \InvalidArgumentException( + "Store switcher '{$switcherName}' is expected to implement interface " + . StoreSwitcherInterface::class + ); + } + } + $this->storeSwitchers = $storeSwitchers; + } + + /** + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws CannotSwitchStoreException + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + $targetUrl = $redirectUrl; + + foreach ($this->storeSwitchers as $storeSwitcher) { + $targetUrl = $storeSwitcher->switch($fromStore, $targetStore, $targetUrl); + } + + return $targetUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/CannotSwitchStoreException.php b/app/code/Magento/Store/Model/StoreSwitcher/CannotSwitchStoreException.php new file mode 100644 index 0000000000000..b19ae419e2f07 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/CannotSwitchStoreException.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Phrase; + +/** + * Exception thrown if store cannot be switched. + */ +class CannotSwitchStoreException extends RuntimeException +{ + /** + * @param \Exception|null $cause + * @param Phrase|null $phrase + * @param int $code + */ + public function __construct(\Exception $cause = null, Phrase $phrase = null, int $code = 0) + { + parent::__construct($phrase ?: __('The store cannot be switched.'), $cause, $code); + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php new file mode 100644 index 0000000000000..31b6eb62fb9e1 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreSwitcherInterface; +use Magento\Framework\Url\Helper\Data as UrlHelper; + +/** + * Remove SID, from_store, store from target url. + */ +class CleanTargetUrl implements StoreSwitcherInterface +{ + /** + * @var UrlHelper + */ + private $urlHelper; + + /** + * @var \Magento\Framework\Session\Generic + */ + private $session; + + /** + * @var \Magento\Framework\Session\SidResolverInterface + */ + private $sidResolver; + + /** + * @param UrlHelper $urlHelper + * @param \Magento\Framework\Session\Generic $session + * @param \Magento\Framework\Session\SidResolverInterface $sidResolver + */ + public function __construct( + UrlHelper $urlHelper, + \Magento\Framework\Session\Generic $session, + \Magento\Framework\Session\SidResolverInterface $sidResolver + ) { + $this->urlHelper = $urlHelper; + $this->session = $session; + $this->sidResolver = $sidResolver; + } + + /** + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string redirect url + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + $targetUrl = $redirectUrl; + $sidName = $this->sidResolver->getSessionIdQueryParam($this->session); + $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, $sidName); + $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, '___from_store'); + $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, StoreResolverInterface::PARAM_NAME); + + return $targetUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php new file mode 100644 index 0000000000000..456941bd41c25 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; +use Magento\Store\Model\StoreSwitcherInterface; +use \Magento\Framework\App\DeploymentConfig as DeploymentConfig; +use Magento\Framework\Url\Helper\Data as UrlHelper; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Authorization\Model\UserContextInterface; +use \Magento\Framework\App\ActionInterface; + +/** + * Generate one time token and build redirect url + */ +class HashGenerator implements StoreSwitcherInterface +{ + /** + * @var \Magento\Framework\App\DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var UrlHelper + */ + private $urlHelper; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @param DeploymentConfig $deploymentConfig + * @param UrlHelper $urlHelper + * @param UserContextInterface $currentUser + */ + public function __construct( + DeploymentConfig $deploymentConfig, + UrlHelper $urlHelper, + UserContextInterface $currentUser + ) { + $this->deploymentConfig = $deploymentConfig; + $this->urlHelper = $urlHelper; + $this->currentUser = $currentUser; + } + + /** + * Builds redirect url with token + * + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string redirect url + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + $targetUrl = $redirectUrl; + $customerId = null; + $encodedUrl = $this->urlHelper->getEncodedUrl($redirectUrl); + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $customerId = $this->currentUser->getUserId(); + } + + if ($customerId) { + // phpcs:ignore + $urlParts = parse_url($targetUrl); + $host = $urlParts['host']; + $scheme = $urlParts['scheme']; + $key = (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $timeStamp = time(); + $fromStoreCode = $fromStore->getCode(); + $data = implode(',', [$customerId, $timeStamp, $fromStoreCode]); + $signature = hash_hmac('sha256', $data, $key); + $targetUrl = $scheme . "://" . $host . '/stores/store/switchrequest'; + $targetUrl = $this->urlHelper->addRequestParam( + $targetUrl, + ['customer_id' => $customerId] + ); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['time_stamp' => $timeStamp]); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['signature' => $signature]); + $targetUrl = $this->urlHelper->addRequestParam($targetUrl, ['___from_store' => $fromStoreCode]); + $targetUrl = $this->urlHelper->addRequestParam( + $targetUrl, + [ActionInterface::PARAM_NAME_URL_ENCODED => $encodedUrl] + ); + } + return $targetUrl; + } + + /** + * Validates one time token + * + * @param string $signature + * @param HashData $hashData + * @return bool + */ + public function validateHash(string $signature, HashData $hashData): bool + { + if (!empty($signature) && !empty($hashData)) { + $timeStamp = $hashData->getTimestamp(); + $fromStoreCode = $hashData->getFromStoreCode(); + $customerId = $hashData->getCustomerId(); + $value = implode(",", [$customerId, $timeStamp, $fromStoreCode]); + $key = (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + + if (time() - $timeStamp <= 5 && hash_equals($signature, hash_hmac('sha256', $value, $key))) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php new file mode 100644 index 0000000000000..2d0068efbdd92 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator/HashData.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher\HashGenerator; + +use Magento\Framework\DataObject; + +/** + * HashData object for one time token + */ +class HashData extends DataObject +{ + /** + * Get CustomerId + * + * @return int + */ + public function getCustomerId(): int + { + return (int)$this->getData('customer_id'); + } + + /** + * Get Timestamp + * + * @return int + */ + public function getTimestamp(): int + { + return (int)$this->getData('time_stamp'); + } + + /** + * Get Fromstore + * + * @return string + */ + public function getFromStoreCode(): string + { + return (string)$this->getData('___from_store'); + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php b/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php new file mode 100644 index 0000000000000..8217cc2a2d5a3 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/ManagePrivateContent.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Model\StoreSwitcherInterface; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Set private content cookie to have actual local storage data on target store after store switching. + */ +class ManagePrivateContent implements StoreSwitcherInterface +{ + /** + * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory + */ + private $cookieMetadataFactory; + + /** + * @var \Magento\Framework\Stdlib\CookieManagerInterface + */ + private $cookieManager; + + /** + * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory + * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager + */ + public function __construct( + \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory, + \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager + ) { + $this->cookieMetadataFactory = $cookieMetadataFactory; + $this->cookieManager = $cookieManager; + } + + /** + * Update version of private content on each store switch. + * + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * + * @return string redirect url + * @throws CannotSwitchStoreException + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + try { + $publicCookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata() + ->setDurationOneYear() + ->setPath('/') + ->setSecure(false) + ->setHttpOnly(false); + $this->cookieManager->setPublicCookie( + \Magento\Framework\App\PageCache\Version::COOKIE_NAME, + \uniqid('updated-', true), + $publicCookieMetadata + ); + } catch (\Exception $e) { + throw new CannotSwitchStoreException($e); + } + + return $redirectUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ManageStoreCookie.php b/app/code/Magento/Store/Model/StoreSwitcher/ManageStoreCookie.php new file mode 100644 index 0000000000000..23fda163e9662 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/ManageStoreCookie.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Model\StoreSwitcherInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Store\Api\StoreCookieManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; + +/** + * Manage store cookie depending on what store view customer is. + */ +class ManageStoreCookie implements StoreSwitcherInterface +{ + /** + * @var StoreCookieManagerInterface + */ + private $storeCookieManager; + + /** + * @var HttpContext + */ + private $httpContext; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreCookieManagerInterface $storeCookieManager + * @param HttpContext $httpContext + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreCookieManagerInterface $storeCookieManager, + HttpContext $httpContext, + StoreManagerInterface $storeManager + ) { + $this->storeCookieManager = $storeCookieManager; + $this->httpContext = $httpContext; + $this->storeManager = $storeManager; + } + + /** + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string redirect url + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string + { + $defaultStoreView = $this->storeManager->getDefaultStoreView(); + if ($defaultStoreView !== null) { + if ($defaultStoreView->getId() === $targetStore->getId()) { + $this->storeCookieManager->deleteStoreCookie($targetStore); + } else { + $this->httpContext->setValue(Store::ENTITY, $targetStore->getCode(), $defaultStoreView->getCode()); + $this->storeCookieManager->setStoreCookie($targetStore); + } + } + + return $redirectUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcherInterface.php b/app/code/Magento/Store/Model/StoreSwitcherInterface.php new file mode 100644 index 0000000000000..b6c5c38d23ef6 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcherInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Model; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\CannotSwitchStoreException; + +/** + * Handles store switching procedure and detects url for final redirect after store switching. + */ +interface StoreSwitcherInterface +{ + /** + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @throws CannotSwitchStoreException + */ + public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string; +} diff --git a/app/code/Magento/Store/Model/System/Store.php b/app/code/Magento/Store/Model/System/Store.php index 86eec555f1d59..30e28e1541539 100644 --- a/app/code/Magento/Store/Model/System/Store.php +++ b/app/code/Magento/Store/Model/System/Store.php @@ -142,6 +142,7 @@ public function getStoreValuesForForm($empty = false, $all = false) $values[] = [ 'label' => str_repeat($nonEscapableNbspChar, 4) . $store->getName(), 'value' => $store->getId(), + '__disableTmpl' => true, ]; } if ($groupShow) { @@ -152,6 +153,12 @@ public function getStoreValuesForForm($empty = false, $all = false) } } } + array_walk( + $options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); return $options; } diff --git a/app/code/Magento/Store/Model/WebsiteRepository.php b/app/code/Magento/Store/Model/WebsiteRepository.php index 94fd59c7634df..1b12164e42cee 100644 --- a/app/code/Magento/Store/Model/WebsiteRepository.php +++ b/app/code/Magento/Store/Model/WebsiteRepository.php @@ -77,7 +77,14 @@ public function get($code) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with code %s that was requested wasn't found. Verify the website and try again.", + $code + ) + ) + ); } $this->entities[$code] = $website; $this->entitiesById[$website->getId()] = $website; @@ -99,7 +106,14 @@ public function getById($id) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with id %s that was requested wasn't found. Verify the website and try again.", + $id + ) + ) + ); } $this->entities[$website->getCode()] = $website; $this->entitiesById[$id] = $website; diff --git a/app/code/Magento/Store/Setup/InstallSchema.php b/app/code/Magento/Store/Setup/InstallSchema.php index f6cdfb6fcba71..69b3f86ebdd7b 100644 --- a/app/code/Magento/Store/Setup/InstallSchema.php +++ b/app/code/Magento/Store/Setup/InstallSchema.php @@ -270,7 +270,13 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con */ $connection->insertForce( $installer->getTable('store_group'), - ['group_id' => 0, 'website_id' => 0, 'name' => 'Default', 'root_category_id' => 0, 'default_store_id' => 0] + [ + 'group_id' => 0, + 'website_id' => 0, + 'name' => 'Default', + 'root_category_id' => 0, + 'default_store_id' => 0 + ] ); $connection->insertForce( $installer->getTable('store_group'), diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml new file mode 100644 index 0000000000000..0b9676b14b776 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateNewStoreGroupActionGroup"> + <arguments> + <argument name="website" type="string"/> + <argument name="storeGroupName" type="string"/> + <argument name="storeGroupCode" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStoreGroupPage.url}}" stepKey="navigateToNewStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <!--Create Store group --> + <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website}}" stepKey="selectWebsite" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroupName}}" stepKey="enterStoreGroupName" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="enterStoreGroupCode" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="chooseRootCategory" /> + <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml new file mode 100644 index 0000000000000..2f541feacd9c2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateStoreViewActionGroup"> + <arguments> + <argument name="storeGroup" defaultValue="_defaultStoreGroup"/> + <argument name="customStore" defaultValue="customStore"/> + </arguments> + <amOnPage url="{{AdminSystemStoreViewPage.url}}" stepKey="navigateToNewStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <!--Create Store View--> + <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeGroup.name}}" stepKey="selectStore" /> + <fillField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{customStore.name}}" stepKey="enterStoreViewName" /> + <fillField selector="{{AdminNewStoreSection.storeCodeTextField}}" userInput="{{customStore.code}}" stepKey="enterStoreViewCode" /> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="setStatus" /> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageReolad"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the store view." stepKey="seeSavedMessage" /> + </actionGroup> + <actionGroup name="AdminCreateStoreViewUseStringArgumentsActionGroup" extends="AdminCreateStoreViewActionGroup"> + <arguments> + <argument name="storeGroupName" type="string"/> + <argument name="customStoreName" type="string"/> + <argument name="customStoreCode" type="string"/> + </arguments> + <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeGroupName}}" stepKey="selectStore" /> + <fillField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{customStoreName}}" stepKey="enterStoreViewName" /> + <fillField selector="{{AdminNewStoreSection.storeCodeTextField}}" userInput="{{customStoreCode}}" stepKey="enterStoreViewCode" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml new file mode 100644 index 0000000000000..10303684e19d5 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new custom website --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateWebsiteActionGroup"> + <arguments> + <argument name="newWebsiteName" type="string"/> + <argument name="websiteCode" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStoreWebsitePage.url}}" stepKey="navigateToNewWebsitePage"/> + <waitForPageLoad stepKey="waitForStoresPageLoad"/> + <!--Create Website--> + <waitForElementVisible selector="{{AdminNewWebsiteSection.name}}" stepKey="waitForWebsiteFormAppeared" /> + <fillField selector="{{AdminNewWebsiteSection.name}}" userInput="{{newWebsiteName}}" stepKey="enterWebsiteName" /> + <fillField selector="{{AdminNewWebsiteSection.code}}" userInput="{{websiteCode}}" stepKey="enterWebsiteCode" /> + <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveWebsite" /> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridToReload"/> + <see userInput="You saved the website." stepKey="seeSavedMessage" /> + </actionGroup> + + <!--Get Website_id--> + <actionGroup name="AdminGetWebsiteIdActionGroup"> + <arguments> + <argument name="website"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> + <fillField selector="{{AdminStoresGridSection.websiteFilterTextField}}" userInput="{{website.name}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton" /> + <see selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" userInput="{{website.name}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabFromCurrentUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml new file mode 100644 index 0000000000000..77473ae235c4a --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteStoreViewActionGroup"> + <arguments> + <argument name="customStore" defaultValue="customStore"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <click selector="{{AdminStoresGridSection.storeViewInFirstRow}}" stepKey="clickStoreViewInGrid"/> + <waitForPageLoad stepKey="waitForStoreViewPage"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreView"/> + <selectOption selector="{{AdminStoreBackupOptionsSection.createBackupSelect}}" userInput="No" stepKey="dontCreateDbBackup"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewAgain"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreDelete"/> + <see userInput="You deleted the store view." stepKey="seeDeleteMessage"/> + </actionGroup> + <actionGroup name="AdminDeleteStoreViewUseStringArgumentsActionGroup" extends="AdminDeleteStoreViewActionGroup"> + <arguments> + <argument name="customStoreName" type="string"/> + </arguments> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{customStoreName}}" stepKey="fillStoreViewFilterField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml new file mode 100644 index 0000000000000..e809f09565fd6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteWebsiteActionGroup"> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.websiteFilter}}" userInput="{{websiteName}}" stepKey="fillWebsiteFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditWebsitePage"/> + <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> + <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteWebsiteButton"/> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridToReload"/> + <see userInput="You deleted the website." stepKey="seeSavedMessage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml new file mode 100644 index 0000000000000..2ccfc0dcc1277 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="filterStoresGridByWebsite"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.websiteFilter}}" userInput="{{website}}" stepKey="fillWebsiteFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> + + <actionGroup name="filterStoresGridByStore"> + <arguments> + <argument name="store" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeFilter}}" userInput="{{store}}" stepKey="fillStoreFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> + + <actionGroup name="filterStoresGridByStoreView"> + <arguments> + <argument name="storeView" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{storeView}}" stepKey="fillStoreViewFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresOpenByNameActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresOpenByNameActionGroup.xml new file mode 100644 index 0000000000000..2d94aff9e117c --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresOpenByNameActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStoreViewByNameActionGroup" extends="filterStoresGridByStoreView"> + <amOnPage url="{{AdminSystemStorePage.url}}" before="clickClearFilters" stepKey="amOnAdminSystemStorePage"/> + <!--Open store view page--> + <see selector="{{AdminStoresGridSection.storeViewInFirstRow}}" userInput="{{storeView}}" after="waitForGridLoad" stepKey="seeAssertStoreViewInGridMessage"/> + <click selector="{{AdminStoresGridSection.storeViewInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml new file mode 100644 index 0000000000000..3ca45a5a378fd --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSwitchBaseActionGroup"> + <arguments> + <argument name="scopeName" defaultValue="customStore.name"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickScopeSwitchDropdown"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmScopeSwitch"/> + <waitForPageLoad stepKey="waitForScopeSwitched"/> + <scrollToTopOfPage stepKey="scrollToStoreSwitcher"/> + <see userInput="{{scopeName}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewScopeName"/> + </actionGroup> + + <actionGroup name="AdminSwitchStoreViewActionGroup" extends="AdminSwitchBaseActionGroup"> + <waitForElementVisible selector="{{AdminMainActionsSection.storeViewByName(scopeName)}}" after="clickScopeSwitchDropdown" stepKey="waitForStoreViewNameIsVisible"/> + <click selector="{{AdminMainActionsSection.storeViewByName(scopeName)}}" after="waitForStoreViewNameIsVisible" stepKey="clickStoreViewByName"/> + </actionGroup> + + <actionGroup name="AdminSwitchWebsiteActionGroup" extends="AdminSwitchBaseActionGroup"> + <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName(scopeName)}}" after="clickScopeSwitchDropdown" stepKey="waitForWebsiteNameIsVisible"/> + <click selector="{{AdminMainActionsSection.websiteByName(scopeName)}}" after="waitForWebsiteNameIsVisible" stepKey="clickStoreViewByName"/> + </actionGroup> + + <actionGroup name="AdminSwitchToAllStoreViewActionGroup" extends="AdminSwitchBaseActionGroup"> + <click selector="{{AdminMainActionsSection.allStoreViews}}" after="clickScopeSwitchDropdown" stepKey="clickStoreViewByName"/> + <see selector="{{AdminMainActionsSection.storeSwitcher}}" userInput="{{scopeName}}" after="clickStoreViewByName" stepKey="seeNewStoreViewName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteByNameActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteByNameActionGroup.xml new file mode 100644 index 0000000000000..6f560a3e84451 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteByNameActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSwitchWebsiteByNameActionGroup"> + <arguments> + <argument name="website"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickWebsiteSwitchDropdown"/> + <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName('Main Website')}}" stepKey="waitForWebsiteAreVisible"/> + <click selector="{{AdminMainActionsSection.websiteByName(website.name)}}" stepKey="clickWebsiteByName"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> + <see userInput="{{website.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewWebsiteName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml new file mode 100644 index 0000000000000..805958909f7fa --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateCustomStoreViewActionGroup"> + <arguments> + <argument name="storeGroupName" defaultValue="customStoreGroup.name"/> + </arguments> + <amOnPage stepKey="amOnAdminSystemStorePage" url="{{AdminSystemStorePage.url}}"/> + <click stepKey="clickCreateStoreViewButton" selector="{{AdminStoresMainActionsSection.createStoreViewButton}}"/> + <waitForPageLoad time="30" stepKey="waitForProductPageLoad"/> + <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup" userInput="{{storeGroupName}}"/> + <fillField stepKey="fillStoreViewName" selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{customStore.name}}"/> + <fillField stepKey="fillStoreViewCode" selector="{{AdminNewStoreSection.storeCodeTextField}}" userInput="{{customStore.code}}"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="selectStoreViewStatus" userInput="{{customStore.is_active}}"/> + <click stepKey="clickSaveStoreViewButton" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="clickAcceptNewStoreViewCreationButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml new file mode 100644 index 0000000000000..d35cf13890114 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteCustomStoreActionGroup"> + <arguments> + <argument name="storeGroupName" defaultValue="customStoreGroup.name"/> + </arguments> + <amOnPage stepKey="amOnAdminSystemStorePage" url="{{AdminSystemStorePage.url}}"/> + <conditionalClick stepKey="resetSearchFilter" selector="{{AdminStoresGridFilterSection.clearAll}}" dependentSelector="{{AdminStoresGridFilterSection.clearAll}}" visible="true"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField stepKey="fillSearchStoreGroupField" selector="{{AdminStoresGridFilterSection.storeFilter}}" userInput="{{storeGroupName}}"/> + <click stepKey="clickSearchButton" selector="{{AdminStoresGridFilterSection.applyFilters}}"/> + <see stepKey="verifyThatCorrectStoreGroupFound" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="{{storeGroupName}}"/> + <click stepKey="clickEditExistingStoreRow" selector="{{AdminStoresGridSection.storeInFirstRow}}"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click stepKey="clickDeleteStoreGroupButtonOnEditStorePage" selector="{{AdminStoresMainActionsSection.deleteButton}}"/> + <selectOption stepKey="setCreateDbBackupToNo" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" userInput="No"/> + <click stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage" selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml new file mode 100644 index 0000000000000..c62ed55c7df49 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchStoreViewActionGroup"> + <arguments> + <argument name="storeView" defaultValue="customStore"/> + </arguments> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown"/> + <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.code)}}" stepKey="clickSelectStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml new file mode 100644 index 0000000000000..8e84b84c8aa49 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAssignToWebsite" type="product_website_link"> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml new file mode 100644 index 0000000000000..158b5ed59c9df --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="StorefrontDisableAddStoreCodeToUrls"> + <!-- Magento default value --> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontEnableAddStoreCodeToUrls"> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml new file mode 100644 index 0000000000000..34b3bbea9386e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultStore" type="store"> + <data key="name">Default Store View</data> + <data key="code">default</data> + <data key="is_active">1</data> + </entity> + <entity name="customStore" type="store"> + <!--data key="group_id">customStoreGroup.id</data--> + <data key="name" unique="suffix">store</data> + <data key="code" unique="suffix">store</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="customStoreEN" type="store"> + <data key="name" unique="suffix">EN</data> + <data key="code" unique="suffix">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="customStoreFR" type="store"> + <data key="name" unique="suffix">FR</data> + <data key="code" unique="suffix">fr</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="CustomStoreENNotUnique" type="store"> + <data key="name">EN</data> + <data key="code">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="CustomStoreNLNotUnique" type="store"> + <data key="name">NL</data> + <data key="code">nl</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="CustomStoreENNotUnique" type="store"> + <data key="name">EN</data> + <data key="code">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="CustomStoreNLNotUnique" type="store"> + <data key="name">NL</data> + <data key="code">nl</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="secondStore" type="store"> + <data key="name">Second Store View</data> + <data key="code">second_store_view</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">secondStoreGroup</requiredEntity> + </entity> + <entity name="SecondStoreUnique" type="store"> + <data key="name" unique="suffix">Second Store View </data> + <data key="code" unique="suffix">second_store_view_</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">secondStoreGroup</requiredEntity> + </entity> + <entity name="staticStore" type="store"> + <data key="name">Second Store View</data> + <data key="code">store123</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">group</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="AllStoreViews" type="store"> + <data key="name">All Store Views</data> + <data key="code">allstoreviews</data> + <data key="is_active">1</data> + </entity> + <entity name="storeViewData" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData1" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData2" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="DefaultAllStoreView" type="store"> + <data key="name">All Store Views</data> + </entity> + <entity name="NewStoreViewData" type="store"> + <data key="name" unique="suffix">StoreView</data> + <data key="code" unique="suffix">StoreViewCode</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml new file mode 100644 index 0000000000000..fb51dbba5dec0 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultStoreGroup" type="group"> + <data key="name">Main Website Store</data> + <data key="code">main_website_store</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + <entity name="customStoreGroup" type="group"> + <data key="group_id">null</data> + <data key="name" unique="suffix">store</data> + <data key="code" unique="suffix">store</data> + <var key="root_category_id" entityKey="id" entityType="category"/> + <data key="website_id">1</data> + <data key="store_action">add</data> + <data key="store_type">group</data> + </entity> + <entity name="secondStoreGroup" type="group"> + <data key="group_id">null</data> + <data key="name">Second Store</data> + <data key="code">second_store</data> + <var key="root_category_id" entityKey="id" entityType="category"/> + <data key="store_action">add</data> + <data key="store_type">group</data> + </entity> + <entity name="SecondStoreGroupUnique" type="group"> + <data key="group_id">null</data> + <data key="name" unique="suffix">Second Store </data> + <data key="code" unique="suffix">second_store_</data> + <var key="root_category_id" entityKey="id" entityType="category"/> + <data key="store_action">add</data> + <data key="store_type">group</data> + </entity> + <entity name="staticStoreGroup" type="group"> + <data key="name">NewStore</data> + <data key="code">Base12</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + <entity name="NewStoreData" type="group"> + <data key="name" unique="suffix">Store</data> + <data key="code" unique="suffix">StoreCode</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml new file mode 100644 index 0000000000000..413540a8f4c12 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="FreeShippingMethodWithMinimumOrderAmount90" type="free_shipping_config_state"> + <requiredEntity type="active">Active</requiredEntity> + <requiredEntity type="free_shipping_subtotal">Price</requiredEntity> + </entity> + <entity name="Active" type="active"> + <data key="value">1</data> + </entity> + <entity name="Price" type="free_shipping_subtotal"> + <data key="value">90</data> + </entity> + + <entity name="ResetFreeShippingMethodWithMinimumOrderAmount90" type="free_shipping_config_state"> + <requiredEntity type="free_shipping_subtotal">DefaultPrice</requiredEntity> + <requiredEntity type="active">DefaultActive</requiredEntity> + </entity> + <entity name="DefaultPrice" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> + <entity name="DefaultActive" type="active"> + <requiredEntity type="active_inherit">DefaultFreeShipping</requiredEntity> + </entity> + <entity name="DefaultFreeShipping" type="active_inherit"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml new file mode 100644 index 0000000000000..9e6aaa3107c3b --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultWebsite" type="group"> + <data key="name">Main Website</data> + <data key="code">base</data> + <data key="sort_order">1</data> + </entity> + <entity name="CustomWebSite" type="website"> + <data key="name" unique="suffix">website</data> + <data key="code" unique="suffix">website</data> + <data key="sort_order">2</data> + <data key="store_action">add</data> + <data key="store_type">website</data> + <data key="website_id">null</data> + </entity> + <entity name="SecondWebsite" type="website"> + <data key="name" unique="suffix">Second Website </data> + <data key="code" unique="suffix">second_website_</data> + <data key="sort_order">10</data> + </entity> + <entity name="secondCustomWebsite" extends="CustomWebSite"> + <data key="name" unique="suffix">Custom Website</data> + <data key="code" unique="suffix">custom_website</data> + </entity> + <entity name="NewWebSiteData" type="webSite"> + <data key="name" unique="suffix">WebSite</data> + <data key="code" unique="suffix">WebSiteCode</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/LICENSE.txt b/app/code/Magento/Store/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Store/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 <insert your license name here>" 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/app/code/Magento/Store/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Store/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Store/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 <insert your license name here>" 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/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml new file mode 100644 index 0000000000000..ca0725f86c289 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ProductWebsiteLink" dataType="product_website_link" type="create" auth="adminOauth" url="/V1/products/{sku}/websites" method="POST"> + <contentType>application/json</contentType> + <object dataType="product_website_link" key="productWebsiteLink"> + <field key="sku">string</field> + <field key="websiteId">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store-meta.xml new file mode 100644 index 0000000000000..e0263b2c88869 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateStore" dataType="store" type="create" + auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" returnRegex="" > + <object dataType="store" key="store"> + <field key="group_id">string</field> + <field key="name">string</field> + <field key="code">string</field> + <field key="is_active">boolean</field> + <field key="store_id">integer</field> + </object> + <field key="store_action">string</field> + <field key="store_type">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_group-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_group-meta.xml new file mode 100644 index 0000000000000..bc117756a542b --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_group-meta.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateStoreGroup" dataType="group" type="create" + auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" returnRegex="" > + <contentType>application/x-www-form-urlencoded</contentType> + <object dataType="group" key="group"> + <field key="group_id">string</field> + <field key="name">string</field> + <field key="code">string</field> + <field key="root_category_id">integer</field> + <field key="website_id">integer</field> + </object> + <field key="store_action">string</field> + <field key="store_type">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml new file mode 100644 index 0000000000000..32aa22fecf9cd --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ChangeFreeShippingConfiguration" dataType="free_shipping_config_state" type="create" auth="adminFormKey" + url="/admin/system_config/save/section/carriers/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="free_shipping_config_state"> + <object key="freeshipping" dataType="free_shipping_config_state"> + <object key="fields" dataType="free_shipping_config_state"> + <object key="active" dataType="active"> + <field key="value">string</field> + <object key="inherit" dataType="active_inherit"> + <field key="value">integer</field> + </object> + </object> + <object key="free_shipping_subtotal" dataType="free_shipping_subtotal"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml new file mode 100644 index 0000000000000..bad274501f710 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateWebsite" dataType="website" type="create" + auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" returnRegex=""> + <object dataType="website" key="website"> + <field key="website_id">string</field> + <field key="name">string</field> + <field key="code">string</field> + <field key="sort_order">integer</field> + </object> + <field key="store_action">string</field> + <field key="store_type">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminNewSystemStorePage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminNewSystemStorePage.xml new file mode 100644 index 0000000000000..6011aec9f33b0 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminNewSystemStorePage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminNewSystemStorePage" url="admin/system_store/newStore" area="admin" module="Magento_Store"> + <section name="AdminNewStoreViewMainActionsSection"/> + <section name="AdminNewStoreSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreDeletePage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreDeletePage.xml new file mode 100644 index 0000000000000..3ec22d3135137 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreDeletePage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreDeletePage" url="system_store/deleteStore" module="Magento_Store" area="admin"> + <section name="AdminStoreBackupOptionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreEditPage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreEditPage.xml new file mode 100644 index 0000000000000..fe7cc22318e0d --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreEditPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreEditPage" url="system_store/editStore" area="admin" module="Magento_Store"> + <section name="AdminNewStoreViewMainActionsSection"/> + <section name="AdminNewStoreSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupEditPage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupEditPage.xml new file mode 100644 index 0000000000000..634ee6d651af1 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupEditPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreGroupEditPage" url="admin/system_store/editGroup" area="admin" module="Magento_Store"> + <section name="AdminStoreGroupActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupPage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupPage.xml new file mode 100644 index 0000000000000..478166361d8f6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreGroupPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreGroupPage" url="admin/system_store/newGroup" module="Magento_Store" area="admin"> + <section name="AdminNewStoreGroupSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStorePage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStorePage.xml new file mode 100644 index 0000000000000..9eed4f6557a59 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStorePage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStorePage" url="/admin/system_store/" area="admin" module="Magento_Store"> + <section name="AdminStoresMainActionsSection"/> + <section name="AdminStoresGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreViewPage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreViewPage.xml new file mode 100644 index 0000000000000..15ed31c19f996 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreViewPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreViewPage" url="admin/system_store/newStore" module="Magento_Store" area="admin"> + <section name="AdminNewStoreViewMainActionsSection"/> + <section name="AdminNewStoreSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreWebsitePage.xml b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreWebsitePage.xml new file mode 100644 index 0000000000000..61ade61552054 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/AdminSystemStoreWebsitePage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminSystemStoreWebsitePage" url="admin/system_store/newWebsite" module="Magento_Store" area="admin"> + <section name="AdminNewWebsiteSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml new file mode 100644 index 0000000000000..0cf1cffceac71 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontStoreHomePage" url="/{{store_view}}/" area="storefront" module="Magento_Store" parameterized="true"> + <section name="StorefrontHeaderSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/README.md b/app/code/Magento/Store/Test/Mftf/README.md new file mode 100644 index 0000000000000..18b37aad6d8ef --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Store Functional Tests + +The Functional Test Module for **Magento Store** module. diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml new file mode 100644 index 0000000000000..14160835af3e1 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMainActionsSection"> + <element name="storeSwitcher" type="text" selector=".store-switcher"/> + <element name="storeViewDropdown" type="button" selector="#store-change-button"/> + <element name="storeViewByName" type="button" selector="//*[contains(@class,'store-switcher-store-view')]/*[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> + <element name="websiteByName" type="button" selector="//*[@class='store-switcher-website ']/a[contains(text(), '{{websiteName}}')]" timeout="30" parameterized="true"/> + <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml new file mode 100644 index 0000000000000..106a0f4de5e8b --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewStoreGroupSection"> + <element name="storeGrpWebsiteDropdown" type="select" selector="#group_website_id"/> + <element name="storeGrpNameTextField" type="input" selector="#group_name"/> + <element name="storeGrpCodeTextField" type="input" selector="#group_code"/> + <element name="storeRootCategoryDropdown" type="select" selector="#group_root_category_id"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreSection.xml new file mode 100644 index 0000000000000..cec7a1f4f81e1 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewStoreSection"> + <element name="storeNameTextField" type="input" selector="#store_name"/> + <element name="storeCodeTextField" type="input" selector="#store_code"/> + <element name="statusDropdown" type="select" selector="#store_is_active"/> + <element name="storeGrpDropdown" type="select" selector="#store_group_id"/> + <element name="sortOrderTextField" type="input" selector="#store_sort_order"/> + <element name="acceptNewStoreViewCreation" type="button" selector=".action-primary.action-accept" /> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml new file mode 100644 index 0000000000000..faffc69dc6975 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewStoreViewActionsSection"> + <element name="backButton" type="button" selector="#back" timeout="30"/> + <element name="delete" type="button" selector="#delete" timeout="30"/> + <element name="resetButton" type="button" selector="#reset" timeout="30"/> + <element name="saveButton" type="button" selector="#save" timeout="60"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml new file mode 100644 index 0000000000000..1f02de731f9a0 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewWebsiteActionsSection"> + <element name="saveWebsite" type="button" selector="#save" timeout="60"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteSection.xml new file mode 100644 index 0000000000000..21dee5f6b6e0d --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewWebsiteSection"> + <element name="name" type="input" selector="#website_name"/> + <element name="code" type="input" selector="#website_code"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreBackupOptionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreBackupOptionsSection.xml new file mode 100644 index 0000000000000..5472eab6ae88e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreBackupOptionsSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoreBackupOptionsSection"> + <element name="createBackupSelect" type="select" selector="#store_create_backup"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml new file mode 100644 index 0000000000000..6dc766c0c02da --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoreGroupActionsSection"> + <element name="saveButton" type="button" selector="#save" timeout="60" /> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteStoreGroupSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteStoreGroupSection.xml new file mode 100644 index 0000000000000..ba3d9660b44b3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteStoreGroupSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresDeleteStoreGroupSection"> + <element name="createDbBackup" type="select" selector="#store_create_backup"/> + <element name="deleteStoreGroupButton" type="button" selector="#delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml new file mode 100644 index 0000000000000..45028b3af1ade --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresGridFilterSection"> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> + <element name="clearAll" type="button" selector=".admin__data-grid-header .admin__data-grid-filters-current._show .action-clear" timeout="30"/> + <element name="columnsDropdown" type="button" selector=".admin__data-grid-action-columns button.admin__action-dropdown"/> + <element name="viewColumnOption" type="checkbox" selector="//div[contains(@class, '_active')]//div[contains(@class, 'admin__data-grid-action-columns-menu')]//div[@class='admin__field-option']//label[text()='{{col}}']/preceding-sibling::input" parameterized="true"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="websiteFilter" type="input" selector="input.admin__control-text[name='name']"/> + <element name="storeFilter" type="input" selector="input.admin__control-text[name='group_title']"/> + <element name="storeViewFilter" type="input" selector="input.admin__control-text[name='store_title']"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml new file mode 100644 index 0000000000000..443edc49b6d00 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresGridSection"> + <element name="websiteInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(1) a"/> + <element name="storeInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(2) a"/> + <element name="storeViewInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(3) a"/> + <element name="websiteNameInFirstRow" type="text" selector="tr:nth-of-type(1) > .col-website_title > a"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml new file mode 100644 index 0000000000000..5fb45f6dee3c2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresMainActionsSection"> + <element name="createStoreViewButton" type="button" selector="#add_store"/> + <element name="createStoreButton" type="button" selector="#add_group"/> + <element name="createWebsiteButton" type="button" selector="#add"/> + <element name="saveButton" type="button" selector="#save"/> + <element name="backButton" type="button" selector="#back"/> + <element name="deleteButton" type="button" selector="#delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml new file mode 100644 index 0000000000000..56cda5376ca4e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontHeaderSection"> + <element name="storeViewSwitcher" type="button" selector="#switcher-language-trigger"/> + <element name="storeViewDropdown" type="button" selector="ul.switcher-dropdown"/> + <element name="storeViewOption" type="button" selector="li.view-{{var1}}>a" parameterized="true"/> + <element name="mainTitle" type="text" selector="#maincontent .page-title"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml new file mode 100644 index 0000000000000..bb1dbb3559d74 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupTest"> + <annotations> + <features value="Create a store group in admin"/> + <stories value="Create a store group in admin"/> + <title value="Create a store group in admin"/> + <description value="Create a store group in admin"/> + <group value="store"/> + </annotations> + <before> + <getData entity="DefaultRootCategoryGetter" stepKey="getDefaultRootCategory"/> + <createData stepKey="createCustomStoreGroup1" entity="customStoreGroup"> + <requiredEntity createDataKey="getDefaultRootCategory"/> + </createData> + <createData stepKey="createCustomStoreGroup2" entity="customStoreGroup"> + <requiredEntity createDataKey="getDefaultRootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup1"> + <argument name="storeGroupName" value="$$createCustomStoreGroup1.group[name]$$"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup2"> + <argument name="storeGroupName" value="$$createCustomStoreGroup2.group[name]$$"/> + </actionGroup> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <amOnPage stepKey="openAdminSystemStorePage" url="{{AdminSystemStorePage.url}}"/> + + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="$$createCustomStoreGroup1.group[name]$$"/> + </actionGroup> + <see stepKey="seeStoreGroup1NameAfterSearch" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="$$createCustomStoreGroup1.group[name]$$"/> + + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup2Name"> + <argument name="store" value="$$createCustomStoreGroup2.group[name]$$"/> + </actionGroup> + <see stepKey="seeStoreGroup1NameAfterSearch2" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="$$createCustomStoreGroup2.group[name]$$"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php new file mode 100644 index 0000000000000..bb2705bff9aab --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextNonDefaultStoreDirectLinkTest.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\App\Action\Plugin; + +use Magento\Framework\App\Action\AbstractAction; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Session\Generic; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\StoreCookieManagerInterface; +use Magento\Store\App\Action\Plugin\Context; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use PHPUnit\Framework\TestCase; + +/** + * Class ContextNonDefaultStoreDirectLinkTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ContextNonDefaultStoreDirectLinkTest extends TestCase +{ + const CURRENCY_SESSION = 'CNY'; + const CURRENCY_DEFAULT = 'USD'; + const CURRENCY_CURRENT_STORE = 'UAH'; + + /** + * Test for full page cache hits from new http clients if store context was specified in the URL + * + * @dataProvider cacheHitOnDirectLinkToNonDefaultStoreView + * @param string $customStore + * @param string $defaultStore + * @param string $expectedDefaultStore + * @param bool $useStoreInUrl + * @return void + */ + public function testCacheHitOnDirectLinkToNonDefaultStoreView( + string $customStore, + string $defaultStore, + string $expectedDefaultStore, + bool $useStoreInUrl + ) { + $sessionMock = $this->createPartialMock(Generic::class, ['getCurrencyCode']); + $httpContextMock = $this->createMock(HttpContext::class); + $storeManager = $this->createMock(StoreManagerInterface::class); + $storeCookieManager = $this->createMock(StoreCookieManagerInterface::class); + $storeMock = $this->createMock(Store::class); + $currentStoreMock = $this->createMock(Store::class); + $requestMock = $this->getMockBuilder(RequestInterface::class)->getMockForAbstractClass(); + $subjectMock = $this->getMockBuilder(AbstractAction::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $httpContextMock->expects($this->once()) + ->method('getValue') + ->with(StoreManagerInterface::CONTEXT_STORE) + ->willReturn(null); + + $websiteMock = $this->createPartialMock( + Website::class, + ['getDefaultStore', '__wakeup'] + ); + + $plugin = (new ObjectManager($this))->getObject( + Context::class, + [ + 'session' => $sessionMock, + 'httpContext' => $httpContextMock, + 'storeManager' => $storeManager, + 'storeCookieManager' => $storeCookieManager, + ] + ); + + $storeManager->method('getDefaultStoreView') + ->willReturn($storeMock); + + $storeCookieManager->expects($this->once()) + ->method('getStoreCodeFromCookie') + ->willReturn('storeCookie'); + + $currentStoreMock->expects($this->any()) + ->method('getDefaultCurrencyCode') + ->willReturn(self::CURRENCY_CURRENT_STORE); + + $currentStoreMock->expects($this->any()) + ->method('getCode') + ->willReturn($customStore); + + $currentStoreMock->method('isUseStoreInUrl')->willReturn($useStoreInUrl); + + $storeManager->expects($this->any()) + ->method('getWebsite') + ->willReturn($websiteMock); + + $websiteMock->expects($this->any()) + ->method('getDefaultStore') + ->willReturn($storeMock); + + $storeMock->expects($this->any()) + ->method('getDefaultCurrencyCode') + ->willReturn(self::CURRENCY_DEFAULT); + + $storeMock->expects($this->any()) + ->method('getCode') + ->willReturn($defaultStore); + + $requestMock->expects($this->any()) + ->method('getParam') + ->with($this->equalTo('___store')) + ->willReturn($defaultStore); + + $storeManager->method('getStore') + ->with($defaultStore) + ->willReturn($currentStoreMock); + + $sessionMock->expects($this->any()) + ->method('getCurrencyCode') + ->willReturn(self::CURRENCY_SESSION); + + $httpContextMock->expects($this->at(1))->method( + 'setValue' + )->with(StoreManagerInterface::CONTEXT_STORE, $customStore, $expectedDefaultStore); + + $httpContextMock->expects($this->at(2))->method('setValue'); + + $plugin->beforeDispatch( + $subjectMock, + $requestMock + ); + } + + public function cacheHitOnDirectLinkToNonDefaultStoreView() + { + return [ + [ + 'custom_store', + 'default', + 'custom_store', + true, + ], + [ + 'custom_store', + 'default', + 'default', + false, + ], + [ + 'default', + 'default', + 'default', + true, + ], + ]; + } +} diff --git a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php index d44724fe302d0..3b8aad834b691 100644 --- a/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Action/Plugin/ContextTest.php @@ -8,8 +8,10 @@ use Magento\Framework\App\Action\AbstractAction; use Magento\Framework\App\Http\Context; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Http\Context as HttpContext; /** * Class ContextPluginTest @@ -33,7 +35,7 @@ class ContextTest extends \PHPUnit\Framework\TestCase protected $sessionMock; /** - * @var \Magento\Framework\App\Http\Context|\PHPUnit_Framework_MockObject_MockObject + * @var HttpContext|\PHPUnit_Framework_MockObject_MockObject */ protected $httpContextMock; @@ -50,7 +52,7 @@ class ContextTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeMock; + protected $store; /** * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject @@ -60,7 +62,7 @@ class ContextTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Store\Model\Website|\PHPUnit_Framework_MockObject_MockObject */ - protected $websiteMock; + protected $website; /** * @var AbstractAction|\PHPUnit_Framework_MockObject_MockObject @@ -78,12 +80,16 @@ class ContextTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->sessionMock = $this->createPartialMock(\Magento\Framework\Session\Generic::class, ['getCurrencyCode']); - $this->httpContextMock = $this->createMock(\Magento\Framework\App\Http\Context::class); + $this->httpContextMock = $this->createMock(HttpContext::class); + $this->httpContextMock->expects($this->once()) + ->method('getValue') + ->with(StoreManagerInterface::CONTEXT_STORE) + ->willReturn(null); $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->storeCookieManager = $this->createMock(\Magento\Store\Api\StoreCookieManagerInterface::class); - $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $this->store = $this->createMock(\Magento\Store\Model\Store::class); $this->currentStoreMock = $this->createMock(\Magento\Store\Model\Store::class); - $this->websiteMock = $this->createPartialMock( + $this->website = $this->createPartialMock( \Magento\Store\Model\Website::class, ['getDefaultStore', '__wakeup'] ); @@ -98,18 +104,9 @@ protected function setUp() 'session' => $this->sessionMock, 'httpContext' => $this->httpContextMock, 'storeManager' => $this->storeManager, - 'storeCookieManager' => $this->storeCookieManager, + 'storeCookieManager' => $this->storeCookieManager ] ); - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->will($this->returnValue($this->websiteMock)); - $this->storeManager->method('getDefaultStoreView') - ->willReturn($this->storeMock); - - $this->websiteMock->expects($this->once()) - ->method('getDefaultStore') - ->will($this->returnValue($this->storeMock)); $this->storeCookieManager->expects($this->once()) ->method('getStoreCodeFromCookie') @@ -121,11 +118,18 @@ protected function setUp() public function testBeforeDispatchCurrencyFromSession() { - $this->storeMock->expects($this->once()) + $this->storeManager->expects($this->once()) + ->method('getWebsite') + ->will($this->returnValue($this->website)); + $this->website->expects($this->once()) + ->method('getDefaultStore') + ->will($this->returnValue($this->store)); + + $this->store->expects($this->once()) ->method('getDefaultCurrencyCode') ->will($this->returnValue(self::CURRENCY_DEFAULT)); - $this->storeMock->expects($this->once()) + $this->store->expects($this->once()) ->method('getCode') ->willReturn('default'); $this->currentStoreMock->expects($this->once()) @@ -145,57 +149,92 @@ public function testBeforeDispatchCurrencyFromSession() ->method('getCurrencyCode') ->will($this->returnValue(self::CURRENCY_SESSION)); - $this->httpContextMock->expects($this->at(0)) - ->method('setValue') - ->with(StoreManagerInterface::CONTEXT_STORE, 'custom_store', 'default'); - /** Make sure that current currency is taken from session if available */ $this->httpContextMock->expects($this->at(1)) ->method('setValue') - ->with(Context::CONTEXT_CURRENCY, self::CURRENCY_SESSION, self::CURRENCY_DEFAULT); - - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); + ->with( + StoreManagerInterface::CONTEXT_STORE, + 'custom_store', + 'default' + ); + // Make sure that current currency is taken from session if available. + $this->httpContextMock->expects($this->at(2)) + ->method('setValue') + ->with( + Context::CONTEXT_CURRENCY, + self::CURRENCY_SESSION, + self::CURRENCY_DEFAULT + ); + + $this->plugin->beforeDispatch( + $this->subjectMock, + $this->requestMock + ); } - public function testDispatchCurrentStoreCurrency() + public function testDispatchCurrentStoreAndCurrency() { - $this->storeMock->expects($this->once()) - ->method('getDefaultCurrencyCode') - ->will($this->returnValue(self::CURRENCY_DEFAULT)); + $defaultStoreCode = 'default_store'; + $customStoreCode = 'custom_store'; - $this->storeMock->expects($this->once()) - ->method('getCode') - ->willReturn('default'); - $this->currentStoreMock->expects($this->once()) - ->method('getCode') - ->willReturn('custom_store'); + $this->storeManager->method('getWebsite') + ->willReturn($this->website); + $this->website->method('getDefaultStore') + ->willReturn($this->store); + + $this->store->method('getDefaultCurrencyCode') + ->willReturn(self::CURRENCY_DEFAULT); + + $this->store->method('getCode') + ->willReturn($defaultStoreCode); + $this->currentStoreMock->method('getCode') + ->willReturn($customStoreCode); $this->requestMock->expects($this->once()) ->method('getParam') ->with($this->equalTo('___store')) - ->will($this->returnValue('default')); + ->willReturn($defaultStoreCode); $this->storeManager->method('getStore') - ->with('default') + ->with($defaultStoreCode) ->willReturn($this->currentStoreMock); - $this->httpContextMock->expects($this->at(0)) - ->method('setValue') - ->with(StoreManagerInterface::CONTEXT_STORE, 'custom_store', 'default'); - /** Make sure that current currency is taken from current store if no value is provided in session */ $this->httpContextMock->expects($this->at(1)) ->method('setValue') - ->with(Context::CONTEXT_CURRENCY, self::CURRENCY_CURRENT_STORE, self::CURRENCY_DEFAULT); - - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); + ->with( + StoreManagerInterface::CONTEXT_STORE, + $customStoreCode, + $defaultStoreCode + ); + // Make sure that current currency is taken from current store + //if no value is provided in session. + $this->httpContextMock->expects($this->at(2)) + ->method('setValue') + ->with( + Context::CONTEXT_CURRENCY, + self::CURRENCY_CURRENT_STORE, + self::CURRENCY_DEFAULT + ); + + $this->plugin->beforeDispatch( + $this->subjectMock, + $this->requestMock + ); } public function testDispatchStoreParameterIsArray() { - $this->storeMock->expects($this->once()) + $this->storeManager->expects($this->once()) + ->method('getWebsite') + ->will($this->returnValue($this->website)); + $this->website->expects($this->once()) + ->method('getDefaultStore') + ->will($this->returnValue($this->store)); + + $this->store->expects($this->once()) ->method('getDefaultCurrencyCode') ->will($this->returnValue(self::CURRENCY_DEFAULT)); - $this->storeMock->expects($this->once()) + $this->store->expects($this->once()) ->method('getCode') ->willReturn('default'); $this->currentStoreMock->expects($this->once()) @@ -218,28 +257,45 @@ public function testDispatchStoreParameterIsArray() ->with('500') ->willReturn($this->currentStoreMock); - $this->httpContextMock->expects($this->at(0)) - ->method('setValue') - ->with(StoreManagerInterface::CONTEXT_STORE, 'custom_store', 'default'); - /** Make sure that current currency is taken from current store if no value is provided in session */ $this->httpContextMock->expects($this->at(1)) ->method('setValue') - ->with(Context::CONTEXT_CURRENCY, self::CURRENCY_CURRENT_STORE, self::CURRENCY_DEFAULT); - - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); + ->with( + StoreManagerInterface::CONTEXT_STORE, + 'custom_store', + 'default' + ); + //Make sure that current currency is taken from current store + //if no value is provided in session. + $this->httpContextMock->expects($this->at(2)) + ->method('setValue') + ->with( + Context::CONTEXT_CURRENCY, + self::CURRENCY_CURRENT_STORE, + self::CURRENCY_DEFAULT + ); + + $this->plugin->beforeDispatch( + $this->subjectMock, + $this->requestMock + ); } /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid store parameter. + * @expectedException \Magento\Framework\Exception\NotFoundException */ public function testDispatchStoreParameterIsInvalidArray() { - $this->storeMock->expects($this->never()) + $this->storeManager->expects($this->once()) + ->method('getWebsite') + ->will($this->returnValue($this->website)); + $this->website->expects($this->once()) + ->method('getDefaultStore') + ->will($this->returnValue($this->store)); + $this->store->expects($this->exactly(2)) ->method('getDefaultCurrencyCode') ->will($this->returnValue(self::CURRENCY_DEFAULT)); - $this->storeMock->expects($this->never()) + $this->store->expects($this->exactly(2)) ->method('getCode') ->willReturn('default'); $this->currentStoreMock->expects($this->never()) @@ -256,6 +312,51 @@ public function testDispatchStoreParameterIsInvalidArray() ->method('getParam') ->with($this->equalTo('___store')) ->will($this->returnValue($store)); + $this->storeManager->expects($this->once()) + ->method('getStore') + ->with() + ->willReturn($this->store); + $this->plugin->beforeDispatch( + $this->subjectMock, + $this->requestMock + ); + } + + /** + * @expectedException \Magento\Framework\Exception\NotFoundException + */ + public function testDispatchNonExistingStore() + { + $storeId = 'NonExisting'; + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('___store') + ->willReturn($storeId); + $this->storeManager->expects($this->at(0)) + ->method('getStore') + ->with($storeId) + ->willThrowException(new NoSuchEntityException()); + $this->storeManager->expects($this->at(1)) + ->method('getStore') + ->with() + ->willReturn($this->store); + $this->storeManager->expects($this->once()) + ->method('getWebsite') + ->will($this->returnValue($this->website)); + $this->website->expects($this->once()) + ->method('getDefaultStore') + ->will($this->returnValue($this->store)); + $this->store->expects($this->exactly(2)) + ->method('getDefaultCurrencyCode') + ->will($this->returnValue(self::CURRENCY_DEFAULT)); + + $this->store->expects($this->exactly(2)) + ->method('getCode') + ->willReturn('default'); + $this->currentStoreMock->expects($this->never()) + ->method('getCode') + ->willReturn('custom_store'); + $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } } diff --git a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php index f2bd401cea3fb..7d2fb54014967 100644 --- a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php @@ -47,6 +47,7 @@ public function testProcessIfStoreExistsAndIsNotDirectAcccessToFrontName() )->with( 'storeCode' )->willReturn($store); + $store->expects($this->once())->method('getCode')->will($this->returnValue('storeCode')); $store->expects($this->once())->method('isUseStoreInUrl')->will($this->returnValue(true)); $this->_requestMock->expects( $this->once() diff --git a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php index 8b4799d2b3437..57cb63e7c2744 100644 --- a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php +++ b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php @@ -25,6 +25,9 @@ class SwitcherTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $urlBuilder; + /** @var \Magento\Store\Api\Data\StoreInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $store; + protected function setUp() { $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)->getMock(); @@ -33,6 +36,9 @@ protected function setUp() $this->context->expects($this->any())->method('getStoreManager')->will($this->returnValue($this->storeManager)); $this->context->expects($this->any())->method('getUrlBuilder')->will($this->returnValue($this->urlBuilder)); $this->corePostDataHelper = $this->createMock(\Magento\Framework\Data\Helper\PostHelper::class); + $this->store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->switcher = (new ObjectManager($this))->getObject( \Magento\Store\Block\Switcher::class, [ @@ -50,14 +56,23 @@ public function testGetTargetStorePostData() $store->expects($this->any()) ->method('getCode') ->willReturn('new-store'); - $storeSwitchUrl = 'http://domain.com/stores/store/switch'; + $storeSwitchUrl = 'http://domain.com/stores/store/redirect'; $store->expects($this->atLeastOnce()) ->method('getCurrentUrl') - ->with(true) + ->with(false) + ->willReturn($storeSwitchUrl); + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->once()) + ->method('getCode') + ->willReturn('old-store'); + $this->urlBuilder->expects($this->once()) + ->method('getUrl') ->willReturn($storeSwitchUrl); $this->corePostDataHelper->expects($this->any()) ->method('getPostData') - ->with($storeSwitchUrl, ['___store' => 'new-store']); + ->with($storeSwitchUrl, ['___store' => 'new-store', 'uenc' => null, '___from_store' => 'old-store']); $this->switcher->getTargetStorePostData($store); } diff --git a/app/code/Magento/Store/Test/Unit/Console/Command/StoreListCommandTest.php b/app/code/Magento/Store/Test/Unit/Console/Command/StoreListCommandTest.php index 4f848def8c353..b7dc081a2be8f 100644 --- a/app/code/Magento/Store/Test/Unit/Console/Command/StoreListCommandTest.php +++ b/app/code/Magento/Store/Test/Unit/Console/Command/StoreListCommandTest.php @@ -6,9 +6,9 @@ namespace Magento\Store\Test\Unit\Console\Command; use Magento\Store\Console\Command\StoreListCommand; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableFactory; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Helper\TableHelper; use Magento\Store\Model\Store; use Magento\Framework\Console\Cli; @@ -38,19 +38,21 @@ protected function setUp() $this->storeManagerMock = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); + $tableHelperFactory = $this->getMockBuilder(TableFactory::class)->disableOriginalConstructor()->getMock(); + $tableHelperFactory->method('create') + ->willReturnCallback( + function ($arguments) { + return $this->objectManager->getObject(Table::class, $arguments); + } + ); + $this->command = $this->objectManager->getObject( StoreListCommand::class, - ['storeManager' => $this->storeManagerMock] - ); - - /** @var HelperSet $helperSet */ - $helperSet = $this->objectManager->getObject( - HelperSet::class, - ['helpers' => [$this->objectManager->getObject(TableHelper::class)]] + [ + 'storeManager' => $this->storeManagerMock, + 'tableHelperFactory' => $tableHelperFactory + ] ); - - //Inject table helper for output - $this->command->setHelperSet($helperSet); } public function testExecuteExceptionNoVerbosity() diff --git a/app/code/Magento/Store/Test/Unit/Console/Command/WebsiteListCommandTest.php b/app/code/Magento/Store/Test/Unit/Console/Command/WebsiteListCommandTest.php index 3978f49522224..9ba95deb005b5 100644 --- a/app/code/Magento/Store/Test/Unit/Console/Command/WebsiteListCommandTest.php +++ b/app/code/Magento/Store/Test/Unit/Console/Command/WebsiteListCommandTest.php @@ -6,9 +6,9 @@ namespace Magento\Store\Test\Unit\Console\Command; use Magento\Store\Console\Command\WebsiteListCommand; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableFactory; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Helper\TableHelper; use Magento\Store\Model\Website; use Magento\Framework\Console\Cli; use Magento\Store\Api\WebsiteRepositoryInterface; @@ -39,19 +39,21 @@ protected function setUp() $this->websiteRepositoryMock = $this->getMockForAbstractClass(WebsiteRepositoryInterface::class); + $tableHelperFactory = $this->getMockBuilder(TableFactory::class)->disableOriginalConstructor()->getMock(); + $tableHelperFactory->method('create') + ->willReturnCallback( + function ($arguments) { + return $this->objectManager->getObject(Table::class, $arguments); + } + ); + $this->command = $this->objectManager->getObject( WebsiteListCommand::class, - ['websiteManagement' => $this->websiteRepositoryMock] - ); - - /** @var HelperSet $helperSet */ - $helperSet = $this->objectManager->getObject( - HelperSet::class, - ['helpers' => [$this->objectManager->getObject(TableHelper::class)]] + [ + 'websiteManagement' => $this->websiteRepositoryMock, + 'tableHelperFactory' => $tableHelperFactory + ] ); - - //Inject table helper for output - $this->command->setHelperSet($helperSet); } public function testExecuteExceptionNoVerbosity() diff --git a/app/code/Magento/Store/Test/Unit/Controller/Store/SwitchActionTest.php b/app/code/Magento/Store/Test/Unit/Controller/Store/SwitchActionTest.php index 1e7b2691a0084..fa7c696bf53cd 100644 --- a/app/code/Magento/Store/Test/Unit/Controller/Store/SwitchActionTest.php +++ b/app/code/Magento/Store/Test/Unit/Controller/Store/SwitchActionTest.php @@ -8,13 +8,15 @@ use Magento\Framework\App\Http\Context as HttpContext; use Magento\Store\Api\StoreCookieManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreResolver; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher; +use Magento\Store\Model\StoreSwitcherInterface; /** * Test class for \Magento\Store\Controller\Store\SwitchAction + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SwitchActionTest extends \PHPUnit\Framework\TestCase { @@ -58,6 +60,9 @@ class SwitchActionTest extends \PHPUnit\Framework\TestCase */ private $redirectMock; + /** @var StoreSwitcherInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $storeSwitcher; + protected function setUp() { $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)->getMock(); @@ -73,6 +78,10 @@ protected function setUp() ->getMockForAbstractClass(); $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class)->getMock(); + $this->storeSwitcher = $this->getMockBuilder(StoreSwitcher::class) + ->disableOriginalConstructor() + ->setMethods(['switch']) + ->getMock(); $this->model = (new ObjectManager($this))->getObject( \Magento\Store\Controller\Store\SwitchAction::class, @@ -83,81 +92,45 @@ protected function setUp() 'storeManager' => $this->storeManagerMock, '_request' => $this->requestMock, '_response' => $this->responseMock, - '_redirect' => $this->redirectMock + '_redirect' => $this->redirectMock, + 'storeSwitcher' => $this->storeSwitcher ] ); } - public function testExecuteSuccessWithoutUseStoreInUrl() + public function testExecute() { $storeToSwitchToCode = 'sv2'; $defaultStoreViewCode = 'default'; $expectedRedirectUrl = "magento.com/{$storeToSwitchToCode}"; - $currentActiveStoreMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); $defaultStoreViewMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); $storeToSwitchToMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->disableOriginalConstructor() ->setMethods(['isUseStoreInUrl']) ->getMockForAbstractClass(); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($currentActiveStoreMock); - $this->requestMock->expects($this->once())->method('getParam')->willReturn($storeToSwitchToCode); + $this->requestMock->expects($this->any())->method('getParam')->willReturnMap( + [ + [StoreResolver::PARAM_NAME, null, $storeToSwitchToCode], + ['___from_store', null, $defaultStoreViewCode] + ] + ); $this->storeRepositoryMock ->expects($this->once()) - ->method('getActiveStoreByCode') - ->willReturn($storeToSwitchToMock); - $this->storeManagerMock - ->expects($this->once()) - ->method('getDefaultStoreView') + ->method('get') + ->with($defaultStoreViewCode) ->willReturn($defaultStoreViewMock); - $defaultStoreViewMock->expects($this->once())->method('getId')->willReturn($defaultStoreViewCode); - $storeToSwitchToMock->expects($this->once())->method('getId')->willReturn($storeToSwitchToCode); - $storeToSwitchToMock->expects($this->once())->method('isUseStoreInUrl')->willReturn(false); - $this->redirectMock->expects($this->once())->method('getRedirectUrl')->willReturn($expectedRedirectUrl); - $this->responseMock->expects($this->once())->method('setRedirect')->with($expectedRedirectUrl); - - $this->model->execute(); - } - - public function testExecuteSuccessWithUseStoreInUrl() - { - $currentActiveStoreCode = 'sv1'; - $storeToSwitchToCode = 'sv2'; - $defaultStoreViewCode = 'default'; - $originalRedirectUrl = "magento.com/{$currentActiveStoreCode}/test-page/test-sub-page"; - $expectedRedirectUrl = "magento.com/{$storeToSwitchToCode}/test-page/test-sub-page"; - $currentActiveStoreMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->disableOriginalConstructor() - ->setMethods(['isUseStoreInUrl', 'getBaseUrl']) - ->getMockForAbstractClass(); - $defaultStoreViewMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); - $storeToSwitchToMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) - ->disableOriginalConstructor() - ->setMethods(['isUseStoreInUrl', 'getBaseUrl']) - ->getMockForAbstractClass(); - - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($currentActiveStoreMock); - $this->requestMock->expects($this->once())->method('getParam')->willReturn($storeToSwitchToCode); $this->storeRepositoryMock ->expects($this->once()) ->method('getActiveStoreByCode') + ->with($storeToSwitchToCode) ->willReturn($storeToSwitchToMock); - $this->storeManagerMock - ->expects($this->once()) - ->method('getDefaultStoreView') - ->willReturn($defaultStoreViewMock); - $defaultStoreViewMock->expects($this->once())->method('getId')->willReturn($defaultStoreViewCode); - $storeToSwitchToMock->expects($this->once())->method('getId')->willReturn($storeToSwitchToCode); - $storeToSwitchToMock->expects($this->once())->method('isUseStoreInUrl')->willReturn(true); - $this->redirectMock->expects($this->any())->method('getRedirectUrl')->willReturn($originalRedirectUrl); - $currentActiveStoreMock - ->expects($this->any()) - ->method('getBaseUrl') - ->willReturn("magento.com/{$currentActiveStoreCode}"); - $storeToSwitchToMock - ->expects($this->once()) - ->method('getBaseUrl') - ->willReturn("magento.com/{$storeToSwitchToCode}"); + $this->storeSwitcher->expects($this->once()) + ->method('switch') + ->with($defaultStoreViewMock, $storeToSwitchToMock, $expectedRedirectUrl) + ->willReturn($expectedRedirectUrl); + + $this->redirectMock->expects($this->once())->method('getRedirectUrl')->willReturn($expectedRedirectUrl); $this->responseMock->expects($this->once())->method('setRedirect')->with($expectedRedirectUrl); $this->model->execute(); diff --git a/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php b/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php index 280b38c392ea9..61b6ba3b9306a 100644 --- a/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php @@ -1,16 +1,14 @@ <?php /** - * Tests Magento\Store\Model\App\Emulation - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Store\Test\Unit\Model\App; /** + * Tests Magento\Store\Model\App\Emulation + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmulationTest extends \PHPUnit\Framework\TestCase @@ -161,7 +159,10 @@ public function testStartDefaults() ->method('setCurrentStore')->with(self::NEW_STORE_ID); // Test - $result = $this->model->startEnvironmentEmulation(self::NEW_STORE_ID, \Magento\Framework\App\Area::AREA_FRONTEND); + $result = $this->model->startEnvironmentEmulation( + self::NEW_STORE_ID, + \Magento\Framework\App\Area::AREA_FRONTEND + ); $this->assertNull($result); } diff --git a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php index 9c7cc648cf8af..0fbf7bb7f044b 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php @@ -196,14 +196,30 @@ private function initTestData() 'root_category_id' => '1', 'default_store_id' => '1', 'code' => 'default', + ], + 2 => [ + 'group_id' => '1', + 'website_id' => '1', + 'name' => 'Default1', + 'default_store_id' => '1', + 'code' => 'default1', ] ]; - $this->trimmedGroup = [ - 'name' => 'Default', - 'root_category_id' => '1', - 'code' => 'default', - 'default_store_id' => '1', - ]; + $this->trimmedGroup = + [ + 0 => [ + 'name' => 'Default', + 'root_category_id' => '1', + 'code' => 'default', + 'default_store_id' => '1', + ], + 1 => [ + 'name' => 'Default1', + 'root_category_id' => '0', + 'code' => 'default1', + 'default_store_id' => '1' + ] + ]; $this->stores = [ 'default' => [ 'store_id' => '1', @@ -280,34 +296,34 @@ public function testRunGroup() [ScopeInterface::SCOPE_GROUPS, $this->groups, $this->groups], ]); - $this->websiteMock->expects($this->once()) + $this->websiteMock->expects($this->exactly(2)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setData') - ->with($this->trimmedGroup) - ->willReturnSelf(); - $this->groupMock->expects($this->exactly(3)) + ->withConsecutive( + [$this->equalTo($this->trimmedGroup[0])], + [$this->equalTo($this->trimmedGroup[1])] + )->willReturnSelf(); + + $this->groupMock->expects($this->exactly(6)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->groupMock->expects($this->once()) - ->method('setRootCategoryId') - ->with(0); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('getDefaultStoreId') ->willReturn($defaultStoreId); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setDefaultStoreId') ->with($storeId); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setWebsite') ->with($this->websiteMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); @@ -315,20 +331,16 @@ public function testRunGroup() ->method('load') ->withConsecutive([$this->websiteMock, 'base', 'code'], [$this->storeMock, 'default', 'code']) ->willReturnSelf(); - $this->abstractDbMock->expects($this->exactly(2)) + $this->abstractDbMock->expects($this->exactly(4)) ->method('save') ->with($this->groupMock) ->willReturnSelf(); - $this->abstractDbMock->expects($this->once()) + $this->abstractDbMock->expects($this->exactly(2)) ->method('addCommitCallback') ->willReturnCallback(function ($function) { return $function(); }); - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('store_group_save', ['group' => $this->groupMock]); - $this->processor->run($this->data); } @@ -382,10 +394,6 @@ public function testRunStore() return $function(); }); - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('store_add', ['store' => $this->storeMock]); - $this->processor->run($this->data); } diff --git a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/DeleteTest.php b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/DeleteTest.php index d16a4a70b00aa..c373643fa03ac 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/DeleteTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/DeleteTest.php @@ -244,8 +244,6 @@ public function testRun() ->method('get') ->with('test') ->willReturn($this->storeMock); - $this->storeResourceMock->expects($this->once()) - ->method('addCommitCallback'); $this->registryMock->expects($this->once()) ->method('unregister') diff --git a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/UpdateTest.php b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/UpdateTest.php index 3b0b932e31d46..e98ad08d46a68 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/UpdateTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/UpdateTest.php @@ -175,7 +175,7 @@ public function testRun() $updateData[ScopeInterface::SCOPE_STORES], ], ]); - $this->websiteMock->expects($this->exactly(4)) + $this->websiteMock->expects($this->atLeastOnce()) ->method('getResource') ->willReturn($this->websiteResourceMock); $this->websiteMock->expects($this->once()) @@ -203,7 +203,7 @@ public function testRun() $this->groupFactoryMock->expects($this->exactly(3)) ->method('create') ->willReturn($this->groupMock); - $this->groupMock->expects($this->exactly(5)) + $this->groupMock->expects($this->atLeastOnce()) ->method('getResource') ->willReturn($this->groupResourceMock); $this->groupMock->expects($this->once()) @@ -227,7 +227,7 @@ public function testRun() $this->storeFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturn($this->storeMock); - $this->storeMock->expects($this->exactly(4)) + $this->storeMock->expects($this->atLeastOnce()) ->method('getResource') ->willReturn($this->storeResourceMock); $this->storeMock->expects($this->once()) @@ -244,15 +244,16 @@ public function testRun() $this->storeMock->expects($this->once()) ->method('setData') ->with($updateData[ScopeInterface::SCOPE_STORES]['test']); - $this->storeResourceMock->expects($this->once()) + $this->storeMock->expects($this->once()) ->method('save') - ->with($this->storeMock); - $this->storeResourceMock->expects($this->once()) - ->method('addCommitCallback'); + ->willReturnSelf(); $this->model->run($data); } + /** + * @return array + */ private function getData() { return [ diff --git a/app/code/Magento/Store/Test/Unit/Model/PathConfigTest.php b/app/code/Magento/Store/Test/Unit/Model/PathConfigTest.php index 1fee23a64a66f..7cbffc06ff98a 100644 --- a/app/code/Magento/Store/Test/Unit/Model/PathConfigTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/PathConfigTest.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Store\Test\Unit\Model; use Magento\Store\Model\StoreManagerInterface; diff --git a/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php b/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php index e56b5c7fcaa19..7aa992064f794 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php @@ -125,10 +125,6 @@ public function testBeforeDispatchNoSuchEntity() $this->storeCookieManagerMock->expects($this->once()) ->method('deleteStoreCookie') ->with($this->storeMock); - $this->requestMock->expects($this->atLeastOnce()) - ->method('getParam') - ->with(StoreResolverInterface::PARAM_NAME) - ->willReturn(null); $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } @@ -148,10 +144,6 @@ public function testBeforeDispatchStoreIsInactive() $this->storeCookieManagerMock->expects($this->once()) ->method('deleteStoreCookie') ->with($this->storeMock); - $this->requestMock->expects($this->atLeastOnce()) - ->method('getParam') - ->with(StoreResolverInterface::PARAM_NAME) - ->willReturn(null); $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } @@ -171,10 +163,6 @@ public function testBeforeDispatchInvalidArgument() $this->storeCookieManagerMock->expects($this->once()) ->method('deleteStoreCookie') ->with($this->storeMock); - $this->requestMock->expects($this->atLeastOnce()) - ->method('getParam') - ->with(StoreResolverInterface::PARAM_NAME) - ->willReturn(null); $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } @@ -194,18 +182,6 @@ public function testBeforeDispatchNoStoreCookie() ->method('deleteStoreCookie') ->with($this->storeMock); - $this->storeResolverMock->expects($this->atLeastOnce()) - ->method('getCurrentStoreId') - ->willReturn(1); - - $this->storeRepositoryMock->expects($this->atLeastOnce()) - ->method('getActiveStoreById') - ->willReturn($this->storeMock); - - $this->storeCookieManagerMock->expects($this->atLeastOnce()) - ->method('setStoreCookie') - ->with($this->storeMock); - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } @@ -222,23 +198,6 @@ public function testBeforeDispatchWithStoreRequestParam() ->method('deleteStoreCookie') ->with($this->storeMock); - $this->requestMock->expects($this->atLeastOnce()) - ->method('getParam') - ->with(StoreResolverInterface::PARAM_NAME) - ->willReturn($storeCode); - - $this->storeResolverMock->expects($this->atLeastOnce()) - ->method('getCurrentStoreId') - ->willReturn(1); - - $this->storeRepositoryMock->expects($this->atLeastOnce()) - ->method('getActiveStoreById') - ->willReturn($this->storeMock); - - $this->storeCookieManagerMock->expects($this->atLeastOnce()) - ->method('setStoreCookie') - ->with($this->storeMock); - $this->plugin->beforeDispatch($this->subjectMock, $this->requestMock); } } diff --git a/app/code/Magento/Store/Test/Unit/Model/Resolver/GroupTest.php b/app/code/Magento/Store/Test/Unit/Model/Resolver/GroupTest.php index 9817bd532c18a..6fade2de934e4 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Resolver/GroupTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Resolver/GroupTest.php @@ -53,7 +53,7 @@ public function testGetScope() */ public function testGetScopeWithInvalidScope() { - $scopeMock = new \StdClass(); + $scopeMock = new \stdClass(); $this->storeManagerMock ->expects($this->once()) ->method('getGroup') diff --git a/app/code/Magento/Store/Test/Unit/Model/Resolver/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/Resolver/StoreTest.php index 958cfdea37bab..50a043f45bc16 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Resolver/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Resolver/StoreTest.php @@ -52,7 +52,7 @@ public function testGetScope() */ public function testGetScopeWithInvalidScope() { - $scopeMock = new \StdClass(); + $scopeMock = new \stdClass(); $this->_storeManagerMock ->expects($this->once()) ->method('getStore') diff --git a/app/code/Magento/Store/Test/Unit/Model/Resolver/WebsiteTest.php b/app/code/Magento/Store/Test/Unit/Model/Resolver/WebsiteTest.php index c5b3dbaff99be..5c33090f28eeb 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Resolver/WebsiteTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Resolver/WebsiteTest.php @@ -52,7 +52,7 @@ public function testGetScope() */ public function testGetScopeWithInvalidScope() { - $scopeMock = new \StdClass(); + $scopeMock = new \stdClass(); $this->_storeManagerMock ->expects($this->once()) ->method('getWebsite') diff --git a/app/code/Magento/Store/Test/Unit/Model/ScopeTreeProviderTest.php b/app/code/Magento/Store/Test/Unit/Model/ScopeTreeProviderTest.php index 1b4cb0b8d5f27..92b50f8fcde08 100644 --- a/app/code/Magento/Store/Test/Unit/Model/ScopeTreeProviderTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/ScopeTreeProviderTest.php @@ -5,9 +5,12 @@ */ namespace Magento\Store\Test\Unit\Model; -use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Api\Data\GroupInterface; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\GroupRepositoryInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\Store\Model\Group; use Magento\Store\Model\ScopeTreeProvider; use Magento\Store\Model\Store; @@ -16,20 +19,42 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\Website; +/** + * @covers \Magento\Store\Model\ScopeTreeProvider + */ class ScopeTreeProviderTest extends \PHPUnit\Framework\TestCase { - /** @var ScopeTreeProvider */ - protected $model; + /** + * @var ScopeTreeProvider + */ + private $model; - /** @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManagerMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|WebsiteRepositoryInterface + */ + private $websiteRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|GroupRepositoryInterface + */ + private $groupRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|StoreRepositoryInterface + */ + private $storeRepositoryMock; protected function setUp() { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->getMockForAbstractClass(); + $this->websiteRepositoryMock = $this->createMock(WebsiteRepositoryInterface::class); + $this->groupRepositoryMock = $this->createMock(GroupRepositoryInterface::class); + $this->storeRepositoryMock = $this->createMock(StoreRepositoryInterface::class); - $this->model = new ScopeTreeProvider($this->storeManagerMock); + $this->model = new ScopeTreeProvider( + $this->websiteRepositoryMock, + $this->groupRepositoryMock, + $this->storeRepositoryMock + ); } public function testGet() @@ -58,40 +83,34 @@ public function testGet() 'scopes' => [$websiteData], ]; - /** @var Website|\PHPUnit_Framework_MockObject_MockObject $websiteMock */ - $websiteMock = $this->getMockBuilder(\Magento\Store\Model\Website::class) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->any()) + $websiteMock = $this->createMock(WebsiteInterface::class); + $websiteMock->expects($this->atLeastOnce()) ->method('getId') ->willReturn($websiteId); + $this->websiteRepositoryMock->expects($this->once()) + ->method('getList') + ->willReturn([$websiteMock]); - /** @var Group|\PHPUnit_Framework_MockObject_MockObject $groupMock */ - $groupMock = $this->getMockBuilder(\Magento\Store\Model\Group::class) - ->disableOriginalConstructor() - ->getMock(); - $groupMock->expects($this->any()) + $groupMock = $this->createMock(GroupInterface::class); + $groupMock->expects($this->atLeastOnce()) ->method('getId') ->willReturn($groupId); + $groupMock->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn($websiteId); + $this->groupRepositoryMock->expects($this->once()) + ->method('getList') + ->willReturn([$groupMock, $groupMock]); - /** @var Store|\PHPUnit_Framework_MockObject_MockObject $storeMock */ - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $storeMock->expects($this->any()) + $storeMock = $this->createMock(StoreInterface::class); + $storeMock->expects($this->atLeastOnce()) ->method('getId') ->willReturn($storeId); - - $this->storeManagerMock->expects($this->any()) - ->method('getWebsites') - ->willReturn([$websiteMock]); - - $websiteMock->expects($this->any()) - ->method('getGroups') - ->willReturn([$groupMock, $groupMock]); - - $groupMock->expects($this->any()) - ->method('getStores') + $storeMock->expects($this->atLeastOnce()) + ->method('getStoreGroupId') + ->willReturn($groupId); + $this->storeRepositoryMock->expects($this->once()) + ->method('getList') ->willReturn([$storeMock, $storeMock, $storeMock]); $this->assertEquals($result, $this->model->get()); diff --git a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php index 702f4eee8db99..72ee0c196b090 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php @@ -55,6 +55,11 @@ protected function setUp() ); } + /** + * @param array $storeConfig + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ protected function getStoreMock(array $storeConfig) { $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) @@ -88,6 +93,9 @@ protected function getStoreMock(array $storeConfig) return $storeMock; } + /** + * @return \Magento\Store\Model\Data\StoreConfig + */ protected function createStoreConfigDataObject() { /** @var \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactoryMock */ diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php index ad3b927258717..a48804f02adc0 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php @@ -97,6 +97,9 @@ public function testGetStores($storesList, $withDefault, $codeKey, $expectedStor $this->assertEquals($expectedStores, $this->model->getStores($withDefault, $codeKey)); } + /** + * @return array + */ public function getStoresDataProvider() { $defaultStoreMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php index c05584c2d8bcb..f49aeaababece 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Store\Test\Unit\Model; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; @@ -40,6 +39,16 @@ class StoreTest extends \PHPUnit\Framework\TestCase */ protected $filesystemMock; + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + /** * @var \Magento\Framework\Url\ModifierInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -60,12 +69,22 @@ protected function setUp() 'isSecure', 'getServer', ]); + $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) ->disableOriginalConstructor() ->getMock(); + $this->configMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->getMock(); + $this->sessionMock = $this->getMockBuilder(SessionManagerInterface::class) + ->setMethods(['getCurrencyCode']) + ->getMockForAbstractClass(); $this->store = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['filesystem' => $this->filesystemMock] + [ + 'filesystem' => $this->filesystemMock, + 'config' => $this->configMock, + 'session' => $this->sessionMock, + ] ); $this->urlModifierMock = $this->createMock(\Magento\Framework\Url\ModifierInterface::class); @@ -83,7 +102,10 @@ protected function setUp() public function testLoad($key, $field) { /** @var \Magento\Store\Model\ResourceModel\Store $resource */ - $resource = $this->createPartialMock(\Magento\Store\Model\ResourceModel\Store::class, ['load', 'getIdFieldName', '__wakeup']); + $resource = $this->createPartialMock( + \Magento\Store\Model\ResourceModel\Store::class, + ['load', 'getIdFieldName', '__wakeup'] + ); $resource->expects($this->atLeastOnce())->method('load') ->with($this->isInstanceOf(\Magento\Store\Model\Store::class), $this->equalTo($key), $this->equalTo($field)) ->will($this->returnSelf()); @@ -93,6 +115,9 @@ public function testLoad($key, $field) $model->load($key); } + /** + * @return array + */ public function loadDataProvider() { return [ @@ -126,9 +151,11 @@ public function testGetWebsite() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( - \Magento\Store\Model\Store::class, [ - 'websiteRepository' => $websiteRepository, - ]); + \Magento\Store\Model\Store::class, + [ + 'websiteRepository' => $websiteRepository, + ] + ); $model->setWebsiteId($websiteId); $this->assertEquals($website, $model->getWebsite()); @@ -144,9 +171,11 @@ public function testGetWebsiteIfWebsiteIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( - \Magento\Store\Model\Store::class, [ - 'websiteRepository' => $websiteRepository, - ]); + \Magento\Store\Model\Store::class, + [ + 'websiteRepository' => $websiteRepository, + ] + ); $model->setWebsiteId(null); $this->assertFalse($model->getWebsite()); @@ -167,9 +196,11 @@ public function testGetGroup() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( - \Magento\Store\Model\Store::class, [ - 'groupRepository' => $groupRepository, - ]); + \Magento\Store\Model\Store::class, + [ + 'groupRepository' => $groupRepository, + ] + ); $model->setGroupId($groupId); $this->assertEquals($group, $model->getGroup()); @@ -185,9 +216,11 @@ public function testGetGroupIfGroupIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( - \Magento\Store\Model\Store::class, [ - 'groupRepository' => $groupRepository, - ]); + \Magento\Store\Model\Store::class, + [ + 'groupRepository' => $groupRepository, + ] + ); $model->setGroupId(null); $this->assertFalse($model->getGroup()); @@ -264,6 +297,9 @@ function ($path, $scope, $scopeCode) use ($secure, $expectedPath) { $this->assertEquals($expectedBaseUrl, $model->getBaseUrl($type, $secure)); } + /** + * @return array + */ public function getBaseUrlDataProvider() { return [ @@ -438,26 +474,26 @@ public function getCurrentUrlDataProvider() [ true, 'http://test/url', - 'http://test/url?SID=sid&___store=scope_code', + 'http://test/url?SID=sid&___store=scope_code', false ], [ true, 'http://test/url?SID=sid1&___store=scope', - 'http://test/url?SID=sid&___store=scope_code', + 'http://test/url?SID=sid&___store=scope_code', false ], [ false, 'https://test/url', - 'https://test/url?SID=sid&___store=scope_code', + 'https://test/url?SID=sid&___store=scope_code', false ], [ true, 'http://test/u/u.2?__store=scope_code', 'http://test/u/u.2?' - . 'SID=sid&___store=scope_code&___from_store=old-store', + . 'SID=sid&___store=scope_code&___from_store=old-store', 'old-store' ] ]; @@ -537,10 +573,12 @@ public function testGetAllowedCurrencies() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( - \Magento\Store\Model\Store::class, [ - 'config' => $configMock, - 'currencyInstalled' => $currencyPath, - ]); + \Magento\Store\Model\Store::class, + [ + 'config' => $configMock, + 'currencyInstalled' => $currencyPath, + ] + ); $this->assertEquals($expectedResult, $model->getAllowedCurrencies()); } @@ -602,6 +640,9 @@ public function testIsCurrentlySecure( } } + /** + * @return array + */ public function isCurrentlySecureDataProvider() { return [ @@ -610,7 +651,8 @@ public function isCurrentlySecureDataProvider() 'unsecure request, no secure base url registered' => [false, 443, false, true, null], 'unsecure request, not using registered port' => [false, 80], 'unsecure request, using registered port, not using secure in frontend' => [false, 443, false, false], - 'unsecure request, no secure base url registered, not using secure in frontend' => [false, 443, false, false, null], + 'unsecure request, no secure base url registered, not using secure in frontend' => + [false, 443, false, false, null], 'unsecure request, not using registered port, not using secure in frontend' => [false, 80, false, false], ]; } @@ -662,4 +704,78 @@ private function setUrlModifier(\Magento\Store\Model\Store $model) $property->setAccessible(true); $property->setValue($model, $this->urlModifierMock); } + + /** + * @param array $availableCodes + * @param string $currencyCode + * @param string $defaultCode + * @param string $expectedCode + * @return void + * @dataProvider currencyCodeDataProvider + */ + public function testGetCurrentCurrencyCode( + array $availableCodes, + string $currencyCode, + string $defaultCode, + string $expectedCode + ) { + $this->store->setData('available_currency_codes', $availableCodes); + $this->sessionMock->method('getCurrencyCode') + ->willReturn($currencyCode); + $this->configMock->method('getValue') + ->with(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT) + ->willReturn($defaultCode); + + $code = $this->store->getCurrentCurrencyCode(); + $this->assertEquals($expectedCode, $code); + } + + /** + * @return array + */ + public function currencyCodeDataProvider(): array + { + return [ + [ + [ + 'USD', + ], + 'USD', + 'USD', + 'USD', + ], + [ + [ + 'USD', + 'EUR', + ], + 'EUR', + 'USD', + 'EUR', + ], + [ + [ + 'EUR', + 'USD', + ], + 'GBP', + 'USD', + 'USD', + ], + [ + [ + 'USD', + ], + 'GBP', + 'EUR', + 'USD', + ], + [ + [], + 'GBP', + 'EUR', + 'EUR', + ], + ]; + } } diff --git a/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php index d70da2ee1ddc6..9ca819876b6ab 100644 --- a/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/System/StoreTest.php @@ -104,6 +104,9 @@ public function testGetStoresStructure( ); } + /** + * @return array + */ public function getStoresStructureDataProvider() { $websiteName = 'website'; @@ -207,6 +210,9 @@ public function testGetStoreValuesForForm( ); } + /** + * @return array + */ public function getStoreValuesForFormDataProvider() { $websiteName = 'website'; @@ -256,14 +262,19 @@ public function getStoreValuesForFormDataProvider() 'storeGroupId' => $groupId, 'groupWebsiteId' => $websiteId, 'expectedResult' => [ - ['label' => '', 'value' => ''], - ['label' => __('All Store Views'), 'value' => 0], - ['label' => $websiteName, 'value' => []], + ['label' => '', 'value' => '','__disableTmpl' => true], + ['label' => __('All Store Views'), 'value' => 0,'__disableTmpl' => true], + ['label' => $websiteName, 'value' => [],'__disableTmpl' => true], [ 'label' => str_repeat($nonEscapableNbspChar, 4) . $groupName, 'value' => [ - ['label' => str_repeat($nonEscapableNbspChar, 4) . $storeName, 'value' => $storeId] - ] + [ + 'label' => str_repeat($nonEscapableNbspChar, 4) . $storeName, + 'value' => $storeId, + '__disableTmpl' => true, + ] + ], + '__disableTmpl' => true ], ] ], diff --git a/app/code/Magento/Store/Test/Unit/Model/WebsiteRepositoryTest.php b/app/code/Magento/Store/Test/Unit/Model/WebsiteRepositoryTest.php index 80e4973bbe7fe..6ebc7ebd4bbd9 100644 --- a/app/code/Magento/Store/Test/Unit/Model/WebsiteRepositoryTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/WebsiteRepositoryTest.php @@ -34,7 +34,7 @@ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->websiteFactoryMock = - $this->getMockBuilder('Magento\Store\Model\WebsiteFactory') + $this->getMockBuilder(\Magento\Store\Model\WebsiteFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); diff --git a/app/code/Magento/Store/Test/Unit/Setup/UpgradeDataTest.php b/app/code/Magento/Store/Test/Unit/Setup/UpgradeDataTest.php index 0dc7de4224c43..210e187b78b47 100644 --- a/app/code/Magento/Store/Test/Unit/Setup/UpgradeDataTest.php +++ b/app/code/Magento/Store/Test/Unit/Setup/UpgradeDataTest.php @@ -102,6 +102,9 @@ public function testUpgradeToVersion210(array $groupList, array $expectedCodes) $this->model->upgrade($this->setupMock, $this->contextMock); } + /** + * @return array + */ public function upgradeDataProvider() { return [ diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php new file mode 100644 index 0000000000000..7c01a33a1db32 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\GroupTitle; + +/** + * GroupTitleTest contains unit test for \Magento\Store\Ui\Component\Listing\Column\GroupTitle + */ +class GroupTitleTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GroupTitle + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new GroupTitle( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\GroupTitle::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Main Website Store', + 'group_id' => 1, + 'group_code' => 'main_website_store', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store" href="%s">Main Website Store</a><br />(Code: main_website_store)', + 'http://magento-2-1.dev/admin/system_store/editGroup' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'group_id' => 1, + 'group_code' => 'main_website_store', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editGroup', + ['group_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editGroup'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php new file mode 100644 index 0000000000000..d1b50acf3b80f --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\StoreTitle; + +/** + * Class StoreTitleTest contains unit test for \Magento\Store\Ui\Component\Listing\Column\StoreTitle + */ +class StoreTitleTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var StoreTitle + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new StoreTitle( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\StoreTitle::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Default Store View', + 'store_id' => 1, + 'store_code' => 'default', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store View" href="%s">Default Store View</a><br />(Code: default)', + 'http://magento-2-1.dev/admin/system_store/editStore' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'store_id' => 1, + 'store_code' => 'default', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editStore', + ['store_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editStore'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php new file mode 100644 index 0000000000000..da9f57e09f28d --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\WebsiteName; + +/** + * Class WebsiteNameTest contains unit tests for \Magento\Store\Ui\Component\Listing\Column\WebsiteName + */ +class WebsiteNameTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var WebsiteName + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new WebsiteName( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\WebsiteName::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Main Website', + 'website_id' => 1, + 'code' => 'base', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store" href="%s">Main Website</a><br />(Code: base)', + 'http://magento-2-1.dev/admin/system_store/editWebsite' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'website_id' => 1, + 'code' => 'base', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editWebsite', + ['website_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editWebsite'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php index ed9ac7ebe438a..9b83714166b12 100644 --- a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php +++ b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php @@ -22,6 +22,11 @@ class RouteParamsResolverTest extends \PHPUnit\Framework\TestCase */ protected $queryParamsResolverMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\Store + */ + protected $storeMock; + /** * @var \Magento\Store\Url\Plugin\RouteParamsResolver */ @@ -30,7 +35,19 @@ class RouteParamsResolverTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['getCode']) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock->expects($this->any())->method('getCode')->willReturn('custom_store'); + $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->storeManagerMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->queryParamsResolverMock = $this->createMock(\Magento\Framework\Url\QueryParamsResolverInterface::class); $this->model = new \Magento\Store\Url\Plugin\RouteParamsResolver( $this->scopeConfigMock, @@ -42,6 +59,8 @@ protected function setUp() public function testBeforeSetRouteParamsScopeInParams() { $storeCode = 'custom_store'; + $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + $this->scopeConfigMock ->expects($this->once()) ->method('getValue') @@ -52,7 +71,7 @@ public function testBeforeSetRouteParamsScopeInParams() ) ->will($this->returnValue(false)); $this->storeManagerMock->expects($this->any())->method('hasSingleStore')->willReturn(false); - $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + /** @var \PHPUnit_Framework_MockObject_MockObject $routeParamsResolverMock */ $routeParamsResolverMock = $this->getMockBuilder(\Magento\Framework\Url\RouteParamsResolver::class) ->setMethods(['setScope', 'getScope']) @@ -61,7 +80,7 @@ public function testBeforeSetRouteParamsScopeInParams() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->any())->method('setQueryParam'); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -72,6 +91,8 @@ public function testBeforeSetRouteParamsScopeInParams() public function testBeforeSetRouteParamsScopeUseStoreInUrl() { $storeCode = 'custom_store'; + $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + $this->scopeConfigMock ->expects($this->once()) ->method('getValue') @@ -81,8 +102,9 @@ public function testBeforeSetRouteParamsScopeUseStoreInUrl() $storeCode ) ->will($this->returnValue(true)); + $this->storeManagerMock->expects($this->any())->method('hasSingleStore')->willReturn(false); - $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + /** @var \PHPUnit_Framework_MockObject_MockObject $routeParamsResolverMock */ $routeParamsResolverMock = $this->getMockBuilder(\Magento\Framework\Url\RouteParamsResolver::class) ->setMethods(['setScope', 'getScope']) @@ -91,7 +113,7 @@ public function testBeforeSetRouteParamsScopeUseStoreInUrl() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam'); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -102,6 +124,8 @@ public function testBeforeSetRouteParamsScopeUseStoreInUrl() public function testBeforeSetRouteParamsSingleStore() { $storeCode = 'custom_store'; + $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + $this->scopeConfigMock ->expects($this->once()) ->method('getValue') @@ -112,7 +136,7 @@ public function testBeforeSetRouteParamsSingleStore() ) ->will($this->returnValue(false)); $this->storeManagerMock->expects($this->any())->method('hasSingleStore')->willReturn(true); - $data = ['_scope' => $storeCode, '_scope_to_url' => true]; + /** @var \PHPUnit_Framework_MockObject_MockObject $routeParamsResolverMock */ $routeParamsResolverMock = $this->getMockBuilder(\Magento\Framework\Url\RouteParamsResolver::class) ->setMethods(['setScope', 'getScope']) @@ -132,6 +156,8 @@ public function testBeforeSetRouteParamsSingleStore() public function testBeforeSetRouteParamsNoScopeInParams() { $storeCode = 'custom_store'; + $data = ['_scope_to_url' => true]; + $this->scopeConfigMock ->expects($this->once()) ->method('getValue') @@ -140,17 +166,10 @@ public function testBeforeSetRouteParamsNoScopeInParams() \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeCode ) - ->will($this->returnValue(false)); + ->will($this->returnValue(true)); + $this->storeManagerMock->expects($this->any())->method('hasSingleStore')->willReturn(false); - /** @var \PHPUnit_Framework_MockObject_MockObject| $routeParamsResolverMock */ - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->setMethods(['getCode']) - ->disableOriginalConstructor() - ->getMock(); - $storeMock->expects($this->any())->method('getCode')->willReturn($storeCode); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - $data = ['_scope_to_url' => true]; /** @var \PHPUnit_Framework_MockObject_MockObject $routeParamsResolverMock */ $routeParamsResolverMock = $this->getMockBuilder(\Magento\Framework\Url\RouteParamsResolver::class) ->setMethods(['setScope', 'getScope']) @@ -159,7 +178,7 @@ public function testBeforeSetRouteParamsNoScopeInParams() $routeParamsResolverMock->expects($this->never())->method('setScope'); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn(false); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php b/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php new file mode 100644 index 0000000000000..1cf0534ea9c6e --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +/** + * Class AbstractNameColumn + */ +abstract class AbstractNameColumn extends \Magento\Ui\Component\Listing\Columns\Column +{ + /** + * @inheritdoc + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->prepareTitle($item); + } + } + } + + return $dataSource; + } + + abstract public function prepareTitle(array $item); +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php b/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php new file mode 100644 index 0000000000000..c4af27d888fc2 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class GroupTitle + */ +class GroupTitle extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editGroup', + ['group_id' => $item['group_id']] + ); + + $html = '<a title="' . __('Edit Store') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['group_code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php index 1fc13390e30b5..816b2af3d84df 100644 --- a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php @@ -88,22 +88,25 @@ protected function generateCurrentOptions() /** @var \Magento\Store\Model\Store $store */ foreach ($storeCollection as $store) { if ($store->getGroupId() == $group->getId()) { - $name = $this->escaper->escapeHtml($store->getName()); + $name = $store->getName(); $stores[$name]['label'] = str_repeat(' ', 8) . $name; $stores[$name]['value'] = $store->getId(); + $stores[$name]['__disableTmpl'] = true; } } if (!empty($stores)) { - $name = $this->escaper->escapeHtml($group->getName()); + $name = $group->getName(); $groups[$name]['label'] = str_repeat(' ', 4) . $name; $groups[$name]['value'] = array_values($stores); + $groups[$name]['__disableTmpl'] = true; } } } if (!empty($groups)) { - $name = $this->escaper->escapeHtml($website->getName()); + $name = $website->getName(); $this->currentOptions[$name]['label'] = $name; $this->currentOptions[$name]['value'] = array_values($groups); + $this->currentOptions[$name]['__disableTmpl'] = true; } } } diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php b/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php new file mode 100644 index 0000000000000..1133b98032bf3 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class StoreView + */ +class StoreTitle extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editStore', + ['store_id' => $item['store_id']] + ); + + $html = '<a title="' . __('Edit Store View') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['store_code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php b/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php new file mode 100644 index 0000000000000..e95c556541943 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class WebsiteName + */ +class WebsiteName extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editWebsite', + ['website_id' => $item['website_id']] + ); + + $html = '<a title="' . __('Edit Store') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php b/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php new file mode 100644 index 0000000000000..bae17a7eeb632 --- /dev/null +++ b/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\DataProvider; + +use Magento\Store\Model\ResourceModel\Website\Grid\CollectionFactory as WebsiteCollectionFactory; + +/** + * Class WebsiteDataProvider + */ +class WebsiteDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * WebsiteDataProvider constructor. + * + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param WebsiteCollectionFactory $collectionFactory + * @param array $meta + * @param array $data + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + WebsiteCollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + + $this->collection = $collectionFactory->create(); + } +} diff --git a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php index c4f8a31430963..9c9d1e6023af0 100644 --- a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php +++ b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php @@ -6,6 +6,7 @@ namespace Magento\Store\Url\Plugin; use \Magento\Store\Model\Store; +use \Magento\Store\Api\Data\StoreInterface; use \Magento\Store\Model\ScopeInterface as StoreScopeInterface; /** @@ -51,6 +52,8 @@ public function __construct( * @param \Magento\Framework\Url\RouteParamsResolver $subject * @param array $data * @param bool $unsetOldParams + * @throws \Magento\Framework\Exception\NoSuchEntityException + * * @return array */ public function beforeSetRouteParams( @@ -63,12 +66,18 @@ public function beforeSetRouteParams( unset($data['_scope']); } if (isset($data['_scope_to_url']) && (bool)$data['_scope_to_url'] === true) { - $storeCode = $subject->getScope() ?: $this->storeManager->getStore()->getCode(); + /** @var StoreInterface $currentScope */ + $currentScope = $subject->getScope(); + $storeCode = $currentScope && $currentScope instanceof StoreInterface ? + $currentScope->getCode() : + $this->storeManager->getStore()->getCode(); + $useStoreInUrl = $this->scopeConfig->getValue( Store::XML_PATH_STORE_IN_URL, StoreScopeInterface::SCOPE_STORE, $storeCode ); + if (!$useStoreInUrl && !$this->storeManager->hasSingleStore()) { $this->queryParamsResolver->setQueryParam('___store', $storeCode); } diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index b3bbb680bcd78..692da300ade3a 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -2,19 +2,21 @@ "name": "magento/module-store", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/module-catalog": "102.0.*", "magento/module-directory": "100.2.*", "magento/module-ui": "101.0.*", "magento/module-config": "101.0.*", "magento/module-media-storage": "100.2.*", - "magento/framework": "101.0.*" + "magento/framework": "101.0.*", + "magento/module-customer": "101.0.*", + "magento/module-authorization": "100.2.*" }, "suggest": { "magento/module-deploy": "100.2.*" }, "type": "magento2-module", - "version": "100.2.2", + "version": "100.2.10", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index bb5b23620df4b..c7f4165c627e3 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -83,7 +83,7 @@ <use_http_via>0</use_http_via> <use_http_x_forwarded_for>0</use_http_x_forwarded_for> <use_http_user_agent>0</use_http_user_agent> - <use_frontend_sid>1</use_frontend_sid> + <use_frontend_sid>0</use_frontend_sid> </session> <browser_capabilities> <cookies>1</cookies> @@ -130,17 +130,19 @@ <html>html</html> <phtml>phtml</phtml> <shtml>shtml</shtml> + <phpt>phpt</phpt> + <pht>pht</pht> </protected_extensions> <public_files_valid_paths> <protected> - <app>/app/*/*</app> - <bin>/bin/*/*</bin> - <dev>/dev/*/*</dev> - <generated>/generated/*/*</generated> - <lib>/lib/*/*</lib> - <setup>/setup/*/*</setup> - <update>/update/*/*</update> - <vendor>/vendor/*/*</vendor> + <app>*/app/*/*</app> + <bin>*/bin/*/*</bin> + <dev>*/dev/*/*</dev> + <generated>*/generated/*/*</generated> + <lib>*/lib/*/*</lib> + <setup>*/setup/*/*</setup> + <update>*/update/*/*</update> + <vendor>*/vendor/*/*</vendor> </protected> </public_files_valid_paths> </file> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 27133de270e2f..b626d8e9c2288 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -25,6 +25,14 @@ <preference for="Magento\Framework\App\ScopeFallbackResolverInterface" type="Magento\Store\Model\ScopeFallbackResolver"/> <preference for="Magento\Framework\App\ScopeTreeProviderInterface" type="Magento\Store\Model\ScopeTreeProvider"/> <preference for="Magento\Framework\App\ScopeValidatorInterface" type="Magento\Store\Model\ScopeValidator"/> + <preference for="Magento\Store\Model\StoreSwitcherInterface" type="Magento\Store\Model\StoreSwitcher" /> + <type name="Magento\Framework\App\Http\Context"> + <arguments> + <argument name="default" xsi:type="array"> + <item name="website" xsi:type="string">0</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\App\Response\Http"> <plugin name="genericHeaderPlugin" type="Magento\Framework\App\Response\HeaderManager"/> </type> @@ -57,7 +65,7 @@ <preference for="Magento\Framework\App\Router\PathConfigInterface" type="Magento\Store\Model\PathConfig" /> <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck" sortOrder="10"/> - <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\Design" sortOrder="30"/> + <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\Design" /> </type> <type name="Magento\Framework\Url\SecurityInfo"> <plugin name="storeUrlSecurityInfo" type="Magento\Store\Url\Plugin\SecurityInfo"/> @@ -224,6 +232,7 @@ <type name="Magento\Framework\App\ScopeResolverPool"> <arguments> <argument name="scopeResolvers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Framework\App\ScopeResolver</item> <item name="store" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="stores" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="group" xsi:type="object">Magento\Store\Model\Resolver\Group</item> @@ -417,4 +426,14 @@ </argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcher"> + <arguments> + <argument name="storeSwitchers" xsi:type="array"> + <item name="cleanTargetUrl" xsi:type="object">Magento\Store\Model\StoreSwitcher\CleanTargetUrl</item> + <item name="manageStoreCookie" xsi:type="object">Magento\Store\Model\StoreSwitcher\ManageStoreCookie</item> + <item name="managePrivateContent" xsi:type="object">Magento\Store\Model\StoreSwitcher\ManagePrivateContent</item> + <item name="hashGenerator" xsi:type="object">Magento\Store\Model\StoreSwitcher\HashGenerator</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Store/etc/frontend/di.xml b/app/code/Magento/Store/etc/frontend/di.xml index c39d5df863939..917aedad3d960 100644 --- a/app/code/Magento/Store/etc/frontend/di.xml +++ b/app/code/Magento/Store/etc/frontend/di.xml @@ -9,7 +9,7 @@ <type name="Magento\Framework\App\FrontController"> <plugin name="requestPreprocessor" type="Magento\Store\App\FrontController\Plugin\RequestPreprocessor" sortOrder="50"/> </type> - <type name="Magento\Framework\App\Action\Action"> + <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="contextPlugin" type="Magento\Store\App\Action\Plugin\Context" sortOrder="10"/> </type> <type name="Magento\Framework\App\RouterList" shared="true"> diff --git a/app/code/Magento/Store/etc/frontend/sections.xml b/app/code/Magento/Store/etc/frontend/sections.xml index b1a9fc3cb1d71..b7dbfe405263b 100644 --- a/app/code/Magento/Store/etc/frontend/sections.xml +++ b/app/code/Magento/Store/etc/frontend/sections.xml @@ -8,4 +8,5 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> <action name="stores/store/switch"/> + <action name="stores/store/switchrequest"/> </config> diff --git a/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml b/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml new file mode 100644 index 0000000000000..4e797b5edf7b6 --- /dev/null +++ b/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">store_listing.store_listing_data_source</item> + </item> + </argument> + <settings> + <buttons> + <button name="add"> + <url path="adminhtml/*/newWebsite"/> + <class>primary</class> + <label translate="true">Create Website</label> + </button> + <button name="add_store"> + <url path="adminhtml/*/newStore"/> + <class>add add-store-view</class> + <label translate="true">Create Store View</label> + </button> + <button name="add_group"> + <url path="adminhtml/*/newGroup"/> + <class>add add-store</class> + <label translate="true">Create Store</label> + </button> + </buttons> + <spinner>store_columns</spinner> + <deps> + <dep>store_listing.store_listing_data_source</dep> + </deps> + </settings> + <dataSource name="store_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">store_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Backend::store</aclResource> + <dataProvider class="Magento\Store\Ui\DataProvider\WebsiteDataProvider" name="store_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>website_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <settings> + <sticky>true</sticky> + </settings> + <columnsControls name="columns_controls"/> + <filters name="listing_filters"/> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="store_columns"> + <settings> + </settings> + <column name="name" sortOrder="10" class="Magento\Store\Ui\Component\Listing\Column\WebsiteName"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Web Site</label> + </settings> + </column> + <column name="group_title" sortOrder="20" class="Magento\Store\Ui\Component\Listing\Column\GroupTitle"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Store</label> + </settings> + </column> + <column name="store_title" sortOrder="30" class="Magento\Store\Ui\Component\Listing\Column\StoreTitle"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Store View</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/Store/view/frontend/templates/switch/flags.phtml b/app/code/Magento/Store/view/frontend/templates/switch/flags.phtml index 5ceb4b3f856d7..9c48469e9d560 100644 --- a/app/code/Magento/Store/view/frontend/templates/switch/flags.phtml +++ b/app/code/Magento/Store/view/frontend/templates/switch/flags.phtml @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - // @deprecated ?> -<?php if (count($block->getStores())>1): ?> +<?php if (count($block->getStores())>1) : ?> <div class="form-language"> <label for="select-language"><?= $block->escapeHtml(__('Your Language:')) ?></label> <select id="select-language" title="<?= $block->escapeHtmlAttr(__('Your Language')) ?>" onchange="window.location.href=this.value" class="flags"> - <?php foreach ($block->getStores() as $_lang): ?> + <?php foreach ($block->getStores() as $_lang) : ?> <?php $_selected = ($_lang->getId() == $block->getCurrentStoreId()) ? ' selected="selected"' : '' ?> <option value="<?= $block->escapeUrl($_lang->getCurrentUrl()) ?>" style="background-image:url('<?= $block->escapeUrl($block->getViewFileUrl('images/flags/flag_' . $_lang->getCode() . '.gif')) ?>');"<?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($_lang->getName()) ?></option> <?php endforeach; ?> diff --git a/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml b/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml index 80152dbb9a08f..a68207447e098 100644 --- a/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml +++ b/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml @@ -4,36 +4,34 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Store\Block\Switcher $block */ ?> -<?php if (count($block->getStores())>1): ?> -<?php $id = $block->getIdModifier() ? '-' . $block->getIdModifier() : '' ?> -<div class="switcher language switcher-language" data-ui-id="language-switcher" id="switcher-language<?= $block->escapeHtmlAttr($id) ?>"> - <strong class="label switcher-label"><span><?= $block->escapeHtml(__('Language')) ?></span></strong> - <div class="actions dropdown options switcher-options"> - <div class="action toggle switcher-trigger" - id="switcher-language-trigger<?= $block->escapeHtmlAttr($id) ?>" - data-mage-init='{"dropdown":{}}' - data-toggle="dropdown" - data-trigger-keypress-button="true"> - <strong class="view-<?= $block->escapeHtml($block->getCurrentStoreCode()) ?>"> - <span><?= $block->escapeHtml($block->getStoreName()) ?></span> - </strong> +<?php if (count($block->getStores())>1) : ?> + <?php $id = $block->getIdModifier() ? '-' . $block->getIdModifier() : '' ?> + <div class="switcher language switcher-language" data-ui-id="language-switcher" id="switcher-language<?= $block->escapeHtmlAttr($id) ?>"> + <strong class="label switcher-label"><span><?= $block->escapeHtml(__('Language')) ?></span></strong> + <div class="actions dropdown options switcher-options"> + <div class="action toggle switcher-trigger" + id="switcher-language-trigger<?= $block->escapeHtmlAttr($id) ?>" + data-mage-init='{"dropdown":{}}' + data-toggle="dropdown" + data-trigger-keypress-button="true"> + <strong class="view-<?= $block->escapeHtml($block->getCurrentStoreCode()) ?>"> + <span><?= $block->escapeHtml($block->getStoreName()) ?></span> + </strong> + </div> + <ul class="dropdown switcher-dropdown" + data-target="dropdown"> + <?php foreach ($block->getStores() as $_lang) : ?> + <?php if ($_lang->getId() != $block->getCurrentStoreId()) : ?> + <li class="view-<?= $block->escapeHtml($_lang->getCode()) ?> switcher-option"> + <a href="#" data-post='<?= /* @noEscape */ $block->getTargetStorePostData($_lang) ?>'> + <?= $block->escapeHtml($_lang->getName()) ?> + </a> + </li> + <?php endif; ?> + <?php endforeach; ?> + </ul> </div> - <ul class="dropdown switcher-dropdown" - data-target="dropdown"> - <?php foreach ($block->getStores() as $_lang): ?> - <?php if ($_lang->getId() != $block->getCurrentStoreId()): ?> - <li class="view-<?= $block->escapeHtml($_lang->getCode()) ?> switcher-option"> - <a href="#" data-post='<?= /* @noEscape */ $block->getTargetStorePostData($_lang) ?>'> - <?= $block->escapeHtml($_lang->getName()) ?> - </a> - </li> - <?php endif; ?> - <?php endforeach; ?> - </ul> </div> -</div> <?php endif; ?> diff --git a/app/code/Magento/Store/view/frontend/templates/switch/stores.phtml b/app/code/Magento/Store/view/frontend/templates/switch/stores.phtml index fb3f316bc16c8..e0c49401a6ce5 100644 --- a/app/code/Magento/Store/view/frontend/templates/switch/stores.phtml +++ b/app/code/Magento/Store/view/frontend/templates/switch/stores.phtml @@ -4,38 +4,36 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - /** @var \Magento\Store\Block\Switcher $block */ ?> -<?php if (count($block->getGroups())>1): ?> +<?php if (count($block->getGroups())>1) : ?> <div class="switcher store switcher-store" id="switcher-store"> <strong class="label switcher-label"><span><?= $block->escapeHtml(__('Select Store')) ?></span></strong> <div class="actions dropdown options switcher-options"> - <?php foreach ($block->getGroups() as $_group): ?> - <?php if ($_group->getId() == $block->getCurrentGroupId()): ?> - <div class="action toggle switcher-trigger" - role="button" - tabindex="0" - data-mage-init='{"dropdown":{}}' - data-toggle="dropdown" - data-trigger-keypress-button="true" - id="switcher-store-trigger"> - <strong> - <span><?= $block->escapeHtml($_group->getName()) ?></span> - </strong> - </div> - <?php endif; ?> + <?php foreach ($block->getGroups() as $_group) : ?> + <?php if ($_group->getId() == $block->getCurrentGroupId()) : ?> + <div class="action toggle switcher-trigger" + role="button" + tabindex="0" + data-mage-init='{"dropdown":{}}' + data-toggle="dropdown" + data-trigger-keypress-button="true" + id="switcher-store-trigger"> + <strong> + <span><?= $block->escapeHtml($_group->getName()) ?></span> + </strong> + </div> + <?php endif; ?> <?php endforeach; ?> <ul class="dropdown switcher-dropdown" data-target="dropdown"> - <?php foreach ($block->getGroups() as $_group): ?> - <?php if (!($_group->getId() == $block->getCurrentGroupId())): ?> - <li class="switcher-option"> - <a href="#" data-post='<?= /* @noEscape */ $block->getTargetStorePostData($_group->getDefaultStore()) ?>'> - <?= $block->escapeHtml($_group->getName()) ?> - </a> - </li> - <?php endif; ?> + <?php foreach ($block->getGroups() as $_group) : ?> + <?php if (!($_group->getId() == $block->getCurrentGroupId())) : ?> + <li class="switcher-option"> + <a href="#" data-post='<?= /* @noEscape */ $block->getTargetStorePostData($_group->getDefaultStore()) ?>'> + <?= $block->escapeHtml($_group->getName()) ?> + </a> + </li> + <?php endif; ?> <?php endforeach; ?> </ul> </div> diff --git a/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php new file mode 100644 index 0000000000000..f1bc6fcc105dc --- /dev/null +++ b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Swagger\Api\Data; + +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Swagger Schema Type. + * + * @api + */ +interface SchemaTypeInterface extends ArgumentInterface +{ + /** + * Retrieve the available types of Swagger schema. + * + * @return string + */ + public function getCode(); + + /** + * Get the URL path for the Swagger schema. + * + * @param string|null $store + * @return string + */ + public function getSchemaUrlPath($store = null); +} diff --git a/app/code/Magento/Swagger/Block/Index.php b/app/code/Magento/Swagger/Block/Index.php index 9df091eb7ff86..549495190ef34 100644 --- a/app/code/Magento/Swagger/Block/Index.php +++ b/app/code/Magento/Swagger/Block/Index.php @@ -5,12 +5,18 @@ */ namespace Magento\Swagger\Block; +use Magento\Framework\Phrase; use Magento\Framework\View\Element\Template; +use Magento\Swagger\Api\Data\SchemaTypeInterface; /** - * Class Index + * Block for swagger index page * * @api + * + * @method SchemaTypeInterface[] getSchemaTypes() + * @method bool hasSchemaTypes() + * @method string getDefaultSchemaTypeCode() */ class Index extends Template { @@ -19,14 +25,42 @@ class Index extends Template */ private function getParamStore() { - return ($this->getRequest()->getParam('store')) ? $this->getRequest()->getParam('store') : 'all'; + return $this->getRequest()->getParam('store') ?: 'all'; + } + + /** + * @return SchemaTypeInterface|null + */ + private function getSchemaType() + { + if (!$this->hasSchemaTypes()) { + return null; + } + + $schemaTypeCode = $this->getRequest()->getParam( + 'type', + $this->getDefaultSchemaTypeCode() + ); + + if (!array_key_exists($schemaTypeCode, $this->getSchemaTypes())) { + throw new \UnexpectedValueException( + new Phrase('Unknown schema type supplied') + ); + } + + return $this->getSchemaTypes()[$schemaTypeCode]; } /** - * @return string + * @return string|null */ public function getSchemaUrl() { - return rtrim($this->getBaseUrl(), '/') . '/rest/' . $this->getParamStore() . '/schema?services=all'; + if ($this->getSchemaType() === null) { + return null; + } + + return rtrim($this->getBaseUrl(), '/') . + $this->getSchemaType()->getSchemaUrlPath($this->getParamStore()); } } diff --git a/app/code/Magento/Swagger/Controller/Index/Index.php b/app/code/Magento/Swagger/Controller/Index/Index.php index e10a9235c4181..162367aaf81f9 100644 --- a/app/code/Magento/Swagger/Controller/Index/Index.php +++ b/app/code/Magento/Swagger/Controller/Index/Index.php @@ -7,6 +7,7 @@ /** * Class Index + * * @package Magento\Swagger\Controller\Index */ class Index extends \Magento\Framework\App\Action\Action diff --git a/app/code/Magento/Swagger/Test/Mftf/LICENSE.txt b/app/code/Magento/Swagger/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Swagger/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 <insert your license name here>" 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/app/code/Magento/Swagger/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/Swagger/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Swagger/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 <insert your license name here>" 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/app/code/Magento/Swagger/Test/Mftf/README.md b/app/code/Magento/Swagger/Test/Mftf/README.md new file mode 100644 index 0000000000000..0ec1c02814c6e --- /dev/null +++ b/app/code/Magento/Swagger/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Swagger Functional Tests + +The Functional Test Module for **Magento Swagger** module. diff --git a/app/code/Magento/Swagger/Test/Unit/Block/IndexTest.php b/app/code/Magento/Swagger/Test/Unit/Block/IndexTest.php new file mode 100644 index 0000000000000..df35e0807a6ab --- /dev/null +++ b/app/code/Magento/Swagger/Test/Unit/Block/IndexTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Swagger\Test\Unit\Block; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Swagger\Api\Data\SchemaTypeInterface; +use Magento\Swagger\Block\Index; +use Magento\SwaggerWebapi\Model\SchemaType\Rest; + +class IndexTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SchemaTypeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $schemaTypeMock; + + /** + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var Index + */ + private $index; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMock(); + $this->schemaTypeMock = $this->getMockBuilder(SchemaTypeInterface::class)->getMock(); + + $this->index = (new ObjectManager($this))->getObject( + Index::class, + [ + 'context' => (new ObjectManager($this))->getObject( + Context::class, + [ + 'request' => $this->requestMock, + ] + ), + 'data' => [ + 'schema_types' => [ + 'test' => $this->schemaTypeMock + ] + ] + ] + ); + } + + /** + * Test that the passed URL parameter is used when it is a valid schema type. + * + * @covers \Magento\Swagger\Block\Index::getSchemaUrl() + */ + public function testGetSchemaUrlValidType() + { + $this->requestMock->expects($this->atLeastOnce()) + ->method('getParam') + ->willReturn('test'); + + $this->schemaTypeMock->expects($this->any()) + ->method('getCode')->willReturn('test'); + + $this->schemaTypeMock->expects($this->once()) + ->method('getSchemaUrlPath') + ->willReturn('/test'); + + $this->assertEquals('/test', $this->index->getSchemaUrl()); + } + + /** + * Test that Swagger UI throws an exception if an invalid schema type is supplied. + * + * @covers \Magento\Swagger\Block\Index::getSchemaUrl() + */ + public function testGetSchemaUrlInvalidType() + { + $this->requestMock->expects($this->atLeastOnce()) + ->method('getParam') + ->willReturn('invalid'); + + $this->schemaTypeMock->expects($this->any()) + ->method('getCode')->willReturn('test'); + + $this->expectException(\UnexpectedValueException::class); + + $this->index->getSchemaUrl(); + } +} diff --git a/app/code/Magento/Swagger/Test/Unit/Controller/Index/IndexTest.php b/app/code/Magento/Swagger/Test/Unit/Controller/Index/IndexTest.php index 6931979b52f98..c409cd6ca7504 100644 --- a/app/code/Magento/Swagger/Test/Unit/Controller/Index/IndexTest.php +++ b/app/code/Magento/Swagger/Test/Unit/Controller/Index/IndexTest.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Swagger\Test\Unit\Controller\Index; class IndexTest extends \PHPUnit\Framework\TestCase diff --git a/app/code/Magento/Swagger/composer.json b/app/code/Magento/Swagger/composer.json index 787d58891c9e0..172d989db8b62 100644 --- a/app/code/Magento/Swagger/composer.json +++ b/app/code/Magento/Swagger/composer.json @@ -2,11 +2,11 @@ "name": "magento/module-swagger", "description": "N/A", "require": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0", + "php": "~7.0.13||~7.1.0||~7.2.0", "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.1", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Swagger/etc/module.xml b/app/code/Magento/Swagger/etc/module.xml index 8f1086057b57e..b825e359db6dd 100644 --- a/app/code/Magento/Swagger/etc/module.xml +++ b/app/code/Magento/Swagger/etc/module.xml @@ -6,5 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Swagger" setup_version="2.0.0" /> + <module name="Magento_Swagger" setup_version="2.0.0"> + <sequence> + <module name="Magento_Webapi" /> + </sequence> + </module> </config> diff --git a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml index 458102c38bf6f..be36f774b90a1 100644 --- a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml +++ b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml @@ -10,32 +10,18 @@ <title>Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + @@ -43,6 +29,7 @@ + diff --git a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml index 6e8ad08efbf6b..520fed29e9e12 100644 --- a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml +++ b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml @@ -12,16 +12,52 @@ * Modified by Magento, Modifications Copyright © Magento, Inc. All rights reserved. */ -/** @var \Magento\Framework\View\Element\Template $block */ +/** + * @var \Magento\Swagger\Block\Index $block + */ $schemaUrl = $block->getSchemaUrl(); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    escapeHtml(__('Subtotal')) ?>
    escapeHtml(__('Feed')) ?>
    escapeHtml(__('Miscellaneous Feeds')) ?>
    escapeHtml($feed['label']) ?> escapeHtml(__('Get Feed')) ?>
    escapeHtml($feed['group']) ?>
    a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:l.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/
    "!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("